use chrono::{DateTime, Utc}; use derive_builder::Builder; use interim_pgtypes::pg_attribute::PgAttribute; use serde::{Deserialize, Serialize}; use sqlx::{ Decode, PgExecutor, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as, }; use thiserror::Error; use uuid::Uuid; 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::Integer => "cell-integer", 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 render(&self, value: &Encodable) -> String { // match (self.field_type.0.clone(), value) { // (FieldType::Integer, Encodable::Integer(Some(value))) => value.to_string(), // (FieldType::Integer, Encodable::Integer(None)) => "".to_owned(), // (FieldType::Integer, _) => "###".to_owned(), // (FieldType::InterimUser, Encodable::Text(value)) => todo!(), // (FieldType::InterimUser, _) => "###".to_owned(), // (FieldType::Text, Encodable::Text(Some(value))) => value.clone(), // (FieldType::Text, Encodable::Text(None)) => "".to_owned(), // (FieldType::Text, _) => "###".to_owned(), // (FieldType::Timestamp { format }, Encodable::Timestamptz(value)) => value // .map(|value| value.format(&format).to_string()) // .unwrap_or("".to_owned()), // (FieldType::Timestamp { .. }, _) => "###".to_owned(), // (FieldType::Uuid, Encodable::Uuid(Some(value))) => value.hyphenated().to_string(), // (FieldType::Uuid, Encodable::Uuid(None)) => "".to_owned(), // (FieldType::Uuid, _) => "###".to_owned(), // (FieldType::Unknown, _) => "###".to_owned(), // } // } 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 { "INT" | "INT4" => { Encodable::Integer( as Decode>::decode(value_ref).unwrap()) } "TEXT" | "VARCHAR" => { Encodable::Text( as Decode>::decode(value_ref).unwrap()) } "UUID" => { Encodable::Uuid( as Decode>::decode(value_ref).unwrap()) } _ => return Err(ParseError::UnknownType), }) } } #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "t", content = "c")] pub enum FieldType { Integer, 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() { "integer" => Self::Integer, "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::Integer => Some("integer"), Self::InterimUser | Self::Text => Some("text"), Self::Timestamp { .. } => Some("timestamptz"), Self::Uuid => Some("uuid"), Self::Unknown => None, } } } // -------- 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<'a, E: PgExecutor<'a>>(self, app_db: E) -> 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(app_db) .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 { Integer(Option), Text(Option), Timestamptz(Option>), Uuid(Option), } impl Encodable { pub fn bind_onto<'a>( &'a self, query: sqlx::query::Query<'a, Postgres, ::Arguments<'a>>, ) -> sqlx::query::Query<'a, Postgres, ::Arguments<'a>> { match self { Self::Integer(value) => query.bind(value), Self::Text(value) => query.bind(value), Self::Timestamptz(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 } }