import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; import { HomeReadModel, OnlineReadModel, } from '@task-stream/api/tracker/domain'; import { computeHome, HomeProjectInput, lastDays } from '../analytics/home'; import { CalendarFeedsRepository } from '../ports/calendar-feeds.repository'; import { FindActivityRepository } from '../ports/find-activity.repository'; import { FindIssuesRepository } from '../ports/find-issues.repository'; import { FindProjectsRepository } from '../ports/find-projects.repository'; import { FindReleasesRepository } from '../ports/find-releases.repository'; import { FindSprintsRepository } from '../ports/find-sprints.repository'; import { FindTimelineRepository } from '../ports/find-timeline.repository'; import { PresenceStore } from '../ports/presence.store'; import { PRESENCE_TTL_MS } from '../presence.service'; import { GetMyHomeQuery } from './get-my-home.query'; import { guestOnly } from './read-access'; /** How much activity the home page lists. */ const ACTIVITY = 30; @QueryHandler(GetMyHomeQuery) export class GetMyHomeQueryHandler implements IQueryHandler { constructor( private readonly findProjects: FindProjectsRepository, private readonly findIssues: FindIssuesRepository, private readonly findTimeline: FindTimelineRepository, private readonly findSprints: FindSprintsRepository, private readonly findReleases: FindReleasesRepository, private readonly findActivity: FindActivityRepository, private readonly feeds: CalendarFeedsRepository, private readonly presence: PresenceStore, ) {} async execute(query: GetMyHomeQuery): Promise { const me = query.viewerId; const now = new Date(); const projects = (await this.findProjects.findByMember(me)).filter( (p) => !p.archivedAt, ); // Project-wide views only of the projects I see whole (not as a guest). const whole = projects.filter((p) => !guestOnly(p, me)); const days = lastDays(now); const [inputs, activity, counts, feed, online] = await Promise.all([ Promise.all( projects.map(async (project) => { const [issues, timeline, sprints, releases] = await Promise.all([ this.findIssues.findByProject(project.id), this.findTimeline.findByProject(project.id), this.findSprints.findByProject(project.id), this.findReleases.findByProject(project.id), ]); return { project, issues, timeline, sprints, releases }; }), ), this.findActivity.findAcross( whole.map((p) => p.id), ACTIVITY, ), this.findActivity.dailyCounts( whole.map((p) => p.id), days[0], ), this.feeds.findByUser(me), Promise.all( whole.map(async (project) => ( await this.presence.listProject( project.id, now.getTime() - PRESENCE_TTL_MS, ) ) .filter((entry) => entry.user.id !== me) .map( (entry): OnlineReadModel => ({ user: entry.user, projectKey: project.key, issueKey: entry.issue?.key ?? null, issueTitle: entry.issue?.title ?? null, editing: entry.editing, at: entry.at, }), ), ), ), ]); const perDay = (projectId: string) => days.map( (day) => counts.find((c) => c.projectId === projectId && c.day === day) ?.count ?? 0, ); return computeHome({ viewerId: me, projects: inputs.map( (i): HomeProjectInput => ({ ...i, activity: perDay(i.project.id) }), ), activity, hasCalendar: !!feed, // One line per person: the latest ticket they are on. online: online .flat() .sort((a, b) => b.at.localeCompare(a.at)) .filter( (entry, i, all) => all.findIndex((e) => e.user.id === entry.user.id) === i, ), now, }); } }