use std::collections::HashMap; use axum::{ Json, debug_handler, extract::{Path, State}, response::{IntoResponse as _, Response}, }; use phono_backends::{ escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass, }; use phono_models::{ accessors::{Accessor, Actor, portal::PortalAccessor}, datum::Datum, }; 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 { // 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 mut workspace_client = workspace_pooler .acquire_for(workspace_id, RoleAssignment::User(user.id)) .await?; let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; // For authorization only. 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_permissions([PgPrivilegeType::Update]) .using_rel(&rel) .using_app_db(&mut app_db) .using_workspace_client(&mut workspace_client) .fetch_one() .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()) }