use uuid::Uuid; use crate::error::AppError; use crate::models::repos::RepoStar; use crate::service::RepoService; use crate::session::Session; use super::util::{clamp_limit_offset, set_local_user_id}; impl RepoService { pub async fn repo_star( &self, ctx: &Session, wk_name: &str, repo_name: &str, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_readable(user_uid, &repo).await?; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query( "INSERT INTO repo_star (id, repo_id, user_id, created_at) VALUES ($1, $2, $3, $4) ON CONFLICT (repo_id, user_id) DO NOTHING", ) .bind(Uuid::now_v7()) .bind(repo_id) .bind(user_uid) .bind(now) .execute(&mut *txn) .await .map_err(AppError::Database)?; // Only increment stars_count if the INSERT actually happened if result.rows_affected() > 0 { sqlx::query( "UPDATE repo_stats SET stars_count = stars_count + 1, updated_at = $1 WHERE repo_id = $2", ) .bind(now) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; } txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_unstar( &self, ctx: &Session, wk_name: &str, repo_name: &str, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query(set_local_user_id(user_uid)) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query("DELETE FROM repo_star WHERE repo_id = $1 AND user_id = $2") .bind(repo_id) .bind(user_uid) .execute(&mut *txn) .await .map_err(AppError::Database)?; if result.rows_affected() > 0 { sqlx::query( "UPDATE repo_stats SET stars_count = GREATEST(stars_count - 1, 0), updated_at = $1 WHERE repo_id = $2", ) .bind(now) .bind(repo_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; } txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } pub async fn repo_stargazers( &self, ctx: &Session, wk_name: &str, repo_name: &str, limit: i64, offset: i64, ) -> Result, AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let repo = self.resolve_repo(wk_name, repo_name).await?; let repo_id = repo.id; self.ensure_repo_readable(user_uid, &repo).await?; let (limit, offset) = clamp_limit_offset(limit, offset); sqlx::query_as::<_, RepoStar>( "SELECT id, repo_id, user_id, created_at FROM repo_star WHERE repo_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3", ) .bind(repo_id) .bind(limit) .bind(offset) .fetch_all(self.ctx.db.reader()) .await .map_err(AppError::Database) } }