Merge pull request #2975 from bluewave-labs/feat/v1/structure

feat/v1/structure
This commit is contained in:
Alexander Holliday
2025-09-23 14:25:46 -07:00
committed by GitHub
75 changed files with 864 additions and 89 deletions

23
.github/workflows/check-build.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Build Check ( Server)
on:
pull_request:
workflow_dispatch:
jobs:
build-server:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install server dependencies
working-directory: server
run: npm install
- name: Check server build
working-directory: server
run: npm run build

View File

@@ -1,16 +1,16 @@
import express from "express";
import path from "path";
import { responseHandler } from "./middleware/responseHandler.js";
import { responseHandler } from "./middleware/v1/responseHandler.js";
import cors from "cors";
import helmet from "helmet";
import compression from "compression";
import cookieParser from "cookie-parser";
import languageMiddleware from "./middleware/languageMiddleware.js";
import languageMiddleware from "./middleware/v1/languageMiddleware.js";
import swaggerUi from "swagger-ui-express";
import { handleErrors } from "./middleware/handleErrors.js";
import { handleErrors } from "./middleware/v1/handleErrors.js";
import { setupRoutes } from "./config/routes.js";
import { generalApiLimiter } from "./middleware/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js";
import { generalApiLimiter } from "./middleware/v1/rateLimiter.js";
import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js";
export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => {
const allowedOrigin = envSettings.clientHost;

View File

@@ -1,5 +1,5 @@
import { verifyJWT } from "../middleware/verifyJWT.js";
import { authApiLimiter } from "../middleware/rateLimiter.js";
import { verifyJWT } from "../middleware/v1/verifyJWT.js";
import { authApiLimiter } from "../middleware/v1/rateLimiter.js";
import AuthRoutes from "../routes/v1/authRoute.js";
import InviteRoutes from "../routes/v1//inviteRoute.js";

View File

@@ -1,22 +1,22 @@
import ServiceRegistry from "../service/system/serviceRegistry.js";
import TranslationService from "../service/system/translationService.js";
import StringService from "../service/system/stringService.js";
import MongoDB from "../db/mongo/MongoDB.js";
import NetworkService from "../service/infrastructure/networkService.js";
import EmailService from "../service/infrastructure/emailService.js";
import BufferService from "../service/infrastructure/bufferService.js";
import StatusService from "../service/infrastructure/statusService.js";
import NotificationUtils from "../service/infrastructure/notificationUtils.js";
import NotificationService from "../service/infrastructure/notificationService.js";
import ErrorService from "../service/infrastructure/errorService.js";
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/business/userService.js";
import CheckService from "../service/business/checkService.js";
import DiagnosticService from "../service/business/diagnosticService.js";
import InviteService from "../service/business/inviteService.js";
import MaintenanceWindowService from "../service/business/maintenanceWindowService.js";
import MonitorService from "../service/business/monitorService.js";
import ServiceRegistry from "../service/v1/system/serviceRegistry.js";
import TranslationService from "../service/v1/system/translationService.js";
import StringService from "../service/v1/system/stringService.js";
import MongoDB from "../db/v1/MongoDB.js";
import NetworkService from "../service/v1/infrastructure/networkService.js";
import EmailService from "../service/v1/infrastructure/emailService.js";
import BufferService from "../service/v1/infrastructure/bufferService.js";
import StatusService from "../service/v1/infrastructure/statusService.js";
import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js";
import NotificationService from "../service/v1/infrastructure/notificationService.js";
import ErrorService from "../service/v1/infrastructure/errorService.js";
import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
import UserService from "../service/v1/business/userService.js";
import CheckService from "../service/v1/business/checkService.js";
import DiagnosticService from "../service/v1/business/diagnosticService.js";
import InviteService from "../service/v1/business/inviteService.js";
import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js";
import MonitorService from "../service/v1/business/monitorService.js";
import papaparse from "papaparse";
import axios from "axios";
import got from "got";
@@ -46,27 +46,27 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js";
import { ParseBoolean } from "../utils/utils.js";
// Models
import Check from "../db/models/Check.js";
import Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js";
import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
import MonitorStats from "../db/models/MonitorStats.js";
import Notification from "../db/models/Notification.js";
import RecoveryToken from "../db/models/RecoveryToken.js";
import AppSettings from "../db/models/AppSettings.js";
import Check from "../db/v1/models/Check.js";
import Monitor from "../db/v1/models/Monitor.js";
import User from "../db/v1/models/User.js";
import InviteToken from "../db/v1/models/InviteToken.js";
import StatusPage from "../db/v1/models/StatusPage.js";
import Team from "../db/v1/models/Team.js";
import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js";
import MonitorStats from "../db/v1/models/MonitorStats.js";
import Notification from "../db/v1/models/Notification.js";
import RecoveryToken from "../db/v1/models/RecoveryToken.js";
import AppSettings from "../db/v1/models/AppSettings.js";
import InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
import UserModule from "../db/mongo/modules/userModule.js";
import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/mongo/modules/monitorModule.js";
import NotificationModule from "../db/mongo/modules/notificationModule.js";
import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
import SettingsModule from "../db/mongo/modules/settingsModule.js";
import InviteModule from "../db/v1/modules/inviteModule.js";
import CheckModule from "../db/v1/modules/checkModule.js";
import StatusPageModule from "../db/v1/modules/statusPageModule.js";
import UserModule from "../db/v1/modules//userModule.js";
import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/v1/modules/monitorModule.js";
import NotificationModule from "../db/v1/modules/notificationModule.js";
import RecoveryModule from "../db/v1/modules/recoveryModule.js";
import SettingsModule from "../db/v1/modules/settingsModule.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });

View File

@@ -1,4 +1,4 @@
import { AppError } from "../../service/infrastructure/errorService.js";
import { AppError } from "../../service/v1/infrastructure/errorService.js";
export const createCommonDependencies = (db, errorService, logger, stringService) => {
return {

View File

@@ -0,0 +1,157 @@
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;

View File

@@ -1,5 +1,5 @@
import mongoose from "mongoose";
import AppSettings from "../models/AppSettings.js";
import AppSettings from "./models/AppSettings.js";
import { runMigrations } from "./migration/index.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";

View File

@@ -1,4 +1,4 @@
import Monitor from "../../models/Monitor.js";
import Monitor from "../models/Monitor.js";
async function migrateStatusWindowThreshold() {
try {
const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } });

View File

@@ -1,6 +1,5 @@
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
import logger from "../../utils/logger.js";
import Monitor from "./Monitor.js";
import Team from "./Team.js";
import Notification from "./Notification.js";

View File

@@ -1,5 +1,5 @@
import Monitor from "../../models/Monitor.js";
import Check from "../../models/Check.js";
import Monitor from "../models/Monitor.js";
import Check from "../models/Check.js";
import { logger } from "../../../utils/logger.js";
const generateRandomUrl = () => {

View File

@@ -7,8 +7,8 @@ import path from "path";
import fs from "fs";
import Logger from "./utils/logger.js";
import SettingsService from "./service/system/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
import SettingsService from "./service/v1/system/settingsService.js";
import AppSettings from "./db/v1/models/AppSettings.js";
const SERVICE_NAME = "Server";
let logger;

View File

@@ -1,6 +1,6 @@
import { logger } from "../utils/logger.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import { logger } from "../../utils/logger.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
const handleErrors = (error, req, res, next) => {
const status = error.status || 500;

View File

@@ -1,9 +1,9 @@
import jwt from "jsonwebtoken";
const TOKEN_PREFIX = "Bearer ";
const SERVICE_NAME = "allowedRoles";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import SettingsService from "../service/system/settingsService.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
import SettingsService from "../../service/v1/system/settingsService.js";
const isAllowed = (allowedRoles) => {
return (req, res, next) => {

View File

@@ -1,4 +1,4 @@
import { logger } from "../utils/logger.js";
import { logger } from "../../utils/logger.js";
const languageMiddleware = (stringService, translationService) => async (req, res, next) => {
try {

View File

@@ -1,7 +1,7 @@
import jwt from "jsonwebtoken";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import SettingsService from "../service/system/settingsService.js";
import StringService from "../service/system/stringService.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import SettingsService from "../../service/v1/system/settingsService.js";
import StringService from "../../service/v1/system/stringService.js";
const SERVICE_NAME = "verifyJWT";
const TOKEN_PREFIX = "Bearer ";

View File

@@ -1,6 +1,6 @@
import { logger } from "../utils/logger.js";
import ServiceRegistry from "../service/system/serviceRegistry.js";
import StringService from "../service/system/stringService.js";
import { logger } from "../../utils/logger.js";
import ServiceRegistry from "../../service/v1/system/serviceRegistry.js";
import StringService from "../../service/v1/system/stringService.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "verifyOwnership";

View File

@@ -1,5 +1,5 @@
const jwt = require("jsonwebtoken");
const logger = require("../utils/logger");
const logger = require("../../utils/logger.js");
const SERVICE_NAME = "verifyAdmin";
const TOKEN_PREFIX = "Bearer ";
import ServiceRegistry from "../service/serviceRegistry.js";

View File

@@ -1,6 +1,6 @@
import { Router } from "express";
import { verifyJWT } from "../../middleware/verifyJWT.js";
import { isAllowed } from "../../middleware/isAllowed.js";
import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
import multer from "multer";
const upload = multer();

View File

@@ -1,6 +1,6 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class CheckRoutes {
constructor(checkController) {

View File

@@ -1,6 +1,6 @@
import { Router } from "express";
import { verifyJWT } from "../../middleware/verifyJWT.js";
import { isAllowed } from "../../middleware/isAllowed.js";
import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class DiagnosticRoutes {
constructor(diagnosticController) {

View File

@@ -1,6 +1,6 @@
import { Router } from "express";
import { verifyJWT } from "../../middleware/verifyJWT.js";
import { isAllowed } from "../../middleware/isAllowed.js";
import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class InviteRoutes {
constructor(inviteController) {

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class LogRoutes {
constructor(logController) {
this.router = Router();

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import MaintenanceWindow from "../../db/models/MaintenanceWindow.js";
import MaintenanceWindow from "../../db/v1/models/MaintenanceWindow.js";
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
this.router = Router();

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js";
const upload = multer({

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class QueueRoutes {
constructor(queueController) {
this.router = Router();

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import { isAllowed } from "../../middleware/isAllowed.js";
import { isAllowed } from "../../middleware/v1/isAllowed.js";
class SettingsRoutes {
constructor(settingsController) {

View File

@@ -1,5 +1,5 @@
import { Router } from "express";
import { verifyJWT } from "../../middleware/verifyJWT.js";
import { verifyJWT } from "../../middleware/v1/verifyJWT.js";
import multer from "multer";
const upload = multer();

View File

@@ -1,4 +1,4 @@
import { createMonitorsBodyValidation } from "../../validation/joi.js";
import { createMonitorsBodyValidation } from "../../../validation/joi.js";
const SERVICE_NAME = "MonitorService";
class MonitorService {

View File

@@ -5,11 +5,11 @@ class SuperSimpleQueueHelper {
/**
* @param {{
* db: import("../database").Database,
* logger: import("../logger").Logger,
* networkService: import("../networkService").NetworkService,
* statusService: import("../statusService").StatusService,
* notificationService: import("../notificationService").NotificationService
* db: import("../database.js").Database,
* logger: import("../logger.js").Logger,
* networkService: import("../networkService.js").NetworkService,
* statusService: import("../statusService.js").StatusService,
* notificationService: import("../notificationService.js").NotificationService
* }}
*/
constructor({ db, logger, networkService, statusService, notificationService }) {

View File

@@ -1,4 +1,4 @@
import MonitorStats from "../../db/models/MonitorStats.js";
import MonitorStats from "../../../db/v1/models/MonitorStats.js";
const SERVICE_NAME = "StatusService";
class StatusService {

View File

@@ -0,0 +1,132 @@
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;

View File

@@ -0,0 +1,464 @@
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;

View File

@@ -22,5 +22,5 @@
"allowJs": true,
"checkJs": false
},
"exclude": ["node_modules", "dist", "**/*.config.js"]
"exclude": ["node_modules", "dist/", "**/*.config.js"]
}