use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::error::AppError; use crate::models::common::SubscriptionLevel; use crate::models::repos::RepoWatch; use crate::service::RepoService; use crate::session::Session; use super::util::clamp_limit_offset; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct WatchParams { pub level: Option, } impl RepoService { pub async fn repo_watch( &self, ctx: &Session, wk_name: &str, repo_name: &str, params: WatchParams, ) -> 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 level = params .level .as_deref() .and_then(|v| v.parse::().ok()) .unwrap_or(SubscriptionLevel::Participating); if level == SubscriptionLevel::Unknown { return Err(AppError::BadRequest("invalid watch level".into())); } let now = chrono::Utc::now(); let mut txn = self .ctx .db .writer() .begin() .await .map_err(|_| AppError::TxnError)?; sqlx::query("SET LOCAL app.current_user_id = $1") .bind(user_uid) .execute(&mut *txn) .await .map_err(AppError::Database)?; let existing = sqlx::query_scalar::<_, bool>( "SELECT EXISTS(SELECT 1 FROM repo_watch WHERE repo_id = $1 AND user_id = $2)", ) .bind(repo_id) .bind(user_uid) .fetch_one(self.ctx.db.reader()) .await .map_err(AppError::Database)?; if existing { sqlx::query("UPDATE repo_watch SET level = $1, updated_at = $2 WHERE repo_id = $3 AND user_id = $4") .bind(level.to_string()) .bind(now) .bind(repo_id) .bind(user_uid) .execute(&mut *txn) .await .map_err(AppError::Database)?; } else { sqlx::query("INSERT INTO repo_watch (id, repo_id, user_id, level, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $5)") .bind(Uuid::now_v7()) .bind(repo_id) .bind(user_uid) .bind(level.to_string()) .bind(now) .execute(&mut *txn) .await .map_err(AppError::Database)?; sqlx::query( "UPDATE repo_stats SET watchers_count = watchers_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_unwatch( &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 app.current_user_id = $1") .bind(user_uid) .execute(&mut *txn) .await .map_err(AppError::Database)?; let result = sqlx::query("DELETE FROM repo_watch 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 watchers_count = GREATEST(watchers_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_watchers( &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::<_, RepoWatch>( "SELECT id, repo_id, user_id, level, created_at, updated_at FROM repo_watch 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) } }