implement remaining check methods

This commit is contained in:
Alex Holliday
2026-01-20 21:18:26 +00:00
parent 9af25a19e5
commit 27dea6a08e
8 changed files with 138 additions and 339 deletions
-4
View File
@@ -64,7 +64,6 @@ import AppSettings from "../db/models/AppSettings.js";
import Incident from "../db/models/Incident.js";
import InviteModule from "../db/modules/inviteModule.js";
import CheckModule from "../db/modules/checkModule.js";
import StatusPageModule from "../db/modules/statusPageModule.js";
import UserModule from "../db/modules/userModule.js";
import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js";
@@ -150,7 +149,6 @@ export const initializeServices = async ({
const stringService = new StringService(translationService);
// Create DB
const checkModule = new CheckModule({ logger, Monitor, User });
const inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
@@ -163,7 +161,6 @@ export const initializeServices = async ({
const db = new MongoDB({
logger,
envSettings,
checkModule,
inviteModule,
statusPageModule,
userModule,
@@ -213,7 +210,6 @@ export const initializeServices = async ({
});
const checkService = new CheckService({
db,
errorService,
monitorsRepository,
logger,
-2
View File
@@ -7,7 +7,6 @@ class MongoDB {
constructor({
logger,
envSettings,
checkModule,
inviteModule,
statusPageModule,
userModule,
@@ -22,7 +21,6 @@ class MongoDB {
this.userModule = userModule;
this.inviteModule = inviteModule;
this.recoveryModule = recoveryModule;
this.checkModule = checkModule;
this.maintenanceWindowModule = maintenanceWindowModule;
this.notificationModule = notificationModule;
this.settingsModule = settingsModule;
-247
View File
@@ -1,247 +0,0 @@
import { ObjectId } from "mongodb";
import mongoose from "mongoose";
import { CheckModel } from "@/db/models/index.js";
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, Monitor, User }) {
this.logger = logger;
this.Monitor = Monitor;
this.User = User;
}
getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Match
const matchStage = {
"metadata.monitorId": new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(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 CheckModel.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, page, rowsPerPage, teamId }) => {
try {
console.log({
sortOrder,
dateRange,
filter,
page,
rowsPerPage,
teamId,
});
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const matchStage = {
"metadata.teamId": new ObjectId(teamId),
status: false,
...(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 CheckModel.aggregate(aggregatePipeline);
return checks[0];
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getChecksByTeam";
throw error;
}
};
getChecksSummaryByTeamId = async ({ teamId }) => {
try {
const matchStage = {
"metadata.teamId": new ObjectId(teamId),
};
const checks = await CheckModel.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 CheckModel.deleteMany({ "metadata.monitorId": 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 CheckModel.deleteMany({ "metadata.monitorId": { $in: monitorIds } });
return deleteResult.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteChecksByTeamId";
throw error;
}
};
updateChecksTTL = async (teamId, ttl) => {
try {
await CheckModel.collection.dropIndex("expiry_1");
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "updateChecksTTL",
stack: error.stack,
});
}
try {
await CheckModel.collection.createIndex(
{ expiry: 1 },
{ expireAfterSeconds: ttl, partialFilterExpression: { "metadata.mode": { $exists: true } } }
);
} 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;
@@ -1,44 +0,0 @@
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 };
@@ -68,15 +68,18 @@ export interface IChecksRepository {
rowsPerPage: number,
status: boolean | undefined
): Promise<any>;
findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findDateRangeChecksByMonitor(
findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<any>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId(
monitorId: string,
startDate: Date,
endDate: Date,
dateString: string,
options?: { type?: MonitorType }
): Promise<UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult>;
findSummaryByTeamId(teamId: string): Promise<any>;
// update
//delete
deleteByMonitorId(monitorId: string): Promise<number>;
deleteByTeamId(teamId: string): Promise<number>;
}
@@ -18,7 +18,7 @@ import mongoose from "mongoose";
const SERVICE_NAME = "StatusService";
const dateRangeLookup: Record<string, Date> = {
const dateRangeLookup: Record<string, Date | undefined> = {
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)),
@@ -269,7 +269,67 @@ class MongoChecksRepository implements IChecksRepository {
return checks[0];
};
findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => {
const matchStage: Record<string, any> = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
status: false,
...(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;
}
}
const parsedSortOrder = sortOrder === "asc" ? 1 : -1;
// pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
const aggregatePipeline: any = [
{ $match: matchStage },
{ $sort: { createdAt: parsedSortOrder } },
{
$facet: {
summary: [{ $count: "checksCount" }],
checks: [{ $skip: skip }, { $limit: rowsPerPage }],
},
},
{
$project: {
checksCount: { $arrayElemAt: ["$summary.checksCount", 0] },
checks: "$checks",
},
},
];
const checks = await CheckModel.aggregate(aggregatePipeline);
return checks[0];
};
findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap> => {
if (monitorIds.length === 0) {
return {};
}
@@ -303,7 +363,7 @@ class MongoChecksRepository implements IChecksRepository {
}, {});
};
findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => {
const monitorObjectId = new mongoose.Types.ObjectId(monitorId);
if (options?.type === "hardware") {
return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString);
@@ -314,6 +374,63 @@ class MongoChecksRepository implements IChecksRepository {
return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString);
};
findSummaryByTeamId = async (teamId: string) => {
const matchStage = {
"metadata.teamId": new mongoose.Types.ObjectId(teamId),
};
const checks = await CheckModel.aggregate([
{ $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] },
},
},
]);
return checks[0].summary;
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) });
return result.deletedCount;
};
deleteByTeamId = async (teamId: string) => {
const deleteResult = await CheckModel.deleteMany({ "metadata.teamId": teamId });
return deleteResult.deletedCount;
};
private findUptimeDateRangeChecks = async (
monitorType: Exclude<MonitorType, "hardware" | "pagespeed">,
monitorObjectId: mongoose.Types.ObjectId,
@@ -477,11 +594,6 @@ class MongoChecksRepository implements IChecksRepository {
};
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) });
return result.deletedCount;
};
private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise<HardwareAggregateData> => {
const result = await CheckModel.aggregate([
{
+9 -28
View File
@@ -1,6 +1,7 @@
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js";
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js";
const SERVICE_NAME = "checkService";
@@ -8,25 +9,21 @@ const SERVICE_NAME = "checkService";
class CheckService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private errorService: any;
private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository;
private logger: any;
constructor({
db,
errorService,
monitorsRepository,
logger,
checksRepository,
}: {
db: any;
errorService: any;
monitorsRepository: IMonitorsRepository;
logger: any;
checksRepository: IChecksRepository;
}) {
this.db = db;
this.errorService = errorService;
this.monitorsRepository = monitorsRepository;
this.logger = logger;
@@ -132,15 +129,6 @@ class CheckService {
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
// const result = await this.db.checkModule.getChecksByMonitor({
// monitorId,
// sortOrder,
// dateRange,
// filter,
// page,
// rowsPerPage,
// status,
// });
return result;
};
@@ -151,14 +139,10 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const checkData = await this.db.checkModule.getChecksByTeam({
sortOrder,
dateRange,
filter,
page,
rowsPerPage,
teamId,
});
const parsedPage = page ? parseInt(page) : page;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage;
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
return checkData;
};
@@ -166,8 +150,7 @@ class CheckService {
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
}
const summary = await this.db.checkModule.getChecksSummaryByTeamId({ teamId });
const summary = await this.checksRepository.findSummaryByTeamId(teamId);
return summary;
};
@@ -183,7 +166,7 @@ class CheckService {
// For verificaiton, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId);
const deletedCount = await this.db.checkModule.deleteChecks(monitorId);
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
return deletedCount;
};
deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => {
@@ -191,14 +174,12 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const deletedCount = await this.db.checkModule.deleteChecksByTeamId(teamId);
const deletedCount = await this.checksRepository.deleteByTeamId(teamId);
return deletedCount;
};
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
const SECONDS_PER_DAY = 86400;
const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY;
await this.db.checkModule.updateChecksTTL(teamId, newTTL);
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 });
};
}
@@ -253,7 +253,7 @@ export class MonitorService implements IMonitorService {
}
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type,
});
const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id);
@@ -292,7 +292,7 @@ export class MonitorService implements IMonitorService {
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type,
});
@@ -323,7 +323,7 @@ export class MonitorService implements IMonitorService {
const rangeKey = (dateRange as DateRangeKey) ?? "recent";
const { start, end } = this.getDateRange(rangeKey);
const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), {
const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), {
type: monitor.type,
});
@@ -394,7 +394,7 @@ export class MonitorService implements IMonitorService {
requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType));
const limitPerMonitor = snapshotOnlyRequest ? 1 : 25;
const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(
const checksMap = await this.checksRepository.findLatestByMonitorIds(
monitorsList.map((monitor) => monitor.id),
{ limitPerMonitor }
);