diff --git a/glm/src/field_adder.gleam b/glm/src/field_adder.gleam index 92b0f84..2b20482 100644 --- a/glm/src/field_adder.gleam +++ b/glm/src/field_adder.gleam @@ -82,7 +82,7 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) { label_value: "", name_value: "", name_customized: False, - field_type: "text", + field_type: "Text", ), effect.none(), ) @@ -144,7 +144,7 @@ fn view(model: Model) -> Element(Msg) { ) True -> html.div([attr.class("header")], [ - html.form([attr.method("post"), attr.action("create-column")], [ + html.form([attr.method("post"), attr.action("add-column")], [ label_input(value: model.label_value, on_input: UserUpdatedLabel), html.button( [attr.type_("button"), attr.popovertarget("config-popover")], @@ -200,7 +200,6 @@ fn config_popover(model: Model) -> Element(Msg) { ), html.select( [ - attr.type_("text"), attr.name("field_type"), attr.class("form-section__input"), attr.id("field-type-select"), @@ -208,11 +207,11 @@ fn config_popover(model: Model) -> Element(Msg) { ], [ html.option( - [attr.value("text"), attr.checked(model.field_type == "text")], + [attr.value("Text"), attr.checked(model.field_type == "Text")], "Text", ), html.option( - [attr.value("decimal"), attr.checked(model.field_type == "decimal")], + [attr.value("Decimal"), attr.checked(model.field_type == "Decimal")], "Decimal", ), ], diff --git a/interim-models/migrations/20250528233517_lenses.down.sql b/interim-models/migrations/20250528233517_lenses.down.sql index b9deaa2..ab64ac4 100644 --- a/interim-models/migrations/20250528233517_lenses.down.sql +++ b/interim-models/migrations/20250528233517_lenses.down.sql @@ -1,3 +1,3 @@ -drop table if exists lens_selections; +drop table if exists fields; drop table if exists lenses; drop type if exists lens_display_type; diff --git a/interim-models/migrations/20250528233517_lenses.up.sql b/interim-models/migrations/20250528233517_lenses.up.sql index a3afb7c..1885ed4 100644 --- a/interim-models/migrations/20250528233517_lenses.up.sql +++ b/interim-models/migrations/20250528233517_lenses.up.sql @@ -11,12 +11,11 @@ create table if not exists lenses ( ); create index on lenses (base_id); -create table if not exists lens_selections ( +create table if not exists fields ( id uuid not null primary key, lens_id uuid not null references lenses(id) on delete cascade, - attr_filters jsonb not null default '[]'::jsonb, + name text not null, label text, - field_type jsonb, - visible boolean not null default true, + field_type jsonb not null, width_px int not null default 200 ); diff --git a/interim-models/src/field.rs b/interim-models/src/field.rs index 97207ed..49d1e10 100644 --- a/interim-models/src/field.rs +++ b/interim-models/src/field.rs @@ -1,16 +1,18 @@ 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}; +use sqlx::{ + Decode, PgExecutor, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as, +}; use thiserror::Error; use uuid::Uuid; -const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; +pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S"; -/// A single column which can be passed to a front-end viewer. A Selection may -/// resolve to zero or more Fields. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Field { + pub id: Uuid, pub name: String, pub label: Option, pub field_type: sqlx::types::Json, @@ -18,8 +20,13 @@ pub struct Field { } 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)), @@ -112,8 +119,73 @@ impl FieldType { _ => 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 { @@ -125,6 +197,9 @@ pub enum ParseError { 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 { @@ -147,26 +222,3 @@ impl Encodable { } } } - -// impl<'a> Encode<'a, Postgres> for Encodable { -// fn encode_by_ref( -// &self, -// buf: &mut ::ArgumentBuffer<'a>, -// ) -> Result { -// match self { -// Self::Integer(value) => Encode::<'a, Postgres>::encode(value, buf), -// Self::Text(value) => Encode::<'a, Postgres>::encode(value, buf), -// Self::Timestamptz(value) => Encode::<'a, Postgres>::encode(value, buf), -// Self::Uuid(value) => Encode::<'a, Postgres>::encode(value, buf), -// } -// } -// } -// -// impl<'a> Decode<'a, Postgres> for FieldType { -// fn decode( -// value: ::ValueRef<'a>, -// ) -> Result { -// let value: String = Decode::<'a, Postgres>::decode(value)?; -// Ok(serde_json::from_str(&value)?) -// } -// } diff --git a/interim-models/src/lens.rs b/interim-models/src/lens.rs index b6e0add..6d6aac9 100644 --- a/interim-models/src/lens.rs +++ b/interim-models/src/lens.rs @@ -3,10 +3,7 @@ use serde::Serialize; use sqlx::{PgExecutor, postgres::types::Oid, query_as}; use uuid::Uuid; -use crate::{ - field::FieldType, - selection::{AttrFilter, Selection}, -}; +use crate::field::{Field, FieldType}; #[derive(Clone, Debug, Serialize)] pub struct Lens { @@ -68,21 +65,20 @@ where base_id = $1 and class_oid = $2 .await } - pub async fn fetch_selections<'a, E: PgExecutor<'a>>( + pub async fn fetch_fields<'a, E: PgExecutor<'a>>( &self, app_db: E, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { query_as!( - Selection, + Field, r#" select id, - attr_filters as "attr_filters: sqlx::types::Json>", + name, label, field_type as "field_type: sqlx::types::Json", - visible, width_px -from lens_selections +from fields where lens_id = $1 "#, self.id diff --git a/interim-models/src/lib.rs b/interim-models/src/lib.rs index 4db5b4f..634a4e4 100644 --- a/interim-models/src/lib.rs +++ b/interim-models/src/lib.rs @@ -1,5 +1,5 @@ pub mod field; pub mod lens; -pub mod selection; +// pub mod selection; pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!(); diff --git a/interim-pgtypes/src/lib.rs b/interim-pgtypes/src/lib.rs index 01905e7..8a742e3 100644 --- a/interim-pgtypes/src/lib.rs +++ b/interim-pgtypes/src/lib.rs @@ -4,3 +4,14 @@ pub mod pg_class; pub mod pg_database; pub mod pg_namespace; pub mod pg_role; + +/// Given a raw identifier (such as a table name, column name, etc.), format it +/// so that it may be safely interpolated into a SQL query. +pub fn escape_identifier(identifier: &str) -> String { + // Escaping identifiers for Postgres is fairly easy, provided that the input is + // already known to contain no invalid multi-byte sequences. Backslashes may + // remain as-is, and embedded double quotes are escaped simply by doubling + // them (`"` becomes `""`). Refer to the PQescapeInternal() function in + // libpq (fe-exec.c) and Diesel's PgQueryBuilder::push_identifier(). + format!("\"{}\"", identifier.replace('"', "\"\"")) +} diff --git a/interim-pgtypes/src/pg_class.rs b/interim-pgtypes/src/pg_class.rs index 18dcbf2..621868c 100644 --- a/interim-pgtypes/src/pg_class.rs +++ b/interim-pgtypes/src/pg_class.rs @@ -1,6 +1,6 @@ use sqlx::{PgExecutor, postgres::types::Oid, query_as}; -use crate::{pg_acl::PgAclItem, pg_namespace::PgNamespace}; +use crate::{escape_identifier, pg_acl::PgAclItem, pg_namespace::PgNamespace}; pub struct PgClass { /// Row identifier @@ -9,6 +9,8 @@ pub struct PgClass { pub relname: String, /// The OID of the namespace that contains this relation pub relnamespace: Oid, + /// SYNTHESIZED: name of this relation's namespace as text + pub regnamespace: String, /// The OID of the data type that corresponds to this table's row type, if any; zero for indexes, sequences, and toast tables, which have no pg_type entry pub reltype: Oid, /// For typed tables, the OID of the underlying composite type; zero for all other relations @@ -50,6 +52,7 @@ select oid, relname, relnamespace, + relnamespace::regnamespace::text as "regnamespace!", reltype, reloftype, relowner, @@ -89,6 +92,7 @@ select oid, relname, relnamespace, + relnamespace::regnamespace::text as "regnamespace!", reltype, reloftype, relowner, @@ -123,6 +127,15 @@ where // to the namespace that contains it. If not, that's an error. .ok_or(sqlx::Error::RowNotFound) } + + /// Get ecaped identifier, including namespace. + pub fn get_identifier(&self) -> String { + format!( + "{0}.{1}", + escape_identifier(&self.regnamespace), + escape_identifier(&self.relname) + ) + } } pub enum PgRelKind { diff --git a/interim-server/src/app_error.rs b/interim-server/src/app_error.rs index edb78c2..bddf6c6 100644 --- a/interim-server/src/app_error.rs +++ b/interim-server/src/app_error.rs @@ -14,6 +14,17 @@ macro_rules! not_found { }; } +macro_rules! bad_request { + ($message:literal) => { + AppError::BadRequest($message.to_owned()) + }; + + ($message:literal, $($param:expr),+) => { + AppError::BadRequest(format!($message, $($param)+)) + }; +} + +pub(crate) use bad_request; pub(crate) use not_found; /// Custom error type that maps to appropriate HTTP responses. diff --git a/interim-server/src/db_conns.rs b/interim-server/src/db_conns.rs index 7cbfa6e..5ef50ec 100644 --- a/interim-server/src/db_conns.rs +++ b/interim-server/src/db_conns.rs @@ -1,13 +1,5 @@ -use sqlx::{query, PgConnection, Row as _}; - -pub fn escape_identifier(identifier: &str) -> String { - // Escaping identifiers for Postgres is fairly easy, provided that the input is - // already known to contain no invalid multi-byte sequences. Backslashes may - // remain as-is, and embedded double quotes are escaped simply by doubling - // them (`"` becomes `""`). Refer to the PQescapeInternal() function in - // libpq (fe-exec.c) and Diesel's PgQueryBuilder::push_identifier(). - format!("\"{}\"", identifier.replace('"', "\"\"")) -} +use interim_pgtypes::escape_identifier; +use sqlx::{PgConnection, Row as _, query}; pub async fn init_role(rolname: &str, client: &mut PgConnection) -> Result<(), sqlx::Error> { let session_user = query!("select session_user;") diff --git a/interim-server/src/main.rs b/interim-server/src/main.rs index a9ba247..cab09ef 100644 --- a/interim-server/src/main.rs +++ b/interim-server/src/main.rs @@ -19,6 +19,7 @@ mod cli; mod db_conns; mod lenses; mod middleware; +mod navigator; mod rel_invitations; mod router; mod routes; diff --git a/interim-server/src/navigator.rs b/interim-server/src/navigator.rs new file mode 100644 index 0000000..3fc66ba --- /dev/null +++ b/interim-server/src/navigator.rs @@ -0,0 +1,48 @@ +use axum::{ + extract::FromRequestParts, + http::request::Parts, + response::{IntoResponse as _, Redirect, Response}, +}; +use interim_models::lens::Lens; + +use crate::{app_error::AppError, app_state::AppState}; + +/// Helper type for semantically generating URI paths, e.g. for redirects. +#[derive(Clone, Debug)] +pub struct Navigator { + root_path: String, + sub_path: String, +} + +impl Navigator { + pub fn lens_page(&self, lens: &Lens) -> Self { + Self { + sub_path: format!( + "/d/{0}/r/{1}/l/{2}/", + lens.base_id.simple(), + lens.class_oid.0, + lens.id.simple() + ), + ..self.clone() + } + } + + pub fn redirect_to(&self) -> Response { + Redirect::to(&format!("{0}{1}", self.root_path, self.sub_path)).into_response() + } +} + +impl FromRequestParts for Navigator +where + S: Into + Clone + Sync, +{ + type Rejection = AppError; + + async fn from_request_parts(_: &mut Parts, state: &S) -> Result { + let app_state: AppState = state.clone().into(); + Ok(Navigator { + root_path: app_state.settings.root_path.clone(), + sub_path: "/".to_owned(), + }) + } +} diff --git a/interim-server/src/router.rs b/interim-server/src/router.rs index 3c83a4c..bd1d49a 100644 --- a/interim-server/src/router.rs +++ b/interim-server/src/router.rs @@ -69,9 +69,13 @@ pub fn new_router(state: AppState) -> Router<()> { "/d/{base_id}/r/{class_oid}/l/{lens_id}/update-lens", post(routes::lenses::update_lens_page_post), ) + // .route( + // "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection", + // post(routes::lenses::add_selection_page_post), + // ) .route( - "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection", - post(routes::lenses::add_selection_page_post), + "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-column", + post(routes::lenses::add_column_page_post), ) .route( "/d/{base_id}/r/{class_oid}/l/{lens_id}/update-value", diff --git a/interim-server/src/routes/bases.rs b/interim-server/src/routes/bases.rs index b989617..4809498 100644 --- a/interim-server/src/routes/bases.rs +++ b/interim-server/src/routes/bases.rs @@ -5,18 +5,14 @@ use axum::{ response::{Html, IntoResponse as _, Redirect, Response}, }; use axum_extra::extract::Form; +use interim_pgtypes::escape_identifier; use serde::Deserialize; use sqlx::{query, query_scalar}; use uuid::Uuid; use crate::{ - app_error::AppError, - app_state::AppDbConn, - base_pooler::BasePooler, - base_user_perms::sync_perms_for_base, - bases::Base, - db_conns::{escape_identifier, init_role}, - settings::Settings, + app_error::AppError, app_state::AppDbConn, base_pooler::BasePooler, + base_user_perms::sync_perms_for_base, bases::Base, db_conns::init_role, settings::Settings, users::CurrentUser, }; diff --git a/interim-server/src/routes/lenses.rs b/interim-server/src/routes/lenses.rs index 414d30f..5d64894 100644 --- a/interim-server/src/routes/lenses.rs +++ b/interim-server/src/routes/lenses.rs @@ -4,15 +4,15 @@ use askama::Template; use axum::{ Json, extract::{Path, State}, - response::{Html, IntoResponse as _, Redirect, Response}, + response::{Html, IntoResponse, Redirect, Response}, }; use axum_extra::extract::Form; use interim_models::{ - field::{Encodable, Field}, + field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S}, lens::{Lens, LensDisplayType}, - selection::{AttrFilter, Selection}, }; use interim_pgtypes::{ + escape_identifier, pg_attribute::{PgAttribute, fetch_attributes_for_rel, fetch_primary_keys_for_rel}, pg_class::PgClass, }; @@ -25,11 +25,12 @@ use sqlx::{ use uuid::Uuid; use crate::{ - app_error::{AppError, not_found}, + app_error::{AppError, bad_request, not_found}, app_state::AppDbConn, base_pooler::BasePooler, bases::Base, - db_conns::{escape_identifier, init_role}, + db_conns::init_role, + navigator::Navigator, settings::Settings, users::CurrentUser, }; @@ -99,13 +100,19 @@ pub struct AddLensPagePostForm { } pub async fn add_lens_page_post( - State(Settings { root_path, .. }): State, + State(mut base_pooler): State, + navigator: Navigator, AppDbConn(mut app_db): AppDbConn, Path(LensesPagePath { base_id, class_oid }): Path, Form(AddLensPagePostForm { name }): Form, ) -> Result { // FIXME auth // FIXME csrf + + let mut client = base_pooler.acquire_for(base_id).await?; + + let attrs = fetch_attributes_for_rel(Oid(class_oid), &mut *client).await?; + let lens = Lens::insertable_builder() .base_id(base_id) .class_oid(Oid(class_oid)) @@ -114,12 +121,16 @@ pub async fn add_lens_page_post( .build()? .insert(&mut *app_db) .await?; - Ok(Redirect::to(&format!( - "{root_path}/d/{0}/r/{class_oid}/l/{1}", - base_id.simple(), - lens.id.simple() - )) - .into_response()) + + for attr in attrs { + InsertableFieldBuilder::default_from_attr(&attr) + .lens_id(lens.id) + .build()? + .insert(&mut *app_db) + .await?; + } + + Ok(navigator.lens_page(&lens).redirect_to()) } #[derive(Deserialize)] @@ -134,27 +145,24 @@ pub async fn lens_page( State(mut base_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, - Path(LensPagePath { - base_id, - class_oid, - lens_id, - }): Path, + Path(LensPagePath { lens_id, .. }): Path, ) -> Result { // FIXME auth - let base = Base::fetch_by_id(base_id, &mut *app_db) + let lens = Lens::fetch_by_id(lens_id, &mut *app_db) .await? - .ok_or(AppError::NotFound("no base found with that id".to_owned()))?; - let mut client = base_pooler.acquire_for(base_id).await?; + .ok_or(not_found!("lens not found"))?; + let base = Base::fetch_by_id(lens.base_id, &mut *app_db) + .await? + .ok_or(not_found!("no base found with that id"))?; + let mut client = base_pooler.acquire_for(lens.base_id).await?; init_role( &format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()), &mut client, ) .await?; - // FIXME auth - - let class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client) + let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client) .await? .ok_or(AppError::NotFound( "no relation found with that oid".to_owned(), @@ -165,15 +173,9 @@ pub async fn lens_page( .await? .ok_or(not_found!("no lens found with that id"))?; - let attrs = fetch_attributes_for_rel(Oid(class_oid), &mut *client).await?; - - let selections = lens.fetch_selections(&mut *app_db).await?; - let mut fields: Vec = Vec::with_capacity(selections.len()); - for selection in selections.clone() { - fields.append(&mut selection.resolve_fields_from_attrs(&attrs)); - } - - let pkey_attrs = fetch_primary_keys_for_rel(Oid(class_oid), &mut *client).await?; + let attrs = fetch_attributes_for_rel(lens.class_oid, &mut *client).await?; + let fields = lens.fetch_fields(&mut *app_db).await?; + let pkey_attrs = fetch_primary_keys_for_rel(lens.class_oid, &mut *client).await?; const FRONTEND_ROW_LIMIT: i64 = 1000; struct Row { @@ -214,7 +216,6 @@ pub async fn lens_page( fields: Vec, all_columns: Vec, rows: Vec, - selections_json: String, settings: Settings, } Ok(Html( @@ -222,7 +223,6 @@ pub async fn lens_page( all_columns: attrs, fields, rows, - selections_json: serde_json::to_string(&selections)?, settings, } .render()?, @@ -231,42 +231,129 @@ pub async fn lens_page( } #[derive(Debug, Deserialize)] -pub struct AddSelectionPageForm { - column: String, +pub struct AddColumnPageForm { + name: String, + label: String, + field_type: String, + timestamp_format: Option, } -pub async fn add_selection_page_post( +fn try_field_type_from_form(form: &AddColumnPageForm) -> Result { + let serialized = match form.field_type.as_str() { + "Timestamp" => { + json!({ + "t": form.field_type, + "c": { + "format": form.timestamp_format.clone().unwrap_or(RFC_3339_S.to_owned()), + }, + }) + } + _ => json!({"t": form.field_type}), + }; + serde_json::from_value(serialized).or(Err(bad_request!("unable to parse field type"))) +} + +pub async fn add_column_page_post( State(settings): State, + State(mut base_pooler): State, + navigator: Navigator, AppDbConn(mut app_db): AppDbConn, CurrentUser(current_user): CurrentUser, - Path(LensPagePath { - base_id, - class_oid, - lens_id, - }): Path, - Form(form): Form, + Path(LensPagePath { lens_id, .. }): Path, + Form(form): Form, ) -> Result { - dbg!(&form); // FIXME auth // FIXME csrf + // FIXME validate column name length is less than 64 let lens = Lens::fetch_by_id(lens_id, &mut *app_db) .await? .ok_or(not_found!("lens not found"))?; - Selection::insertable_builder() + let base = Base::fetch_by_id(lens.base_id, &mut *app_db) + .await? + .ok_or(not_found!("no base found with that id"))?; + + let mut client = base_pooler.acquire_for(base.id).await?; + init_role( + &format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()), + &mut client, + ) + .await?; + + let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client) + .await? + .ok_or(not_found!("pg class not found"))?; + + let field_type = try_field_type_from_form(&form)?; + let data_type_fragment = field_type.attr_data_type_fragment().ok_or(bad_request!( + "cannot create column with type specified as Unknown" + ))?; + + query(&format!( + r#" +alter table {0} +add column if not exists {1} {2} +"#, + class.get_identifier(), + escape_identifier(&form.name), + data_type_fragment + )) + .execute(&mut *client) + .await?; + + Field::insertable_builder() .lens_id(lens.id) - .attr_filters(vec![AttrFilter::NameEq(form.column)]) + .name(form.name) + .label(if form.label.is_empty() { + None + } else { + Some(form.label) + }) + .field_type(field_type) .build()? .insert(&mut *app_db) .await?; - Ok(Redirect::to(&format!( - "{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/", - settings.root_path - )) - .into_response()) + Ok(navigator.lens_page(&lens).redirect_to()) } +// #[derive(Debug, Deserialize)] +// pub struct AddSelectionPageForm { +// column: String, +// } +// +// pub async fn add_selection_page_post( +// State(settings): State, +// AppDbConn(mut app_db): AppDbConn, +// CurrentUser(current_user): CurrentUser, +// Path(LensPagePath { +// base_id, +// class_oid, +// lens_id, +// }): Path, +// Form(form): Form, +// ) -> Result { +// dbg!(&form); +// // FIXME auth +// // FIXME csrf +// +// let lens = Lens::fetch_by_id(lens_id, &mut *app_db) +// .await? +// .ok_or(not_found!("lens not found"))?; +// Selection::insertable_builder() +// .lens_id(lens.id) +// .attr_filters(vec![AttrFilter::NameEq(form.column)]) +// .build()? +// .insert(&mut *app_db) +// .await?; +// +// Ok(Redirect::to(&format!( +// "{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/", +// settings.root_path +// )) +// .into_response()) +// } + pub async fn update_lens_page_post( State(settings): State, AppDbConn(mut app_db): AppDbConn, diff --git a/interim-server/templates/lens.html b/interim-server/templates/lens.html index eb7a001..b783231 100644 --- a/interim-server/templates/lens.html +++ b/interim-server/templates/lens.html @@ -46,5 +46,4 @@ {% endfor %} - {% endblock %}