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 phono_backends::{escape_identifier, pg_class::PgClass}; use phono_models::{ accessors::{Accessor as _, Actor, portal::PortalAccessor}, field::Field, presentation::Presentation, }; 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, roles::get_writer_role, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[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 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: csrf let mut workspace_client = pooler .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; let portal = PortalAccessor::new() .id(portal_id) .as_actor(Actor::User(user.id)) .verify_workspace_id(workspace_id) .verify_rel_oid(Oid(rel_oid)) .verify_rel_ownership() .using_rel(&rel) .using_app_db(&mut app_db) .using_workspace_client(&mut workspace_client) .fetch_one() .await?; let presentation = Presentation::try_from(form.presentation_form)?; query(&format!( "alter table {ident} add column if not exists {col} {typ}", ident = rel.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 = rel.get_identifier(), writer_role = escape_identifier(&get_writer_role(&rel)?), )) .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()) }