Skip to content
Clone
8fefadc
212 lines 6.9 KB Rust
use models::issue::issue_dependency;
use models::issue::issue_event_line::IssueEventType;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set, TransactionTrait,
};
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToSchema};
use uuid::Uuid;
use crate::errors::IssueError;
use crate::services::service::IssueEventDetail;
use crate::services::service::IssueService;
use crate::share::authz::require_board_manage;
use crate::share::lookup::{lookup_board, lookup_issue, require_issue_access, space_context};
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DependencyDto {
pub id: Uuid,
pub issue_id: Uuid,
pub dependency_id: Uuid,
pub created_at: OffsetDateTime,
}
impl From<issue_dependency::Model> for DependencyDto {
fn from(m: issue_dependency::Model) -> Self {
Self {
id: m.id,
issue_id: m.issue_id,
dependency_id: m.dependency_id,
created_at: m.created_at,
}
}
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct CreateDependencyCmd {
pub dependency_id: Uuid,
}
#[derive(Debug, Clone, Deserialize, IntoParams, ToSchema)]
pub struct DependencyPath {
pub space_name: String,
pub board_id: Uuid,
pub issue_id: String,
pub dep_id: Uuid,
}
impl IssueService {
#[tracing::instrument(name = "issues.create_dependency", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id))]
pub async fn create_dependency(
&self,
caller: Uuid,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
cmd: CreateDependencyCmd,
) -> Result<DependencyDto, IssueError> {
let space = space_context(self, caller, space_name).await?;
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?;
if cmd.dependency_id == issue_id {
return Err(IssueError::BadRequest(
"issue cannot depend on itself".to_string(),
));
}
let _ = lookup_issue(self, &board, cmd.dependency_id).await?;
let existing = issue_dependency::Entity::find()
.filter(issue_dependency::Column::IssueId.eq(issue_id))
.filter(issue_dependency::Column::DependencyId.eq(cmd.dependency_id))
.one(self.db().read())
.await?;
if existing.is_some() {
return Err(IssueError::Conflict(
"dependency already exists".to_string(),
));
}
let now = OffsetDateTime::now_utc();
let dep_id = Uuid::now_v7();
let dep_target = cmd.dependency_id;
let this = self.clone();
let inserted = self
.db()
.primary()
.transaction::<_, issue_dependency::Model, IssueError>(move |txn| {
Box::pin(async move {
let row = issue_dependency::ActiveModel {
id: Set(dep_id),
issue_id: Set(issue_id),
dependency_id: Set(dep_target),
created_at: Set(now),
};
let ins = row.insert(txn).await?;
this.write_event(
txn,
issue_id,
caller,
IssueEventType::DependencyAdded,
IssueEventDetail {
comment_id: None,
old_title: None,
new_title: None,
content: Some(dep_target.to_string()),
},
)
.await?;
Ok(ins)
})
})
.await?;
Ok(DependencyDto::from(inserted))
}
pub async fn list_dependents(
&self,
caller: Uuid,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<Vec<DependencyDto>, IssueError> {
require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let rows = issue_dependency::Entity::find()
.filter(issue_dependency::Column::DependencyId.eq(issue_id))
.order_by_asc(issue_dependency::Column::CreatedAt)
.all(self.db().read())
.await?;
Ok(rows.into_iter().map(DependencyDto::from).collect())
}
pub async fn list_dependencies(
&self,
caller: Uuid,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
) -> Result<Vec<DependencyDto>, IssueError> {
require_issue_access(self, caller, space_name, board_id, issue_id).await?;
let rows = issue_dependency::Entity::find()
.filter(issue_dependency::Column::IssueId.eq(issue_id))
.order_by_asc(issue_dependency::Column::CreatedAt)
.all(self.db().read())
.await?;
Ok(rows.into_iter().map(DependencyDto::from).collect())
}
#[tracing::instrument(name = "issues.delete_dependency", skip_all, fields(caller = %caller, space = %space_name, board_id = %board_id, issue_id = %issue_id, dep_id = %dep_id))]
pub async fn delete_dependency(
&self,
caller: Uuid,
space_name: &str,
board_id: Uuid,
issue_id: Uuid,
dep_id: Uuid,
) -> Result<(), IssueError> {
let space = space_context(self, caller, space_name).await?;
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 dep = issue_dependency::Entity::find_by_id(dep_id)
.filter(issue_dependency::Column::IssueId.eq(issue_id))
.one(self.db().read())
.await?
.ok_or(IssueError::NotFound)?;
let dep_target = dep.dependency_id;
let this = self.clone();
self.db()
.primary()
.transaction::<_, (), IssueError>(move |txn| {
Box::pin(async move {
issue_dependency::Entity::delete_by_id(dep_id)
.exec(txn)
.await?;
this.write_event(
txn,
issue_id,
caller,
IssueEventType::DependencyRemoved,
IssueEventDetail {
comment_id: None,
old_title: None,
new_title: None,
content: Some(dep_target.to_string()),
},
)
.await?;
Ok(())
})
})
.await?;
Ok(())
}
}