Merge pull request #2714 from bluewave-labs/feat/statuspage-module

feat: refactor status page module into class
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-07-30 10:22:46 -07:00
commit e7cf6d385c
5 files changed
+282 -287

No files matched your search

+6 -1
View File
@@ -33,15 +33,19 @@ import jwt from "jsonwebtoken";
import crypto from "crypto";
// DB Modules
import { NormalizeData } from "../utils/dataUtils.js";
import Check from "../db/models/Check.js";
import HardwareCheck from "../db/models/HardwareCheck.js";
import PageSpeedCheck from "../db/models/PageSpeedCheck.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 InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
export const initializeServices = async ({ logger, envSettings, settingsService }) => {
const serviceRegistry = new ServiceRegistry({ logger });
@@ -55,7 +59,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService
// Create DB
const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const db = new MongoDB({ logger, envSettings, checkModule, inviteModule });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const db = new MongoDB({ logger, envSettings, checkModule, inviteModule, statusPageModule });
await db.connect();
const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService);
@@ -19,7 +19,7 @@ class StatusPageController extends BaseController {
await imageValidation.validateAsync(req.file);
const { _id, teamId } = req.user;
const statusPage = await this.db.createStatusPage({
const statusPage = await this.db.statusPageModule.createStatusPage({
statusPageData: req.body,
image: req.file,
userId: _id,
@@ -39,7 +39,7 @@ class StatusPageController extends BaseController {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
const statusPage = await this.db.updateStatusPage(req.body, req.file);
const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file);
if (statusPage === null) {
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
}
@@ -54,7 +54,7 @@ class StatusPageController extends BaseController {
getStatusPage = this.asyncHandler(
async (req, res) => {
const statusPage = await this.db.getStatusPage();
const statusPage = await this.db.statusPageModule.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
@@ -69,7 +69,7 @@ class StatusPageController extends BaseController {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type);
const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url);
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
@@ -82,7 +82,7 @@ class StatusPageController extends BaseController {
getStatusPagesByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId);
return res.success({
msg: this.stringService.statusPageByTeamId,
+2 -7
View File
@@ -44,11 +44,6 @@ import * as notificationModule from "./modules/notificationModule.js";
//****************************************
import * as settingsModule from "./modules/settingsModule.js";
//****************************************
// Status Page
//****************************************
import * as statusPageModule from "./modules/statusPageModule.js";
//****************************************
// Diagnostic
//****************************************
@@ -57,7 +52,7 @@ import * as diagnosticModule from "./modules/diagnosticModule.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
constructor({ logger, envSettings, checkModule, inviteModule }) {
constructor({ logger, envSettings, checkModule, inviteModule, statusPageModule }) {
this.logger = logger;
this.envSettings = envSettings;
Object.assign(this, userModule);
@@ -70,7 +65,7 @@ class MongoDB {
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
Object.assign(this, statusPageModule);
this.statusPageModule = statusPageModule;
Object.assign(this, diagnosticModule);
}
+268 -273
View File
@@ -1,292 +1,287 @@
import StatusPage from "../../models/StatusPage.js";
import { NormalizeData } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/system/serviceRegistry.js";
import StringService from "../../../service/system/stringService.js";
// import StatusPage from "../../models/StatusPage.js";
// import { NormalizeData } from "../../../utils/dataUtils.js";
// import ServiceRegistry from "../../../service/system/serviceRegistry.js";
// import StringService from "../../../service/system/stringService.js";
const SERVICE_NAME = "statusPageModule";
const createStatusPage = async ({ statusPageData, image, userId, teamId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const statusPage = new StatusPage({
...statusPageData,
userId,
teamId,
});
if (image) {
statusPage.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
await statusPage.save();
return statusPage;
} catch (error) {
if (error?.code === 11000) {
// Handle duplicate URL errors
error.status = 400;
error.message = stringService.statusPageUrlNotUnique;
}
error.service = SERVICE_NAME;
error.method = "createStatusPage";
throw error;
class StatusPageModule {
constructor({ StatusPage, NormalizeData, stringService }) {
this.StatusPage = StatusPage;
this.NormalizeData = NormalizeData;
this.stringService = stringService;
}
};
const updateStatusPage = async (statusPageData, image) => {
try {
if (image) {
statusPageData.logo = {
data: image.buffer,
contentType: image.mimetype,
};
} else {
statusPageData.logo = null;
}
if (statusPageData.deleteSubmonitors === "true") {
statusPageData.subMonitors = [];
}
const statusPage = await StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, {
new: true,
});
return statusPage;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateStatusPage";
throw error;
}
};
const getStatusPageByUrl = async (url, type) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
return getStatusPage(url);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const getStatusPagesByTeamId = async (teamId) => {
try {
const statusPages = await StatusPage.find({ teamId });
return statusPages;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPagesByTeamId";
throw error;
}
};
const getStatusPage = async (url) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
const preliminaryStatusPage = await StatusPage.findOne({ url });
if (!preliminaryStatusPage) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
createStatusPage = async ({ statusPageData, image, userId, teamId }) => {
try {
const statusPage = new this.StatusPage({
...statusPageData,
userId,
teamId,
});
if (image) {
statusPage.logo = {
data: image.buffer,
contentType: image.mimetype,
};
}
await statusPage.save();
return statusPage;
} catch (error) {
if (error?.code === 11000) {
// Handle duplicate URL errors
error.status = 400;
error.message = this.stringService.statusPageUrlNotUnique;
}
error.service = SERVICE_NAME;
error.method = "createStatusPage";
throw error;
}
};
if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) {
const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } =
preliminaryStatusPage;
return {
statusPage: {
_id,
color,
companyName,
isPublished,
logo,
originalMonitors,
showCharts,
showUptimePercentage,
timezone,
showAdminLoginLink,
url,
},
monitors: [],
};
updateStatusPage = async (statusPageData, image) => {
try {
if (image) {
statusPageData.logo = {
data: image.buffer,
contentType: image.mimetype,
};
} else {
statusPageData.logo = null;
}
if (statusPageData.deleteSubmonitors === "true") {
statusPageData.subMonitors = [];
}
const statusPage = await this.StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, {
new: true,
});
return statusPage;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateStatusPage";
throw error;
}
};
const statusPageQuery = await StatusPage.aggregate([
{ $match: { url: url } },
{
$set: {
originalMonitors: "$monitors",
},
},
{
$lookup: {
from: "monitors",
localField: "monitors",
foreignField: "_id",
as: "monitors",
},
},
{
$unwind: {
path: "$monitors",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "maintenancewindows",
let: { monitorId: "$monitors._id" },
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
as: "monitors.maintenanceWindows",
},
},
{
$lookup: {
from: "checks",
let: { monitorId: "$monitors._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: 25 },
],
as: "monitors.checks",
},
},
{
$addFields: {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
"monitors.isMaintenance": {
$reduce: {
input: "$monitors.maintenanceWindows",
initialValue: false,
in: {
$or: [
"$$value",
{
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
},
],
},
},
},
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },
{ $sort: { "monitors.orderIndex": 1 } },
getStatusPageByUrl = async (url) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
return this.getStatusPage(url);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
{
$group: {
_id: "$_id",
statusPage: { $first: "$$ROOT" },
monitors: { $push: "$monitors" },
},
},
{
$project: {
getStatusPagesByTeamId = async (teamId) => {
try {
const statusPages = await this.StatusPage.find({ teamId });
return statusPages;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPagesByTeamId";
throw error;
}
};
getStatusPage = async (url) => {
try {
const preliminaryStatusPage = await this.StatusPage.findOne({ url });
if (!preliminaryStatusPage) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
}
if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) {
const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } =
preliminaryStatusPage;
return {
statusPage: {
_id: 1,
color: 1,
companyName: 1,
isPublished: 1,
logo: 1,
originalMonitors: 1,
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
showAdminLoginLink: 1,
url: 1,
_id,
color,
companyName,
isPublished,
logo,
originalMonitors,
showCharts,
showUptimePercentage,
timezone,
showAdminLoginLink,
url,
},
monitors: {
_id: 1,
userId: 1,
teamId: 1,
name: 1,
description: 1,
status: 1,
type: 1,
ignoreTlsErrors: 1,
jsonPath: 1,
expectedValue: 1,
matchMethod: 1,
url: 1,
port: 1,
isActive: 1,
interval: 1,
uptimePercentage: 1,
notifications: 1,
secret: 1,
thresholds: 1,
alertThreshold: 1,
cpuAlertThreshold: 1,
memoryAlertThreshold: 1,
diskAlertThreshold: 1,
tempAlertThreshold: 1,
checks: 1,
isMaintenance: 1,
createdAt: 1,
updatedAt: 1,
monitors: [],
};
}
const statusPageQuery = await this.StatusPage.aggregate([
{ $match: { url: url } },
{
$set: {
originalMonitors: "$monitors",
},
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(stringService.statusPageNotFound);
error.status = 404;
{
$lookup: {
from: "monitors",
localField: "monitors",
foreignField: "_id",
as: "monitors",
},
},
{
$unwind: {
path: "$monitors",
preserveNullAndEmptyArrays: true,
},
},
{
$lookup: {
from: "maintenancewindows",
let: { monitorId: "$monitors._id" },
pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }],
as: "monitors.maintenanceWindows",
},
},
{
$lookup: {
from: "checks",
let: { monitorId: "$monitors._id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: 25 },
],
as: "monitors.checks",
},
},
{
$addFields: {
"monitors.orderIndex": {
$indexOfArray: ["$originalMonitors", "$monitors._id"],
},
"monitors.isMaintenance": {
$reduce: {
input: "$monitors.maintenanceWindows",
initialValue: false,
in: {
$or: [
"$$value",
{
$and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }],
},
],
},
},
},
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },
{ $sort: { "monitors.orderIndex": 1 } },
{
$group: {
_id: "$_id",
statusPage: { $first: "$$ROOT" },
monitors: { $push: "$monitors" },
},
},
{
$project: {
statusPage: {
_id: 1,
color: 1,
companyName: 1,
isPublished: 1,
logo: 1,
originalMonitors: 1,
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
showAdminLoginLink: 1,
url: 1,
},
monitors: {
_id: 1,
userId: 1,
teamId: 1,
name: 1,
description: 1,
status: 1,
type: 1,
ignoreTlsErrors: 1,
jsonPath: 1,
expectedValue: 1,
matchMethod: 1,
url: 1,
port: 1,
isActive: 1,
interval: 1,
uptimePercentage: 1,
notifications: 1,
secret: 1,
thresholds: 1,
alertThreshold: 1,
cpuAlertThreshold: 1,
memoryAlertThreshold: 1,
diskAlertThreshold: 1,
tempAlertThreshold: 1,
checks: 1,
isMaintenance: 1,
createdAt: 1,
updatedAt: 1,
},
},
},
]);
if (!statusPageQuery.length) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
}
const { statusPage, monitors } = statusPageQuery[0];
const normalizedMonitors = monitors.map((monitor) => {
return {
...monitor,
checks: this.NormalizeData(monitor.checks, 10, 100),
};
});
return { statusPage, monitors: normalizedMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const { statusPage, monitors } = statusPageQuery[0];
deleteStatusPage = async (url) => {
try {
await this.StatusPage.deleteOne({ url });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPage";
throw error;
}
};
deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
throw error;
}
};
}
const normalizedMonitors = monitors.map((monitor) => {
return {
...monitor,
checks: NormalizeData(monitor.checks, 10, 100),
};
});
return { statusPage, monitors: normalizedMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getStatusPageByUrl";
throw error;
}
};
const deleteStatusPage = async (url) => {
try {
await StatusPage.deleteOne({ url });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPage";
throw error;
}
};
const deleteStatusPagesByMonitorId = async (monitorId) => {
try {
await StatusPage.deleteMany({ monitors: { $in: [monitorId] } });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteStatusPageByMonitorId";
throw error;
}
};
export {
createStatusPage,
updateStatusPage,
getStatusPagesByTeamId,
getStatusPage,
getStatusPageByUrl,
deleteStatusPage,
deleteStatusPagesByMonitorId,
};
export default StatusPageModule;
@@ -143,7 +143,7 @@ class MonitorService {
await this.verifyTeamAccess({ teamId, monitorId });
const monitor = await this.db.deleteMonitor({ teamId, monitorId });
await this.jobQueue.deleteJob(monitor);
await this.db.deleteStatusPagesByMonitorId(monitor._id);
await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id);
return monitor;
};