use axum::{ debug_handler, extract::{Path, State}, response::Response, }; // [`axum_extra`]'s form extractor is preferred: // https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform use axum_extra::extract::Form; use interim_models::{ field::Field, portal::Portal, presentation::Presentation, workspace::Workspace, }; use interim_pgtypes::{escape_identifier, pg_class::PgClass}; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; use crate::{ app::{App, AppDbConn}, errors::AppError, navigator::{Navigator, NavigatorPage}, presentation_form::PresentationForm, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, workspace_utils::{get_reader_role, get_writer_role}, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, workspace_id: Uuid, } // FIXME: validate name, prevent leading underscore #[derive(Debug, Deserialize)] pub(super) struct FormBody { name: String, table_label: String, #[serde(flatten)] presentation_form: PresentationForm, } /// HTTP POST handler for adding a [`Field`] to a [`Portal`]. If the field name /// does not match a column in the backing database, a new column is created /// with a compatible type. /// /// This handler expects 3 path parameters with the structure described by /// [`PathParams`]. #[debug_handler(state = App)] pub(super) async fn post( State(mut workspace_pooler): State, AppDbConn(mut app_db): AppDbConn, CurrentUser(user): CurrentUser, navigator: Navigator, Path(PathParams { portal_id, rel_oid, workspace_id, }): Path, Form(form): Form, ) -> Result { // FIXME: Check workspace authorization. // FIXME ensure workspace corresponds to rel/portal, and that user has // permission to access/alter both as needed. let portal = Portal::with_id(portal_id).fetch_one(&mut app_db).await?; let workspace = Workspace::with_id(portal.workspace_id) .fetch_one(&mut app_db) .await?; let mut workspace_client = workspace_pooler .acquire_for(workspace.id, RoleAssignment::User(user.id)) .await?; let class = PgClass::with_oid(portal.class_oid) .fetch_one(&mut workspace_client) .await?; let presentation = Presentation::try_from(form.presentation_form)?; query(&format!( "alter table {ident} add column if not exists {col} {typ}", ident = class.get_identifier(), col = escape_identifier(&form.name), typ = presentation.attr_data_type_fragment(), )) .execute(workspace_client.get_conn()) .await?; query(&format!( "grant insert ({col}), update ({col}) on table {ident} to {writer_role}", col = escape_identifier(&form.name), ident = class.get_identifier(), writer_role = escape_identifier(&get_writer_role(class.clone())?), )) .execute(workspace_client.get_conn()) .await?; Field::insert() .portal_id(portal.id) .name(form.name) .table_label(if form.table_label.is_empty() { None } else { Some(form.table_label) }) .presentation(presentation) .build()? .insert(&mut app_db) .await?; Ok(navigator .portal_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) .portal_id(portal_id) .build()? .redirect_to()) }