use cm_domain::{AgentId, FileDrive, FileNode, WorkspaceId}; use sqlx::PgPool; use uuid::Uuid; use crate::DbError; fn row_node( id: Uuid, workspace_id: Uuid, agent_id: Option, drive: String, path: String, size: i64, blob_ref: Option, ) -> FileNode { FileNode { id, workspace_id: WorkspaceId::from(workspace_id), agent_id: agent_id.map(AgentId::from), drive: drive.parse().expect("drive CHECK constraint"), path, size, blob_ref: blob_ref.unwrap_or_default(), } } /// Creates or replaces a file entry (same path on the same drive updates /// size and blob reference, like a filesystem overwrite). pub async fn upsert(pool: &PgPool, node: &FileNode) -> Result<(), DbError> { sqlx::query!( r#"INSERT INTO file_nodes (id, workspace_id, agent_id, drive, path, kind, size, blob_ref, owner_kind, owner_id) VALUES ($1, $2, $3, $4, $5, 'file', $6, $7, 'agent', $8) ON CONFLICT (workspace_id, drive, COALESCE(agent_id, '00000000-0000-0000-0000-000000000000'::uuid), path) DO UPDATE SET size = $6, blob_ref = $7"#, node.id, node.workspace_id.as_uuid(), node.agent_id.map(|a| a.as_uuid()), node.drive.as_str(), node.path, node.size, node.blob_ref, node.agent_id .map(|a| a.as_uuid()) .unwrap_or(node.workspace_id.as_uuid()), ) .execute(pool) .await?; Ok(()) } /// Lists a drive's entries. Agent-scoped drives filter by the agent; the /// shared drive is workspace-wide (ยง7.4). pub async fn list( pool: &PgPool, workspace_id: WorkspaceId, drive: FileDrive, agent_id: AgentId, ) -> Result, DbError> { let scope = drive.is_agent_scoped().then_some(agent_id.as_uuid()); let rows = sqlx::query!( r#"SELECT id, workspace_id, agent_id, drive, path, size, blob_ref FROM file_nodes WHERE workspace_id = $1 AND drive = $2 AND ($3::uuid IS NULL OR agent_id = $3) ORDER BY path"#, workspace_id.as_uuid(), drive.as_str(), scope, ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| { row_node( r.id, r.workspace_id, r.agent_id, r.drive, r.path, r.size, r.blob_ref, ) }) .collect()) } pub async fn get( pool: &PgPool, workspace_id: WorkspaceId, drive: FileDrive, agent_id: AgentId, path: &str, ) -> Result { let scope = drive.is_agent_scoped().then_some(agent_id.as_uuid()); let row = sqlx::query!( r#"SELECT id, workspace_id, agent_id, drive, path, size, blob_ref FROM file_nodes WHERE workspace_id = $1 AND drive = $2 AND path = $4 AND ($3::uuid IS NULL OR agent_id = $3)"#, workspace_id.as_uuid(), drive.as_str(), scope, path, ) .fetch_optional(pool) .await? .ok_or(DbError::NotFound)?; Ok(row_node( row.id, row.workspace_id, row.agent_id, row.drive, row.path, row.size, row.blob_ref, )) } pub async fn delete(pool: &PgPool, id: Uuid) -> Result<(), DbError> { let result = sqlx::query!("DELETE FROM file_nodes WHERE id = $1", id) .execute(pool) .await?; if result.rows_affected() == 0 { return Err(DbError::NotFound); } Ok(()) }