use std::collections::HashMap; use axum::{ debug_handler, extract::{Path, State}, response::Response, }; // [`axum_extra`]'s form extractor is required to support repeated keys: // 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_acl::PgPrivilegeType, pg_class::PgClass}; use phono_models::{ accessors::{Accessor as _, Actor, portal::PortalAccessor}, datum::Datum, }; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; use crate::{ app::AppDbConn, errors::{AppError, forbidden}, navigator::{Navigator, NavigatorPage as _}, user::CurrentUser, workspace_pooler::{RoleAssignment, WorkspacePooler}, }; #[derive(Debug, Deserialize)] pub(super) struct PathParams { portal_id: Uuid, rel_oid: u32, workspace_id: Uuid, } /// HTTP POST handler for inserting one or more rows into a table. This handler /// takes a form where the keys are column names, with keys optionally repeated /// to insert multiple rows at once. If any key is repeated, the others should /// be repeated the same number of times. Form values are expected to be JSON- /// serialized representations of the `[Datum]` type. #[debug_handler(state = crate::app::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 CSRF 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::Insert]) .using_rel(&rel) .using_app_db(&mut app_db) .using_workspace_client(&mut workspace_client) .fetch_one() .await?; let col_names: Vec = form.keys().cloned().collect(); // Prevent users from modifying Phonograph metadata columns. if form.iter().any(|(col, values)| { col.starts_with('_') && values.iter().any(|value| { serde_json::from_str::(value) .ok() .map(|value| !value.is_none()) .unwrap_or(false) }) }) { return Err(forbidden!("access denied to update system metadata column")); } let col_list_sql = col_names .iter() .map(|value| escape_identifier(value)) .collect::>() .join(", "); let n_rows = form.values().map(|value| value.len()).max().unwrap_or(0); if n_rows > 0 { let mut param_index = 1; let mut params: Vec = vec![]; let mut row_list: Vec = vec![]; for i in 0..n_rows { let mut param_slots: Vec = vec![]; for col in col_names.iter() { let maybe_value: Option = form .get(col) .and_then(|col_values| col_values.get(i)) .map(|value_raw| serde_json::from_str(value_raw)) .transpose()?; if let Some(value) = maybe_value.filter(|value| !value.is_none()) { params.push(value); param_slots.push(format!("${param_index}")); param_index += 1; } else { param_slots.push("default".to_owned()); } } row_list.push(format!("({0})", param_slots.join(", "))); } let row_list_sql = row_list.join(",\n"); let query_sql = &format!( "insert into {ident} ({col_list_sql}) values {row_list_sql}", ident = rel.get_identifier(), ); let mut q = query(query_sql); for param in params { q = param.bind_onto(q); } q.execute(workspace_client.get_conn()).await?; } Ok(navigator .portal_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) .portal_id(portal_id) .build()? .redirect_to()) }