use uuid::Uuid; use crate::error::AppError; use crate::models::common::RelationType; use crate::models::issues::IssueRepoRelation; use crate::service::IssueService; use crate::session::Session; use super::util::{clamp_limit_offset, ensure_affected, parse_enum, set_local_user_id}; #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, utoipa::ToSchema)] pub struct LinkRepoParams { pub repo_id: Uuid, pub relation_type: Option, } impl IssueService { pub async fn issue_repo_relations( &self, ctx: &Session, wk_name: &str, number: i64, limit: i64, offset: i64, ) -> Result, AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let issue = self.resolve_issue(wk_name, number).await?; let issue_id = issue.id; self.ensure_issue_readable(user_uid, &issue).await?; let (limit, offset) = clamp_limit_offset(limit, offset); sqlx::query_as::<_, IssueRepoRelation>( "SELECT id, issue_id, repo_id, relation_type, created_by, created_at \ FROM issue_repo_relation WHERE issue_id = $1 ORDER BY created_at ASC LIMIT $2 OFFSET $3", ) .bind(issue_id).bind(limit).bind(offset) .fetch_all(self.ctx.db.reader()).await.map_err(AppError::Database) } pub async fn issue_link_repo( &self, ctx: &Session, wk_name: &str, number: i64, params: LinkRepoParams, ) -> Result { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let issue = self.resolve_issue(wk_name, number).await?; let issue_id = issue.id; self.ensure_issue_editable(user_uid, &issue).await?; let relation_type = match params.relation_type { Some(ref v) => parse_enum( Some(v.clone()), RelationType::References, RelationType::Unknown, "relation_type", )?, None => RelationType::References, }; 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 rel = sqlx::query_as::<_, IssueRepoRelation>( "INSERT INTO issue_repo_relation (id, issue_id, repo_id, relation_type, created_by, created_at) \ VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (issue_id, repo_id) DO NOTHING \ RETURNING id, issue_id, repo_id, relation_type, created_by, created_at", ) .bind(Uuid::now_v7()).bind(issue_id).bind(params.repo_id) .bind(relation_type).bind(user_uid).bind(now) .fetch_optional(&mut *txn).await.map_err(AppError::Database)? .ok_or(AppError::Conflict("repo already linked".into()))?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(rel) } pub async fn issue_unlink_repo( &self, ctx: &Session, wk_name: &str, number: i64, relation_id: Uuid, ) -> Result<(), AppError> { let user_uid = ctx.user().ok_or(AppError::Unauthorized)?; let issue = self.resolve_issue(wk_name, number).await?; let issue_id = issue.id; self.ensure_issue_editable(user_uid, &issue).await?; 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 issue_repo_relation WHERE id = $1 AND issue_id = $2") .bind(relation_id) .bind(issue_id) .execute(&mut *txn) .await .map_err(AppError::Database)?; ensure_affected(result.rows_affected(), "repo relation not found")?; txn.commit().await.map_err(|_| AppError::TxnError)?; Ok(()) } }