import { CommandHandler, EventPublisher, ICommandHandler } from '@nestjs/cqrs'; import { BusinessRuleError } from '@task-stream/api/shared/domain'; import { CommentFactory, SYSTEM_ACTOR_ID, } from '@task-stream/api/tracker/domain'; import { AddCommentCommand } from './add-comment.command'; import { CommandResult, ack } from './command-result'; import { WriteAccess } from './write-access'; @CommandHandler(AddCommentCommand) export class AddCommentCommandHandler implements ICommandHandler { constructor( private readonly access: WriteAccess, private readonly commentFactory: CommentFactory, private readonly eventPublisher: EventPublisher, ) {} async execute(command: AddCommentCommand): Promise { // TaskStream itself (alerts that repeat) is no member; only internal // commands act as it, never a request from outside. const issue = command.actorId === SYSTEM_ACTOR_ID ? await this.access.issue(command.issueId) : (await this.access.issueForReporter(command.issueId, command.actorId)) .issue; if (issue.trashedAt) throw new BusinessRuleError( `${issue.key} is in the trash: restore it to change it`, ); const comment = this.commentFactory.create({ issueId: issue.id, projectId: issue.projectId, authorId: command.actorId, body: command.body, replyTo: command.replyTo ? await this.threadOf(command.replyTo, issue.id) : null, poll: command.poll, }); this.eventPublisher.mergeObjectContext(comment); await comment.commitAndWait(); return ack(comment); } /** * The top-level comment a reply hangs from: one level only, so answering * a reply joins its thread. */ private async threadOf(commentId: string, issueId: string): Promise { const parent = await this.access.comment(commentId); if (parent.issueId !== issueId) throw new BusinessRuleError('A reply must be on the same ticket'); return parent.replyTo ?? parent.id; } }