import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; import { ActivityReadModel, CommentAddedEvent, CommentDeletedEvent, CommentEditedEvent, IssueAssignedEvent, IssueCreatedEvent, IssueLinkedEvent, IssueReleaseChangedEvent, IssueStatusChangedEvent, IssueUnlinkedEvent, IssueUpdatedEvent, ProjectCreatedEvent, ProjectMemberAddedEvent, ProjectMemberRemovedEvent, ProjectMemberRoleChangedEvent, ProjectUpdatedEvent, ReleaseArchivedEvent, ReleaseCreatedEvent, ReleaseShippedEvent, ReleaseUpdatedEvent, TrackerEvent, DEFAULT_PROJECT_SETTINGS, SYSTEM_ACTOR_ID, SYSTEM_ACTOR_NAME, ProjectSettingsChangedEvent, ProjectWorkflow, } from '@task-stream/api/tracker/domain'; import { ChangeFeed } from '../ports/change-feed'; import { FindIssuesRepository } from '../ports/find-issues.repository'; import { FindProjectsRepository } from '../ports/find-projects.repository'; import { FindReleasesRepository } from '../ports/find-releases.repository'; import { UpsertMaterializedActivityRepository } from '../ports/upsert-materialized-activity.repository'; import { UserDirectory } from '../ports/user-directory'; import { describeEvent } from './activity-summary'; import { versionOf } from './read-model-notifier'; /** Project activity feed / ticket history: one entry per domain event. */ @EventsHandler( ProjectCreatedEvent, ProjectUpdatedEvent, ProjectSettingsChangedEvent, ProjectMemberAddedEvent, ProjectMemberRemovedEvent, ProjectMemberRoleChangedEvent, IssueCreatedEvent, IssueUpdatedEvent, IssueStatusChangedEvent, IssueAssignedEvent, IssueLinkedEvent, IssueUnlinkedEvent, IssueReleaseChangedEvent, CommentAddedEvent, CommentEditedEvent, CommentDeletedEvent, ReleaseCreatedEvent, ReleaseUpdatedEvent, ReleaseShippedEvent, ReleaseArchivedEvent, ) export class ActivityEventHandler implements IEventHandler { constructor( private readonly upsertMaterializedActivityRepository: UpsertMaterializedActivityRepository, private readonly findProjects: FindProjectsRepository, private readonly findIssues: FindIssuesRepository, private readonly findReleases: FindReleasesRepository, private readonly users: UserDirectory, private readonly changeFeed: ChangeFeed, ) {} async handle(event: TrackerEvent) { const issueId = this.issueIdOf(event); const issueKey = event instanceof IssueCreatedEvent ? event.issue.key : issueId ? ((await this.findIssues.findById(issueId))?.key ?? null) : null; const releaseId = this.releaseIdOf(event); const release = releaseId ? await this.findReleases.findById(releaseId) : null; const project = await this.findProjects.findById(event.projectId); // The project was deleted (cascade in progress): no feed to write to. if (!project) return; const mentioned = [ event.actorId, (event as { userId?: string }).userId, (event as { assigneeId?: string }).assigneeId, ]; const users = await this.users.refs(mentioned); const actorOf = (id: string) => id === SYSTEM_ACTOR_ID ? { id, name: SYSTEM_ACTOR_NAME } : users.of(id); const activity: ActivityReadModel = { id: `${event.aggregateId}:${versionOf(event)}`, type: event.constructor.name, projectId: event.projectId, issueId, issueKey, actor: actorOf(event.actorId), summary: describeEvent(event, { actor: actorOf(event.actorId).name, user: (id) => users.ofNullable(id)?.name ?? '?', issueKey: issueKey ?? '', projectKey: project?.key ?? '', releaseName: release?.name ?? '', workflow: new ProjectWorkflow( project?.settings ?? DEFAULT_PROJECT_SETTINGS, ), }), occurredAt: event.occurredAt, }; if (await this.upsertMaterializedActivityRepository.insert(activity)) { await this.changeFeed.publishActivity(activity); } } private issueIdOf(event: TrackerEvent): string | null { if (event.constructor.name.startsWith('Issue')) return event.aggregateId; if (event instanceof CommentAddedEvent) return event.comment.issueId; if ( event instanceof CommentEditedEvent || event instanceof CommentDeletedEvent ) return event.issueId; return null; } private releaseIdOf(event: TrackerEvent): string | null { if (event.constructor.name.startsWith('Release')) return event.aggregateId; if (event instanceof IssueReleaseChangedEvent) return event.releaseId; return null; } }