Opening a ticket notes the visit and answers the one before (issue_visits), so the page marks others' comments and changes since. A comment can become a ticket of its own, as a subtask or linked; it remembers where it went. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
568 lines
21 KiB
TypeScript
568 lines
21 KiB
TypeScript
import {
|
|
Automation,
|
|
OutgoingWebhook,
|
|
} from '@task-stream/api/tracker/application';
|
|
import {
|
|
ActivityReadModel,
|
|
CommentReadModel,
|
|
NotificationReadModel,
|
|
IssueLinkReadModel,
|
|
IssueReadModel,
|
|
IssueTimelineEntry,
|
|
ProjectReadModel,
|
|
ReleaseReadModel,
|
|
SavedFilterReadModel,
|
|
MyViewReadModel,
|
|
PinnedIssueReadModel,
|
|
SprintReadModel,
|
|
implicitRank,
|
|
} from '@task-stream/api/tracker/domain';
|
|
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
import { Types } from 'mongoose';
|
|
|
|
/* Read model (MongoDB): denormalised documents shaped for the GraphQL queries. */
|
|
|
|
/**
|
|
* Each document keeps the domain id (a uuid, the same one the event store
|
|
* uses) in `id`, unique; `_id` is Mongo's own ObjectId and means nothing
|
|
* outside the database.
|
|
*/
|
|
type Doc<T extends { id: string }> = T & { _id?: Types.ObjectId };
|
|
|
|
export type ProjectDoc = Doc<ProjectReadModel> & { memberIds: string[] };
|
|
/** Links are split: outward ones come from the issue itself, inward ones from other issues' events. */
|
|
export type IssueDoc = Doc<Omit<IssueReadModel, 'links'>> & {
|
|
outwardLinks: IssueLinkReadModel[];
|
|
inwardLinks: IssueLinkReadModel[];
|
|
};
|
|
export type CommentDoc = Doc<CommentReadModel>;
|
|
export type ReleaseDoc = Doc<ReleaseReadModel>;
|
|
export type SprintDoc = Doc<SprintReadModel>;
|
|
export type ActivityDoc = Doc<ActivityReadModel>;
|
|
export type TimelineDoc = Doc<IssueTimelineEntry>;
|
|
export type NotificationDoc = Doc<NotificationReadModel>;
|
|
export type SavedFilterDoc = Doc<SavedFilterReadModel>;
|
|
export type MyViewDoc = Doc<MyViewReadModel>;
|
|
export type PinnedIssueDoc = PinnedIssueReadModel & { _id?: Types.ObjectId };
|
|
export type OutgoingWebhookDoc = Doc<OutgoingWebhook>;
|
|
export type AutomationDoc = Doc<Automation>;
|
|
export interface AutomationRunDoc {
|
|
/** `ruleId:eventKey`: a rule runs once per event. */
|
|
id: string;
|
|
at: Date;
|
|
}
|
|
export interface GitRepositoryDoc {
|
|
id: string;
|
|
name: string;
|
|
secret: string;
|
|
createdAt: string;
|
|
}
|
|
/**
|
|
* One per project. `secret` alone is how the single webhook of before was
|
|
* kept: it reads as a repository named "principal" until the list is saved.
|
|
*/
|
|
export interface GitIntegrationDoc {
|
|
/** The project's id. */
|
|
id: string;
|
|
repositories?: GitRepositoryDoc[];
|
|
secret?: string;
|
|
createdAt?: string;
|
|
}
|
|
export interface PushSubscriptionDoc {
|
|
/** sha256 of the endpoint. */
|
|
id: string;
|
|
userId: string;
|
|
endpoint: string;
|
|
keys: { p256dh: string; auth: string };
|
|
createdAt: string;
|
|
}
|
|
export interface DashboardShareDoc {
|
|
/** The project's id. */
|
|
id: string;
|
|
token: string;
|
|
blocks: string[];
|
|
createdAt: string;
|
|
}
|
|
export interface CalendarFeedDoc {
|
|
/** The user's id. */
|
|
id: string;
|
|
token: string;
|
|
createdAt: string;
|
|
}
|
|
export interface NotificationSettingsDoc {
|
|
/** The user's id. */
|
|
id: string;
|
|
email: boolean;
|
|
}
|
|
|
|
export const ReadModels = {
|
|
Project: 'ProjectReadModel',
|
|
Issue: 'IssueReadModel',
|
|
Comment: 'CommentReadModel',
|
|
Release: 'ReleaseReadModel',
|
|
Activity: 'ActivityReadModel',
|
|
Timeline: 'IssueTimelineReadModel',
|
|
Notification: 'NotificationReadModel',
|
|
NotificationSettings: 'NotificationSettings',
|
|
SavedFilter: 'SavedFilter',
|
|
MyView: 'MyView',
|
|
PinnedIssue: 'PinnedIssue',
|
|
AlertIntegration: 'AlertIntegration',
|
|
AlertTrace: 'AlertTrace',
|
|
TreeFollow: 'TreeFollow',
|
|
IssueVisit: 'IssueVisit',
|
|
IssueReminder: 'IssueReminder',
|
|
MyDay: 'MyDay',
|
|
ReleaseNotesShare: 'ReleaseNotesShare',
|
|
Sprint: 'SprintReadModel',
|
|
GitIntegration: 'GitIntegration',
|
|
OutgoingWebhook: 'OutgoingWebhook',
|
|
Automation: 'Automation',
|
|
AutomationRun: 'AutomationRun',
|
|
CalendarFeed: 'CalendarFeed',
|
|
DashboardShare: 'DashboardShare',
|
|
PushSubscription: 'PushSubscription',
|
|
} as const;
|
|
|
|
/*
|
|
* Every collection keeps the whole read model (`strict: false`); the props
|
|
* declared here are the ones with an index or a shape worth stating. `id` is
|
|
* the domain id (unique, what every query goes by); `_id` is Mongo's own.
|
|
*/
|
|
const LOOSE = { strict: false, versionKey: false } as const;
|
|
|
|
@Schema({ _id: false })
|
|
class UserRefProp {
|
|
@Prop({ type: String }) id!: string;
|
|
@Prop({ type: String }) name!: string;
|
|
}
|
|
const UserRefSchema = SchemaFactory.createForClass(UserRefProp);
|
|
|
|
@Schema({ _id: false })
|
|
class MemberProp {
|
|
@Prop({ type: UserRefSchema }) user!: UserRefProp;
|
|
@Prop({ type: String }) role!: string;
|
|
}
|
|
|
|
@Schema({ _id: false })
|
|
class LinkProp {
|
|
@Prop({ type: String }) type!: string;
|
|
@Prop({ type: String }) direction!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
@Prop({ type: String }) issueKey!: string;
|
|
}
|
|
const LinkSchema = SchemaFactory.createForClass(LinkProp);
|
|
|
|
@Schema({ _id: false })
|
|
class NamedRefProp {
|
|
@Prop({ type: String }) id!: string;
|
|
@Prop({ type: String }) name!: string;
|
|
}
|
|
|
|
@Schema({ _id: false })
|
|
class ParentProp {
|
|
@Prop({ type: String }) id!: string;
|
|
@Prop({ type: String }) key!: string;
|
|
@Prop({ type: String }) title!: string;
|
|
}
|
|
|
|
@Schema({ _id: false })
|
|
class ChecklistItemProp {
|
|
@Prop({ type: String }) id!: string;
|
|
@Prop({ type: String }) text!: string;
|
|
@Prop({ type: Boolean }) done!: boolean;
|
|
}
|
|
|
|
@Schema({ _id: false })
|
|
class AttachmentProp {
|
|
@Prop({ type: String }) id!: string;
|
|
@Prop({ type: String }) fileName!: string;
|
|
@Prop({ type: String }) mimeType!: string;
|
|
@Prop({ type: Number }) size!: number;
|
|
@Prop({ type: UserRefSchema }) uploadedBy!: UserRefProp;
|
|
@Prop({ type: String }) uploadedAt!: string;
|
|
@Prop({ type: String }) commentId!: string;
|
|
}
|
|
|
|
@Schema({ ...LOOSE, collection: 'projects_view' })
|
|
class ProjectDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, unique: true }) key!: string;
|
|
@Prop({ type: Boolean, index: true }) isPublic!: boolean;
|
|
@Prop({ type: [String], index: true }) memberIds!: string[];
|
|
@Prop({ type: UserRefSchema }) owner!: UserRefProp;
|
|
@Prop({ type: [SchemaFactory.createForClass(MemberProp)] })
|
|
members!: MemberProp[];
|
|
@Prop({ type: Number }) version!: number;
|
|
}
|
|
export const ProjectSchema = SchemaFactory.createForClass(ProjectDocument);
|
|
|
|
@Schema({ ...LOOSE, collection: 'issues_view' })
|
|
class IssueDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) projectId!: string;
|
|
@Prop({ type: String, unique: true }) key!: string;
|
|
@Prop({ type: String }) status!: string;
|
|
@Prop({ type: String }) statusCategory!: string;
|
|
@Prop({ type: UserRefSchema }) reporter!: UserRefProp;
|
|
@Prop({ type: UserRefSchema }) assignee!: UserRefProp | null;
|
|
@Prop({ type: SchemaFactory.createForClass(NamedRefProp) })
|
|
release!: NamedRefProp | null;
|
|
@Prop({ type: SchemaFactory.createForClass(NamedRefProp) })
|
|
sprint!: NamedRefProp | null;
|
|
@Prop({ type: [LinkSchema] }) outwardLinks!: LinkProp[];
|
|
@Prop({ type: [LinkSchema] }) inwardLinks!: LinkProp[];
|
|
@Prop({ type: [UserRefSchema] }) watchers!: UserRefProp[];
|
|
@Prop({ type: SchemaFactory.createForClass(ParentProp) })
|
|
parent!: ParentProp | null;
|
|
@Prop({ type: [SchemaFactory.createForClass(ChecklistItemProp)] })
|
|
checklist!: ChecklistItemProp[];
|
|
@Prop({ type: [SchemaFactory.createForClass(AttachmentProp)] })
|
|
attachments!: AttachmentProp[];
|
|
@Prop({ type: [String] }) labels!: string[];
|
|
/** Field id → value: filtered as `customFields.<id>`. */
|
|
@Prop({ type: Object }) customFields!: Record<string, string>;
|
|
@Prop({ type: Number }) version!: number;
|
|
}
|
|
export const IssueSchema = SchemaFactory.createForClass(IssueDocument);
|
|
IssueSchema.index({ projectId: 1, status: 1, updatedAt: -1 });
|
|
IssueSchema.index({ projectId: 1, statusCategory: 1 });
|
|
IssueSchema.index({ 'assignee.id': 1 });
|
|
IssueSchema.index({ 'watchers.id': 1 });
|
|
IssueSchema.index({ 'parent.id': 1 });
|
|
IssueSchema.index({ projectId: 1, rank: 1 });
|
|
IssueSchema.index({ 'release.id': 1 });
|
|
IssueSchema.index({ 'sprint.id': 1 });
|
|
IssueSchema.index({ trashedAt: 1 }, { sparse: true });
|
|
IssueSchema.index({ title: 'text', description: 'text', key: 'text' });
|
|
|
|
@Schema({ ...LOOSE, collection: 'comments_view' })
|
|
class CommentDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) issueId!: string;
|
|
@Prop({ type: String, index: true }) replyTo!: string | null;
|
|
@Prop({ type: UserRefSchema }) author!: UserRefProp;
|
|
@Prop({ type: Number }) version!: number;
|
|
}
|
|
export const CommentSchema = SchemaFactory.createForClass(CommentDocument);
|
|
CommentSchema.index({ issueId: 1, createdAt: 1 });
|
|
CommentSchema.index(
|
|
{ projectId: 1, decidedAt: -1 },
|
|
{ partialFilterExpression: { decision: true } },
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'releases_view' })
|
|
class ReleaseDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) projectId!: string;
|
|
@Prop({ type: Number }) version!: number;
|
|
}
|
|
export const ReleaseSchema = SchemaFactory.createForClass(ReleaseDocument);
|
|
|
|
@Schema({ ...LOOSE, collection: 'sprints_view' })
|
|
class SprintDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) projectId!: string;
|
|
@Prop({ type: String }) status!: string;
|
|
@Prop({ type: Number }) version!: number;
|
|
}
|
|
export const SprintSchema = SchemaFactory.createForClass(SprintDocument);
|
|
|
|
@Schema({ ...LOOSE, collection: 'activity' })
|
|
class ActivityDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String }) projectId!: string;
|
|
@Prop({ type: String }) issueId!: string | null;
|
|
@Prop({ type: UserRefSchema }) actor!: UserRefProp;
|
|
}
|
|
export const ActivitySchema = SchemaFactory.createForClass(ActivityDocument);
|
|
ActivitySchema.index({ projectId: 1, occurredAt: -1 });
|
|
ActivitySchema.index({ issueId: 1, occurredAt: -1 });
|
|
|
|
/** Every status and release step of every ticket (statistics). */
|
|
@Schema({ ...LOOSE, collection: 'issue_timeline' })
|
|
class TimelineDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String }) projectId!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
@Prop({ type: String }) at!: string;
|
|
}
|
|
export const TimelineSchema = SchemaFactory.createForClass(TimelineDocument);
|
|
TimelineSchema.index({ projectId: 1, at: 1 });
|
|
|
|
@Schema({ ...LOOSE, collection: 'notifications' })
|
|
class NotificationDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String }) userId!: string;
|
|
@Prop({ type: String }) readAt!: string | null;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
@Prop({ type: UserRefSchema }) actor!: UserRefProp;
|
|
}
|
|
export const NotificationSchema =
|
|
SchemaFactory.createForClass(NotificationDocument);
|
|
NotificationSchema.index({ userId: 1, createdAt: -1 });
|
|
NotificationSchema.index({ userId: 1, readAt: 1 });
|
|
|
|
@Schema({ ...LOOSE, collection: 'notification_settings' })
|
|
class NotificationSettingsDocument {
|
|
/** The user's id. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: Boolean }) email!: boolean;
|
|
}
|
|
export const NotificationSettingsSchema = SchemaFactory.createForClass(
|
|
NotificationSettingsDocument,
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'saved_filters' })
|
|
class SavedFilterDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String }) projectId!: string;
|
|
@Prop({ type: UserRefSchema }) owner!: UserRefProp;
|
|
@Prop({ type: String }) name!: string;
|
|
@Prop({ type: Boolean }) shared!: boolean;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const SavedFilterSchema =
|
|
SchemaFactory.createForClass(SavedFilterDocument);
|
|
SavedFilterSchema.index({ projectId: 1, 'owner.id': 1 });
|
|
SavedFilterSchema.index({ projectId: 1, shared: 1 });
|
|
|
|
@Schema({ ...LOOSE, collection: 'my_views' })
|
|
class MyViewDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) ownerId!: string;
|
|
@Prop({ type: String }) name!: string;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const MyViewSchema = SchemaFactory.createForClass(MyViewDocument);
|
|
|
|
@Schema({ ...LOOSE, collection: 'pinned_issues' })
|
|
class PinnedIssueDocument {
|
|
@Prop({ type: String }) ownerId!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
@Prop({ type: String }) pinnedAt!: string;
|
|
}
|
|
export const PinnedIssueSchema =
|
|
SchemaFactory.createForClass(PinnedIssueDocument);
|
|
PinnedIssueSchema.index({ ownerId: 1, issueId: 1 }, { unique: true });
|
|
|
|
@Schema({ ...LOOSE, collection: 'alert_integrations' })
|
|
class AlertIntegrationDocument {
|
|
@Prop({ type: String, required: true, unique: true }) projectId!: string;
|
|
@Prop({ type: String }) token!: string;
|
|
}
|
|
export const AlertIntegrationSchema = SchemaFactory.createForClass(
|
|
AlertIntegrationDocument,
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'alert_traces' })
|
|
class AlertTraceDocument {
|
|
@Prop({ type: String }) projectId!: string;
|
|
@Prop({ type: String }) fingerprint!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
}
|
|
export const AlertTraceSchema =
|
|
SchemaFactory.createForClass(AlertTraceDocument);
|
|
AlertTraceSchema.index({ projectId: 1, fingerprint: 1 }, { unique: true });
|
|
|
|
@Schema({ ...LOOSE, collection: 'tree_follows' })
|
|
class TreeFollowDocument {
|
|
@Prop({ type: String }) userId!: string;
|
|
@Prop({ type: String, index: true }) issueId!: string;
|
|
}
|
|
export const TreeFollowSchema =
|
|
SchemaFactory.createForClass(TreeFollowDocument);
|
|
TreeFollowSchema.index({ userId: 1, issueId: 1 }, { unique: true });
|
|
|
|
@Schema({ ...LOOSE, collection: 'issue_visits' })
|
|
class IssueVisitDocument {
|
|
@Prop({ type: String }) userId!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
@Prop({ type: String }) seenAt!: string;
|
|
}
|
|
export const IssueVisitSchema =
|
|
SchemaFactory.createForClass(IssueVisitDocument);
|
|
IssueVisitSchema.index({ userId: 1, issueId: 1 }, { unique: true });
|
|
|
|
@Schema({ ...LOOSE, collection: 'issue_reminders' })
|
|
class IssueReminderDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String }) userId!: string;
|
|
@Prop({ type: String }) issueId!: string;
|
|
@Prop({ type: String }) at!: string;
|
|
@Prop({ type: String, default: null }) sentAt!: string | null;
|
|
}
|
|
export const IssueReminderSchema = SchemaFactory.createForClass(
|
|
IssueReminderDocument,
|
|
);
|
|
IssueReminderSchema.index({ sentAt: 1, at: 1 });
|
|
IssueReminderSchema.index({ userId: 1, issueId: 1 });
|
|
|
|
@Schema({ ...LOOSE, collection: 'my_days' })
|
|
class MyDayDocument {
|
|
@Prop({ type: String }) userId!: string;
|
|
@Prop({ type: String }) day!: string;
|
|
@Prop({ type: [String] }) issueIds!: string[];
|
|
}
|
|
export const MyDaySchema = SchemaFactory.createForClass(MyDayDocument);
|
|
MyDaySchema.index({ userId: 1, day: -1 }, { unique: true });
|
|
|
|
@Schema({ ...LOOSE, collection: 'release_note_shares' })
|
|
class ReleaseNotesShareDocument {
|
|
@Prop({ type: String, required: true, unique: true }) releaseId!: string;
|
|
@Prop({ type: String, required: true, unique: true }) token!: string;
|
|
}
|
|
export const ReleaseNotesShareSchema = SchemaFactory.createForClass(
|
|
ReleaseNotesShareDocument,
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'git_integrations' })
|
|
class GitIntegrationDocument {
|
|
/** The project's id. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: [Object] }) repositories!: GitRepositoryDoc[];
|
|
@Prop({ type: String }) secret!: string;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const GitIntegrationSchema = SchemaFactory.createForClass(
|
|
GitIntegrationDocument,
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'outgoing_webhooks' })
|
|
class OutgoingWebhookDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) projectId!: string;
|
|
@Prop({ type: Boolean }) active!: boolean;
|
|
@Prop({ type: [String] }) events!: string[];
|
|
@Prop({ type: UserRefSchema }) createdBy!: UserRefProp;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const OutgoingWebhookSchema = SchemaFactory.createForClass(
|
|
OutgoingWebhookDocument,
|
|
);
|
|
|
|
@Schema({ ...LOOSE, collection: 'automations' })
|
|
class AutomationDocument {
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) projectId!: string;
|
|
@Prop({ type: Boolean }) active!: boolean;
|
|
@Prop({ type: UserRefSchema }) createdBy!: UserRefProp;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const AutomationSchema =
|
|
SchemaFactory.createForClass(AutomationDocument);
|
|
|
|
@Schema({ ...LOOSE, collection: 'automation_runs' })
|
|
class AutomationRunDocument {
|
|
/** `ruleId:eventKey`: a rule runs once per event. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: Date }) at!: Date;
|
|
}
|
|
export const AutomationRunSchema = SchemaFactory.createForClass(
|
|
AutomationRunDocument,
|
|
);
|
|
/** One per person: the secret of their calendar URL. */
|
|
@Schema({ ...LOOSE, collection: 'calendar_feeds' })
|
|
class CalendarFeedDocument {
|
|
/** The user's id. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, required: true, unique: true }) token!: string;
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const CalendarFeedSchema =
|
|
SchemaFactory.createForClass(CalendarFeedDocument);
|
|
|
|
/** One per project: the secret of its link for clients, and what it shows. */
|
|
@Schema({ ...LOOSE, collection: 'dashboard_shares' })
|
|
class DashboardShareDocument {
|
|
/** The project's id. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, required: true, unique: true }) token!: string;
|
|
@Prop({ type: [String] }) blocks!: string[];
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const DashboardShareSchema = SchemaFactory.createForClass(
|
|
DashboardShareDocument,
|
|
);
|
|
|
|
/** The devices where each person turned push on. */
|
|
@Schema({ ...LOOSE, collection: 'push_subscriptions' })
|
|
class PushSubscriptionDocument {
|
|
/** sha256 of the endpoint. */
|
|
@Prop({ type: String, required: true, unique: true }) id!: string;
|
|
@Prop({ type: String, index: true }) userId!: string;
|
|
@Prop({ type: String }) endpoint!: string;
|
|
@Prop({ type: Object }) keys!: { p256dh: string; auth: string };
|
|
@Prop({ type: String }) createdAt!: string;
|
|
}
|
|
export const PushSubscriptionSchema = SchemaFactory.createForClass(
|
|
PushSubscriptionDocument,
|
|
);
|
|
|
|
// Redeliveries happen within minutes; a month of memory is plenty.
|
|
AutomationRunSchema.index({ at: 1 }, { expireAfterSeconds: 30 * 86_400 });
|
|
|
|
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.Sprint, schema: SprintSchema },
|
|
{ name: ReadModels.GitIntegration, schema: GitIntegrationSchema },
|
|
{ name: ReadModels.OutgoingWebhook, schema: OutgoingWebhookSchema },
|
|
{ name: ReadModels.Automation, schema: AutomationSchema },
|
|
{ name: ReadModels.AutomationRun, schema: AutomationRunSchema },
|
|
{ name: ReadModels.Activity, schema: ActivitySchema },
|
|
{ name: ReadModels.Timeline, schema: TimelineSchema },
|
|
{ name: ReadModels.Notification, schema: NotificationSchema },
|
|
{ name: ReadModels.NotificationSettings, schema: NotificationSettingsSchema },
|
|
{ name: ReadModels.SavedFilter, schema: SavedFilterSchema },
|
|
{ name: ReadModels.MyView, schema: MyViewSchema },
|
|
{ name: ReadModels.PinnedIssue, schema: PinnedIssueSchema },
|
|
{ name: ReadModels.AlertIntegration, schema: AlertIntegrationSchema },
|
|
{ name: ReadModels.AlertTrace, schema: AlertTraceSchema },
|
|
{ name: ReadModels.TreeFollow, schema: TreeFollowSchema },
|
|
{ name: ReadModels.IssueVisit, schema: IssueVisitSchema },
|
|
{ name: ReadModels.IssueReminder, schema: IssueReminderSchema },
|
|
{ name: ReadModels.MyDay, schema: MyDaySchema },
|
|
{ name: ReadModels.ReleaseNotesShare, schema: ReleaseNotesShareSchema },
|
|
{ name: ReadModels.CalendarFeed, schema: CalendarFeedSchema },
|
|
{ name: ReadModels.DashboardShare, schema: DashboardShareSchema },
|
|
{ name: ReadModels.PushSubscription, schema: PushSubscriptionSchema },
|
|
];
|
|
|
|
/** A document as the model it holds: Mongo's own `_id` left out. */
|
|
export function fromDoc<T extends { id: string }>(
|
|
doc: T & { _id?: unknown },
|
|
): T {
|
|
const { _id: _ignored, ...rest } = doc;
|
|
return rest as T;
|
|
}
|
|
|
|
export function issueFromDoc(doc: IssueDoc): IssueReadModel {
|
|
const { _id: _ignored, outwardLinks, inwardLinks, ...rest } = doc;
|
|
return {
|
|
...rest,
|
|
links: [...(outwardLinks ?? []), ...(inwardLinks ?? [])],
|
|
// Tickets projected before attachments existed have none.
|
|
attachments: rest.attachments ?? [],
|
|
watchers: rest.watchers ?? [],
|
|
reviews: rest.reviews ?? [],
|
|
votes: rest.votes ?? 0,
|
|
voterIds: rest.voterIds ?? [],
|
|
parent: rest.parent?.id ? rest.parent : null,
|
|
rank: rest.rank ?? implicitRank(rest.number),
|
|
startDate: rest.startDate ?? null,
|
|
team: rest.team ?? null,
|
|
customFields: rest.customFields ?? {},
|
|
sprint: rest.sprint?.id ? rest.sprint : null,
|
|
// Links recorded before projects had several repositories name none.
|
|
devLinks: (rest.devLinks ?? []).map((link) => ({
|
|
...link,
|
|
repository: link.repository ?? null,
|
|
})),
|
|
trashedAt: rest.trashedAt ?? null,
|
|
checklist: rest.checklist ?? [],
|
|
};
|
|
}
|