use models::issue::issue_relation_repo; use models::repo::repos; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; use crate::errors::IssueError; use crate::services::service::IssueService; use crate::share::authz; use crate::share::lookup::{lookup_board, lookup_issue, require_issue_access, space_context}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct RepoRelDto { pub id: Uuid, pub issue_id: Uuid, pub repo_id: Uuid, pub created_at: OffsetDateTime, } impl From for RepoRelDto { fn from(m: issue_relation_repo::Model) -> Self { Self { id: m.id, issue_id: m.issue_id, repo_id: m.repo_id, created_at: m.created_at, } } } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct CreateRepoRelCmd { pub repo_id: Uuid, } #[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)] pub struct RepoRelPath { pub space_name: String, pub board_id: Uuid, pub issue_id: String, pub rel_id: Uuid, } impl IssueService { #[tracing::instrument(name = "issues.create_repo_relation", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))] pub async fn create_repo_relation( &self, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, cmd: CreateRepoRelCmd, ) -> Result { let space = space_context(self, caller, space_name).await?; authz::require_board_manage(self, &space, caller).await?; let board = lookup_board(self, &space, board_id, caller).await?; let _ = lookup_issue(self, &board, issue_id).await?; let _ = repos::Entity::find_by_id(cmd.repo_id) .one(self.database.read()) .await? .ok_or(IssueError::NotFound)?; let existing = issue_relation_repo::Entity::find() .filter(issue_relation_repo::Column::IssueId.eq(issue_id)) .filter(issue_relation_repo::Column::RepoId.eq(cmd.repo_id)) .one(self.database.read()) .await?; if existing.is_some() { return Err(IssueError::Conflict( "repo relation already exists".to_string(), )); } let now = OffsetDateTime::now_utc(); let row = issue_relation_repo::ActiveModel { id: Set(Uuid::now_v7()), issue_id: Set(issue_id), repo_id: Set(cmd.repo_id), created_at: Set(now), }; let inserted = row.insert(self.database.write()).await?; Ok(RepoRelDto::from(inserted)) } pub async fn list_repo_relations( &self, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, ) -> Result, IssueError> { require_issue_access(self, caller, space_name, board_id, issue_id).await?; let rows = issue_relation_repo::Entity::find() .filter(issue_relation_repo::Column::IssueId.eq(issue_id)) .order_by_asc(issue_relation_repo::Column::CreatedAt) .all(self.database.read()) .await?; Ok(rows.into_iter().map(RepoRelDto::from).collect()) } #[tracing::instrument(name = "issues.delete_repo_relation", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id, rel_id = %rel_id))] pub async fn delete_repo_relation( &self, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, rel_id: Uuid, ) -> Result<(), IssueError> { let space = space_context(self, caller, space_name).await?; authz::require_board_manage(self, &space, caller).await?; let board = lookup_board(self, &space, board_id, caller).await?; let _ = lookup_issue(self, &board, issue_id).await?; let r = issue_relation_repo::Entity::find_by_id(rel_id) .filter(issue_relation_repo::Column::IssueId.eq(issue_id)) .one(self.database.read()) .await? .ok_or(IssueError::NotFound)?; issue_relation_repo::Entity::delete_by_id(r.id) .exec(self.database.write()) .await?; Ok(()) } }