import { ActivityReadModel, CommentReadModel, IssueLinkReadModel, IssueReadModel, IssueTimelineEntry, ProjectReadModel, ReleaseReadModel, } from '@task-stream/api/tracker/domain'; import { Schema } from 'mongoose'; /* Read model (MongoDB): denormalised documents shaped for the GraphQL queries. */ type Doc = Omit & { _id: string }; export type ProjectDoc = Doc & { memberIds: string[] }; /** Links are split: outward ones come from the issue itself, inward ones from other issues' events. */ export type IssueDoc = Doc> & { outwardLinks: IssueLinkReadModel[]; inwardLinks: IssueLinkReadModel[]; }; export type CommentDoc = Doc; export type ReleaseDoc = Doc; export type ActivityDoc = Doc; export type TimelineDoc = Doc; export const ReadModels = { Project: 'ProjectReadModel', Issue: 'IssueReadModel', Comment: 'CommentReadModel', Release: 'ReleaseReadModel', Activity: 'ActivityReadModel', Timeline: 'IssueTimelineReadModel', } as const; const userRef = { _id: false, id: String, name: String }; // `type` must be wrapped: a bare `type` key would declare the whole subdocument a String. const link = { _id: false, type: { type: String }, direction: String, issueId: String, issueKey: String, }; const loose = { strict: false, versionKey: false, _id: false } as const; export const ProjectSchema = new Schema( { _id: String, key: { type: String, unique: true }, isPublic: { type: Boolean, index: true }, memberIds: { type: [String], index: true }, owner: userRef, members: [{ _id: false, user: userRef, role: String }], version: Number, }, { ...loose, collection: 'projects_view' }, ); export const IssueSchema = new Schema( { _id: String, projectId: { type: String, index: true }, key: { type: String, unique: true }, status: String, statusCategory: String, reporter: userRef, assignee: userRef, release: { _id: false, id: String, name: String }, outwardLinks: [link], inwardLinks: [link], attachments: [ { _id: false, id: String, fileName: String, mimeType: String, size: Number, uploadedBy: userRef, uploadedAt: String, commentId: String, }, ], labels: [String], version: Number, }, { ...loose, collection: 'issues_view' }, ); IssueSchema.index({ projectId: 1, status: 1, updatedAt: -1 }); IssueSchema.index({ projectId: 1, statusCategory: 1 }); IssueSchema.index({ 'assignee.id': 1 }); IssueSchema.index({ 'release.id': 1 }); IssueSchema.index({ title: 'text', description: 'text', key: 'text' }); export const CommentSchema = new Schema( { _id: String, issueId: { type: String, index: true }, author: userRef, version: Number, }, { ...loose, collection: 'comments_view' }, ); CommentSchema.index({ issueId: 1, createdAt: 1 }); export const ReleaseSchema = new Schema( { _id: String, projectId: { type: String, index: true }, version: Number }, { ...loose, collection: 'releases_view' }, ); export const ActivitySchema = new Schema( { _id: String, projectId: String, issueId: String, actor: userRef, }, { ...loose, collection: 'activity' }, ); ActivitySchema.index({ projectId: 1, occurredAt: -1 }); ActivitySchema.index({ issueId: 1, occurredAt: -1 }); /** Every status and release step of every ticket (statistics). */ export const TimelineSchema = new Schema( { _id: String, projectId: String, issueId: String, at: String }, { ...loose, collection: 'issue_timeline' }, ); TimelineSchema.index({ projectId: 1, at: 1 }); export const READ_MODEL_SCHEMAS = [ { name: ReadModels.Project, schema: ProjectSchema }, { name: ReadModels.Issue, schema: IssueSchema }, { name: ReadModels.Comment, schema: CommentSchema }, { name: ReadModels.Release, schema: ReleaseSchema }, { name: ReadModels.Activity, schema: ActivitySchema }, { name: ReadModels.Timeline, schema: TimelineSchema }, ]; export function fromDoc( doc: T, ): Omit & { id: string } { const { _id, ...rest } = doc; return { id: _id, ...rest }; } export function issueFromDoc(doc: IssueDoc): IssueReadModel { const { _id, outwardLinks, inwardLinks, ...rest } = doc; return { id: _id, ...rest, links: [...(outwardLinks ?? []), ...(inwardLinks ?? [])], // Tickets projected before attachments existed have none. attachments: rest.attachments ?? [], }; }