Survey + fixes so the pipeline passes at the Docker level (no k8s).
- Remove k8s: drop the `sandbox-k8s` job (kind/Calico/--features k8s-tests) and the
"Helm chart lints" gate step. release.yml was already k8s-clean.
- Rust job:
- `cargo fmt --all` — fix pre-existing formatting drift (fmt --check was failing).
- clippy -D warnings: fix 3 lib warnings (cm-brain sort_by_key→Reverse, cm-api
fleet.rs doc list indentation, node_rules map_or→is_none_or).
- Regenerate the .sqlx offline cache (was missing the cm-runtime run_loop test
query → offline compile failed). DB-backed tests use testcontainers at runtime.
- Set SQLX_OFFLINE=true on the rust + e2e jobs so query! macros compile against
the committed cache deterministically (no DB needed at compile time).
- Frontend job:
- Fix the 1 ESLint error (useAgentTelemetry: no setState-synchronously-in-effect;
tag the slice with agentId + derive null on mismatch).
- Fix 2 stale panel-params tests (`terminal` is a valid app id now; assert the
current APP_IDS + use a genuinely-unknown id for the reject case).
Verified locally: fmt clean, clippy --all-targets -D warnings clean (offline),
frontend lint 0 errors, tsc clean, 86/86 frontend tests pass, build OK.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
197 lines
5.0 KiB
Rust
197 lines
5.0 KiB
Rust
//! 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<Vec<CompanySummary>, 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<Company, DbError> {
|
|
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<Vec<CompanyTeam>, 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())
|
|
}
|