use cm_domain::{AgentId, UserId, WorkspaceId}; use sqlx::PgPool; use uuid::Uuid; use crate::DbError; /// Who performed an audited action (ยง15: every decision is attributable). #[derive(Debug, Clone, Copy)] pub enum Actor { User(UserId), Agent(AgentId), System, } impl Actor { fn kind(&self) -> &'static str { match self { Actor::User(_) => "user", Actor::Agent(_) => "agent", Actor::System => "system", } } fn id(&self) -> Option { match self { Actor::User(id) => Some(id.as_uuid()), Actor::Agent(id) => Some(id.as_uuid()), Actor::System => None, } } } /// Appends an audit entry and returns its sequence id. The table rejects /// UPDATE/DELETE at the database level (see migration 0001). pub async fn append( pool: &PgPool, workspace_id: WorkspaceId, actor: Actor, event_type: &str, subject_type: &str, subject_id: &str, detail: serde_json::Value, ) -> Result { let row = sqlx::query!( "INSERT INTO audit_log (workspace_id, actor_kind, actor_id, event_type, subject_type, subject_id, detail) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id", workspace_id.as_uuid(), actor.kind(), actor.id(), event_type, subject_type, subject_id, detail, ) .fetch_one(pool) .await?; Ok(row.id) }