Files
Checkmate/server/src/db/v1/modules/incidentModule.js
T
karenvicent 0c3fdbc539 feat: add incident model, module and service with StatusService integration
- Create Incident model with resolution tracking fields
- Implement IncidentModule for data access
- Implement IncidentService with business logic
- Integrate with StatusService to auto-create/resolve incidents on status change
2025-11-06 17:06:33 -05:00

259 lines
6.0 KiB
JavaScript

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: "active" });
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getActiveIncidentByMonitor";
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: "resolved",
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;
}
};
getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, filter, page, rowsPerPage, status, monitorId, resolutionType }) => {
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const matchStage = {
teamId: new ObjectId(teamId),
...(status && { status }),
...(monitorId && { monitorId: new ObjectId(monitorId) }),
...(resolutionType && { resolutionType }),
...(dateRangeLookup[dateRange] && {
startTime: {
$gte: dateRangeLookup[dateRange],
},
}),
};
if (filter !== undefined) {
switch (filter) {
case "all":
break;
case "active":
matchStage.status = "active";
break;
case "resolved":
matchStage.status = "resolved";
break;
default:
break;
}
}
sortOrder = sortOrder === "asc" ? 1 : -1;
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const incidents = await this.Incident.aggregate([
{ $match: matchStage },
{ $sort: { startTime: sortOrder } },
{
$facet: {
summary: [{ $count: "incidentsCount" }],
incidents: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
incidentsCount: {
$ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0],
},
incidents: {
$ifNull: ["$incidents", []],
},
},
},
]);
return incidents[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getIncidentsByTeam";
throw error;
}
};
getIncidentSummary = async ({ teamId, dateRange }) => {
try {
const matchStage = {
teamId: new ObjectId(teamId),
...(dateRangeLookup[dateRange] && {
startTime: {
$gte: dateRangeLookup[dateRange],
},
}),
};
const summary = await this.Incident.aggregate([
{ $match: matchStage },
{
$group: {
_id: "$status",
count: { $sum: 1 },
manualResolutions: {
$sum: {
$cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0],
},
},
},
},
]);
const result = {
total: 0,
active: 0,
resolved: 0,
manual: 0,
};
summary.forEach((item) => {
result.total += item.count;
if (item._id === "active") {
result.active = item.count;
}
if (item._id === "resolved") {
result.resolved = item.count;
}
result.manual += item.manualResolutions;
});
return result;
} 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;