use std::sync::LazyLock; use derive_builder::Builder; use regex::Regex; use serde::Serialize; use sqlx::{postgres::types::Oid, query, query_as, types::Json}; use uuid::Uuid; use validator::Validate; use crate::{ client::AppDbClient, errors::QueryError, expression::PgExpressionAny, macros::with_id_query, }; pub static RE_PORTAL_NAME: LazyLock = LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9][()a-zA-Z0-9 _-]*[a-zA-Z0-9()_-]$").unwrap()); /// A portal is a derivative representation of a Postgres relation. #[derive(Clone, Debug, Serialize)] pub struct Portal { /// Primary key (defaults to UUIDv7). pub id: Uuid, /// Human friendly name for portal. pub name: String, /// Workspace to which this portal belongs. pub workspace_id: Uuid, /// OID of the underlying Postgres relation. Currently, this is expected /// to be a normal table, not a view, etc. pub class_oid: Oid, /// Whether the portal's form may be viewed or submitted by users without /// direct access to the portal itself. pub form_public: bool, /// JSONB-encoded expression to use for filtering rows in the web-based /// table view. pub table_filter: Json>, } impl Portal { /// Build an insert statement to create a new portal. pub fn insert() -> InsertBuilder { InsertBuilder::default() } /// Build an update statement to alter an existing portal. pub fn update() -> UpdateBuilder { UpdateBuilder::default() } /// Build a query by workspace ID and relation OID. pub fn belonging_to_workspace(workspace_id: Uuid) -> BelongingToWorkspaceQuery { BelongingToWorkspaceQuery { workspace_id } } } with_id_query!( Portal, sql = r#" select id, name, workspace_id, class_oid, form_public, table_filter as "table_filter: Json>" from portals where id = $1 "#, ); #[derive(Clone, Debug)] pub struct BelongingToWorkspaceQuery { workspace_id: Uuid, } impl BelongingToWorkspaceQuery { pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result, sqlx::Error> { query_as!( Portal, r#" select id, name, workspace_id, class_oid, form_public, table_filter as "table_filter: Json>" from portals where workspace_id = $1 "#, self.workspace_id, ) .fetch_all(&mut *app_db.conn) .await } pub fn belonging_to_rel(self, rel_oid: Oid) -> BelongingToRelQuery { BelongingToRelQuery { workspace_id: self.workspace_id, rel_oid, } } } #[derive(Clone, Debug)] pub struct BelongingToRelQuery { workspace_id: Uuid, rel_oid: Oid, } impl BelongingToRelQuery { pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result, sqlx::Error> { query_as!( Portal, r#" select id, name, workspace_id, class_oid, form_public, table_filter as "table_filter: Json>" from portals where workspace_id = $1 and class_oid = $2 "#, self.workspace_id, self.rel_oid ) .fetch_all(&mut *app_db.conn) .await } } #[derive(Clone, Debug, Serialize, sqlx::Type)] #[sqlx(type_name = "lens_display_type", rename_all = "lowercase")] pub enum LensDisplayType { Table, } #[derive(Builder, Clone, Debug)] pub struct Insert { name: String, workspace_id: Uuid, class_oid: Oid, } impl Insert { pub async fn execute(self, app_db: &mut AppDbClient) -> Result { query_as!( Portal, r#" insert into portals (workspace_id, class_oid, name) values ($1, $2, $3) returning id, name, workspace_id, class_oid, form_public, table_filter as "table_filter: Json>" "#, self.workspace_id, self.class_oid, self.name, ) .fetch_one(&mut *app_db.conn) .await } } #[derive(Builder, Clone, Debug, Validate)] pub struct Update { id: Uuid, #[builder(default, setter(strip_option = true))] form_public: Option, #[builder(default, setter(strip_option = true))] table_filter: Option>, #[builder(default, setter(strip_option = true))] #[validate(regex(path = *RE_PORTAL_NAME))] name: Option, } impl Update { pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), QueryError> { self.validate()?; // TODO: consolidate queries if let Some(form_public) = self.form_public { query!( "update portals set form_public = $1 where id = $2", form_public, self.id ) .execute(app_db.get_conn()) .await?; } if let Some(table_filter) = self.table_filter { query!( "update portals set table_filter = $1 where id = $2", Json(table_filter) as Json>, self.id ) .execute(app_db.get_conn()) .await?; } if let Some(name) = self.name { query!("update portals set name = $1 where id = $2", name, self.id) .execute(app_db.get_conn()) .await?; } Ok(()) } }