use crate::domain::models::Column; use common_telemetry::AppError; use sqlx::PgPool; use uuid::Uuid; pub async fn create_column( pool: &PgPool, tenant_id: Uuid, name: String, slug: String, description: Option, parent_id: Option, sort_order: i32, ) -> Result { let column = sqlx::query_as::<_, Column>( r#" INSERT INTO cms_columns (tenant_id, name, slug, description, parent_id, sort_order) VALUES ($1, $2, $3, $4, $5, $6) RETURNING tenant_id, id, name, slug, description, parent_id, sort_order, created_at, updated_at "#, ) .bind(tenant_id) .bind(name) .bind(slug) .bind(description) .bind(parent_id) .bind(sort_order) .fetch_one(pool) .await?; Ok(column) } pub async fn get_column(pool: &PgPool, tenant_id: Uuid, id: Uuid) -> Result { let column = sqlx::query_as::<_, Column>( r#" SELECT tenant_id, id, name, slug, description, parent_id, sort_order, created_at, updated_at FROM cms_columns WHERE tenant_id = $1 AND id = $2 "#, ) .bind(tenant_id) .bind(id) .fetch_one(pool) .await?; Ok(column) } pub async fn update_column( pool: &PgPool, tenant_id: Uuid, id: Uuid, name: Option, slug: Option, description: Option>, parent_id: Option>, sort_order: Option, ) -> Result { let column = sqlx::query_as::<_, Column>( r#" UPDATE cms_columns SET name = COALESCE($3, name), slug = COALESCE($4, slug), description = COALESCE($5, description), parent_id = COALESCE($6, parent_id), sort_order = COALESCE($7, sort_order), updated_at = now() WHERE tenant_id = $1 AND id = $2 RETURNING tenant_id, id, name, slug, description, parent_id, sort_order, created_at, updated_at "#, ) .bind(tenant_id) .bind(id) .bind(name) .bind(slug) .bind(description) .bind(parent_id) .bind(sort_order) .fetch_one(pool) .await?; Ok(column) } pub async fn delete_column(pool: &PgPool, tenant_id: Uuid, id: Uuid) -> Result<(), AppError> { let res = sqlx::query( r#" DELETE FROM cms_columns WHERE tenant_id = $1 AND id = $2 "#, ) .bind(tenant_id) .bind(id) .execute(pool) .await?; if res.rows_affected() == 0 { return Err(AppError::NotFound("column:not_found".into())); } Ok(()) } #[derive(Debug, Clone)] pub struct ListColumnsQuery { pub page: u32, pub page_size: u32, pub search: Option, pub parent_id: Option, } #[derive(Debug, Clone, serde::Serialize, utoipa::ToSchema)] pub struct Paged { pub items: Vec, pub page: u32, pub page_size: u32, pub total: i64, } pub async fn list_columns( pool: &PgPool, tenant_id: Uuid, q: ListColumnsQuery, ) -> Result, AppError> { let page = q.page.max(1); let page_size = q.page_size.clamp(1, 200); let offset = ((page - 1) * page_size) as i64; let limit = page_size as i64; let like = q.search.map(|s| format!("%{}%", s)); let total: i64 = sqlx::query_scalar( r#" SELECT COUNT(*) FROM cms_columns WHERE tenant_id = $1 AND ($2::uuid IS NULL OR parent_id = $2) AND ($3::text IS NULL OR name ILIKE $3 OR slug ILIKE $3) "#, ) .bind(tenant_id) .bind(q.parent_id) .bind(like.as_deref()) .fetch_one(pool) .await?; let items = sqlx::query_as::<_, Column>( r#" SELECT tenant_id, id, name, slug, description, parent_id, sort_order, created_at, updated_at FROM cms_columns WHERE tenant_id = $1 AND ($2::uuid IS NULL OR parent_id = $2) AND ($3::text IS NULL OR name ILIKE $3 OR slug ILIKE $3) ORDER BY sort_order ASC, updated_at DESC OFFSET $4 LIMIT $5 "#, ) .bind(tenant_id) .bind(q.parent_id) .bind(like.as_deref()) .bind(offset) .bind(limit) .fetch_all(pool) .await?; Ok(Paged { items, page, page_size, total, }) }