use cm_domain::{Role, User, UserId, WorkspaceId}; use sqlx::PgPool; use crate::DbError; fn role_to_str(role: Role) -> &'static str { match role { Role::Owner => "owner", Role::Member => "member", } } fn role_from_str(s: &str) -> Role { // The CHECK constraint guarantees only these two values exist. if s == "owner" { Role::Owner } else { Role::Member } } /// Inserts a user. `created_at` is assigned by the database; the value on /// the input struct is ignored. pub async fn insert(pool: &PgPool, user: &User) -> Result<(), DbError> { sqlx::query!( "INSERT INTO users (id, workspace_id, email, role, display_name) VALUES ($1, $2, $3, $4, $5)", user.id.as_uuid(), user.workspace_id.as_uuid(), user.email, role_to_str(user.role), user.display_name, ) .execute(pool) .await?; Ok(()) } pub async fn get(pool: &PgPool, id: UserId) -> Result { let row = sqlx::query!( "SELECT id, workspace_id, email, role, display_name, created_at FROM users WHERE id = $1", id.as_uuid(), ) .fetch_one(pool) .await?; Ok(User { id: UserId::from(row.id), workspace_id: WorkspaceId::from(row.workspace_id), email: row.email, role: role_from_str(&row.role), display_name: row.display_name, created_at: row.created_at, }) } pub async fn find_by_email(pool: &PgPool, email: &str) -> Result { let row = sqlx::query!( "SELECT id, workspace_id, email, role, display_name, created_at FROM users WHERE email = $1", email, ) .fetch_one(pool) .await?; Ok(User { id: UserId::from(row.id), workspace_id: WorkspaceId::from(row.workspace_id), email: row.email, role: role_from_str(&row.role), display_name: row.display_name, created_at: row.created_at, }) } /// The workspace's owner (earliest-joined user with role=owner). Used by /// internal service paths (e.g. per-team runtime MCP auth) that need to /// mint a bearer scoped to the workspace but don't have a caller in hand. pub async fn owner_of_workspace( pool: &PgPool, workspace_id: WorkspaceId, ) -> Result { let row = sqlx::query!( "SELECT id FROM users WHERE workspace_id = $1 AND role = 'owner' ORDER BY created_at, id LIMIT 1", workspace_id.as_uuid(), ) .fetch_one(pool) .await?; Ok(UserId::from(row.id)) } /// Members table for the Team page (ยง8.3), in join order. pub async fn list_by_workspace( pool: &PgPool, workspace_id: WorkspaceId, ) -> Result, DbError> { let rows = sqlx::query!( "SELECT id, workspace_id, email, role, display_name, created_at FROM users WHERE workspace_id = $1 ORDER BY created_at, id", workspace_id.as_uuid(), ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|row| User { id: UserId::from(row.id), workspace_id: WorkspaceId::from(row.workspace_id), email: row.email, role: role_from_str(&row.role), display_name: row.display_name, created_at: row.created_at, }) .collect()) }