Files
zhenyi 420dedbc1e feat(service): expand service layer with new domain operations
- Add IM service modules: audit, channel roles, custom emojis, forum
  tags, integrations, invitations, repo links, slash commands, stages,
  voice, webhooks
- Add PR service modules: review requests, templates
- Add repo service modules: contributors, release assets, git extras
  (archive, branch rename, commit extras, diff/merge, tag, tree)
- Add user service: social (follow/block)
- Add internal auth service
- Update existing service modules with expanded functionality
- Remove deleted IM modules: articles, delivery trace, drafts,
  follows, messages, polls, presence, reactions, threads
2026-06-10 18:49:32 +08:00

121 lines
4.2 KiB
Rust

use uuid::Uuid;
use crate::error::AppError;
use crate::models::common::RelationType;
use crate::models::issues::IssuePrRelation;
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 LinkPrParams {
pub pull_request_id: Uuid,
pub relation_type: Option<String>,
}
impl IssueService {
pub async fn issue_pr_relations(
&self,
ctx: &Session,
wk_name: &str,
number: i64,
limit: i64,
offset: i64,
) -> Result<Vec<IssuePrRelation>, 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::<_, IssuePrRelation>(
"SELECT id, issue_id, pull_request_id, relation_type, created_by, created_at \
FROM issue_pr_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_pr(
&self,
ctx: &Session,
wk_name: &str,
number: i64,
params: LinkPrParams,
) -> Result<IssuePrRelation, 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 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::<_, IssuePrRelation>(
"INSERT INTO issue_pr_relation (id, issue_id, pull_request_id, relation_type, created_by, created_at) \
VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (issue_id, pull_request_id) DO NOTHING \
RETURNING id, issue_id, pull_request_id, relation_type, created_by, created_at",
)
.bind(Uuid::now_v7()).bind(issue_id).bind(params.pull_request_id)
.bind(relation_type).bind(user_uid).bind(now)
.fetch_optional(&mut *txn).await.map_err(AppError::Database)?
.ok_or(AppError::Conflict("PR already linked".into()))?;
txn.commit().await.map_err(|_| AppError::TxnError)?;
Ok(rel)
}
pub async fn issue_unlink_pr(
&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_pr_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(), "PR relation not found")?;
txn.commit().await.map_err(|_| AppError::TxnError)?;
Ok(())
}
}