//! Repos — workspace-scoped cache of the repositories pulled from each //! connected provider. Rows are populated by the provider-specific sync //! path (see cm-api::providers) and served to the REPOS tier UI. //! //! Ownership: `workspace_id` scopes visibility; `connection_id` groups by //! provider account. The (`connection_id`, `external_id`) unique makes //! upsert idempotent across re-syncs. use cm_domain::WorkspaceId; use sqlx::PgPool; use time::OffsetDateTime; use uuid::Uuid; use crate::DbError; /// A repo row, as pulled from the provider. pub struct Repo { pub id: Uuid, pub workspace_id: Uuid, pub connection_id: Uuid, pub provider: String, pub external_id: String, pub owner: String, pub name: String, pub description: Option, pub default_branch: Option, pub clone_url: Option, pub html_url: Option, pub private: bool, pub stars: i32, pub forks: i32, pub provider_updated_at: Option, pub last_synced_at: OffsetDateTime, } /// Payload for one sync-driven upsert. Fields mirror the columns 1:1 minus /// the workspace + timestamps the DB owns. pub struct RepoUpsert<'a> { pub connection_id: Uuid, pub workspace_id: WorkspaceId, pub provider: &'a str, pub external_id: &'a str, pub owner: &'a str, pub name: &'a str, pub description: Option<&'a str>, pub default_branch: Option<&'a str>, pub clone_url: Option<&'a str>, pub html_url: Option<&'a str>, pub private: bool, pub stars: i32, pub forks: i32, pub provider_updated_at: Option, } /// Insert-or-update on (connection_id, external_id). Bumps `last_synced_at` /// on every call so the UI can show a "last synced" hint even when the /// provider hasn't changed anything. pub async fn upsert(pool: &PgPool, r: RepoUpsert<'_>) -> Result { let id = Uuid::now_v7(); let row = sqlx::query!( "INSERT INTO repos (id, workspace_id, connection_id, provider, external_id, owner, name, description, default_branch, clone_url, html_url, private, stars, forks, provider_updated_at, last_synced_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, now()) ON CONFLICT (connection_id, external_id) DO UPDATE SET owner = EXCLUDED.owner, name = EXCLUDED.name, description = EXCLUDED.description, default_branch = EXCLUDED.default_branch, clone_url = EXCLUDED.clone_url, html_url = EXCLUDED.html_url, private = EXCLUDED.private, stars = EXCLUDED.stars, forks = EXCLUDED.forks, provider_updated_at = EXCLUDED.provider_updated_at, last_synced_at = now() RETURNING id", id, r.workspace_id.as_uuid(), r.connection_id, r.provider, r.external_id, r.owner, r.name, r.description, r.default_branch, r.clone_url, r.html_url, r.private, r.stars, r.forks, r.provider_updated_at, ) .fetch_one(pool) .await?; Ok(row.id) } /// All repos in a workspace, newest-provider-update first, cap-limited. pub async fn list_for_workspace( pool: &PgPool, workspace_id: WorkspaceId, limit: i64, ) -> Result, DbError> { let rows = sqlx::query!( "SELECT id, workspace_id, connection_id, provider, external_id, owner, name, description, default_branch, clone_url, html_url, private, stars, forks, provider_updated_at, last_synced_at FROM repos WHERE workspace_id = $1 ORDER BY provider_updated_at DESC NULLS LAST, name LIMIT $2", workspace_id.as_uuid(), limit, ) .fetch_all(pool) .await?; Ok(rows .into_iter() .map(|r| Repo { id: r.id, workspace_id: r.workspace_id, connection_id: r.connection_id, provider: r.provider, external_id: r.external_id, owner: r.owner, name: r.name, description: r.description, default_branch: r.default_branch, clone_url: r.clone_url, html_url: r.html_url, private: r.private, stars: r.stars, forks: r.forks, provider_updated_at: r.provider_updated_at, last_synced_at: r.last_synced_at, }) .collect()) } /// A single repo, workspace-scoped so cross-tenant reads return NotFound. pub async fn get(pool: &PgPool, id: Uuid, workspace_id: WorkspaceId) -> Result { let r = sqlx::query!( "SELECT id, workspace_id, connection_id, provider, external_id, owner, name, description, default_branch, clone_url, html_url, private, stars, forks, provider_updated_at, last_synced_at FROM repos WHERE id = $1 AND workspace_id = $2", id, workspace_id.as_uuid(), ) .fetch_optional(pool) .await? .ok_or(DbError::NotFound)?; Ok(Repo { id: r.id, workspace_id: r.workspace_id, connection_id: r.connection_id, provider: r.provider, external_id: r.external_id, owner: r.owner, name: r.name, description: r.description, default_branch: r.default_branch, clone_url: r.clone_url, html_url: r.html_url, private: r.private, stars: r.stars, forks: r.forks, provider_updated_at: r.provider_updated_at, last_synced_at: r.last_synced_at, }) } /// Delete every repo cached under a connection. Called when the /// connection itself is deleted, or as part of a full re-sync. pub async fn delete_by_connection( pool: &PgPool, workspace_id: WorkspaceId, connection_id: Uuid, ) -> Result { let res = sqlx::query!( "DELETE FROM repos WHERE connection_id = $1 AND workspace_id = $2", connection_id, workspace_id.as_uuid(), ) .execute(pool) .await?; Ok(res.rows_affected()) }