import { Injectable } from '@nestjs/common'; import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { ActivityReadModel, EpicReadModel, IssueDetails, IssueLinkType, IssueReadModel, } from '@task-stream/api/tracker/domain'; import { AssignIssueCommand } from './commands/assign-issue.command'; import { DuplicateIssueCommand } from './commands/duplicate-issue.command'; import { VoteIssueCommand } from './commands/vote-issue.command'; import { AttachFileCommand, AttachmentResult, IncomingFile, } from './commands/attach-file.command'; import { DetachFileCommand } from './commands/detach-file.command'; import { WatchIssueCommand } from './commands/watch-issue.command'; import { SetIssueParentCommand } from './commands/set-issue-parent.command'; import { RankIssueCommand } from './commands/rank-issue.command'; import { RestoreIssueCommand, TrashIssueCommand } from './commands/trash'; import { GetTrashQuery } from './queries/get-trash.query'; import { BulkChange, BulkResult, BulkUpdateIssuesCommand, } from './commands/bulk-update-issues'; import { AddChecklistItemCommand, RemoveChecklistItemCommand, UpdateChecklistItemCommand, } from './commands/change-checklist.command'; import { ChangeIssueStatusCommand } from './commands/change-issue-status.command'; import { CommandResult } from './commands/command-result'; import { CreateIssueCommand } from './commands/create-issue.command'; import { LinkIssuesCommand } from './commands/link-issues.command'; import { SetIssueReleaseCommand } from './commands/set-issue-release.command'; import { UnlinkIssuesCommand } from './commands/unlink-issues.command'; import { UpdateIssueCommand } from './commands/update-issue.command'; import { IssueFilter, Page } from './ports/find-issues.repository'; import { GetActivityQuery } from './queries/get-activity.query'; import { AttachmentDownload, GetAttachmentQuery, } from './queries/get-attachment.query'; import { GetIssueQuery } from './queries/get-issue.query'; import { GetIssuesByIdsQuery } from './queries/get-issues-by-ids.query'; import { SearchIssuesQuery } from './queries/search-issues.query'; import { GetEpicsQuery } from './queries/get-epics.query'; import { MyIssuesFilter, SearchMyIssuesQuery, } from './queries/search-my-issues.query'; export interface CreateIssueInput extends Omit { projectId: string; /** A type id of the project's settings. */ type: string; title: string; assigneeId?: string | null; releaseId?: string | null; /** Epic or ticket the new one breaks down. */ parentId?: string | null; } export interface IssueLinkInput { issueId: string; type: IssueLinkType; targetIssueId: string; } @Injectable() export class IssuesService { constructor( private readonly commandBus: CommandBus, private readonly queryBus: QueryBus, ) {} create(actorId: string, input: CreateIssueInput): Promise { const { projectId, type, title, assigneeId, releaseId, parentId, ...details } = input; return this.commandBus.execute( new CreateIssueCommand( actorId, projectId, type, title, details, assigneeId, releaseId, parentId, ), ); } update( actorId: string, issueId: string, changes: IssueDetails, ): Promise { return this.commandBus.execute( new UpdateIssueCommand(actorId, issueId, changes), ); } changeStatus( actorId: string, issueId: string, status: string, reason?: string | null, ): Promise { return this.commandBus.execute( new ChangeIssueStatusCommand(actorId, issueId, status, reason), ); } /** @param assigneeId null to unassign */ assign( actorId: string, issueId: string, assigneeId: string | null, ): Promise { return this.commandBus.execute( new AssignIssueCommand(actorId, issueId, assigneeId), ); } link(actorId: string, input: IssueLinkInput): Promise { return this.commandBus.execute( new LinkIssuesCommand( actorId, input.issueId, input.type, input.targetIssueId, ), ); } unlink(actorId: string, input: IssueLinkInput): Promise { return this.commandBus.execute( new UnlinkIssuesCommand( actorId, input.issueId, input.type, input.targetIssueId, ), ); } /** @param releaseId null to take the ticket out of its release */ setRelease( actorId: string, issueId: string, releaseId: string | null, ): Promise { return this.commandBus.execute( new SetIssueReleaseCommand(actorId, issueId, releaseId), ); } /** Attaches a file to the ticket, or to one of its comments. */ attach( actorId: string, issueId: string, file: IncomingFile, commentId: string | null = null, ): Promise { return this.commandBus.execute( new AttachFileCommand(actorId, issueId, file, commentId), ); } detach( actorId: string, issueId: string, attachmentId: string, ): Promise { return this.commandBus.execute( new DetachFileCommand(actorId, issueId, attachmentId), ); } /** The actor follows (or stops following) the ticket. */ watch( actorId: string, issueId: string, watching: boolean, ): Promise { return this.commandBus.execute( new WatchIssueCommand(actorId, issueId, actorId, watching), ); } /** A ticket's file, with its bytes, for whoever can read the ticket. */ download( viewerId: string | null, issueId: string, attachmentId: string, ): Promise { return this.queryBus.execute( new GetAttachmentQuery(viewerId, issueId, attachmentId), ); } findOne(viewerId: string | null, idOrKey: string): Promise { return this.queryBus.execute(new GetIssueQuery(viewerId, idOrKey)); } findMany(viewerId: string | null, ids: string[]): Promise { return this.queryBus.execute(new GetIssuesByIdsQuery(viewerId, ids)); } /** Places the ticket under `parentId` (null: back to the top). */ setParent( actorId: string, issueId: string, parentId: string | null, ): Promise { return this.commandBus.execute( new SetIssueParentCommand(actorId, issueId, parentId), ); } /** Moves a ticket in the backlog between `above` and `below` (null: an end). */ rank( actorId: string, issueId: string, above: string | null, below: string | null, ): Promise { return this.commandBus.execute( new RankIssueCommand(actorId, issueId, above, below), ); } /** The same change on several tickets; each one may succeed or not. */ bulkUpdate( actorId: string, issueIds: string[], change: BulkChange, ): Promise { return this.commandBus.execute( new BulkUpdateIssuesCommand(actorId, issueIds, change), ); } /** To the trash (whoever opened it, or a manager); back with `restore`. */ trash(actorId: string, issueId: string): Promise { return this.commandBus.execute(new TrashIssueCommand(actorId, issueId)); } restore(actorId: string, issueId: string): Promise { return this.commandBus.execute(new RestoreIssueCommand(actorId, issueId)); } /** Votes for the ticket (`on`) or takes the vote back. */ vote(actorId: string, issueId: string, on: boolean): Promise { return this.commandBus.execute(new VoteIssueCommand(actorId, issueId, on)); } /** A new ticket like this one (what it says, not where it stands). */ duplicate(actorId: string, issueId: string): Promise { return this.commandBus.execute(new DuplicateIssueCommand(actorId, issueId)); } trashed( viewerId: string | null, projectId: string, ): Promise { return this.queryBus.execute(new GetTrashQuery(viewerId, projectId)); } addChecklistItem( actorId: string, issueId: string, text: string, ): Promise { return this.commandBus.execute( new AddChecklistItemCommand(actorId, issueId, text), ); } updateChecklistItem( actorId: string, issueId: string, itemId: string, changes: { text?: string; done?: boolean }, ): Promise { return this.commandBus.execute( new UpdateChecklistItemCommand(actorId, issueId, itemId, changes), ); } removeChecklistItem( actorId: string, issueId: string, itemId: string, ): Promise { return this.commandBus.execute( new RemoveChecklistItemCommand(actorId, issueId, itemId), ); } epics(viewerId: string | null, projectId: string): Promise { return this.queryBus.execute(new GetEpicsQuery(viewerId, projectId)); } search( viewerId: string | null, filter: IssueFilter, ): Promise> { return this.queryBus.execute(new SearchIssuesQuery(viewerId, filter)); } /** Across all my projects: assigned to me, or any ticket (e.g. waiting for QA). */ mine( viewerId: string, filter: MyIssuesFilter, ): Promise> { return this.queryBus.execute(new SearchMyIssuesQuery(viewerId, filter)); } activity( viewerId: string | null, params: { projectId: string; issueId?: string; limit?: number; before?: string; }, ): Promise { return this.queryBus.execute( new GetActivityQuery( viewerId, params.projectId, params.issueId, params.limit, params.before, ), ); } }