use chrono::{DateTime, Utc}; use derive_builder::Builder; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; use sqlx::{Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as}; use thiserror::Error; use uuid::Uuid; use crate::client::AppDbClient; pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Field { pub id: Uuid, pub name: String, pub label: Option, pub field_type: sqlx::types::Json, pub width_px: i32, } impl Field { pub fn insertable_builder() -> InsertableFieldBuilder { InsertableFieldBuilder::default() } pub fn default_from_attr(attr: &PgAttribute) -> Self { Self { id: Uuid::now_v7(), name: attr.attname.clone(), label: None, field_type: sqlx::types::Json(FieldType::default_from_attr(attr)), width_px: 200, } } pub fn webc_tag(&self) -> &str { match self.field_type.0 { FieldType::InterimUser {} => "cell-interim-user", FieldType::Text {} => "cell-text", FieldType::Timestamp { .. } => "cell-timestamp", FieldType::Uuid {} => "cell-uuid", FieldType::Unknown => "cell-unknown", } } pub fn webc_custom_attrs(&self) -> Vec<(String, String)> { vec![] } pub fn get_value_encodable(&self, row: &PgRow) -> Result { let value_ref = row .try_get_raw(self.name.as_str()) .or(Err(ParseError::FieldNotFound))?; let type_info = value_ref.type_info(); let ty = type_info.name(); Ok(match ty { "TEXT" | "VARCHAR" => { Encodable::Text( as Decode>::decode(value_ref).unwrap()) } "UUID" => { Encodable::Uuid( as Decode>::decode(value_ref).unwrap()) } _ => return Err(ParseError::UnknownType), }) } pub fn belonging_to_lens(lens_id: Uuid) -> BelongingToLensQuery { BelongingToLensQuery { lens_id } } } #[derive(Clone, Debug)] pub struct BelongingToLensQuery { lens_id: Uuid, } impl BelongingToLensQuery { pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result, sqlx::Error> { query_as!( Field, r#" select id, name, label, field_type as "field_type: sqlx::types::Json", width_px from fields where lens_id = $1 "#, self.lens_id ) .fetch_all(&mut *app_db.conn) .await } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "t", content = "c")] pub enum FieldType { InterimUser {}, Text {}, Timestamp { format: String, }, Uuid {}, /// A special variant for when the field type is not specified and cannot be /// inferred. This isn't represented as an error, because we still want to /// be able to define display behavior via the .render() method. Unknown, } impl FieldType { pub fn default_from_attr(attr: &PgAttribute) -> Self { match attr.regtype.as_str() { "text" => Self::Text {}, "timestamp" => Self::Timestamp { format: RFC_3339_S.to_owned(), }, "uuid" => Self::Uuid {}, _ => Self::Unknown, } } /// Returns a SQL fragment for the default data type for creating or /// altering a backing column, such as "integer", or "timestamptz". Returns /// None if the field type is Unknown. pub fn attr_data_type_fragment(&self) -> Option<&'static str> { match self { Self::InterimUser {} | Self::Text {} => Some("text"), Self::Timestamp { .. } => Some("timestamptz"), Self::Uuid { .. } => Some("uuid"), Self::Unknown => None, } } } #[derive(Clone, Debug, Error)] #[error("field type is unknown")] pub struct FieldTypeUnknownError {} // -------- Insertable -------- #[derive(Builder, Clone, Debug)] pub struct InsertableField { lens_id: Uuid, name: String, #[builder(default)] label: Option, field_type: FieldType, #[builder(default = 200)] width_px: i32, } impl InsertableField { pub async fn insert(self, app_db: &mut AppDbClient) -> Result { query_as!( Field, r#" insert into fields (id, lens_id, name, label, field_type, width_px) values ($1, $2, $3, $4, $5, $6) returning id, name, label, field_type as "field_type: sqlx::types::Json", width_px "#, Uuid::now_v7(), self.lens_id, self.name, self.label, sqlx::types::Json::<_>(self.field_type) as sqlx::types::Json, self.width_px, ) .fetch_one(&mut *app_db.conn) .await } } impl InsertableFieldBuilder { pub fn default_from_attr(attr: &PgAttribute) -> Self { Self { name: Some(attr.attname.clone()), field_type: Some(FieldType::default_from_attr(attr)), ..Self::default() } } } // -------- Errors -------- /// Error when parsing a sqlx value to JSON #[derive(Debug, Error)] pub enum ParseError { #[error("incompatible json type")] BadJsonType, #[error("field not found in row")] FieldNotFound, #[error("unknown postgres type")] UnknownType, } // -------- Encodable -------- // TODO this should probably be moved to another crate #[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(tag = "t", content = "c")] pub enum Encodable { Text(Option), Timestamp(Option>), Uuid(Option), } impl Encodable { pub fn bind_onto<'a>( self, query: sqlx::query::Query<'a, Postgres, ::Arguments<'a>>, ) -> sqlx::query::Query<'a, Postgres, ::Arguments<'a>> { match self { Self::Text(value) => query.bind(value), Self::Timestamp(value) => query.bind(value), Self::Uuid(value) => query.bind(value), } } /// Transform the contained value into a serde_json::Value. pub fn inner_as_value(&self) -> serde_json::Value { let serialized = serde_json::to_value(self).unwrap(); #[derive(Deserialize)] struct Tagged { c: serde_json::Value, } let deserialized: Tagged = serde_json::from_value(serialized).unwrap(); deserialized.c } pub fn is_none(&self) -> bool { match self { Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => true, Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false, } } }