use std::collections::HashMap; use axum::{ Json, debug_handler, extract::{Path, State}, response::{IntoResponse as _, Response}, }; use interim_models::{datum::Datum, portal::Portal, workspace::Workspace}; use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass}; use serde::Deserialize; use serde_json::json; use sqlx::{Acquire as _, postgres::types::Oid, query}; use uuid::Uuid; use crate::{ app::{App, AppDbConn}, errors::{AppError, forbidden}, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, workspace_id: Uuid, } #[derive(Debug, Deserialize)] pub(super) struct FormBody { cells: Vec, } #[derive(Debug, Deserialize)] pub(super) struct CellInfo { column: String, pkey: HashMap, value: Datum, } /// HTTP POST handler for updating cell values in a backing Postgres table. /// /// 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, Path(PathParams { portal_id, rel_oid, workspace_id, }): Path, Json(form): Json, ) -> Result { // FIXME: Check workspace authorization. // FIXME ensure workspace corresponds to rel/portal, and that user has // permission to access/alter both as needed. // Prevent users from modifying Phonograph metadata columns. if form.cells.iter().any(|cell| cell.column.starts_with('_')) { return Err(forbidden!("access denied to update system metadata column")); } 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 rel = PgClass::with_oid(portal.class_oid) .fetch_one(&mut workspace_client) .await?; let pkey_attrs = PgAttribute::pkeys_for_rel(Oid(rel_oid)) .fetch_all(&mut workspace_client) .await?; let conn = workspace_client.get_conn(); let mut txn = conn.begin().await?; for cell in form.cells { // TODO: simplify pkey management cell.pkey .get(&pkey_attrs.first().unwrap().attname) .unwrap() .clone() .bind_onto(cell.value.bind_onto(query(&format!( "update {ident} set {value_col} = $1 where {pkey_col} = $2", ident = rel.get_identifier(), value_col = escape_identifier(&cell.column), pkey_col = escape_identifier(&pkey_attrs.first().unwrap().attname), )))) .execute(&mut *txn) .await?; } txn.commit().await?; Ok(Json(json!({ "ok": true })).into_response()) }