import { CommandRunner } from '@task-stream/api/shared/application'; import { Injectable } from '@nestjs/common'; import { QueryBus } from '@nestjs/cqrs'; import { CommentReadModel, DecisionReadModel, } from '@task-stream/api/tracker/domain'; import { AddCommentCommand } from './commands/add-comment.command'; import { CommandResult } from './commands/command-result'; import { DeleteCommentCommand } from './commands/delete-comment.command'; import { EditCommentCommand } from './commands/edit-comment.command'; import { ReactToCommentCommand } from './commands/react-to-comment.command'; import { MarkDecisionCommand } from './commands/mark-decision.command'; import { TurnCommentIntoIssueCommand } from './commands/turn-comment-into-issue'; import { ResolveThreadCommand } from './commands/resolve-thread.command'; import { ClosePollCommand, VotePollCommand } from './commands/polls'; import { Page } from './ports/find-issues.repository'; import { GetCommentsQuery } from './queries/get-comments.query'; import { GetIssueDecisionsQuery, SearchDecisionsQuery, } from './queries/decisions.query'; @Injectable() export class CommentsService { constructor( private readonly commands: CommandRunner, private readonly queryBus: QueryBus, ) {} add( actorId: string, issueId: string, body: string, replyTo: string | null = null, poll: string[] | null = null, ): Promise { return this.commands.execute( new AddCommentCommand(actorId, issueId, body, replyTo, poll), ); } /** Votes in a comment's poll (null takes the vote back). */ votePoll( actorId: string, commentId: string, option: number | null, ): Promise { return this.commands.execute( new VotePollCommand(actorId, commentId, option), ); } /** Closes a poll; with `decide`, its winner stays as a decision. */ closePoll( actorId: string, commentId: string, decide = true, ): Promise { return this.commands.execute( new ClosePollCommand(actorId, commentId, decide), ); } edit( actorId: string, commentId: string, body: string, ): Promise { return this.commands.execute( new EditCommentCommand(actorId, commentId, body), ); } /** Adds (`on`) or takes back one's reaction (👍…). */ react( actorId: string, commentId: string, emoji: string, on: boolean, ): Promise { return this.commands.execute( new ReactToCommentCommand(actorId, commentId, emoji, on), ); } /** Marks it as a decision of its ticket (`on`) or unmarks it: the team does. */ markDecision( actorId: string, commentId: string, on: boolean, ): Promise { return this.commands.execute( new MarkDecisionCommand(actorId, commentId, on), ); } /** * Settles a conversation or reopens it; a conclusion stays as a reply * marked as a decision. */ resolveThread( actorId: string, commentId: string, resolved: boolean, conclusion: string | null = null, ): Promise { return this.commands.execute( new ResolveThreadCommand(actorId, commentId, resolved, conclusion), ); } /** Makes a ticket of a comment: a subtask of its ticket, or linked to it. */ turnIntoIssue( actorId: string, commentId: string, title: string, type: string, asSubtask: boolean, ): Promise { return this.commands.execute( new TurnCommentIntoIssueCommand( actorId, commentId, title, type, asSubtask, ), ); } /** A ticket's decisions, oldest first. */ decisions( viewerId: string | null, issueId: string, ): Promise { return this.queryBus.execute(new GetIssueDecisionsQuery(viewerId, issueId)); } /** Decisions by their text: of a project, or of all mine without one. */ searchDecisions( viewerId: string | null, filter: { projectId?: string; search?: string; limit?: number }, ): Promise { return this.queryBus.execute( new SearchDecisionsQuery(viewerId, { limit: 20, ...filter }), ); } delete(actorId: string, commentId: string): Promise { return this.commands.execute(new DeleteCommentCommand(actorId, commentId)); } list( viewerId: string | null, issueId: string, limit?: number, offset?: number, ): Promise> { return this.queryBus.execute( new GetCommentsQuery(viewerId, issueId, limit, offset), ); } }