use db::sqlx; use model::pull_request::{ PullRequestReviewCommentModel, PullRequestReviewModel, }; use serde::Deserialize; use session::Session; use uuid::Uuid; use crate::{ AppService, error::AppError, pull_request::types::{ PullRequestReviewCommentResponse, PullRequestReviewResponse, }, session_user, }; #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreatePrReview { pub state: String, pub body: Option, pub commit_sha: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct CreatePrReviewComment { pub body: String, pub path: String, pub line: Option, pub side: Option, pub commit_sha: Option, } #[derive(Debug, Clone, Deserialize, utoipa::ToSchema)] pub struct DismissPrReview { pub dismiss_reason: Option, } impl AppService { pub async fn pr_review_create( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, params: CreatePrReview, ) -> Result { let user_uid = session_user(ctx)?; let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; let valid_states = ["pending", "commented", "approved", "changes_requested"]; if !valid_states.contains(¶ms.state.as_str()) { return Err(AppError::BadRequest(format!( "invalid review state: {}", params.state ))); } let now = chrono::Utc::now(); let review = sqlx::query_as::<_, PullRequestReviewModel>( "INSERT INTO pull_request_review (id, pull_request, reviewer, state, body, commit_sha, \ submitted_at, created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, $7, $7) \ RETURNING id, pull_request, reviewer, state, body, commit_sha, submitted_at, \ created_at, updated_at, dismissed_by, dismissed_at, dismiss_reason", ) .bind(uuid::Uuid::now_v7()) .bind(pr.id) .bind(user_uid) .bind(¶ms.state) .bind(¶ms.body) .bind(¶ms.commit_sha) .bind(now) .fetch_one(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let reviewer = self.users_find_by_id(review.reviewer).await?; Ok(PullRequestReviewResponse { id: review.id, reviewer: crate::issues::types::issue_author(reviewer), state: review.state, body: review.body, commit_sha: review.commit_sha, submitted_at: review.submitted_at, created_at: review.created_at, }) } pub async fn pr_review_list( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, ) -> Result, AppError> { let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; let reviews = sqlx::query_as::<_, PullRequestReviewModel>( "SELECT id, pull_request, reviewer, state, body, commit_sha, submitted_at, \ created_at, updated_at, dismissed_by, dismissed_at, dismiss_reason \ FROM pull_request_review WHERE pull_request = $1 \ ORDER BY created_at DESC", ) .bind(pr.id) .fetch_all(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let mut results = Vec::new(); for review in reviews { let reviewer = self.users_find_by_id(review.reviewer).await?; results.push(PullRequestReviewResponse { id: review.id, reviewer: crate::issues::types::issue_author(reviewer), state: review.state, body: review.body, commit_sha: review.commit_sha, submitted_at: review.submitted_at, created_at: review.created_at, }); } Ok(results) } pub async fn pr_review_dismiss( &self, ctx: &Session, wk_name: &str, repo_name: &str, _number: i64, review_id: Uuid, params: DismissPrReview, ) -> Result<(), AppError> { let user_uid = session_user(ctx)?; let _repo_id = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; sqlx::query( "UPDATE pull_request_review SET state = 'dismissed', dismissed_by = $1, dismissed_at = $2, \ dismiss_reason = $3, updated_at = $2 WHERE id = $4", ) .bind(user_uid) .bind(chrono::Utc::now()) .bind(¶ms.dismiss_reason) .bind(review_id) .execute(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; Ok(()) } pub async fn pr_review_comment_create( &self, ctx: &Session, wk_name: &str, repo_name: &str, number: i64, review_id: Option, params: CreatePrReviewComment, ) -> Result { let user_uid = session_user(ctx)?; let (repo_id, _) = self.pr_resolve_repo(ctx, wk_name, repo_name).await?; let pr = self.pr_resolve(repo_id, number).await?; let now = chrono::Utc::now(); let comment = sqlx::query_as::<_, PullRequestReviewCommentModel>( "INSERT INTO pull_request_review_comment (id, pull_request, review, author, body, path, \ commit_sha, original_commit_sha, line, original_line, side, resolved, \ created_at, updated_at) \ VALUES ($1, $2, $3, $4, $5, $6, $7, NULL, $8, $8, $9, false, $10, $10) \ RETURNING id, pull_request, review, author, body, path, commit_sha, \ original_commit_sha, line, original_line, side, resolved, resolved_by, resolved_at, \ created_at, updated_at, deleted_at", ) .bind(uuid::Uuid::now_v7()) .bind(pr.id) .bind(review_id) .bind(user_uid) .bind(¶ms.body) .bind(¶ms.path) .bind(params.commit_sha.unwrap_or(pr.source_sha.clone())) .bind(params.line) .bind(¶ms.side) .bind(now) .fetch_one(self.db.writer()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let author = self.users_find_by_id(comment.author).await?; Ok(PullRequestReviewCommentResponse { id: comment.id, author: crate::issues::types::issue_author(author), path: comment.path, line: comment.line, body: comment.body, commit_sha: comment.commit_sha, resolved: comment.resolved, created_at: comment.created_at, updated_at: comment.updated_at, }) } pub async fn pr_reviews_list( &self, pr_id: uuid::Uuid, ) -> Result, AppError> { let reviews = sqlx::query_as::<_, PullRequestReviewModel>( "SELECT id, pull_request, reviewer, state, body, commit_sha, submitted_at, \ created_at, updated_at, dismissed_by, dismissed_at, dismiss_reason \ FROM pull_request_review WHERE pull_request = $1 \ ORDER BY created_at DESC", ) .bind(pr_id) .fetch_all(self.db.reader()) .await .map_err(|e| AppError::DatabaseError(e.to_string()))?; let mut results = Vec::new(); for review in reviews { let reviewer = self.users_find_by_id(review.reviewer).await?; results.push(PullRequestReviewResponse { id: review.id, reviewer: crate::issues::types::issue_author(reviewer), state: review.state, body: review.body, commit_sha: review.commit_sha, submitted_at: review.submitted_at, created_at: review.created_at, }); } Ok(results) } }