use uuid::Uuid; use crate::error::AppError; use crate::models::common::Role; use crate::models::workspaces::WorkspaceStats; use crate::service::WorkspaceService; use crate::session::Session; impl WorkspaceService { pub async fn workspace_stats( &self, ctx: &Session, workspace_id: Uuid, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let ws = self.find_workspace_by_id(workspace_id).await?; self.ensure_workspace_readable(user_uid, &ws).await?; self.ensure_workspace_stats(workspace_id).await } pub async fn workspace_refresh_stats( &self, ctx: &Session, workspace_id: Uuid, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let ws = self.find_workspace_by_id(workspace_id).await?; self.ensure_workspace_role_at_least(user_uid, &ws, Role::Admin) .await?; let members_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM workspace_member WHERE workspace_id = $1 AND status = 'active'", ) .bind(workspace_id) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; let repos_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM repo WHERE workspace_id = $1 AND deleted_at IS NULL", ) .bind(workspace_id) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; let issues_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM issue WHERE repo_id IN (SELECT id FROM repo WHERE workspace_id = $1 AND deleted_at IS NULL) AND deleted_at IS NULL", ) .bind(workspace_id) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; let prs_count = sqlx::query_scalar::<_, i64>( "SELECT COUNT(*) FROM pull_request WHERE repo_id IN (SELECT id FROM repo WHERE workspace_id = $1 AND deleted_at IS NULL) AND deleted_at IS NULL", ) .bind(workspace_id) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; let now = chrono::Utc::now(); let result = sqlx::query_as::<_, WorkspaceStats>( "UPDATE workspace_stats SET members_count = $1, repos_count = $2, issues_count = $3, \ pull_requests_count = $4, updated_at = $5 WHERE workspace_id = $6 \ RETURNING workspace_id, members_count, repos_count, issues_count, pull_requests_count, \ storage_bytes, bandwidth_bytes, build_minutes_used, last_activity_at, updated_at", ) .bind(members_count) .bind(repos_count) .bind(issues_count) .bind(prs_count) .bind(now) .bind(workspace_id) .fetch_one(self.ctx.db.writer()) .await .map_err(AppError::Database)?; Ok(result) } async fn ensure_workspace_stats(&self, workspace_id: Uuid) -> Result { if let Some(stats) = sqlx::query_as::<_, WorkspaceStats>( "SELECT workspace_id, members_count, repos_count, issues_count, pull_requests_count, \ storage_bytes, bandwidth_bytes, build_minutes_used, last_activity_at, updated_at \ FROM workspace_stats WHERE workspace_id = $1", ) .bind(workspace_id) .fetch_optional(self.ctx.db.reader()) .await .map_err(AppError::Database)? { return Ok(stats); } sqlx::query( "INSERT INTO workspace_stats (workspace_id, members_count, repos_count, issues_count, \ pull_requests_count, storage_bytes, bandwidth_bytes, build_minutes_used, updated_at) \ VALUES ($1, 0, 0, 0, 0, 0, 0, 0, $2) ON CONFLICT (workspace_id) DO NOTHING", ) .bind(workspace_id) .bind(chrono::Utc::now()) .execute(self.ctx.db.writer()) .await .map_err(AppError::Database)?; sqlx::query_as::<_, WorkspaceStats>( "SELECT workspace_id, members_count, repos_count, issues_count, pull_requests_count, \ storage_bytes, bandwidth_bytes, build_minutes_used, last_activity_at, updated_at \ FROM workspace_stats WHERE workspace_id = $1", ) .bind(workspace_id) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database) } }