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}; 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, /// 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 single-field query by portal ID. pub fn with_id(id: Uuid) -> WithIdQuery { WithIdQuery { id } } /// Build a query by workspace ID and relation OID. pub fn belonging_to_workspace(workspace_id: Uuid) -> BelongingToWorkspaceQuery { BelongingToWorkspaceQuery { workspace_id } } } #[derive(Clone, Debug)] pub struct WithIdQuery { id: Uuid, } impl WithIdQuery { pub async fn fetch_optional( self, app_db: &mut AppDbClient, ) -> Result, sqlx::Error> { query_as!( Portal, r#" select id, name, workspace_id, class_oid, table_filter as "table_filter: Json>" from portals where id = $1 "#, self.id ) .fetch_optional(&mut *app_db.conn) .await } pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result { query_as!( Portal, r#" select id, name, workspace_id, class_oid, table_filter as "table_filter: Json>" from portals where id = $1 "#, self.id ) .fetch_one(&mut *app_db.conn) .await } } #[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, 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, 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, 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))] 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(filter) = self.filter { query!( "update portals set table_filter = $1 where id = $2", Json(filter) as Json>, self.id ) .execute(&mut *app_db.conn) .await?; } if let Some(name) = self.name { query!("update portals set name = $1 where id = $2", name, self.id) .execute(&mut *app_db.conn) .await?; } Ok(()) } }