Merge pull request #2976 from bluewave-labs/feat/v2/models

feat: add v2 models
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-09-23 14:57:03 -07:00
commit 35a712eafb
13 files changed
+812 -753

No files matched your search

@@ -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;
+25
View File
@@ -0,0 +1,25 @@
import mongoose from "mongoose";
const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate";
export const connectDatabase = async (): Promise<boolean> => {
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<boolean> => {
try {
await mongoose.disconnect();
console.log("Disconnected from MongoDB");
return true;
} catch (error) {
console.error("MongoDB disconnection error:", error);
return false;
}
};
+50
View File
@@ -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<IRole>(
{
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<IRole>("Role_v2", roleSchema);
+102
View File
@@ -0,0 +1,102 @@
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<IUser>(
{
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,
}
);
export const User = mongoose.model<IUser>("User_v2", userSchema);
+297
View File
@@ -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<string, ILighthouseAudit>;
}
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<ICheck>(
{
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<ICheck>("Check_v2", CheckSchema);
+33
View File
@@ -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";
+43
View File
@@ -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<IInvite>(
{
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<IInvite>("Invite_v2", InviteSchema);
@@ -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<IMaintenance>(
{
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<IMaintenance>("Maintenance_v2", MaintenanceSchema);
@@ -0,0 +1,96 @@
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<IMonitor>(
{
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: [{ 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 },
},
{ 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<IMonitor>("Monitor_v2", MonitorSchema);
@@ -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<IMonitorStats>(
{
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<IMonitorStats>("MonitorStats_v2", MonitorStatsSchema);
@@ -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<INotificationChannelConfig>(
{
url: { type: String, required: false },
emailAddress: { type: String, required: false },
},
{ _id: false, strict: "throw" }
);
const NotificationChannelSchema = new Schema<INotificationChannel>(
{
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<INotificationChannel>("NotificationChannel_v2", NotificationChannelSchema);
@@ -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<ICheck>;
cleanupOrphanedChecks: () => Promise<boolean>;
}
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<ISystemInfo>;
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<string, any>;
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<ICapturePayload>) => {
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<ILighthousePayload>) => {
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<ICheck> => {
switch (type) {
case "infrastructure":
return this.buildInfrastructureCheck(statusResponse as StatusResponse<ICapturePayload>);
case "pagespeed":
return this.buildPagespeedCheck(statusResponse as StatusResponse<ILighthousePayload>);
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;
@@ -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<IMonitor>;
getAll: () => Promise<IMonitor[]>;
getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise<any[]>;
get: (monitorId: string) => Promise<IMonitor>;
getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise<MonitorWithChecksResponse>;
toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise<IMonitor>;
update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial<IMonitor>) => Promise<IMonitor>;
delete: (monitorId: string) => Promise<boolean>;
}
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<string, any> => {
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<string, any> => {
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<string, any> => {
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<MonitorWithChecksResponse> => {
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<IMonitor>) {
const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "m", "notificationChannels"];
const safeUpdate: Partial<IMonitor> = {};
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;