//! Persistence for companies — a baseline topology staffed with real workspace //! teams (the 3rd rung of the deploy ladder). `companies` holds the topology //! graph; `company_teams` is the durable node→team binding. Mirrors `teams` //! one tier up: where a team binds nodes to claws, a company binds nodes to //! whole teams, and the recursive executor runs each bound team's sub-topology. use cm_domain::WorkspaceId; use serde_json::Value; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; /// A company row summary (list view). pub struct CompanySummary { pub id: Uuid, pub name: String, pub kind: String, pub status: String, pub created_at: OffsetDateTime, } /// A full company (graph + metadata). pub struct Company { pub id: Uuid, pub name: String, pub kind: String, pub graph: Value, pub status: String, pub created_at: OffsetDateTime, } /// A node→team binding within a company. pub struct CompanyTeam { pub node_id: String, pub team_id: Uuid, pub role: String, } /// Insert a company (the topology graph). Members are added separately. pub async fn insert_company( pool: &PgPool, id: Uuid, workspace_id: WorkspaceId, name: &str, kind: &str, graph: &Value, ) -> Result<(), DbError> { sqlx::query!( "INSERT INTO companies (id, workspace_id, name, kind, graph) VALUES ($1, $2, $3, $4, $5)", id, workspace_id.as_uuid(), name, kind, graph, ) .execute(pool) .await?; Ok(()) } /// Rebuild a company's topology (kind + graph) in place; node→team bindings /// (keyed off stable node ids `n0..`) are left untouched. Non-macro query. pub async fn set_topology( pool: &PgPool, id: Uuid, workspace_id: WorkspaceId, kind: &str, graph: &Value, ) -> Result<(), DbError> { let res = sqlx::query( "UPDATE companies SET kind = $3, graph = $4 WHERE id = $1 AND workspace_id = $2", ) .bind(id) .bind(workspace_id.as_uuid()) .bind(kind) .bind(graph) .execute(pool) .await?; if res.rows_affected() == 0 { return Err(DbError::NotFound); } Ok(()) } /// Delete a company and its node→team bindings (teams themselves remain). pub async fn delete_company( pool: &PgPool, id: Uuid, workspace_id: WorkspaceId, ) -> Result<(), DbError> { sqlx::query("DELETE FROM company_teams WHERE company_id = $1") .bind(id) .execute(pool) .await?; let res = sqlx::query("DELETE FROM companies WHERE id = $1 AND workspace_id = $2") .bind(id) .bind(workspace_id.as_uuid()) .execute(pool) .await?; if res.rows_affected() == 0 { return Err(DbError::NotFound); } Ok(()) } /// Bind a team to a topology node within a company. pub async fn add_team( pool: &PgPool, company_id: Uuid, node_id: &str, team_id: Uuid, role: &str, ) -> Result<(), DbError> { sqlx::query!( "INSERT INTO company_teams (company_id, node_id, team_id, role) VALUES ($1, $2, $3, $4)", company_id, node_id, team_id, role, ) .execute(pool) .await?; Ok(()) } /// The most recent companies for a workspace, newest first. pub async fn list_for_workspace( pool: &PgPool, workspace_id: WorkspaceId, limit: i64, ) -> Result, DbError> { let rows = sqlx::query!( "SELECT id, name, kind, status, created_at FROM companies WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2", workspace_id.as_uuid(), limit, ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| CompanySummary { id: r.id, name: r.name, kind: r.kind, status: r.status, created_at: r.created_at, }) .collect()) } /// A single company, workspace-scoped. pub async fn get(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result { let row = sqlx::query!( "SELECT id, name, kind, graph, status, created_at FROM companies WHERE id = $1 AND workspace_id = $2", id, workspace_id.as_uuid(), ) .fetch_optional(pool) .await? .ok_or(DbError::NotFound)?; Ok(Company { id: row.id, name: row.name, kind: row.kind, graph: row.graph, status: row.status, created_at: row.created_at, }) } /// The node→team bindings for a company. pub async fn teams_for_company( pool: &PgPool, company_id: Uuid, ) -> Result, DbError> { let rows = sqlx::query!( "SELECT node_id, team_id, role FROM company_teams WHERE company_id = $1 ORDER BY node_id", company_id, ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| CompanyTeam { node_id: r.node_id, team_id: r.team_id, role: r.role, }) .collect()) }