use std::sync::LazyLock; use axum::{ debug_handler, extract::{Path, State}, response::Response, }; use phono_backends::{escape_identifier, pg_class::PgClass}; use regex::Regex; use serde::Deserialize; use sqlx::{postgres::types::Oid, query}; use uuid::Uuid; use validator::Validate; use crate::{ app::App, errors::AppError, extractors::ValidatedForm, navigator::{Navigator, NavigatorPage as _}, user::CurrentUser, workspace_pooler::WorkspacePooler, }; static RE_REL_NAME: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z][a-z0-9_]*$").unwrap()); #[derive(Debug, Deserialize)] pub(super) struct PathParams { rel_oid: u32, workspace_id: Uuid, } #[derive(Debug, Deserialize, Validate)] pub(super) struct FormBody { #[validate(regex(path = *RE_REL_NAME))] name: String, } /// HTTP POST handler for updating a relation's name. /// /// Currently, names must begin with a letter and may only contain lowercase /// alphanumeric characters and underscores. #[debug_handler(state = App)] pub(super) async fn post( State(mut pooler): State, CurrentUser(user): CurrentUser, navigator: Navigator, Path(PathParams { rel_oid, workspace_id, }): Path, ValidatedForm(FormBody { name }): ValidatedForm, ) -> Result { // FIXME: Check workspace authorization. let mut workspace_client = pooler .acquire_for( workspace_id, crate::workspace_pooler::RoleAssignment::User(user.id), ) .await?; let rel = PgClass::with_oid(Oid(rel_oid)) .fetch_one(&mut workspace_client) .await?; // FIXME ensure that user has ownership of the table. // TODO: move this to a function in `phono_backends`. query(&format!( "alter table {ident} rename to {name_esc}", ident = rel.get_identifier(), // `_esc` suffixes to make sure that the macro won't fall back to // similarly named variable(s) in scope if anything inadvertently // changes. name_esc = escape_identifier(&name) )) .execute(workspace_client.get_conn()) .await?; Ok(navigator .rel_page() .workspace_id(workspace_id) .rel_oid(Oid(rel_oid)) .suffix("settings/") .build()? .redirect_to()) }