-> timeseries
This commit is contained in:
1 parent
5c14c68899
commit
e090d3cfd3
91 files changed
+941
-987
No files matched your search
Executable
+283
@@ -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 };
|
||||
@@ -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;
|
||||
Executable
+56
@@ -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
@@ -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;
|
||||
Executable
+593
@@ -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;
|
||||
+1214
File diff suppressed because it is too large.
Load diff
Executable
+100
@@ -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;
|
||||
Executable
+80
@@ -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;
|
||||
Executable
+36
@@ -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;
|
||||
Executable
+298
@@ -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;
|
||||
Executable
+171
@@ -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;
|
||||
Reference in new issue
Block a user