Merge pull request #3131 from bluewave-labs/fix/remove-monitor-db-module

fix: remove db module
This commit is contained in:
Alexander Holliday
2026-01-15 13:05:12 -08:00
committed by GitHub
15 changed files with 481 additions and 1469 deletions
+3 -3
View File
@@ -10,8 +10,8 @@ import StatusPageController from "../controllers/statusPageController.js";
import NotificationController from "../controllers/notificationController.js";
import DiagnosticController from "../controllers/diagnosticController.js";
import IncidentController from "../controllers/incidentController.js";
export const initializeControllers = (services: any) => {
import type { InitializedSerivces } from "@/config/services.js";
export const initializeControllers = (services: InitializedSerivces) => {
const controllers: Record<string, any> = {};
controllers.authController = new AuthController(services.userService);
@@ -26,7 +26,7 @@ export const initializeControllers = (services: any) => {
controllers.queueController = new QueueController(services.jobQueue);
controllers.logController = new LogController(services.logger);
controllers.statusPageController = new StatusPageController(services.db);
controllers.notificationController = new NotificationController(services.notificationService, services.db);
controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository);
controllers.diagnosticController = new DiagnosticController(services.diagnosticService);
controllers.incidentController = new IncidentController(services.incidentService);
+57 -15
View File
@@ -63,16 +63,61 @@ 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";
import MonitorModule from "../db/modules/monitorModule.js";
import NotificationModule from "../db/modules/notificationModule.js";
import RecoveryModule from "../db/modules/recoveryModule.js";
import SettingsModule from "../db/modules/settingsModule.js";
import IncidentModule from "../db/modules/incidentModule.js";
// repositories
import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js";
import {
MongoMonitorsRepository,
MongoChecksRepository,
MongoMonitorStatsRepository,
MongoStatusPagesRepository,
IMonitorsRepository,
IChecksRepository,
IMonitorStatsRepository,
IStatusPagesRepository,
} from "@/repositories/index.js";
export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => {
export type InitializedSerivces = {
//v1
settingsService: any;
translationService: any;
stringService: any;
db: any;
networkService: any;
emailService: any;
bufferService: any;
statusService: any;
notificationService: any;
jobQueue: any;
userService: any;
checkService: any;
diagnosticService: any;
inviteService: any;
maintenanceWindowService: any;
monitorService: any;
incidentService: any;
errorService: any;
logger: any;
// Repositories
monitorsRepository: IMonitorsRepository;
checksRepository: IChecksRepository;
monitorStatsRepository: IMonitorStatsRepository;
statusPagesRepository: IStatusPagesRepository;
};
export const initializeServices = async ({
logger,
envSettings,
settingsService,
}: {
logger: any;
envSettings: any;
settingsService: any;
}): Promise<InitializedSerivces> => {
const serviceRegistry = new ServiceRegistry({ logger });
(ServiceRegistry as any).instance = serviceRegistry;
@@ -87,17 +132,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
const monitorModule = new MonitorModule({
Monitor,
MonitorStats,
stringService,
fs,
path,
fileURLToPath,
ObjectId,
NormalizeData,
NormalizeDataUptimeDetails,
});
const notificationModule = new NotificationModule({ Notification, Monitor });
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
const settingsModule = new SettingsModule({ AppSettings });
@@ -111,7 +145,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService
statusPageModule,
userModule,
maintenanceWindowModule,
monitorModule,
notificationModule,
recoveryModule,
settingsModule,
@@ -196,12 +229,14 @@ export const initializeServices = async ({ logger, envSettings, settingsService
jwt,
errorService,
jobQueue: superSimpleQueue,
monitorsRepository,
});
const checkService = new CheckService({
db,
settingsService,
stringService,
errorService,
monitorsRepository,
});
const diagnosticService = new DiagnosticService();
const inviteService = new InviteService({
@@ -216,6 +251,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
settingsService,
stringService,
errorService,
monitorsRepository,
});
const monitorService = new MonitorService({
jobQueue: superSimpleQueue,
@@ -252,6 +288,12 @@ export const initializeServices = async ({ logger, envSettings, settingsService
incidentService,
errorService,
logger,
// Repositories
monitorsRepository,
checksRepository,
monitorStatsRepository,
statusPagesRepository,
};
Object.values(services).forEach((service) => {
@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from "express";
import { createNotificationBodyValidation } from "@/validation/joi.js";
import { AppError } from "@/utils/AppError.js";
import { IMonitorsRepository } from "@/repositories/index.js";
const SERVICE_NAME = "NotificationController";
@@ -9,9 +10,11 @@ class NotificationController {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private notificationService: any;
constructor(notificationService: any, db: any) {
private monitorsRepository: IMonitorsRepository;
constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) {
this.notificationService = notificationService;
this.db = db;
this.monitorsRepository = monitorsRepository;
}
get serviceName() {
@@ -167,11 +170,7 @@ class NotificationController {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
throw new AppError({ message: "Unauthorized", status: 403 });
}
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
const notifications = monitor.notifications;
if (notifications.length === 0) {
-2
View File
@@ -12,7 +12,6 @@ class MongoDB {
statusPageModule,
userModule,
maintenanceWindowModule,
monitorModule,
notificationModule,
recoveryModule,
settingsModule,
@@ -25,7 +24,6 @@ class MongoDB {
this.recoveryModule = recoveryModule;
this.checkModule = checkModule;
this.maintenanceWindowModule = maintenanceWindowModule;
this.monitorModule = monitorModule;
this.notificationModule = notificationModule;
this.settingsModule = settingsModule;
this.statusPageModule = statusPageModule;
-539
View File
@@ -1,539 +0,0 @@
import {
buildUptimeDetailsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
getHardwareStats,
getUpChecks,
getAggregateData,
} from "./monitorModuleQueries.js";
import { CheckModel } from "@/db/models/index.js";
const SERVICE_NAME = "monitorModule";
class MonitorModule {
constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
this.Monitor = Monitor;
this.MonitorStats = MonitorStats;
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 objectId = new this.ObjectId(monitorId);
const indexSpec = {
"metadata.monitorId": 1,
updatedAt: sortOrder,
};
const matchBase = { "metadata.monitorId": objectId };
const [checksAll, checksForDateRange] = await Promise.all([
CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
CheckModel.find({
...matchBase,
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,
};
};
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 CheckModel.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;
}
};
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 [aggregateData, upChecksCount, metrics] = await Promise.all([
getAggregateData(monitorId, dates),
getUpChecks(monitorId, dates),
getHardwareStats(monitorId, dates, dateString),
]);
const stats = {
aggregateData: aggregateData,
upChecks: upChecksCount,
checks: metrics,
};
return {
...monitor.toObject(),
stats,
};
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getHardwareDetailsById";
throw error;
}
};
getMonitorsByTeamId = async ({ teamId, type, filter }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
if (filter !== undefined && filter !== null && filter !== "") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
const monitors = await this.Monitor.find(matchStage)
.sort({ name: 1 })
.select({
_id: 1,
name: 1,
type: 1,
url: 1,
status: 1,
isActive: 1,
teamId: 1,
})
.lean();
return monitors;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorsByTeamId";
throw error;
}
};
findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => {
try {
const matchStage = { teamId: new this.ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage });
if (explain === true) {
return this.Monitor.aggregate(pipeline).explain("executionStats");
}
const [summary] = await this.Monitor.aggregate(pipeline);
return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "findMonitorsSummaryByTeamId";
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;
@@ -1,740 +0,0 @@
import { ObjectId } from "mongodb";
import { CheckModel } from "@/db/models/index.js";
const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
return [
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
updatedAt: { $gte: dates.start, $lte: dates.end },
},
},
{
$sort: {
updatedAt: 1,
},
},
{
$facet: {
// For the response time chart, should return checks for date window
// Grouped by: {day: hour}, {week: day}, {month: day}
uptimePercentage: [
{
$group: {
_id: null,
upChecks: {
$sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] },
},
totalChecks: { $sum: 1 },
},
},
{
$project: {
_id: 0,
percentage: {
$cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }],
},
},
},
],
groupedAvgResponseTime: [
{
$group: {
_id: null,
avgResponseTime: {
$avg: "$responseTime",
},
},
},
],
groupedChecks: [
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
avgResponseTime: {
$avg: "$responseTime",
},
totalChecks: {
$sum: 1,
},
},
},
{
$sort: {
_id: 1,
},
},
],
// Up checks grouped by: {day: hour}, {week: day}, {month: day}
groupedUpChecks: [
{
$match: {
status: true,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
// Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window
groupedDownChecks: [
{
$match: {
status: false,
},
},
{
$group: {
_id: {
$dateToString: {
format: dateString,
date: "$createdAt",
},
},
totalChecks: {
$sum: 1,
},
avgResponseTime: {
$avg: "$responseTime",
},
},
},
{
$sort: { _id: 1 },
},
],
},
},
{
$lookup: {
from: "monitors",
let: { monitor_id: { $toObjectId: monitorId } },
pipeline: [
{
$match: {
$expr: { $eq: ["$_id", "$$monitor_id"] },
},
},
{
$project: {
_id: 1,
teamId: 1,
name: 1,
status: 1,
interval: 1,
type: 1,
url: 1,
isActive: 1,
notifications: 1,
},
},
],
as: "monitor",
},
},
{
$project: {
groupedAvgResponseTime: {
$arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0],
},
groupedChecks: "$groupedChecks",
groupedUpChecks: "$groupedUpChecks",
groupedDownChecks: "$groupedDownChecks",
groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] },
monitor: { $arrayElemAt: ["$monitor", 0] },
},
},
];
};
const buildMonitorStatsPipeline = (monitor) => {
return [
{
$match: {
monitorId: monitor._id,
},
},
{
$project: {
avgResponseTime: 1,
uptimePercentage: 1,
totalChecks: 1,
timeSinceLastCheck: {
$subtract: [Date.now(), "$lastCheckTimestamp"],
},
lastCheckTimestamp: 1,
uptBurnt: { $toString: "$uptBurnt" },
},
},
];
};
const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
];
};
const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => {
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
type: 1,
port: 1,
},
},
];
};
const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => {
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: { name: 1 } },
{
$project: {
_id: 1,
name: 1,
type: 1,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
monitors: 1,
},
},
];
};
const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
// Match name
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
// Match isActive
if (typeof filter !== "undefined" && field === "isActive") {
matchStage.isActive = filter === "true" ? true : false;
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true" ? true : false;
}
// Match type
if (typeof filter !== "undefined" && field === "type") {
matchStage.type = filter;
}
const monitorsPipeline = [
{ $sort: sort },
{ $skip: skip },
...limitStage,
{
$project: {
_id: 1,
name: 1,
description: 1,
type: 1,
url: 1,
isActive: 1,
createdAt: 1,
updatedAt: 1,
uptimePercentage: 1,
status: 1,
},
},
];
// Add checks
if (limit) {
const checksCollection = "checks";
monitorsPipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { updatedAt: -1 } },
{ $limit: limit },
{
$project: {
_id: 1,
status: 1,
responseTime: 1,
statusCode: 1,
createdAt: 1,
updatedAt: 1,
originalResponseTime: 1,
},
},
],
as: "checks",
},
});
}
const pipeline = [
{ $match: matchStage },
{
$facet: {
count: [{ $count: "monitorsCount" }],
monitors: monitorsPipeline,
},
},
{
$project: {
count: { $arrayElemAt: ["$count", 0] },
monitors: 1,
},
},
];
return pipeline;
};
const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => {
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : [];
if (typeof filter !== "undefined" && field === "name") {
matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }];
}
if (typeof filter !== "undefined" && field === "status") {
matchStage.status = filter === "true";
}
const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage];
// Add checks
if (limit) {
const checksCollection = "checks";
pipeline.push({
$lookup: {
from: checksCollection,
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
{ $limit: limit },
],
as: "checks",
},
});
}
return pipeline;
};
const buildGetMonitorsByTeamIdPipeline = (req) => {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
if (field === undefined) {
field = "name";
order = "asc";
}
// Build the match stage
const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const skip = page && rowsPerPage ? page * rowsPerPage : 0;
const sort = { [field]: order === "asc" ? 1 : -1 };
return [
{ $match: matchStage },
{
$facet: {
summary: [
{
$group: {
_id: null,
totalMonitors: { $sum: 1 },
upMonitors: {
$sum: {
$cond: [{ $eq: ["$status", true] }, 1, 0],
},
},
downMonitors: {
$sum: {
$cond: [{ $eq: ["$status", false] }, 1, 0],
},
},
pausedMonitors: {
$sum: {
$cond: [{ $eq: ["$isActive", false] }, 1, 0],
},
},
},
},
{
$project: {
_id: 0,
},
},
],
monitors: [
{ $sort: sort },
{
$project: {
_id: 1,
name: 1,
},
},
],
filteredMonitors: [
...(filter !== undefined
? [
{
$match: {
$or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }],
},
},
]
: []),
{ $sort: sort },
{ $skip: skip },
...(rowsPerPage ? [{ $limit: rowsPerPage }] : []),
...(limit
? [
{
$lookup: {
from: "checks",
let: { monitorId: "$_id" },
pipeline: [
{
$match: {
$expr: { $eq: ["$monitorId", "$$monitorId"] },
},
},
{ $sort: { createdAt: -1 } },
...(limit ? [{ $limit: limit }] : []),
],
as: "standardchecks",
},
},
]
: []),
{
$addFields: {
checks: {
$switch: {
branches: [
{
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
then: "$standardchecks",
},
],
default: [],
},
},
},
},
{
$project: {
standardchecks: 0,
},
},
],
},
},
{
$project: {
summary: { $arrayElemAt: ["$summary", 0] },
filteredMonitors: 1,
monitors: 1,
},
},
];
};
export {
buildUptimeDetailsPipeline,
buildMonitorStatsPipeline,
buildGetMonitorsByTeamIdPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
};
export const getAggregateData = async (monitorId, dates) => {
const result = await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: null,
latestCheck: { $first: "$$ROOT" },
totalChecks: { $sum: 1 },
},
},
]);
return result[0] || { totalChecks: 0, latestCheck: null };
};
export const getUpChecks = async (monitorId, dates) => {
const count = await CheckModel.countDocuments({
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
status: true,
});
return { totalChecks: count };
};
export const getHardwareStats = async (monitorId, dates, dateString) => {
return await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: 1 } },
{
$group: {
_id: { $dateToString: { format: dateString, date: "$createdAt" } },
avgCpuUsage: { $avg: "$cpu.usage_percent" },
avgMemoryUsage: { $avg: "$memory.usage_percent" },
avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } },
disks: { $push: "$disk" },
net: { $push: "$net" },
updatedAts: { $push: "$updatedAt" },
sampleDoc: { $first: "$$ROOT" },
},
},
{
$project: {
_id: 1,
avgCpuUsage: 1,
avgMemoryUsage: 1,
avgTemperature: {
$map: {
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] },
as: "idx",
in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } },
},
},
disks: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] },
as: "dIdx",
in: {
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
},
},
},
net: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] },
as: "nIdx",
in: {
name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] },
bytesSentPerSecond: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaBytesRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsSent: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
},
},
},
},
},
{ $sort: { _id: 1 } },
]);
};
@@ -15,13 +15,11 @@ import type {
} from "@/types/index.js";
import { CheckModel, type CheckDocument } from "@/db/models/index.js";
import mongoose from "mongoose";
import {
getAggregateData as getHardwareAggregateData,
getHardwareStats,
getUpChecks as getHardwareUpChecks,
} from "@/db/modules/monitorModuleQueries.js";
export type LatestChecksMap = Record<string, Check[]>;
type DateRange = { start: Date; end: Date };
type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number };
type HardwareUpChecks = { totalChecks: number };
class MongoChecksRepository implements IChecksRepository {
private toEntity = (doc: CheckDocument): Check => {
@@ -323,9 +321,9 @@ class MongoChecksRepository implements IChecksRepository {
const monitorId = monitorObjectId.toHexString();
const dates = { start: startDate, end: endDate };
const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([
getHardwareAggregateData(monitorId, dates),
getHardwareUpChecks(monitorId, dates),
getHardwareStats(monitorId, dates, dateString),
this.getHardwareAggregateData(monitorId, dates),
this.getHardwareUpChecks(monitorId, dates),
this.getHardwareStats(monitorId, dates, dateString),
]);
const aggregateData = {
@@ -387,9 +385,183 @@ class MongoChecksRepository implements IChecksRepository {
};
deleteByMonitorId = async (monitorId: string): Promise<number> => {
const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId });
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([
{
$match: {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: -1 } },
{
$group: {
_id: null,
latestCheck: { $first: "$$ROOT" },
totalChecks: { $sum: 1 },
},
},
]);
return result[0] || { totalChecks: 0, latestCheck: null };
};
private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise<HardwareUpChecks> => {
const count = await CheckModel.countDocuments({
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
status: true,
});
return { totalChecks: count };
};
private getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => {
return await CheckModel.aggregate([
{
$match: {
"metadata.monitorId": new mongoose.Types.ObjectId(monitorId),
"metadata.type": "hardware",
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
{ $sort: { createdAt: 1 } },
{
$group: {
_id: { $dateToString: { format: dateString, date: "$createdAt" } },
avgCpuUsage: { $avg: "$cpu.usage_percent" },
avgMemoryUsage: { $avg: "$memory.usage_percent" },
avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } },
disks: { $push: "$disk" },
net: { $push: "$net" },
updatedAts: { $push: "$updatedAt" },
sampleDoc: { $first: "$$ROOT" },
},
},
{
$project: {
_id: 1,
avgCpuUsage: 1,
avgMemoryUsage: 1,
avgTemperature: {
$map: {
input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] },
as: "idx",
in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } },
},
},
disks: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] },
as: "dIdx",
in: {
name: { $concat: ["disk", { $toString: "$$dIdx" }] },
readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } },
writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } },
totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } },
freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } },
usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } },
},
},
},
net: {
$map: {
input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] },
as: "nIdx",
in: {
name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] },
bytesSentPerSecond: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaBytesRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsSent: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaPacketsRecv: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaErrOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropIn: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
deltaDropOut: {
$let: {
vars: {
tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] },
f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] },
},
in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] },
},
},
},
},
},
},
},
{ $sort: { _id: 1 } },
]);
};
}
export default MongoChecksRepository;
@@ -19,11 +19,12 @@ export interface IMonitorsRepository {
create(monitor: Monitor, teamId: string, userId: string): Promise<Monitor | null>;
createBulkMonitors(monitors: Monitor[]): Promise<Monitor[]>;
// single fetch
findById(monitorId: string, teamId?: string): Promise<Monitor | null>;
findById(monitorId: string, teamId: string): Promise<Monitor>;
// collection fetch
findAll(): Promise<Monitor[] | null>;
findByTeamId(teamId: string, config: TeamQueryConfig): Promise<Monitor[] | null>;
findByIds(monitorIds: string[]): Promise<Monitor[]>;
// update
updateById(monitorId: string, teamId: string, updates: Partial<Monitor>): Promise<Monitor>;
@@ -21,19 +21,11 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.mapDocuments(inserted);
};
findById = async (monitorId: string, teamId?: string): Promise<Monitor> => {
const match: { _id: string; teamId?: string } = { _id: monitorId };
if (teamId) {
match.teamId = teamId;
}
findById = async (monitorId: string, teamId: string): Promise<Monitor> => {
const match: { _id: string; teamId: string } = { _id: monitorId, teamId };
const monitor = await MonitorModel.findOne(match);
if (!monitor) {
if (monitor === null || monitor === undefined) {
throw new AppError({
message: `Monitor with ID ${monitorId} not found.`,
status: 404,
});
}
throw new AppError({ message: `Monitor with ID ${monitorId} not found`, status: 404 });
}
return this.toEntity(monitor);
};
@@ -81,6 +73,12 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return this.mapDocuments(documents);
};
findByIds = async (monitorIds: string[]): Promise<Monitor[]> => {
const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id));
const monitors = await MonitorModel.find({ _id: { $in: objectIds } });
return this.mapDocuments(monitors);
};
findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise<number> => {
const { type } = config ?? {};
@@ -1,47 +1,60 @@
import { IStatusPagesRepository } from "@/repositories/index.js";
import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js";
import type { StatusPage } from "@/types/statusPage.js";
import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js";
import mongoose from "mongoose";
class MongoStatusPagesRepository implements IStatusPagesRepository {
private toEntity(doc: StatusPageDocument): StatusPage {
const toStringId = (value: unknown): string => {
if (value instanceof mongoose.Types.ObjectId) {
return value.toString();
}
return value?.toString() ?? "";
};
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value);
};
const toDateString = (value: Date | string): string => {
return value instanceof Date ? value.toISOString() : value;
};
private toDateString = (value?: Date | string | null): string => {
if (!value) {
return new Date(0).toISOString();
}
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
};
const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => {
return values?.map((value) => toStringId(value)) ?? [];
};
private mapIdArray = (values?: Array<mongoose.Types.ObjectId | string>): string[] => {
return values?.map((value) => this.toStringId(value)) ?? [];
};
private mapLogo = (logo?: StatusPageLogo | null): StatusPageLogo | undefined => {
if (!logo) {
return undefined;
}
return {
id: toStringId(doc._id),
userId: toStringId(doc.userId),
teamId: toStringId(doc.teamId),
data: logo.data,
contentType: logo.contentType,
};
};
private toEntity = (doc: StatusPageDocument): StatusPage => {
return {
id: this.toStringId(doc._id),
userId: this.toStringId(doc.userId),
teamId: this.toStringId(doc.teamId),
type: doc.type,
companyName: doc.companyName,
url: doc.url,
timezone: doc.timezone ?? undefined,
color: doc.color,
monitors: mapIdArray(doc.monitors),
subMonitors: mapIdArray(doc.subMonitors),
originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)),
logo: doc.logo ?? undefined,
monitors: this.mapIdArray(doc.monitors),
subMonitors: this.mapIdArray(doc.subMonitors),
originalMonitors: this.mapIdArray(doc.originalMonitors),
logo: this.mapLogo(doc.logo),
isPublished: doc.isPublished,
showCharts: doc.showCharts,
showUptimePercentage: doc.showUptimePercentage,
showAdminLoginLink: doc.showAdminLoginLink,
customCSS: doc.customCSS,
createdAt: toDateString(doc.createdAt),
updatedAt: toDateString(doc.updatedAt),
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
}
};
removeMonitorFromStatusPages = async (monitorId: string): Promise<number> => {
const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } });
@@ -1,20 +1,41 @@
import { IMonitorsRepository } from "@/repositories/index.js";
const SERVICE_NAME = "checkService";
class CheckService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ db, settingsService, stringService, errorService }) {
private db: any;
private settingsService: any;
private stringService: any;
private errorService: any;
private monitorsRepository: IMonitorsRepository;
constructor({
db,
settingsService,
stringService,
errorService,
monitorsRepository,
}: {
db: any;
settingsService: any;
stringService: any;
errorService: any;
monitorsRepository: IMonitorsRepository;
}) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
this.monitorsRepository = monitorsRepository;
}
get serviceName() {
return CheckService.SERVICE_NAME;
}
getChecksByMonitor = async ({ monitorId, query, teamId }) => {
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => {
if (!monitorId) {
throw this.errorService.createBadRequestError("No monitor ID in request");
}
@@ -23,15 +44,8 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
// For verificaiton, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId);
let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query;
const result = await this.db.checkModule.getChecksByMonitor({
@@ -47,7 +61,7 @@ class CheckService {
return result;
};
getChecksByTeam = async ({ teamId, query }) => {
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => {
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query;
if (!teamId) {
@@ -66,7 +80,7 @@ class CheckService {
return checkData;
};
getChecksSummaryByTeamId = async ({ teamId }) => {
getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => {
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
}
@@ -75,7 +89,7 @@ class CheckService {
return summary;
};
ackCheck = async ({ checkId, teamId, ack }) => {
ackCheck = async ({ checkId, teamId, ack }: { checkId: string; teamId: string; ack: any }) => {
if (!checkId) {
throw this.errorService.createBadRequestError("No check ID in request");
}
@@ -88,27 +102,21 @@ class CheckService {
return updatedCheck;
};
ackAllChecks = async ({ monitorId, path, teamId, ack }) => {
ackAllChecks = async ({ monitorId, path, teamId, ack }: { monitorId: string; path: string; teamId: string; ack: any }) => {
if (path === "monitor") {
if (!monitorId) {
throw this.errorService.createBadRequestError("No monitor ID in request");
}
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
// For verificaiton, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId);
}
const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path);
return updatedChecks;
};
deleteChecks = async ({ monitorId, teamId }) => {
deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => {
if (!monitorId) {
throw this.errorService.createBadRequestError("No monitor ID in request");
}
@@ -117,20 +125,13 @@ class CheckService {
throw this.errorService.createBadRequestError("No team ID in request");
}
const monitor = await this.db.monitorModule.getMonitorById(monitorId);
if (!monitor) {
throw this.errorService.createNotFoundError("Monitor not found");
}
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
// 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);
return deletedCount;
};
deleteChecksByTeamId = async ({ teamId }) => {
deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => {
if (!teamId) {
throw this.errorService.createBadRequestError("No team ID in request");
}
@@ -139,7 +140,7 @@ class CheckService {
return deletedCount;
};
updateChecksTTL = async ({ teamId, ttl }) => {
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);
@@ -1,30 +1,50 @@
import { IMonitorsRepository } from "@/repositories/index.js";
const SERVICE_NAME = "maintenanceWindowService";
class MaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private settingsService: any;
private stringService: any;
private errorService: any;
private monitorsRepository: IMonitorsRepository;
constructor({ db, settingsService, stringService, errorService }) {
constructor({
db,
settingsService,
stringService,
errorService,
monitorsRepository,
}: {
db: any;
settingsService: any;
stringService: any;
errorService: any;
monitorsRepository: IMonitorsRepository;
}) {
this.db = db;
this.settingsService = settingsService;
this.stringService = stringService;
this.errorService = errorService;
this.monitorsRepository = monitorsRepository;
}
get serviceName() {
return MaintenanceWindowService.SERVICE_NAME;
}
createMaintenanceWindow = async ({ teamId, body }) => {
createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => {
const monitorIds = body.monitors;
const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds);
const monitors = await this.monitorsRepository.findByIds(monitorIds);
const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId));
const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId);
if (unauthorizedMonitors.length > 0) {
throw this.errorService.createAuthorizationError();
}
const dbTransactions = monitorIds.map((monitorId) => {
const dbTransactions = monitorIds.map((monitorId: string) => {
return this.db.maintenanceWindowModule.createMaintenanceWindow({
teamId,
monitorId,
@@ -38,26 +58,26 @@ class MaintenanceWindowService {
await Promise.all(dbTransactions);
};
getMaintenanceWindowById = async ({ id, teamId }) => {
getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => {
const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId });
return maintenanceWindow;
};
getMaintenanceWindowsByTeamId = async ({ teamId, query }) => {
getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query);
return maintenanceWindows;
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
return maintenanceWindows;
};
deleteMaintenanceWindow = async ({ id, teamId }) => {
deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => {
await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId });
};
editMaintenanceWindow = async ({ id, teamId, body }) => {
editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => {
const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId });
return editedMaintenanceWindow;
};
@@ -1,9 +1,44 @@
import { IMonitorsRepository } from "@/repositories/index.js";
const SERVICE_NAME = "userService";
class UserService {
static SERVICE_NAME = SERVICE_NAME;
constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) {
private db: any;
private emailService: any;
private settingsService: any;
private logger: any;
private stringService: any;
private jwt: any;
private errorService: any;
private jobQueue: any;
private crypto: any;
private monitorsRepository: IMonitorsRepository;
constructor({
crypto,
db,
emailService,
settingsService,
logger,
stringService,
jwt,
errorService,
jobQueue,
monitorsRepository,
}: {
crypto: any;
db: any;
emailService: any;
settingsService: any;
logger: any;
stringService: any;
jwt: any;
errorService: any;
jobQueue: any;
monitorsRepository: IMonitorsRepository;
}) {
this.db = db;
this.emailService = emailService;
this.settingsService = settingsService;
@@ -13,20 +48,21 @@ class UserService {
this.errorService = errorService;
this.jobQueue = jobQueue;
this.crypto = crypto;
this.monitorsRepository = monitorsRepository;
}
get serviceName() {
return UserService.SERVICE_NAME;
}
issueToken = (payload, appSettings) => {
issueToken = (payload: any, appSettings: any) => {
const tokenTTL = appSettings?.jwtTTL ?? "2h";
const tokenSecret = appSettings?.jwtSecret;
const payloadData = payload;
return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
};
registerUser = async (user, file) => {
registerUser = async (user: any, file: any) => {
// Create a new user
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this.db.userModule.checkSuperadmin();
@@ -61,7 +97,7 @@ class UserService {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => {
this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
@@ -69,7 +105,7 @@ class UserService {
stack: error.stack,
});
});
} catch (error) {
} catch (error: any) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
@@ -81,7 +117,7 @@ class UserService {
return { user: newUser, token };
};
loginUser = async (email, password) => {
loginUser = async (email: string, password: string) => {
// Check if user exists
const user = await this.db.userModule.getUserByEmail(email);
// Compare password
@@ -103,7 +139,7 @@ class UserService {
return { user: userWithoutPassword, token };
};
editUser = async (updates, file, currentUser) => {
editUser = async (updates: any, file: any, currentUser: any) => {
// Change Password check
if (updates?.password && updates?.newPassword) {
// Get user's email
@@ -131,7 +167,7 @@ class UserService {
return superAdminExists;
};
requestRecovery = async (email) => {
requestRecovery = async (email: string) => {
const user = await this.db.userModule.getUserByEmail(email);
const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email);
const name = user.firstName;
@@ -147,18 +183,18 @@ class UserService {
return msgId;
};
validateRecovery = async (recoveryToken) => {
validateRecovery = async (recoveryToken: string) => {
await this.db.recoveryModule.validateRecoveryToken(recoveryToken);
};
resetPassword = async (password, recoveryToken) => {
resetPassword = async (password: string, recoveryToken: string) => {
const user = await this.db.recoveryModule.resetPassword(password, recoveryToken);
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(user._doc, appSettings);
return { user, token };
};
deleteUser = async (user) => {
deleteUser = async (user: any) => {
const email = user?.email;
if (!email) {
throw this.errorService.createBadRequestError("No email in request");
@@ -181,15 +217,14 @@ class UserService {
}
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.db.monitorModule.getMonitorsByTeamId({
teamId: teamId,
});
const res = await this.monitorsRepository.findByTeamId(teamId, {});
if (roles.includes("superadmin")) {
// 2. Remove all jobs, delete checks and alerts
result?.monitors.length > 0 &&
res &&
res?.length > 0 &&
(await Promise.all(
result.monitors.map(async (monitor) => {
res.map(async (monitor) => {
await this.jobQueue.deleteJob(monitor);
})
));
@@ -203,15 +238,15 @@ class UserService {
return users;
};
getUserById = async (roles, userId) => {
getUserById = async (roles: any, userId: any) => {
const user = await this.db.userModule.getUserById(roles, userId);
return user;
};
editUserById = async (userId, user) => {
editUserById = async (userId: any, user: any) => {
await this.db.userModule.editUserById(userId, user);
};
setPasswordByUserId = async (userId, password) => {
setPasswordByUserId = async (userId: any, password: string) => {
const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null });
return updatedUser;
};
@@ -167,9 +167,8 @@ class StatusService {
const check = this.buildCheck(networkResponse);
await this.insertCheck(check);
try {
const { monitorId, status, code } = networkResponse;
const monitor = await this.monitorsRepository.findById(monitorId);
const { monitorId, teamId, status, code } = networkResponse;
const monitor = await this.monitorsRepository.findById(monitorId, teamId);
// Update running stats
this.updateRunningStats({ monitor, networkResponse });
+65 -52
View File
@@ -1,12 +1,14 @@
import { jest } from "@jest/globals";
import { MonitorService } from "../src/service/business/monitorService.ts";
import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts";
import type { IChecksRepository, IMonitorStatsRepository, IMonitorsRepository, IStatusPagesRepository } from "../src/repositories/index.ts";
const createMonitorsRepositoryMock = () =>
({
findMonitorCountByTeamIdAndType: jest.fn(),
findByTeamId: jest.fn(),
findById: jest.fn(),
findMonitorsSummaryByTeamId: jest.fn(),
findGroupsByTeamId: jest.fn(),
create: jest.fn(),
createBulkMonitors: jest.fn(),
deleteByTeamId: jest.fn(),
@@ -18,32 +20,34 @@ const createChecksRepositoryMock = () =>
findDateRangeChecksByMonitor: jest.fn(),
}) as unknown as IChecksRepository;
const createMonitorStatsRepositoryMock = () =>
({
findByMonitorId: jest.fn(),
deleteByMonitorId: jest.fn(),
}) as unknown as IMonitorStatsRepository;
const createStatusPagesRepositoryMock = () =>
({
removeMonitorFromStatusPages: jest.fn(),
}) as unknown as IStatusPagesRepository;
const createService = ({
monitorsRepository = createMonitorsRepositoryMock(),
checksRepository = createChecksRepositoryMock(),
monitorStatsRepository = { findByMonitorId: jest.fn() },
monitorModuleOverrides = {},
monitorStatsRepository = createMonitorStatsRepositoryMock(),
statusPagesRepository = createStatusPagesRepositoryMock(),
}: {
monitorsRepository?: IMonitorsRepository;
checksRepository?: IChecksRepository;
monitorStatsRepository?: { findByMonitorId: jest.Mock };
monitorModuleOverrides?: Record<string, unknown>;
monitorStatsRepository?: IMonitorStatsRepository;
statusPagesRepository?: IStatusPagesRepository;
} = {}) => {
const monitorModule = {
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }),
getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }),
getMonitorsByTeamId: jest.fn().mockResolvedValue([]),
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }),
...monitorModuleOverrides,
};
return new MonitorService({
db: {
monitorModule,
statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() },
checkModule: { deleteChecks: jest.fn() },
statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() },
pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() },
notificationsModule: { deleteNotificationsByMonitorId: jest.fn() },
notificationModule: { deleteNotificationsByMonitorId: jest.fn() },
},
jobQueue: {
addJob: jest.fn(),
@@ -66,6 +70,7 @@ const createService = ({
monitorsRepository,
checksRepository,
monitorStatsRepository,
statusPagesRepository,
});
};
@@ -106,29 +111,41 @@ describe("MonitorService", () => {
});
describe("getMonitorsByTeamId", () => {
it("returns monitors array from db module", async () => {
const monitorsPayload = [
it("returns monitors array from repository", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([
{ id: "m1", name: "Monitor 1" },
{ id: "m2", name: "Monitor 2" },
];
const monitorModuleOverrides = {
getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload),
};
const service = createService({ monitorModuleOverrides });
]);
const service = createService({ monitorsRepository });
const result = await service.getMonitorsByTeamId({ teamId: "team" } as any);
expect(result).toHaveLength(2);
expect(result[0]).toHaveProperty("id", "m1");
});
});
describe("getMonitorsAndSummaryByTeamId", () => {
it("returns monitors with summary block", async () => {
const monitorModuleOverrides = {
getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }),
};
const service = createService({ monitorModuleOverrides });
const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" });
expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } });
describe("getMonitorsWithChecksByTeamId summary", () => {
it("includes summary and monitors with checks", async () => {
const monitorsRepository = createMonitorsRepositoryMock();
(monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1);
(monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([{ id: "m1", type: "http" }]);
(monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({
totalMonitors: 1,
upMonitors: 1,
downMonitors: 0,
pausedMonitors: 0,
});
const checksRepository = createChecksRepositoryMock();
(checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ m1: [] });
const service = createService({ monitorsRepository, checksRepository });
const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" });
expect(result).toEqual({
summary: { totalMonitors: 1, upMonitors: 1, downMonitors: 0, pausedMonitors: 0 },
count: 1,
monitors: [{ id: "m1", type: "http", checks: [] }],
});
});
});
@@ -163,7 +180,7 @@ describe("MonitorService", () => {
(monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor);
const checksRepository = createChecksRepositoryMock();
(checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({
monitorType: "uptime",
monitorType: "http",
groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }],
groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }],
groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }],
@@ -171,27 +188,23 @@ describe("MonitorService", () => {
avgResponseTime: 95,
});
const monitorStatsRepository = {
findByMonitorId: jest.fn().mockResolvedValue({
id: "stats-1",
monitorId: monitor.id,
avgResponseTime: 90,
totalChecks: 10,
totalUpChecks: 9,
totalDownChecks: 1,
uptimePercentage: 0.9,
lastCheckTimestamp: 123456789,
lastResponseTime: 80,
timeOfLastFailure: 123456700,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
} as any;
const monitorModuleOverrides = {
getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }),
};
const monitorStatsRepository = createMonitorStatsRepositoryMock();
(monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({
id: "stats-1",
monitorId: monitor.id,
avgResponseTime: 90,
totalChecks: 10,
totalUpChecks: 9,
totalDownChecks: 1,
uptimePercentage: 0.9,
lastCheckTimestamp: 123456789,
lastResponseTime: 80,
timeOfLastFailure: 123456700,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository });
const service = createService({ monitorsRepository, checksRepository, monitorStatsRepository });
const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" });
expect(result).toHaveProperty("monitorData");