use crate::config::{load_from_pool, resolve};
use crate::db::pool::Pool;
use crate::db::Dialect;
use crate::error::AppError;
use crate::extractors::tenant::TenantId;
use crate::migration::apply_migrations;
use crate::state::AppState;
use crate::store::{
qualified_sys_table, replace_config_rows, sys_table_for_kind, DEFAULT_PACKAGE_ID,
};
use axum::extract::State;
use axum::Json;
use serde_json::Value;
fn require_tenant(state: &AppState, tenant_id_opt: &Option<String>) -> Result<(), AppError> {
let tenant_id = tenant_id_opt
.as_deref()
.filter(|s| !s.is_empty())
.ok_or_else(|| AppError::BadRequest("X-Tenant-ID header is required".into()))?;
state
.tenant_registry
.get(tenant_id)
.ok_or_else(|| AppError::NotFound(format!("tenant not found: {}", tenant_id)))?;
Ok(())
}
pub(crate) async fn replace_config(
pool: &Pool,
kind: &str,
body: Vec<Value>,
run_migrations: bool,
package_id: &str,
dialect: Option<&dyn Dialect>,
) -> Result<(Vec<Value>, u64), AppError> {
let table = sys_table_for_kind(kind)
.ok_or_else(|| AppError::BadRequest(format!("unknown config kind: {}", kind)))?;
let mut tx = pool.begin().await?;
let (count, _version) = replace_config_rows(&mut tx, table, package_id, &body).await?;
tx.commit().await?;
if run_migrations && count > 0 {
let config = load_from_pool(pool, package_id)
.await
.map_err(AppError::Config)?;
apply_migrations(pool, &config, None, None, dialect.unwrap()).await?;
}
Ok((body, count))
}
pub(crate) async fn reload_model(state: &AppState) -> Result<(), AppError> {
let config = load_from_pool(&state.pool, DEFAULT_PACKAGE_ID)
.await
.map_err(AppError::Config)?;
let new_model = resolve(&config)
.map_err(AppError::Config)?
.with_package_id(DEFAULT_PACKAGE_ID);
let mut guard = state
.model
.write()
.map_err(|_| AppError::BadRequest("state lock".into()))?;
*guard = new_model;
Ok(())
}
pub(crate) async fn get_config(
pool: &Pool,
kind: &str,
package_id: &str,
) -> Result<Vec<Value>, AppError> {
let table = sys_table_for_kind(kind)
.ok_or_else(|| AppError::BadRequest(format!("unknown config kind: {}", kind)))?;
let q_table = qualified_sys_table(table);
let sql = format!(
"SELECT payload FROM {} WHERE package_id = $1 ORDER BY id",
q_table
);
tracing::debug!(sql = %sql, package_id = %package_id, "query");
let rows = sqlx::query_scalar::<_, Value>(&sql)
.bind(package_id)
.fetch_all(pool)
.await?;
Ok(rows)
}
macro_rules! config_handler {
($method:ident, $kind:expr) => {
pub async fn $method(
TenantId(tenant_id_opt): TenantId,
State(state): State<AppState>,
Json(body): Json<Vec<Value>>,
) -> Result<impl axum::response::IntoResponse, AppError> {
require_tenant(&state, &tenant_id_opt)?;
if $kind == "columns" && state.storage.is_none() {
use crate::config::types::ColumnConfig;
use crate::db::{parse_canonical, CanonicalType};
let asset_cols: Vec<String> = body
.iter()
.filter_map(|v| serde_json::from_value::<ColumnConfig>(v.clone()).ok())
.filter(|c| {
matches!(
parse_canonical(&c.type_),
CanonicalType::Asset | CanonicalType::AssetArray
)
})
.map(|c| c.name)
.collect();
if !asset_cols.is_empty() {
return Err(AppError::BadRequest(format!(
"Column(s) [{}] use asset type but no storage provider is configured. \
Set STORAGE_PROVIDER (s3 | azure | gcs | rustfs) before defining asset columns.",
asset_cols.join(", ")
)));
}
}
let (out, num_written) = replace_config(
&state.pool,
$kind,
body,
true,
DEFAULT_PACKAGE_ID,
Some(state.dialect.as_ref()),
)
.await?;
if num_written > 0 {
reload_model(&state).await?;
}
let count = out.len() as u64;
Ok((
axum::http::StatusCode::OK,
Json(crate::response::SuccessMany {
data: out,
meta: crate::response::MetaCount { count },
}),
))
}
};
}
macro_rules! get_config_handler {
($method:ident, $kind:expr) => {
pub async fn $method(
TenantId(tenant_id_opt): TenantId,
State(state): State<AppState>,
) -> Result<impl axum::response::IntoResponse, AppError> {
require_tenant(&state, &tenant_id_opt)?;
let out = get_config(&state.pool, $kind, DEFAULT_PACKAGE_ID).await?;
Ok((
axum::http::StatusCode::OK,
Json(crate::response::SuccessMany {
data: out.clone(),
meta: crate::response::MetaCount {
count: out.len() as u64,
},
}),
))
}
};
}
config_handler!(post_schemas, "schemas");
config_handler!(post_enums, "enums");
config_handler!(post_tables, "tables");
config_handler!(post_columns, "columns");
config_handler!(post_indexes, "indexes");
config_handler!(post_relationships, "relationships");
config_handler!(post_api_entities, "api_entities");
config_handler!(post_kv_stores, "kv_stores");
get_config_handler!(get_schemas, "schemas");
get_config_handler!(get_enums, "enums");
get_config_handler!(get_tables, "tables");
get_config_handler!(get_columns, "columns");
get_config_handler!(get_indexes, "indexes");
get_config_handler!(get_relationships, "relationships");
get_config_handler!(get_api_entities, "api_entities");
get_config_handler!(get_kv_stores, "kv_stores");