-> timeseries

This commit is contained in:
Alex Holliday committed 2026-01-12 19:32:23 +00:00
1 parent 5c14c68899
commit e090d3cfd3
91 files changed
+941 -987

No files matched your search

+283
View File
@@ -0,0 +1,283 @@
import { ObjectId } from "mongodb";
import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js";
const SERVICE_NAME = "checkModule";
const dateRangeLookup = {
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: undefined,
};
class CheckModule {
constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) {
this.logger = logger;
this.Check = Check;
this.HardwareCheck = HardwareCheck;
this.PageSpeedCheck = PageSpeedCheck;
this.Monitor = Monitor;
this.User = User;
}
createChecks = async (checks) => {
try {
await this.Check.insertMany(checks, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createCheck";
throw error;
}
};
getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
// Match
const matchStage = {
monitorId: new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecks",
});
break;
}
}
//Sort
sortOrder = sortOrder === "asc" ? 1 : -1;
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const checks = await this.Check.aggregate([
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: {
$ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0],
},
checks: {
$ifNull: ["$checks", []],
},
},
},
]);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecks";
throw error;
}
};
getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => {
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const matchStage = {
teamId: new ObjectId(teamId),
status: false,
...(typeof ack !== "undefined" && ackStage),
...(dateRangeLookup[dateRange] && {
createdAt: {
$gte: dateRangeLookup[dateRange],
},
}),
};
// Add filter to match stage
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "down":
break;
case "resolve":
matchStage.statusCode = 5000;
break;
default:
this.logger.warn({
message: "invalid filter",
service: SERVICE_NAME,
method: "getChecksByTeam",
});
break;
}
}
sortOrder = sortOrder === "asc" ? 1 : -1;
// pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const aggregatePipeline = [
{ $match: matchStage },
{ $sort: { createdAt: sortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
];
const checks = await this.Check.aggregate(aggregatePipeline);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksByTeam";
throw error;
}
};
ackCheck = async (checkId, teamId, ack) => {
try {
const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
if (!updatedCheck) {
throw new Error("Check not found");
}
return updatedCheck;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackCheck";
throw error;
}
};
ackAllChecks = async (monitorId, teamId, ack, path) => {
try {
const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
return updatedChecks.modifiedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "ackAllChecks";
throw error;
}
};
getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksSummaryByTeamId";
throw error;
}
};
deleteChecks = async (monitorId) => {
try {
const result = await this.Check.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecks";
throw error;
}
};
deleteChecksByTeamId = async (teamId) => {
try {
// Find all monitor IDs for this team (only get _id field for efficiency)
const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 });
const monitorIds = teamMonitors.map((monitor) => monitor._id);
// Delete all checks for these monitors in one operation
const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } });
return deleteResult.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecksByTeamId";
throw error;
}
};
updateChecksTTL = async (teamId, ttl) => {
try {
await this.Check.collection.dropIndex("expiry_1");
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "updateChecksTTL",
stack: error.stack,
});
}
try {
await this.Check.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary
);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
// Update user
try {
await this.User.updateMany({ teamId: teamId }, { checkTTL: ttl });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateChecksTTL";
throw error;
}
};
}
export default CheckModule;
@@ -0,0 +1,44 @@
const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
downChecks: {
$sum: {
$cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
cannotResolveChecks: {
$sum: {
$cond: [{ $eq: ["$statusCode", 5000] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
},
},
];
};
export { buildChecksSummaryByTeamIdPipeline };
+484
View File
@@ -0,0 +1,484 @@
import { ObjectId } from "mongodb";
const SERVICE_NAME = "incidentModule";
const dateRangeLookup = {
recent: new Date(new Date().setDate(new Date().getDate() - 2)),
hour: new Date(new Date().setHours(new Date().getHours() - 1)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: undefined,
};
class IncidentModule {
constructor({ logger, Incident, Monitor, User }) {
this.logger = logger;
this.Incident = Incident;
this.Monitor = Monitor;
this.User = User;
}
createIncident = async (incidentData) => {
try {
const incident = await this.Incident.create(incidentData);
return incident;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createIncident";
throw error;
}
};
createIncidents = async (incidents) => {
try {
await this.Incident.insertMany(incidents, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createIncidents";
throw error;
}
};
getActiveIncidentByMonitor = async (monitorId) => {
try {
return await this.Incident.findOne({ monitorId: new ObjectId(monitorId), status: true });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getActiveIncidentByMonitor";
throw error;
}
};
getActiveIncidentsByMonitors = async (monitorIds) => {
try {
if (!monitorIds || monitorIds.length === 0) {
return new Map();
}
const objectIds = monitorIds.map((id) => new ObjectId(id));
const incidents = await this.Incident.find({
monitorId: { $in: objectIds },
status: true,
});
const map = new Map();
incidents.forEach((incident) => {
const monitorIdStr = incident.monitorId.toString();
map.set(monitorIdStr, incident);
});
return map;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getActiveIncidentsByMonitors";
throw error;
}
};
getLastManuallyResolvedIncident = async (monitorId) => {
try {
return await this.Incident.findOne({
monitorId: new ObjectId(monitorId),
status: false,
resolutionType: "manual",
})
.sort({ endTime: -1 })
.limit(1);
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getLastManuallyResolvedIncident";
throw error;
}
};
getIncidentById = async (incidentId) => {
try {
return await this.Incident.findById(incidentId).populate("monitorId", "name type url").populate("resolvedBy", "name email");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getIncidentById";
throw error;
}
};
resolveIncident = async (incidentId, { resolutionType = "automatic", resolvedBy = null, comment = null, endTime = new Date() } = {}) => {
try {
const update = {
status: false,
endTime,
resolutionType,
resolvedBy,
...(comment !== null && { comment }),
};
const incident = await this.Incident.findOneAndUpdate({ _id: new ObjectId(incidentId) }, { $set: update }, { new: true });
if (!incident) {
throw new Error("Incident not found");
}
return incident;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "resolveIncident";
throw error;
}
};
addCheckToIncident = async (incidentId, checkId) => {
try {
const incident = await this.Incident.findOneAndUpdate(
{ _id: new ObjectId(incidentId) },
{ $addToSet: { checks: new ObjectId(checkId) } },
{ new: true }
);
if (!incident) {
throw new Error("Incident not found");
}
return incident;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "addCheckToIncident";
throw error;
}
};
addChecksToIncidentsBatch = async (operations) => {
try {
if (!operations || operations.length === 0) {
return;
}
const bulkOps = operations.map((op) => ({
updateOne: {
filter: { _id: new ObjectId(op.incidentId) },
update: { $addToSet: { checks: new ObjectId(op.checkId) } },
},
}));
await this.Incident.bulkWrite(bulkOps, { ordered: false });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "addChecksToIncidentsBatch";
throw error;
}
};
/**
* Get incidents by team with filtering
*
* STATUS PARAMETER CONTRACT (IMPORTANT - Frontend dependency):
* This method has an implicit contract with frontend's useFetchIncidents hook:
*
* - status === undefined: Frontend uses this to represent "all incidents" (filter === "all")
* Backend applies $or logic: { status: true } OR { status: false, endTime: >= dateRange }
* This shows all active incidents + resolved incidents within the date range
*
* - status === true: Frontend uses for "active incidents" (filter === "active")
* Backend shows all active incidents (no date filter applied)
* Active incidents are currently happening, so date range doesn't apply
*
* - status === false: Frontend uses for "resolved incidents" (filter === "resolved")
* Backend filters by endTime >= dateRange
* Only shows incidents that were resolved within the specified date range
*
* WARNING: Changing this logic will break frontend filtering behavior.
* The frontend depends on undefined status triggering the $or logic for "all incidents".
* This contract must be maintained when modifying either frontend or backend code.
*
* @param {Object} params - Query parameters
* @param {string} params.teamId - Team ID
* @param {string} [params.sortOrder] - Sort order (asc/desc)
* @param {string} [params.dateRange] - Date range filter
* @param {number} [params.page] - Page number
* @param {number} [params.rowsPerPage] - Rows per page
* @param {boolean|undefined} [params.status] - Status filter. undefined = all, true = active, false = resolved
* @param {string} [params.monitorId] - Monitor ID filter
* @param {string} [params.resolutionType] - Resolution type filter
*/
getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType }) => {
try {
page = Number.isFinite(parseInt(page)) ? parseInt(page) : 0;
rowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20;
let statusBoolean = undefined;
if (status !== undefined && status !== null) {
if (typeof status === "string") {
statusBoolean = status === "true";
} else if (typeof status === "boolean") {
statusBoolean = status;
}
}
const matchStage = {
teamId: new ObjectId(teamId),
...(statusBoolean !== undefined && { status: statusBoolean }),
...(monitorId && { monitorId: new ObjectId(monitorId) }),
...(resolutionType && { resolutionType }),
};
// Date range filter logic (see contract documentation above):
// - Active incidents (statusBoolean === true): always show (no date filter) - they're currently happening
// - Resolved incidents (statusBoolean === false): filter by endTime (when they were resolved)
// - All incidents (statusBoolean === undefined): show all active + resolved in the range using $or
if (dateRangeLookup[dateRange]) {
const dateThreshold = dateRangeLookup[dateRange];
if (statusBoolean === true) {
// Active incidents: show all active incidents regardless of when they started
// No date filter applied
} else if (statusBoolean === false) {
// Resolved incidents: only show if resolved in the range
matchStage.endTime = { $gte: dateThreshold };
} else {
// All incidents: show all active + resolved incidents in the range
// This is the critical contract: undefined status triggers $or logic
matchStage.$or = [
{ status: true }, // All active incidents
{ status: false, endTime: { $gte: dateThreshold } }, // Resolved in range
];
}
}
sortOrder = sortOrder === "asc" ? 1 : -1;
const skip = page * rowsPerPage;
const incidents = await this.Incident.aggregate([
{ $match: matchStage },
{ $sort: { startTime: sortOrder } },
{
$lookup: {
from: "monitors",
localField: "monitorId",
foreignField: "_id",
as: "monitor",
},
},
{
$facet: {
summary: [{ $count: "incidentsCount" }],
incidents: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
incidentsCount: {
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
},
incidents: {
$map: {
input: { $ifNull: ["$incidents", []] },
as: "incident",
in: {
$mergeObjects: [
"$$incident",
{
monitorName: {
$let: {
vars: {
monitor: { $arrayElemAt: ["$$incident.monitor", 0] },
},
in: {
$ifNull: ["$$monitor.name", null],
},
},
},
},
],
},
},
},
},
},
]);
return incidents[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getIncidentsByTeam";
throw error;
}
};
getIncidentSummary = async ({ teamId, limit = 10 }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
};
// Get basic counts and resolution types
const countsPipeline = [
{ $match: matchStage },
{
$group: {
_id: "$status",
count: { $sum: 1 },
manualResolutions: {
$sum: {
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
},
},
automaticResolutions: {
$sum: {
$cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0],
},
},
},
},
];
const counts = await this.Incident.aggregate(countsPipeline);
// Calculate totals
let total = 0;
let active = 0;
let resolved = 0;
let manual = 0;
let automatic = 0;
counts.forEach((item) => {
total += item.count;
if (item._id === true) {
active = item.count;
}
if (item._id === false) {
resolved = item.count;
}
manual += item.manualResolutions;
automatic += item.automaticResolutions;
});
// Calculate average resolution time (in milliseconds)
const resolutionTimePipeline = [
{ $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } },
{
$project: {
resolutionTime: {
$subtract: ["$endTime", "$startTime"],
},
},
},
{
$group: {
_id: null,
avgResolutionTime: { $avg: "$resolutionTime" },
},
},
];
const resolutionTimeResult = await this.Incident.aggregate(resolutionTimePipeline);
const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0;
const avgResolutionTimeHours = avgResolutionTimeMs / (1000 * 60 * 60); // Convert to hours
// Get monitor with most incidents
const monitorPipeline = [
{ $match: matchStage },
{
$group: {
_id: "$monitorId",
count: { $sum: 1 },
},
},
{ $sort: { count: -1 } },
{ $limit: 1 },
{
$lookup: {
from: "monitors",
localField: "_id",
foreignField: "_id",
as: "monitor",
},
},
{
$project: {
monitorId: "$_id",
count: 1,
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
},
},
];
const monitorResult = await this.Incident.aggregate(monitorPipeline);
const topMonitor = monitorResult[0] || null;
// Get latest incidents
const latestIncidentsPipeline = [
{ $match: matchStage },
{ $sort: { createdAt: -1 } },
{ $limit: Math.max(1, parseInt(limit) || 10) },
{
$lookup: {
from: "monitors",
localField: "monitorId",
foreignField: "_id",
as: "monitor",
},
},
{
$project: {
_id: 1,
monitorId: 1,
monitorName: { $arrayElemAt: ["$monitor.name", 0] },
status: 1,
startTime: 1,
endTime: 1,
resolutionType: 1,
message: 1,
statusCode: 1,
createdAt: 1,
},
},
];
const latestIncidents = await this.Incident.aggregate(latestIncidentsPipeline);
return {
totalActive: active,
avgResolutionTimeHours: Math.round(avgResolutionTimeHours * 100) / 100, // Round to 2 decimal places
topMonitor: topMonitor
? {
monitorId: topMonitor.monitorId,
monitorName: topMonitor.monitorName,
incidentCount: topMonitor.count,
}
: null,
total: total,
totalManualResolutions: manual,
totalAutomaticResolutions: automatic,
latestIncidents: latestIncidents,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getIncidentSummary";
throw error;
}
};
deleteIncidentsByMonitor = async (monitorId) => {
try {
const result = await this.Incident.deleteMany({ monitorId: new ObjectId(monitorId) });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteIncidentsByMonitor";
throw error;
}
};
deleteIncidentsByTeamId = async (teamId) => {
try {
const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 });
const monitorIds = teamMonitors.map((monitor) => monitor._id);
const deleteResult = await this.Incident.deleteMany({ monitorId: { $in: monitorIds } });
return deleteResult.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteIncidentsByTeamId";
throw error;
}
};
}
export default IncidentModule;
+56
View File
@@ -0,0 +1,56 @@
const SERVICE_NAME = "inviteModule";
class InviteModule {
constructor({ InviteToken, crypto, stringService }) {
this.InviteToken = InviteToken;
this.crypto = crypto;
this.stringService = stringService;
}
requestInviteToken = async (userData) => {
try {
await this.InviteToken.deleteMany({ email: userData.email });
userData.token = this.crypto.randomBytes(32).toString("hex");
let inviteToken = new this.InviteToken(userData);
await inviteToken.save();
return inviteToken;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestInviteToken";
throw error;
}
};
getInviteToken = async (token) => {
try {
const invite = await this.InviteToken.findOne({
token,
});
if (invite === null) {
throw new Error(this.stringService.authInviteNotFound);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteToken";
throw error;
}
};
getInviteTokenAndDelete = async (token) => {
try {
const invite = await this.InviteToken.findOneAndDelete({
token,
});
if (invite === null) {
throw new Error(this.stringService.authInviteNotFound);
}
return invite;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getInviteTokenAndDelete";
throw error;
}
};
}
export default InviteModule;
+106
View File
@@ -0,0 +1,106 @@
const SERVICE_NAME = "maintenanceWindowModule";
class MaintenanceWindowModule {
constructor({ MaintenanceWindow }) {
this.MaintenanceWindow = MaintenanceWindow;
}
createMaintenanceWindow = async (maintenanceWindowData) => {
try {
const maintenanceWindow = new this.MaintenanceWindow({
...maintenanceWindowData,
});
// If the maintenance window is a one time window, set the expiry to the end date
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
}
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindow";
throw error;
}
};
getMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOne({
_id: id,
teamId: teamId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
throw error;
}
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
try {
const maintenanceWindows = await this.MaintenanceWindow.find({
monitorId: monitorId,
teamId: teamId,
});
return maintenanceWindows;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowsByMonitorId";
throw error;
}
};
deleteMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId });
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
editMaintenanceWindowById = async ({ id, body }) => {
try {
const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true });
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
}
export default MaintenanceWindowModule;
+593
View File
@@ -0,0 +1,593 @@
import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
} from "./monitorModuleQueries.js";
const SERVICE_NAME = "monitorModule";
class MonitorModule {
constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
this.Monitor = Monitor;
this.MonitorStats = MonitorStats;
this.Check = Check;
this.stringService = stringService;
this.fs = fs;
this.path = path;
this.fileURLToPath = fileURLToPath;
this.ObjectId = ObjectId;
this.NormalizeData = NormalizeData;
this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json");
}
// Helper
calculateUptimeDuration = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const latestCheck = new Date(checks[0].createdAt);
let latestDownCheck = 0;
for (let i = checks.length - 1; i >= 0; i--) {
if (checks[i].status === false) {
latestDownCheck = new Date(checks[i].createdAt);
break;
}
}
// If no down check is found, uptime is from the last check to now
if (latestDownCheck === 0) {
return Date.now() - new Date(checks[checks.length - 1].createdAt);
}
// Otherwise the uptime is from the last check to the last down check
return latestCheck - latestDownCheck;
};
// Helper
getLastChecked = (checks) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
// Data is sorted newest->oldest, so last check is the most recent
return new Date() - new Date(checks[0].createdAt);
};
getLatestResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
return checks[0]?.responseTime ?? 0;
};
// Helper
getAverageResponseTime = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const validChecks = checks.filter((check) => typeof check.responseTime === "number");
if (validChecks.length === 0) {
return 0;
}
const aggResponseTime = validChecks.reduce((sum, check) => {
return sum + check.responseTime;
}, 0);
return aggResponseTime / validChecks.length;
};
// Helper
getUptimePercentage = (checks) => {
if (!checks || checks.length === 0) {
return 0;
}
const upCount = checks.reduce((count, check) => {
return check.status === true ? count + 1 : count;
}, 0);
return (upCount / checks.length) * 100;
};
// Helper
getIncidents = (checks) => {
if (!checks || checks.length === 0) {
return 0; // Handle case when no checks are available
}
return checks.reduce((acc, check) => {
return check.status === false ? (acc += 1) : acc;
}, 0);
};
// Helper
getDateRange = (dateRange) => {
const startDates = {
recent: new Date(new Date().setHours(new Date().getHours() - 2)),
day: new Date(new Date().setDate(new Date().getDate() - 1)),
week: new Date(new Date().setDate(new Date().getDate() - 7)),
month: new Date(new Date().setMonth(new Date().getMonth() - 1)),
all: new Date(0),
};
return {
start: startDates[dateRange],
end: new Date(),
};
};
//Helper
getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
const indexSpec = {
monitorId: 1,
updatedAt: sortOrder, // This will be 1 or -1
};
const [checksAll, checksForDateRange] = await Promise.all([
this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
this.Check.find({
monitorId,
createdAt: { $gte: dateRange.start, $lte: dateRange.end },
})
.hint(indexSpec)
.lean(),
]);
return { checksAll, checksForDateRange };
};
// Helper
processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => {
let processedChecks = checks;
if (numToDisplay && checks.length > numToDisplay) {
const n = Math.ceil(checks.length / numToDisplay);
processedChecks = checks.filter((_, index) => index % n === 0);
}
return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks;
};
// Helper
groupChecksByTime = (checks, dateRange) => {
return checks.reduce((acc, check) => {
// Validate the date
const checkDate = new Date(check.createdAt);
if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) {
return acc;
}
const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0];
if (!acc[time]) {
acc[time] = { time, checks: [] };
}
acc[time].checks.push(check);
return acc;
}, {});
};
// Helper
calculateGroupStats = (group) => {
const totalChecks = group.checks.length;
const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime));
return {
time: group.time,
uptimePercentage: this.getUptimePercentage(group.checks),
totalChecks,
totalIncidents: group.checks.filter((check) => !check.status).length,
avgResponseTime:
checksWithResponseTime.length > 0
? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length
: 0,
};
};
getAllMonitors = async () => {
try {
const monitors = await this.Monitor.find();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllMonitors";
throw error;
}
};
getMonitorById = async (monitorId) => {
try {
const monitor = await this.Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
const error = new Error(this.stringService.getDbFindMonitorById(monitorId));
error.status = 404;
throw error;
}
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorById";
throw error;
}
};
getMonitorsByIds = async (monitorIds) => {
try {
const objectIds = monitorIds.map((id) => new this.ObjectId(id));
return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean();
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByIds";
throw error;
}
};
getUptimeDetailsById = async ({ monitorId, dateRange }) => {
try {
const dates = this.getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString));
const monitorData = results[0];
monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100);
monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100);
const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100);
monitorData.groupedChecks = normalizedGroupChecks;
const monitorStats = await this.MonitorStats.findOne({ monitorId });
return { monitorData, monitorStats };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUptimeDetailsById";
throw error;
}
};
getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => {
try {
// Get monitor, if we can't find it, abort with error
const monitor = await this.Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
}
// Get query params
const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested
const dates = this.getDateRange(dateRange);
const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort);
// Build monitor stats
const monitorStats = {
...monitor.toObject(),
uptimeDuration: this.calculateUptimeDuration(checksAll),
lastChecked: this.getLastChecked(checksAll),
latestResponseTime: this.getLatestResponseTime(checksAll),
periodIncidents: this.getIncidents(checksForDateRange),
periodTotalChecks: checksForDateRange.length,
checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize),
};
if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") {
// HTTP/PING Specific stats
monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange);
monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange);
const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange);
monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats);
}
return monitorStats;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorStatsById";
throw error;
}
};
getHardwareDetailsById = async ({ monitorId, dateRange }) => {
try {
const monitor = await this.Monitor.findById(monitorId);
const dates = this.getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
day: "%Y-%m-%dT%H:00:00Z",
week: "%Y-%m-%dT00:00:00Z",
month: "%Y-%m-%dT00:00:00Z",
};
const dateString = formatLookup[dateRange];
const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString));
const stats = hardwareStats[0];
return {
...monitor.toObject(),
stats,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getHardwareDetailsById";
throw error;
}
};
getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage }));
const summary = summaryResult[0];
const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order }));
const filteredMonitors = await this.Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const normalizedFilteredMonitors = filteredMonitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
}
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
};
getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
}
const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage }));
const { monitors, summary } = queryResult?.[0] ?? {};
return { monitors, summary };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsAndSummaryByTeamId";
throw error;
}
};
getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => {
try {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build match stage
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (explain === true) {
return this.Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
).explain("executionStats");
}
const queryResult = await this.Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
filter,
page,
rowsPerPage,
field,
order,
limit,
type,
})
);
const monitors = queryResult[0]?.monitors;
const count = queryResult[0]?.count;
const normalizedFilteredMonitors = monitors.map((monitor) => {
if (!monitor.checks) {
return monitor;
}
monitor.checks = this.NormalizeData(monitor.checks, 10, 100);
return monitor;
});
return { count, monitors: normalizedFilteredMonitors };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsWithChecksByTeamId";
throw error;
}
};
createMonitor = async ({ body, teamId, userId }) => {
try {
const monitor = new this.Monitor({ ...body, teamId, userId });
const saved = await monitor.save();
return saved;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMonitor";
throw error;
}
};
createBulkMonitors = async (req) => {
try {
const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined }));
await this.Monitor.bulkSave(monitors);
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createBulkMonitors";
throw error;
}
};
deleteMonitor = async ({ teamId, monitorId }) => {
try {
const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId });
if (!deletedMonitor) {
throw new Error(this.stringService.getDbFindMonitorById(monitorId));
}
return deletedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitor";
throw error;
}
};
deleteAllMonitors = async (teamId) => {
try {
const monitors = await this.Monitor.find({ teamId });
const { deletedCount } = await this.Monitor.deleteMany({ teamId });
return { monitors, deletedCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteAllMonitors";
throw error;
}
};
deleteMonitorsByUserId = async (userId) => {
try {
const result = await this.Monitor.deleteMany({ userId: userId });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMonitorsByUserId";
throw error;
}
};
editMonitor = async ({ monitorId, body }) => {
try {
const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, {
new: true,
});
return editedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMonitor";
throw error;
}
};
addDemoMonitors = async (userId, teamId) => {
try {
const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8"));
const demoMonitorsToInsert = demoMonitors.map((monitor) => {
return {
userId,
teamId,
name: monitor.name,
description: monitor.name,
type: "http",
url: monitor.url,
interval: 60000,
};
});
const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert);
return insertedMonitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "addDemoMonitors";
throw error;
}
};
pauseMonitor = async ({ monitorId }) => {
try {
const monitor = await this.Monitor.findOneAndUpdate(
{ _id: monitorId },
[
{
$set: {
isActive: { $not: "$isActive" },
status: "$$REMOVE",
},
},
],
{ new: true }
);
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "pauseMonitor";
throw error;
}
};
getGroupsByTeamId = async ({ teamId }) => {
try {
const groups = await this.Monitor.distinct("group", {
teamId: new this.ObjectId(teamId),
group: { $ne: null, $ne: "" },
});
return groups.filter(Boolean).sort();
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getGroupsByTeamId";
throw error;
}
};
}
export default MonitorModule;
File diff suppressed because it is too large. Load diff
+100
View File
@@ -0,0 +1,100 @@
// import Notification from "../../models/Notification.js";
// import Monitor from "../../models/Monitor.js";
const SERVICE_NAME = "notificationModule";
class NotificationModule {
constructor({ Notification, Monitor }) {
this.Notification = Notification;
this.Monitor = Monitor;
}
createNotification = async (notificationData) => {
try {
const notification = await new this.Notification({ ...notificationData }).save();
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNotification";
throw error;
}
};
getNotificationsByTeamId = async (teamId) => {
try {
const notifications = await this.Notification.find({ teamId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByTeamId";
throw error;
}
};
getNotificationsByIds = async (notificationIds) => {
try {
const notifications = await this.Notification.find({ _id: { $in: notificationIds } });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByIds";
throw error;
}
};
getNotificationsByMonitorId = async (monitorId) => {
try {
const notifications = await this.Notification.find({ monitorId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByMonitorId";
throw error;
}
};
deleteNotificationsByMonitorId = async (monitorId) => {
try {
const result = await this.Notification.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationsByMonitorId";
throw error;
}
};
deleteNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
if (!notification) {
throw new Error("Notification not found");
}
const result = await this.Notification.findByIdAndDelete(id);
await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationById";
throw error;
}
};
getNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationById";
throw error;
}
};
editNotification = async (id, notificationData) => {
try {
const notification = await this.Notification.findByIdAndUpdate(id, notificationData, {
new: true,
});
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editNotification";
throw error;
}
};
}
export default NotificationModule;
+80
View File
@@ -0,0 +1,80 @@
const SERVICE_NAME = "recoveryModule";
class RecoveryModule {
constructor({ User, RecoveryToken, crypto, stringService }) {
this.User = User;
this.RecoveryToken = RecoveryToken;
this.crypto = crypto;
this.stringService = stringService;
}
requestRecoveryToken = async (email) => {
try {
// Delete any existing tokens
await this.RecoveryToken.deleteMany({ email });
let recoveryToken = new this.RecoveryToken({
email,
token: this.crypto.randomBytes(32).toString("hex"),
});
await recoveryToken.save();
return recoveryToken;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "requestRecoveryToken";
throw error;
}
};
validateRecoveryToken = async (candidateToken) => {
try {
const recoveryToken = await this.RecoveryToken.findOne({
token: candidateToken,
});
if (recoveryToken !== null) {
return recoveryToken;
} else {
throw new Error(this.stringService.dbTokenNotFound);
}
} catch (error) {
error.service = SERVICE_NAME;
error.method = "validateRecoveryToken";
throw error;
}
};
resetPassword = async (password, candidateToken) => {
try {
const newPassword = password;
// Validate token again
const recoveryToken = await this.validateRecoveryToken(candidateToken);
const user = await this.User.findOne({ email: recoveryToken.email });
if (user === null) {
throw new Error(this.stringService.dbUserNotFound);
}
const match = await user.comparePassword(newPassword);
if (match === true) {
throw new Error("Password cannot be the same as the old password");
}
user.password = newPassword;
await user.save();
await this.RecoveryToken.deleteMany({ email: recoveryToken.email });
// Fetch the user again without the password
const userWithoutPassword = await this.User.findOne({
email: recoveryToken.email,
})
.select("-password")
.select("-profileImage");
return userWithoutPassword;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "resetPassword";
throw error;
}
};
}
export default RecoveryModule;
+36
View File
@@ -0,0 +1,36 @@
// import AppSettings from "../../models/AppSettings.js";
const SERVICE_NAME = "SettingsModule";
class SettingsModule {
constructor({ AppSettings }) {
this.AppSettings = AppSettings;
}
updateAppSettings = async (newSettings) => {
try {
const update = { $set: { ...newSettings } };
if (newSettings.pagespeedApiKey === "") {
update.$unset = { pagespeedApiKey: "" };
delete update.$set.pagespeedApiKey;
}
if (newSettings.systemEmailPassword === "") {
update.$unset = { systemEmailPassword: "" };
delete update.$set.systemEmailPassword;
}
await this.AppSettings.findOneAndUpdate({}, update, {
upsert: true,
});
const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateAppSettings";
throw error;
}
};
}
export default SettingsModule;
+298
View File
@@ -0,0 +1,298 @@
// 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";
class StatusPageModule {
constructor({ StatusPage, NormalizeData, stringService }) {
this.StatusPage = StatusPage;
this.NormalizeData = NormalizeData;
this.stringService = stringService;
}
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;
}
};
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;
}
};
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;
}
};
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,
color,
companyName,
isPublished,
logo,
originalMonitors,
showCharts,
showUptimePercentage,
timezone,
showAdminLoginLink,
url,
},
monitors: [],
};
}
const statusPageQuery = await this.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",
},
},
{
$lookup: {
from: "monitorstats",
localField: "monitors._id",
foreignField: "monitorId",
as: "monitors.stats",
},
},
{
$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"] }],
},
],
},
},
},
"monitors.uptimePercentage": {
$arrayElemAt: ["$monitors.stats.uptimePercentage", 0],
},
},
},
{ $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;
}
};
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;
}
};
}
export default StatusPageModule;
+171
View File
@@ -0,0 +1,171 @@
const SERVICE_NAME = "userModule";
const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key
class UserModule {
constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) {
this.User = User;
this.Team = Team;
this.GenerateAvatarImage = GenerateAvatarImage;
this.ParseBoolean = ParseBoolean;
this.stringService = stringService;
}
checkSuperadmin = async () => {
try {
const superAdmin = await this.User.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "checkSuperadmin";
throw error;
}
};
insertUser = async (userData, imageFile) => {
try {
if (imageFile) {
// 1. Save the full size image
userData.profileImage = {
data: imageFile.buffer,
contentType: imageFile.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(imageFile);
userData.avatarImage = avatar;
}
// Handle creating team if superadmin
if (userData.role.includes("superadmin")) {
const team = new this.Team({
email: userData.email,
});
userData.teamId = team._id;
userData.checkTTL = 60 * 60 * 24 * 30;
await team.save();
}
const newUser = new this.User(userData);
await newUser.save();
return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find
} catch (error) {
if (error.code === DUPLICATE_KEY_CODE) {
error.message = this.stringService.dbUserExists;
}
error.service = SERVICE_NAME;
error.method = "insertUser";
throw error;
}
};
getUserByEmail = async (email) => {
try {
// Need the password to be able to compare, removed .select()
// We can strip the hash before returning the user
const user = await this.User.findOne({ email: email }).select("-profileImage");
if (!user) {
throw new Error(this.stringService.dbUserNotFound);
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserByEmail";
throw error;
}
};
updateUser = async ({ userId, user, file }) => {
if (!userId) {
throw new Error("No user in request");
}
try {
const candidateUser = { ...user };
if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) {
candidateUser.profileImage = null;
candidateUser.avatarImage = null;
} else if (file) {
// 1. Save the full size image
candidateUser.profileImage = {
data: file.buffer,
contentType: file.mimetype,
};
// 2. Get the avatar sized image
const avatar = await this.GenerateAvatarImage(file);
candidateUser.avatarImage = avatar;
}
const updatedUser = await this.User.findByIdAndUpdate(
userId,
candidateUser,
{ new: true } // Returns updated user instead of pre-update user
)
.select("-password")
.select("-profileImage");
return updatedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "updateUser";
throw error;
}
};
deleteUser = async (userId) => {
try {
const deletedUser = await this.User.findByIdAndDelete(userId);
if (!deletedUser) {
throw new Error(this.stringService.dbUserNotFound);
}
return deletedUser;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteUser";
throw error;
}
};
getAllUsers = async () => {
try {
const users = await this.User.find().select("-password").select("-profileImage");
return users;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getAllUsers";
throw error;
}
};
getUserById = async (roles, userId) => {
try {
if (!roles.includes("superadmin")) {
throw new Error("User is not a superadmin");
}
const user = await this.User.findById(userId).select("-password").select("-profileImage");
if (!user) {
throw new Error("User not found");
}
return user;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getUserById";
throw error;
}
};
editUserById = async (userId, user) => {
try {
await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage");
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editUserById";
throw error;
}
};
}
export default UserModule;