From 059408f098a9307884cc34c7bc4d9d29296b7a7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:40:57 -0700 Subject: [PATCH 1/3] add v2 models --- server/src/db/v2/models/auth/Role.ts | 50 +++ server/src/db/v2/models/auth/User.ts | 108 +++++++ server/src/db/v2/models/checks/Check.ts | 297 ++++++++++++++++++ server/src/db/v2/models/index.ts | 33 ++ server/src/db/v2/models/invite/Invite.ts | 43 +++ .../db/v2/models/maintenance/Maintenance.ts | 39 +++ server/src/db/v2/models/monitors/Monitor.ts | 100 ++++++ .../src/db/v2/models/monitors/MonitorStats.ts | 77 +++++ .../NotificationChannel.ts | 50 +++ 9 files changed, 797 insertions(+) create mode 100644 server/src/db/v2/models/auth/Role.ts create mode 100644 server/src/db/v2/models/auth/User.ts create mode 100644 server/src/db/v2/models/checks/Check.ts create mode 100644 server/src/db/v2/models/index.ts create mode 100644 server/src/db/v2/models/invite/Invite.ts create mode 100644 server/src/db/v2/models/maintenance/Maintenance.ts create mode 100644 server/src/db/v2/models/monitors/Monitor.ts create mode 100644 server/src/db/v2/models/monitors/MonitorStats.ts create mode 100644 server/src/db/v2/models/notification-channel/NotificationChannel.ts diff --git a/server/src/db/v2/models/auth/Role.ts b/server/src/db/v2/models/auth/Role.ts new file mode 100644 index 000000000..1031de8b2 --- /dev/null +++ b/server/src/db/v2/models/auth/Role.ts @@ -0,0 +1,50 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IRole extends Document { + _id: Types.ObjectId; + name: string; + description?: string; + permissions: string[]; + isSystem: boolean; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +const roleSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + description: { + type: String, + trim: true, + maxlength: 200, + }, + + permissions: [ + { + type: String, + required: true, + }, + ], + isSystem: { + type: Boolean, + default: false, + }, + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +roleSchema.index({ name: 1 }, { unique: true }); + +export const Role = mongoose.model("Role_v2", roleSchema); diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts new file mode 100644 index 000000000..b72a29e70 --- /dev/null +++ b/server/src/db/v2/models/auth/User.ts @@ -0,0 +1,108 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface ITokenizedUser { + sub: string; + roles: string[]; +} +export interface IUser extends Document { + _id: Types.ObjectId; + email: string; + firstName: string; + lastName: string; + passwordHash: string; + roles: Types.ObjectId[]; + profile: { + avatar?: string; + bio?: string; + timezone: string; + locale: string; + }; + preferences: { + theme: "light" | "dark" | "system"; + }; + lastLoginAt?: Date; + isActive: boolean; + isVerified: boolean; + createdAt: Date; + updatedAt: Date; +} + +const userSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + firstName: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + lastName: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + passwordHash: { + type: String, + required: true, + }, + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role_v2", + }, + ], + profile: { + avatar: { + type: String, + }, + bio: { + type: String, + maxlength: 200, + }, + timezone: { + type: String, + default: "UTC", + }, + locale: { + type: String, + default: "en", + }, + }, + preferences: { + theme: { + type: String, + enum: ["light", "dark", "system"], + default: "system", + }, + }, + lastLoginAt: { + type: Date, + }, + isActive: { + type: Boolean, + default: true, + }, + isVerified: { + type: Boolean, + default: false, + }, + }, + { + timestamps: true, + toJSON: { + transform: function (_doc, ret) { + delete ret.passwordHash; + return ret; + }, + }, + } +); + +export const User = mongoose.model("User_v2", userSchema); diff --git a/server/src/db/v2/models/checks/Check.ts b/server/src/db/v2/models/checks/Check.ts new file mode 100644 index 000000000..83bbc9f91 --- /dev/null +++ b/server/src/db/v2/models/checks/Check.ts @@ -0,0 +1,297 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; +export interface ITimingPhases { + wait: number; + dns: number; + tcp: number; + tls: number; + request: number; + firstByte: number; + download: number; + total: number; +} + +export interface ICpuInfo { + physical_core: number; + logical_core: number; + frequency: number; + current_frequency: number; + temperature: number[]; // per-core temps + free_percent: number; + usage_percent: number; +} + +export interface IMemoryInfo { + total_bytes: number; + available_bytes: number; + used_bytes: number; + usage_percent: number; +} + +export interface IHostInfo { + os?: string; + platform?: string; + kernel_version?: string; + pretty_name?: string; +} + +export interface IDiskInfo { + device: string; + total_bytes: number; + free_bytes: number; + used_bytes: number; + usage_percent: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; +} + +export interface INetInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} + +export interface ICaptureInfo { + version?: string; + mode?: string; +} + +export interface ISystemInfo { + cpu: ICpuInfo; + memory: IMemoryInfo; + disk: IDiskInfo[]; + host: IHostInfo; + net: INetInfo[]; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} +export interface ILighthouseCategories { + accessibility?: { score?: number | null }; + "best-practices"?: { score?: number | null }; + seo?: { score?: number | null }; + performance?: { score?: number | null }; +} + +export interface ILighthouseResult { + categories?: ILighthouseCategories; + audits?: Record; +} + +export interface ICheckLighthouseFields { + accessibility: number; + bestPractices: number; + seo: number; + performance: number; + audits: { + cls: ILighthouseAudit; + si: ILighthouseAudit; + fcp: ILighthouseAudit; + lcp: ILighthouseAudit; + tbt: ILighthouseAudit; + }; +} + +export interface ICheck extends Document { + _id: Types.ObjectId; + monitorId: Types.ObjectId; + type: MonitorType; + status: MonitorStatus; + message: string; + responseTime?: number; // in ms + timings?: GotTimings; + httpStatusCode?: number; + errorMessage?: string; + ack: boolean; + ackAt?: Date; + ackBy?: Types.ObjectId; + expiry: Date; + createdAt: Date; + updatedAt: Date; + system?: ISystemInfo; + capture?: ICaptureInfo; + lighthouse?: ICheckLighthouseFields; +} + +const CheckSchema = new Schema( + { + monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true }, + + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + message: { type: String, trim: true }, + responseTime: { type: Number }, + timings: { + start: { type: Date }, + socket: { type: Date }, + lookup: { type: Date }, + connect: { type: Date }, + secureConnect: { type: Date }, + response: { type: Date }, + end: { type: Date }, + phases: { + wait: { type: Number }, + dns: { type: Number }, + tcp: { type: Number }, + tls: { type: Number }, + request: { type: Number }, + firstByte: { type: Number }, + download: { type: Number }, + total: { type: Number }, + }, + }, + system: { + type: { + cpu: { + physical_core: { type: Number }, + logical_core: { type: Number }, + frequency: { type: Number }, + current_frequency: { type: Number }, + temperature: [{ type: Number }], + free_percent: { type: Number }, + usage_percent: { type: Number }, + }, + memory: { + total_bytes: { type: Number }, + available_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + }, + disk: [ + { + device: { type: String }, + total_bytes: { type: Number }, + free_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + total_inodes: { type: Number }, + free_inodes: { type: Number }, + used_inodes: { type: Number }, + inodes_usage_percent: { type: Number }, + read_bytes: { type: Number }, + write_bytes: { type: Number }, + read_time: { type: Number }, + write_time: { type: Number }, + }, + ], + host: { + os: { type: String }, + platform: { type: String }, + kernel_version: { type: String }, + pretty_name: { type: String }, + }, + net: [ + { + name: { type: String }, + bytes_sent: { type: Number }, + bytes_recv: { type: Number }, + packets_sent: { type: Number }, + packets_recv: { type: Number }, + err_in: { type: Number }, + err_out: { type: Number }, + drop_in: { type: Number }, + drop_out: { type: Number }, + fifo_in: { type: Number }, + fifo_out: { type: Number }, + }, + ], + }, + required: false, + }, + + capture: { + type: { + version: { type: String }, + mode: { type: String }, + }, + required: false, + }, + lighthouse: { + accessibility: { type: Number, required: false }, + bestPractices: { type: Number, required: false }, + seo: { type: Number, required: false }, + performance: { type: Number, required: false }, + audits: { + cls: { + type: Object, + }, + si: { + type: Object, + }, + fcp: { + type: Object, + }, + lcp: { + type: Object, + }, + tbt: { + type: Object, + }, + }, + type: { + accessibility: { type: Number }, + bestPractices: { type: Number }, + seo: { type: Number }, + performance: { type: Number }, + audits: { + cls: { type: Object }, + si: { type: Object }, + fcp: { type: Object }, + lcp: { type: Object }, + tbt: { type: Object }, + }, + }, + required: false, + }, + + httpStatusCode: { type: Number }, + errorMessage: { type: String, trim: true }, + ack: { type: Boolean, required: true, default: false }, + ackAt: { type: Date }, + ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, + }, + }, + { timestamps: true } +); + +CheckSchema.index({ monitorId: 1, createdAt: -1 }); +CheckSchema.index({ status: 1 }); +CheckSchema.index({ status: 1, ack: 1 }); +CheckSchema.index({ ack: 1, ackAt: 1 }); +CheckSchema.index({ createdAt: -1 }); + +export const Check = mongoose.model("Check_v2", CheckSchema); diff --git a/server/src/db/v2/models/index.ts b/server/src/db/v2/models/index.ts new file mode 100644 index 000000000..fbc05c10f --- /dev/null +++ b/server/src/db/v2/models/index.ts @@ -0,0 +1,33 @@ +export { User } from "./auth/User.js"; +export type { IUser } from "./auth/User.js"; +export type { ITokenizedUser } from "./auth/User.js"; +export { Role } from "./auth/Role.js"; +export type { IRole } from "./auth/Role.js"; +export { connectDatabase, disconnectDatabase } from "../index.js"; +export { Monitor } from "./monitors/Monitor.js"; +export { MonitorStatuses } from "./monitors/Monitor.js"; +export type { IMonitor } from "./monitors/Monitor.js"; +export { Check } from "./checks/Check.js"; +export type { + ICheck, + ISystemInfo, + ICaptureInfo, + INetInfo, + IDiskInfo, + IHostInfo, + IMemoryInfo, + ICpuInfo, + ILighthouseAudit, + ITimingPhases, + ILighthouseCategories, + ILighthouseResult, + ICheckLighthouseFields, +} from "./checks/Check.js"; +export type { IMonitorStats } from "./monitors/MonitorStats.js"; +export { MonitorStats } from "./monitors/MonitorStats.js"; +export type { INotificationChannel } from "./notification-channel/NotificationChannel.js"; +export { NotificationChannel } from "./notification-channel/NotificationChannel.js"; +export type { IMaintenance } from "./maintenance/Maintenance.js"; +export { Maintenance } from "./maintenance/Maintenance.js"; +export type { IInvite } from "./invite/Invite.js"; +export { Invite } from "./invite/Invite.js"; diff --git a/server/src/db/v2/models/invite/Invite.ts b/server/src/db/v2/models/invite/Invite.ts new file mode 100644 index 000000000..2bc569900 --- /dev/null +++ b/server/src/db/v2/models/invite/Invite.ts @@ -0,0 +1,43 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IInvite extends Document { + _id: Types.ObjectId; + email: string; + tokenHash: string; + roles: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + expiry: Date; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role_v2", + required: true, + }, + ], + tokenHash: { type: String, required: true, unique: true }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24, + }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +export const Invite = mongoose.model("Invite_v2", InviteSchema); diff --git a/server/src/db/v2/models/maintenance/Maintenance.ts b/server/src/db/v2/models/maintenance/Maintenance.ts new file mode 100644 index 000000000..c2ce33a2b --- /dev/null +++ b/server/src/db/v2/models/maintenance/Maintenance.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IMaintenance extends Document { + _id: Types.ObjectId; + name: string; + isActive: boolean; + monitors: Types.ObjectId[]; + startTime: Date; + endTime: Date; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MaintenanceSchema = new Schema( + { + name: { type: String, required: true, trim: true }, + isActive: { type: Boolean, required: true, default: true }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor_v2", + required: true, + }, + ], + startTime: { type: Date, required: true }, + endTime: { type: Date, required: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +MaintenanceSchema.index({ isActive: 1 }); +MaintenanceSchema.index({ startTime: 1 }); +MaintenanceSchema.index({ endTime: 1 }); + +export const Maintenance = mongoose.model("Maintenance_v2", MaintenanceSchema); diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts new file mode 100644 index 000000000..055d36a1b --- /dev/null +++ b/server/src/db/v2/models/monitors/Monitor.ts @@ -0,0 +1,100 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { Check, MonitorStats } from "../index.js"; + +export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; +export interface IMonitor extends Document { + _id: Types.ObjectId; + name: string; + url: string; + secret?: string; + type: MonitorType; + interval: number; // in ms + isActive: boolean; + status: MonitorStatus; + n: number; // Number of consecutive successes required to change status + lastCheckedAt?: Date; + latestChecks: { + status: MonitorStatus; + responseTime: number; + checkedAt: Date; + }[]; + notificationChannels?: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MonitorSchema = new Schema( + { + name: { type: String, required: true, trim: true, maxlength: 100 }, + url: { type: String, required: true, trim: true }, + secret: { type: String, required: false }, + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + interval: { type: Number, required: true, default: 60000 }, + isActive: { type: Boolean, required: true, default: true }, + status: { + type: String, + required: true, + default: "initializing", + enum: MonitorStatuses, + }, + n: { type: Number, required: true, default: 1 }, + lastCheckedAt: { type: Date }, + latestChecks: { + type: [ + { + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + responseTime: { type: Number, required: true }, + checkedAt: { type: Date, required: true }, + }, + ], + default: [], + }, + notificationChannels: [ + { + type: Schema.Types.ObjectId, + ref: "NotificationChannel_v2", + required: false, + default: [], + }, + ], + + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) { + try { + const monitorId = this._id; + await Check.deleteMany({ monitorId }); + await MonitorStats.deleteMany({ monitorId }); + next(); + } catch (error: any) { + next(error); + } +}); + +MonitorSchema.index({ isActive: 1 }); +MonitorSchema.index({ status: 1 }); +MonitorSchema.index({ type: 1 }); +MonitorSchema.index({ lastCheckedAt: 1 }); +MonitorSchema.index({ isActive: 1, status: 1 }); +MonitorSchema.index({ createdBy: 1 }); +MonitorSchema.index({ updatedBy: 1 }); + +export const Monitor = mongoose.model("Monitor_v2", MonitorSchema); diff --git a/server/src/db/v2/models/monitors/MonitorStats.ts b/server/src/db/v2/models/monitors/MonitorStats.ts new file mode 100644 index 000000000..56e76f9ab --- /dev/null +++ b/server/src/db/v2/models/monitors/MonitorStats.ts @@ -0,0 +1,77 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { MonitorStatus, MonitorStatuses } from "./Monitor.js"; + +export interface IMonitorStats extends mongoose.Document { + monitorId: mongoose.Types.ObjectId; + avgResponseTime: number; + maxResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure: number; + currentStreak: number; + currentStreakStatus: MonitorStatus; + currentStreakStartedAt: number; + createdAt: Date; + updatedAt: Date; +} + +const MonitorStatsSchema = new Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor_v2", + immutable: true, + index: true, + }, + avgResponseTime: { + type: Number, + default: 0, + }, + maxResponseTime: { + type: Number, + default: 0, + }, + lastResponseTime: { + type: Number, + default: 0, + }, + totalChecks: { + type: Number, + default: 0, + }, + totalUpChecks: { + type: Number, + default: 0, + }, + totalDownChecks: { + type: Number, + default: 0, + }, + uptimePercentage: { + type: Number, + default: 0, + }, + lastCheckTimestamp: { + type: Number, + default: 0, + }, + timeOfLastFailure: { + type: Number, + default: 0, + }, + currentStreak: { type: Number, required: false, default: 0 }, + currentStreakStatus: { + type: String, + required: false, + enum: MonitorStatuses, + }, + currentStreakStartedAt: { type: Number, required: false }, + }, + { timestamps: true } +); + +export const MonitorStats = mongoose.model("MonitorStats_v2", MonitorStatsSchema); diff --git a/server/src/db/v2/models/notification-channel/NotificationChannel.ts b/server/src/db/v2/models/notification-channel/NotificationChannel.ts new file mode 100644 index 000000000..e77639608 --- /dev/null +++ b/server/src/db/v2/models/notification-channel/NotificationChannel.ts @@ -0,0 +1,50 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; +export type ChannelType = (typeof ChannelTypes)[number]; + +export interface INotificationChannelConfig { + url?: string; // For webhook, slack, discord + emailAddress?: string; // For email +} + +export interface INotificationChannel { + _id: Types.ObjectId; + name: string; + type: ChannelType; + config: INotificationChannelConfig; + isActive: boolean; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationChannelConfigSchema = new Schema( + { + url: { type: String, required: false }, + emailAddress: { type: String, required: false }, + }, + { _id: false, strict: "throw" } +); + +const NotificationChannelSchema = new Schema( + { + name: { type: String, required: true, trim: true }, + type: { + type: String, + required: true, + enum: ChannelTypes, + }, + config: { type: NotificationChannelConfigSchema, required: true }, + isActive: { type: Boolean, required: true, default: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +NotificationChannelSchema.index({ isActive: 1 }); +NotificationChannelSchema.index({ type: 1 }); +NotificationChannelSchema.index({ type: 1, isActive: 1 }); + +export const NotificationChannel = mongoose.model("NotificationChannel_v2", NotificationChannelSchema); From c0ff162ed925de8f70280ce40e043a4dfea50915 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:46:32 -0700 Subject: [PATCH 2/3] remove v2 files --- .../src/controllers/v2/MonitorController.ts | 157 ------ server/src/db/v2/index.ts | 25 + server/src/db/v2/models/auth/User.ts | 6 - .../src/service/v2/business/CheckService.ts | 132 ----- .../src/service/v2/business/MonitorService.ts | 464 ------------------ 5 files changed, 25 insertions(+), 759 deletions(-) delete mode 100644 server/src/controllers/v2/MonitorController.ts create mode 100644 server/src/db/v2/index.ts delete mode 100644 server/src/service/v2/business/CheckService.ts delete mode 100644 server/src/service/v2/business/MonitorService.ts diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts deleted file mode 100644 index 6493b7f44..000000000 --- a/server/src/controllers/v2/MonitorController.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import ApiError from "../../utils/ApiError.js"; -import MonitorService from "../../service/v2/business/MonitorService.js"; -import { MonitorType } from "../../db/v1/models/Monitor.js"; -class MonitorController { - private monitorService: MonitorService; - constructor(monitorService: MonitorService) { - this.monitorService = monitorService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const monitor = await this.monitorService.create(tokenizedUser, req.body); - res.status(201).json({ - message: "Monitor created successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const range = req.query.range; - if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400); - - let monitor; - - const status = req.query.status; - if (status && typeof status !== "string") { - throw new ApiError("Status query parameter must be a string", 400); - } - - if (req.query.embedChecks === "true") { - monitor = await this.monitorService.getEmbedChecks(id, range, status); - } else { - monitor = await this.monitorService.get(id); - } - - res.status(200).json({ - message: "Monitor retrieved successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - let monitors; - if (req.query.embedChecks === "true") { - const page = Math.max(1, Number(req.query.page) || 1); - const limit = Math.max(1, Number(req.query.limit) || 10); - const type: MonitorType[] = req.query.type as MonitorType[]; - - monitors = await this.monitorService.getAllEmbedChecks(page, limit, type); - } else { - monitors = await this.monitorService.getAll(); - } - - res.status(200).json({ - message: "Monitors retrieved successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - - toggleActive = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.toggleActive(id, tokenizedUser); - res.status(200).json({ - message: "Monitor paused/unpaused successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - update = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.update(tokenizedUser, id, req.body); - res.status(200).json({ - message: "Monitor updated successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - await this.monitorService.delete(id); - - res.status(200).json({ - message: "Monitor deleted successfully", - }); - } catch (error) { - next(error); - } - }; -} - -export default MonitorController; diff --git a/server/src/db/v2/index.ts b/server/src/db/v2/index.ts new file mode 100644 index 000000000..2cd194283 --- /dev/null +++ b/server/src/db/v2/index.ts @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; + +const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate"; + +export const connectDatabase = async (): Promise => { + try { + await mongoose.connect(MONGODB_URI); + console.log("Connected to MongoDB"); + return true; + } catch (error) { + console.error("MongoDB connection error:", error); + process.exit(1); + } +}; + +export const disconnectDatabase = async (): Promise => { + try { + await mongoose.disconnect(); + console.log("Disconnected from MongoDB"); + return true; + } catch (error) { + console.error("MongoDB disconnection error:", error); + return false; + } +}; diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts index b72a29e70..25b9794c7 100644 --- a/server/src/db/v2/models/auth/User.ts +++ b/server/src/db/v2/models/auth/User.ts @@ -96,12 +96,6 @@ const userSchema = new Schema( }, { timestamps: true, - toJSON: { - transform: function (_doc, ret) { - delete ret.passwordHash; - return ret; - }, - }, } ); diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts deleted file mode 100644 index 181fe048d..000000000 --- a/server/src/service/v2/business/CheckService.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { json } from "stream/consumers"; -import { ICheck, Check, Monitor, ISystemInfo, ICaptureInfo } from "../../../db/v1/models/index.js"; -import { MonitorType } from "../../../db/v1/models/Monitor.js"; -import { StatusResponse } from "../infrastructure/NetworkService.js"; -import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; -import mongoose from "mongoose"; - -export interface ICheckService { - buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; - cleanupOrphanedChecks: () => Promise; -} - -class CheckService implements ICheckService { - private isCapturePayload = (payload: any): payload is ICapturePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("data" in payload) || typeof payload.data !== "object") { - return false; - } - - const data = payload.data as Partial; - if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { - return false; - } - s; - - if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { - return false; - } - - if (data.disk && !Array.isArray(data.disk)) { - return false; - } - if (data.net && !Array.isArray(data.net)) { - return false; - } - - if (!("capture" in payload) || typeof payload.capture !== "object") return false; - const capture = payload.capture as Record; - if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; - - return true; - }; - - private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { - return false; - } - return true; - }; - - private buildBaseCheck = (statusResponse: StatusResponse) => { - const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); - const check = new Check({ - monitorId: monitorId, - type: statusResponse?.type, - status: statusResponse?.status, - message: statusResponse?.message, - responseTime: statusResponse?.responseTime, - timings: statusResponse?.timings, - }); - return check; - }; - - private buildInfrastructureCheck = (statusResponse: StatusResponse) => { - if (!this.isCapturePayload(statusResponse.payload)) { - throw new Error("Invalid payload for infrastructure monitor"); - } - const check = this.buildBaseCheck(statusResponse); - check.system = statusResponse.payload.data; - check.capture = statusResponse.payload.capture; - return check; - }; - - private buildPagespeedCheck = (statusResponse: StatusResponse) => { - if (!this.isPagespeedPayload(statusResponse.payload)) { - throw new Error("Invalid payload for pagespeed monitor"); - } - const check = this.buildBaseCheck(statusResponse); - const lighthouseResult = statusResponse?.payload?.lighthouseResult; - check.lighthouse = { - accessibility: lighthouseResult?.categories?.accessibility?.score || 0, - bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, - seo: lighthouseResult?.categories?.seo?.score || 0, - performance: lighthouseResult?.categories?.performance?.score || 0, - audits: { - cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, - si: lighthouseResult?.audits?.["speed-index"] || {}, - fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, - lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, - tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, - }, - }; - return check; - }; - - buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { - switch (type) { - case "infrastructure": - return this.buildInfrastructureCheck(statusResponse as StatusResponse); - - case "pagespeed": - return this.buildPagespeedCheck(statusResponse as StatusResponse); - case "http": - case "https": - return this.buildBaseCheck(statusResponse); - - case "ping": - return this.buildBaseCheck(statusResponse); - default: - throw new Error(`Unsupported monitor type: ${type}`); - } - }; - - cleanupOrphanedChecks = async () => { - try { - const monitorIds = await Monitor.find().distinct("_id"); - const result = await Check.deleteMany({ - monitorId: { $nin: monitorIds }, - }); - console.log(`Deleted ${result.deletedCount} orphaned Checks.`); - return true; - } catch (error) { - console.error("Error cleaning up orphaned Checks:", error); - return false; - } - }; -} - -export default CheckService; diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts deleted file mode 100644 index 6e48e79e5..000000000 --- a/server/src/service/v2/business/MonitorService.ts +++ /dev/null @@ -1,464 +0,0 @@ -import mongoose from "mongoose"; - -import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v1/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; -import { IJobQueue } from "../infrastructure/JobQueue.js"; -import { MonitorWithChecksResponse } from "../../../types/monitor-response-with-checks.js"; -import { MonitorStatus, MonitorType } from "../../../db/v1/models/monitors/Monitor.js"; -export interface IMonitorService { - create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise; - getAll: () => Promise; - getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise; - get: (monitorId: string) => Promise; - getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise; - toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise; - update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise; - delete: (monitorId: string) => Promise; -} - -class MonitorService implements IMonitorService { - private jobQueue: IJobQueue; - constructor(jobQueue: IJobQueue) { - this.jobQueue = jobQueue; - } - - create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => { - const monitor = await Monitor.create({ - ...monitorData, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - await MonitorStats.create({ - monitorId: monitor._id, - currentStreakStartedAt: Date.now(), - }); - await this.jobQueue.addJob(monitor); - return monitor; - }; - - getAll = async () => { - return Monitor.find(); - }; - - getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => { - const skip = (page - 1) * limit; - let find = {}; - if (type.length > 0) find = { type: { $in: type } }; - const monitors = await Monitor.find(find).skip(skip).limit(limit); - return monitors; - }; - - get = async (monitorId: string) => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - return monitor; - }; - - private getStartDate(range: string): Date { - const now = new Date(); - switch (range) { - case "30m": - return new Date(now.getTime() - 30 * 60 * 1000); - case "24h": - return new Date(now.getTime() - 24 * 60 * 60 * 1000); - case "7d": - return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - case "30d": - return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getDateFormat(range: string): string { - switch (range) { - case "30m": - return "%Y-%m-%dT%H:%M:00Z"; - case "24h": - case "7d": - return "%Y-%m-%dT%H:00:00Z"; - case "30d": - return "%Y-%m-%d"; - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getBaseGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - }; - }; - - private getBaseProjection = (): object => { - return { status: 1, responseTime: 1, createdAt: 1 }; - }; - - private getPageSpeedGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - accessibility: { $avg: "$lighthouse.accessibility" }, - bestPractices: { $avg: "$lighthouse.bestPractices" }, - seo: { $avg: "$lighthouse.seo" }, - performance: { $avg: "$lighthouse.performance" }, - cls: { $avg: "$lighthouse.audits.cls.score" }, - si: { $avg: "$lighthouse.audits.si.score" }, - fcp: { $avg: "$lighthouse.audits.fcp.score" }, - lcp: { $avg: "$lighthouse.audits.lcp.score" }, - tbt: { $avg: "$lighthouse.audits.tbt.score" }, - }; - }; - - private getPageSpeedProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["lighthouse.accessibility"] = 1; - projectStage["lighthouse.seo"] = 1; - projectStage["lighthouse.bestPractices"] = 1; - projectStage["lighthouse.performance"] = 1; - projectStage["lighthouse.audits.cls.score"] = 1; - projectStage["lighthouse.audits.si.score"] = 1; - projectStage["lighthouse.audits.fcp.score"] = 1; - projectStage["lighthouse.audits.lcp.score"] = 1; - projectStage["lighthouse.audits.tbt.score"] = 1; - return projectStage; - }; - - private getInfraGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - physicalCores: { $last: "$system.cpu.physical_core" }, - logicalCores: { $last: "$system.cpu.logical_core" }, - frequency: { $avg: "$system.cpu.frequency" }, - currentFrequency: { $last: "$system.cpu.current_frequency" }, - tempsArrays: { $push: "$system.cpu.temperature" }, - freePercent: { $avg: "$system.cpu.free_percent" }, - usedPercent: { $avg: "$system.cpu.usage_percent" }, - total_bytes: { $last: "$system.memory.total_bytes" }, - available_bytes: { $last: "$system.memory.available_bytes" }, - used_bytes: { $last: "$system.memory.used_bytes" }, - memory_usage_percent: { $avg: "$system.memory.usage_percent" }, - disksArray: { $push: "$system.disk" }, - os: { $last: "$system.host.os" }, - platform: { $last: "$system.host.platform" }, - kernel_version: { $last: "$system.host.kernel_version" }, - pretty_name: { $last: "$system.host.pretty_name" }, - netsArray: { $push: "$system.net" }, - }; - }; - - private getInfraProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["system.cpu.physical_core"] = 1; - projectStage["system.cpu.logical_core"] = 1; - projectStage["system.cpu.frequency"] = 1; - projectStage["system.cpu.current_frequency"] = 1; - projectStage["system.cpu.temperature"] = 1; - projectStage["system.cpu.free_percent"] = 1; - projectStage["system.cpu.usage_percent"] = 1; - projectStage["system.memory.total_bytes"] = 1; - projectStage["system.memory.available_bytes"] = 1; - projectStage["system.memory.used_bytes"] = 1; - projectStage["system.memory.usage_percent"] = 1; - projectStage["system.disk"] = 1; - projectStage["system.host.os"] = 1; - projectStage["system.host.platform"] = 1; - projectStage["system.host.kernel_version"] = 1; - projectStage["system.host.pretty_name"] = 1; - projectStage["system.net"] = 1; - return projectStage; - }; - - private getFinalProjection = (type: string): object => { - if (type === "pagespeed") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - accessibility: "$accessibility", - seo: "$seo", - bestPractices: "$bestPractices", - performance: "$performance", - cls: "$cls", - si: "$si", - fcp: "$fcp", - lcp: "$lcp", - tbt: "$tbt", - }; - } - - if (type === "infrastructure") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - cpu: { - physicalCores: "$physicalCores", - logicalCores: "$logicalCores", - frequency: "$frequency", - currentFrequency: "$currentFrequency", - temperatures: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], - }, - as: "idx", - in: { - $avg: { - $map: { - input: "$tempsArrays", - as: "arr", - in: { $arrayElemAt: ["$$arr", "$$idx"] }, - }, - }, - }, - }, - }, - freePercent: "$freePercent", - usedPercent: "$usedPercent", - }, - memory: { - total_bytes: "$total_bytes", - available_bytes: "$available_bytes", - used_bytes: "$used_bytes", - usage_percent: "$memory_usage_percent", - }, - disks: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - diskGroup: { - $map: { - input: "$disksArray", - as: "diskArr", - in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, - }, - }, - }, - in: { - device: { $arrayElemAt: ["$$diskGroup.device", 0] }, - total_bytes: { $avg: "$$diskGroup.total_bytes" }, - free_bytes: { $avg: "$$diskGroup.free_bytes" }, - used_bytes: { $avg: "$$diskGroup.used_bytes" }, - usage_percent: { $avg: "$$diskGroup.usage_percent" }, - total_inodes: { $avg: "$$diskGroup.total_inodes" }, - free_inodes: { $avg: "$$diskGroup.free_inodes" }, - used_inodes: { $avg: "$$diskGroup.used_inodes" }, - inodes_usage_percent: { - $avg: "$$diskGroup.inodes_usage_percent", - }, - read_bytes: { $avg: "$$diskGroup.read_bytes" }, - write_bytes: { $avg: "$$diskGroup.write_bytes" }, - read_time: { $avg: "$$diskGroup.read_time" }, - write_time: { $avg: "$$diskGroup.write_time" }, - }, - }, - }, - }, - }, - host: { - os: "$os", - platform: "$platform", - kernel_version: "$kernel_version", - pretty_name: "$pretty_name", - }, - net: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - netGroup: { - $map: { - input: "$netsArray", - as: "netArr", - in: { $arrayElemAt: ["$$netArr", "$$idx"] }, - }, - }, - }, - in: { - name: { $arrayElemAt: ["$$netGroup.name", 0] }, - bytes_sent: { $avg: "$$netGroup.bytes_sent" }, - bytes_recv: { $avg: "$$netGroup.bytes_recv" }, - packets_sent: { $avg: "$$netGroup.packets_sent" }, - packets_recv: { $avg: "$$netGroup.packets_recv" }, - err_in: { $avg: "$$netGroup.err_in" }, - err_out: { $avg: "$$netGroup.err_out" }, - drop_in: { $avg: "$$netGroup.drop_in" }, - drop_out: { $avg: "$$netGroup.drop_out" }, - fifo_in: { $avg: "$$netGroup.fifo_in" }, - fifo_out: { $avg: "$$netGroup.fifo_out" }, - }, - }, - }, - }, - }, - }; - } - return {}; - }; - - getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - const startDate = this.getStartDate(range); - const dateFormat = this.getDateFormat(range); - - // Build match stage - const matchStage: { - monitorId: mongoose.Types.ObjectId; - createdAt: { $gte: Date }; - status?: string; - } = { - monitorId: monitor._id, - createdAt: { $gte: startDate }, - }; - - if (status) { - matchStage.status = status; - } - - let groupClause; - - if (monitor.type === "pagespeed") { - groupClause = this.getPageSpeedGroup(dateFormat); - } else if (monitor.type === "infrastructure") { - groupClause = this.getInfraGroup(dateFormat); - } else { - groupClause = this.getBaseGroup(dateFormat); - } - - let projectStage; - if (monitor.type === "pagespeed") { - projectStage = this.getPageSpeedProjection(); - } else if (monitor.type === "infrastructure") { - projectStage = this.getInfraProjection(); - } else { - projectStage = this.getBaseProjection(); - } - - let finalProjection = {}; - if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { - finalProjection = this.getFinalProjection(monitor.type); - } else { - finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; - } - - const checks = await Check.aggregate([ - { - $match: matchStage, - }, - { $sort: { createdAt: 1 } }, - { $project: projectStage }, - { $group: groupClause }, - { $sort: { _id: -1 } }, - { - $project: finalProjection, - }, - ]); - - // Get monitor stats - const monitorStats = await MonitorStats.findOne({ - monitorId: monitor._id, - }).lean(); - - if (!monitorStats) { - throw new ApiError("Monitor stats not found", 404); - } - - return { - monitor: monitor.toObject(), - checks, - stats: monitorStats, - }; - }; - - async toggleActive(id: string, tokenizedUser: ITokenizedUser) { - const pendingStatus: MonitorStatus = "initializing"; - const updatedMonitor = await Monitor.findOneAndUpdate( - { _id: id }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: pendingStatus, - updatedBy: tokenizedUser.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - - await this.jobQueue.updateJob(updatedMonitor); - - if (updatedMonitor?.isActive) { - await this.jobQueue.resumeJob(updatedMonitor); - } else { - await this.jobQueue.pauseJob(updatedMonitor); - } - return updatedMonitor; - } - - async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) { - const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "m", "notificationChannels"]; - const safeUpdate: Partial = {}; - - for (const field of allowedFields) { - if (updateData[field] !== undefined) { - (safeUpdate as any)[field] = updateData[field]; - } - } - - const updatedMonitor = await Monitor.findByIdAndUpdate( - monitorId, - { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: tokenizedUser.sub, - }, - }, - { new: true, runValidators: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - await this.jobQueue.updateJob(updatedMonitor); - return updatedMonitor; - } - - async delete(monitorId: string) { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - await monitor.deleteOne(); - await this.jobQueue.deleteJob(monitor); - return true; - } -} - -export default MonitorService; From 2d12fbdff11c2c3fae0600258e4544d0da044487 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:56:02 -0700 Subject: [PATCH 3/3] fix notification schema --- server/src/db/v2/models/monitors/Monitor.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts index 055d36a1b..52cbae9d8 100644 --- a/server/src/db/v2/models/monitors/Monitor.ts +++ b/server/src/db/v2/models/monitors/Monitor.ts @@ -63,14 +63,10 @@ const MonitorSchema = new Schema( ], default: [], }, - notificationChannels: [ - { - type: Schema.Types.ObjectId, - ref: "NotificationChannel_v2", - required: false, - default: [], - }, - ], + notificationChannels: { + type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }], + default: [], + }, createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true },