use models::issue::{issue_item, issues}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use space::Space; use uuid::Uuid; use crate::errors::IssueError; use crate::services::service::IssueService; pub async fn lookup_space_by_name( service: &IssueService, space_name: &str, ) -> Result { Ok(service.access.by_slug(space_name).await?) } pub async fn lookup_board( service: &IssueService, space: &Space, board_id: Uuid, _caller: i64, ) -> Result { let board = issues::Entity::find_by_id(board_id) .one(service.database.read()) .await? .ok_or(IssueError::NotFound)?; if board.space_id != space.id() { return Err(IssueError::NotFound); } Ok(board) } pub async fn lookup_issue( service: &IssueService, board: &issues::Model, issue_id: Uuid, ) -> Result { issue_item::Entity::find_by_id(issue_id) .filter(issue_item::Column::CollectionId.eq(board.id)) .one(service.database.read()) .await? .ok_or(IssueError::NotFound) } pub async fn lookup_issue_by_ref( service: &IssueService, board: &issues::Model, issue_ref: &str, ) -> Result { let base = issue_item::Entity::find().filter(issue_item::Column::CollectionId.eq(board.id)); let item = if let Ok(id) = Uuid::parse_str(issue_ref) { base.filter(issue_item::Column::Id.eq(id)) .one(service.database.read()) .await? } else if let Ok(number) = issue_ref.parse::() { base.filter(issue_item::Column::AutoincrementId.eq(number)) .one(service.database.read()) .await? } else if let Some(number) = { let __inline = |prefix: Option<&str>, issue_ref: &str| -> Option { let prefix = prefix?; let (head, tail) = issue_ref.rsplit_once('-')?; if !head.eq_ignore_ascii_case(prefix) { return None; } tail.parse::().ok() }; __inline(board.prefix.as_deref(), issue_ref) } { // `CAD-12` keeps pasted links readable; only this board's own prefix // resolves, so a foreign key never silently points at another board. base.filter(issue_item::Column::AutoincrementId.eq(number)) .one(service.database.read()) .await? } else { None }; item.ok_or(IssueError::NotFound) } pub async fn require_member( _service: &IssueService, space: &Space, caller: i64, ) -> Result<(), IssueError> { Ok(space::require(space, caller, space::Capability::Read).await?) } pub async fn space_context( service: &IssueService, caller: i64, space_name: &str, ) -> Result { let space = lookup_space_by_name(service, space_name).await?; require_member(service, &space, caller).await?; Ok(space) } pub async fn board_context( service: &IssueService, caller: i64, space_name: &str, board_id: Uuid, ) -> Result<(Space, issues::Model), IssueError> { let space = space_context(service, caller, space_name).await?; let board = lookup_board(service, &space, board_id, caller).await?; Ok((space, board)) } pub async fn issue_context( service: &IssueService, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, ) -> Result<(Space, issues::Model, issue_item::Model), IssueError> { let (space, board) = board_context(service, caller, space_name, board_id).await?; let item = lookup_issue(service, &board, issue_id).await?; Ok((space, board, item)) } pub async fn require_issue_access( service: &IssueService, caller: i64, space_name: &str, board_id: Uuid, issue_id: Uuid, ) -> Result<(), IssueError> { issue_context(service, caller, space_name, board_id, issue_id).await?; Ok(()) } /// Same access rules as [`issue_context`], resolved from the issue id alone — /// used by routes that carry no space/board in their path (attachment reads). pub async fn issue_context_by_id( service: &IssueService, caller: i64, issue_id: Uuid, ) -> Result<(Space, issues::Model, issue_item::Model), IssueError> { let item = issue_item::Entity::find_by_id(issue_id) .one(service.database.read()) .await? .ok_or(IssueError::NotFound)?; let board = issues::Entity::find_by_id(item.collection_id) .one(service.database.read()) .await? .ok_or(IssueError::NotFound)?; let space = service .access .by_id(board.space_id) .await .map_err(|_| IssueError::NotFound)?; require_member(service, &space, caller).await?; let board = lookup_board(service, &space, board.id, caller).await?; Ok((space, board, item)) }