phonograph/interim-models/src/portal.rs

259 lines
6.2 KiB
Rust
Raw Normal View History

2025-10-01 22:36:19 -07:00
use std::sync::LazyLock;
2025-07-08 14:37:03 -07:00
use derive_builder::Builder;
2025-10-01 22:36:19 -07:00
use regex::Regex;
2025-07-08 14:37:03 -07:00
use serde::Serialize;
2025-08-24 23:24:01 -07:00
use sqlx::{postgres::types::Oid, query, query_as, types::Json};
2025-07-08 14:37:03 -07:00
use uuid::Uuid;
2025-10-01 22:36:19 -07:00
use validator::Validate;
use crate::{client::AppDbClient, errors::QueryError, expression::PgExpressionAny};
2025-07-08 14:37:03 -07:00
2025-10-01 22:36:19 -07:00
pub static RE_PORTAL_NAME: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9][()a-zA-Z0-9 _-]*[a-zA-Z0-9()_-]$").unwrap());
2025-07-08 14:37:03 -07:00
/// A portal is a derivative representation of a Postgres relation.
2025-07-08 14:37:03 -07:00
#[derive(Clone, Debug, Serialize)]
pub struct Portal {
/// Primary key (defaults to UUIDv7).
2025-07-08 14:37:03 -07:00
pub id: Uuid,
/// Human friendly name for portal.
2025-07-08 14:37:03 -07:00
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.
2025-07-08 14:37:03 -07:00
pub class_oid: Oid,
2025-10-01 22:37:11 -07:00
/// 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<Option<PgExpressionAny>>,
2025-07-08 14:37:03 -07:00
}
impl Portal {
/// Build an insert statement to create a new portal.
2025-10-01 22:36:19 -07:00
pub fn insert() -> InsertBuilder {
InsertBuilder::default()
2025-07-08 14:37:03 -07:00
}
/// Build an update statement to alter an existing portal.
2025-10-01 22:36:19 -07:00
pub fn update() -> UpdateBuilder {
UpdateBuilder::default()
2025-08-24 23:24:01 -07:00
}
/// Build a single-field query by portal ID.
2025-08-04 13:59:42 -07:00
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 }
2025-08-04 13:59:42 -07:00
}
}
#[derive(Clone, Debug)]
pub struct WithIdQuery {
id: Uuid,
}
impl WithIdQuery {
pub async fn fetch_optional(
self,
app_db: &mut AppDbClient,
) -> Result<Option<Portal>, sqlx::Error> {
2025-07-08 14:37:03 -07:00
query_as!(
Portal,
2025-07-08 14:37:03 -07:00
r#"
select
id,
name,
workspace_id,
2025-07-08 14:37:03 -07:00
class_oid,
2025-10-01 22:37:11 -07:00
form_public,
table_filter as "table_filter: Json<Option<PgExpressionAny>>"
from portals
2025-07-08 14:37:03 -07:00
where id = $1
"#,
2025-08-04 13:59:42 -07:00
self.id
2025-07-08 14:37:03 -07:00
)
2025-08-04 13:59:42 -07:00
.fetch_optional(&mut *app_db.conn)
2025-07-08 14:37:03 -07:00
.await
}
pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result<Portal, sqlx::Error> {
2025-07-08 14:37:03 -07:00
query_as!(
Portal,
2025-07-08 14:37:03 -07:00
r#"
select
id,
name,
workspace_id,
2025-07-08 14:37:03 -07:00
class_oid,
2025-10-01 22:37:11 -07:00
form_public,
table_filter as "table_filter: Json<Option<PgExpressionAny>>"
from portals
2025-08-04 13:59:42 -07:00
where id = $1
2025-07-08 14:37:03 -07:00
"#,
2025-08-04 13:59:42 -07:00
self.id
2025-07-08 14:37:03 -07:00
)
2025-08-04 13:59:42 -07:00
.fetch_one(&mut *app_db.conn)
2025-07-08 14:37:03 -07:00
.await
}
2025-08-04 13:59:42 -07:00
}
2025-07-08 14:37:03 -07:00
2025-08-04 13:59:42 -07:00
#[derive(Clone, Debug)]
pub struct BelongingToWorkspaceQuery {
workspace_id: Uuid,
2025-08-04 13:59:42 -07:00
}
impl BelongingToWorkspaceQuery {
2025-10-01 22:36:19 -07:00
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Portal>, sqlx::Error> {
query_as!(
Portal,
r#"
select
id,
name,
workspace_id,
class_oid,
2025-10-01 22:37:11 -07:00
form_public,
2025-10-01 22:36:19 -07:00
table_filter as "table_filter: Json<Option<PgExpressionAny>>"
from portals
where workspace_id = $1
"#,
self.workspace_id,
)
.fetch_all(&mut *app_db.conn)
.await
}
2025-08-04 13:59:42 -07:00
pub fn belonging_to_rel(self, rel_oid: Oid) -> BelongingToRelQuery {
BelongingToRelQuery {
workspace_id: self.workspace_id,
2025-08-04 13:59:42 -07:00
rel_oid,
}
}
}
#[derive(Clone, Debug)]
pub struct BelongingToRelQuery {
workspace_id: Uuid,
2025-08-04 13:59:42 -07:00
rel_oid: Oid,
}
impl BelongingToRelQuery {
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Portal>, sqlx::Error> {
2025-07-08 14:37:03 -07:00
query_as!(
Portal,
2025-07-08 14:37:03 -07:00
r#"
select
id,
2025-07-18 16:20:03 -07:00
name,
workspace_id,
2025-08-04 13:59:42 -07:00
class_oid,
2025-10-01 22:37:11 -07:00
form_public,
table_filter as "table_filter: Json<Option<PgExpressionAny>>"
from portals
where workspace_id = $1 and class_oid = $2
2025-07-08 14:37:03 -07:00
"#,
self.workspace_id,
2025-08-04 13:59:42 -07:00
self.rel_oid
2025-07-08 14:37:03 -07:00
)
2025-08-04 13:59:42 -07:00
.fetch_all(&mut *app_db.conn)
2025-07-08 14:37:03 -07:00
.await
}
}
#[derive(Clone, Debug, Serialize, sqlx::Type)]
#[sqlx(type_name = "lens_display_type", rename_all = "lowercase")]
pub enum LensDisplayType {
Table,
}
#[derive(Builder, Clone, Debug)]
2025-10-01 22:36:19 -07:00
pub struct Insert {
2025-07-08 14:37:03 -07:00
name: String,
workspace_id: Uuid,
2025-07-08 14:37:03 -07:00
class_oid: Oid,
}
2025-10-01 22:36:19 -07:00
impl Insert {
pub async fn execute(self, app_db: &mut AppDbClient) -> Result<Portal, sqlx::Error> {
2025-07-08 14:37:03 -07:00
query_as!(
Portal,
2025-07-08 14:37:03 -07:00
r#"
insert into portals
(workspace_id, class_oid, name)
values ($1, $2, $3)
2025-07-08 14:37:03 -07:00
returning
id,
name,
workspace_id,
2025-07-08 14:37:03 -07:00
class_oid,
2025-10-01 22:37:11 -07:00
form_public,
table_filter as "table_filter: Json<Option<PgExpressionAny>>"
2025-07-08 14:37:03 -07:00
"#,
self.workspace_id,
2025-07-08 14:37:03 -07:00
self.class_oid,
self.name,
)
2025-08-04 13:59:42 -07:00
.fetch_one(&mut *app_db.conn)
2025-07-08 14:37:03 -07:00
.await
}
}
2025-08-24 23:24:01 -07:00
2025-10-01 22:36:19 -07:00
#[derive(Builder, Clone, Debug, Validate)]
pub struct Update {
2025-08-24 23:24:01 -07:00
id: Uuid,
2025-10-01 22:37:11 -07:00
2025-10-01 22:36:19 -07:00
#[builder(default, setter(strip_option = true))]
2025-10-01 22:37:11 -07:00
form_public: Option<bool>,
#[builder(default, setter(strip_option = true))]
table_filter: Option<Option<PgExpressionAny>>,
2025-10-01 22:36:19 -07:00
#[builder(default, setter(strip_option = true))]
#[validate(regex(path = *RE_PORTAL_NAME))]
name: Option<String>,
2025-08-24 23:24:01 -07:00
}
2025-10-01 22:36:19 -07:00
impl Update {
pub async fn execute(self, app_db: &mut AppDbClient) -> Result<(), QueryError> {
self.validate()?;
// TODO: consolidate queries
2025-10-01 22:37:11 -07:00
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 {
2025-08-24 23:24:01 -07:00
query!(
"update portals set table_filter = $1 where id = $2",
2025-10-01 22:37:11 -07:00
Json(table_filter) as Json<Option<PgExpressionAny>>,
2025-08-24 23:24:01 -07:00
self.id
)
2025-10-01 22:37:11 -07:00
.execute(app_db.get_conn())
2025-08-24 23:24:01 -07:00
.await?;
}
2025-10-01 22:36:19 -07:00
if let Some(name) = self.name {
query!("update portals set name = $1 where id = $2", name, self.id)
2025-10-01 22:37:11 -07:00
.execute(app_db.get_conn())
2025-10-01 22:36:19 -07:00
.await?;
}
2025-08-24 23:24:01 -07:00
Ok(())
}
}