Merge pull request #3169 from bluewave-labs/feat/maintenance-window-repository

feat/maintenance window repository
This commit is contained in:
Alexander Holliday authored and GitHub committed 2026-01-22 10:14:22 -08:00
commit 2359255ef7
18 files changed
+297 -351

No files matched your search

@@ -27,7 +27,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
try {
setIsLoading(true);
await networkService.deleteMaintenanceWindow({
maintenanceWindowId: maintenanceWindow._id,
maintenanceWindowId: maintenanceWindow.id,
});
updateCallback();
createToast({ body: "Maintenance window deleted successfully." });
@@ -47,7 +47,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
active: !maintenanceWindow.active,
};
await networkService.editMaintenanceWindow({
maintenanceWindowId: maintenanceWindow._id,
maintenanceWindowId: maintenanceWindow.id,
maintenanceWindow: data,
});
updateCallback();
@@ -60,7 +60,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => {
};
const handleEdit = () => {
navigate(`/maintenance/create/${maintenanceWindow._id}`);
navigate(`/maintenance/create/${maintenanceWindow.id}`);
};
const openMenu = (event) => {
@@ -203,7 +203,7 @@ const MaintenanceTable = ({
},
},
onRowClick: (row) => {
navigate(`/maintenance/create/${row._id}`);
navigate(`/maintenance/create/${row.id}`);
},
}}
headers={headers}
+7 -8
View File
@@ -60,8 +60,6 @@ import AppSettings from "../db/models/AppSettings.js";
import Incident from "../db/models/Incident.js";
import StatusPageModule from "../db/modules/statusPageModule.js";
import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js";
import NotificationModule from "../db/modules/notificationModule.js";
import IncidentModule from "../db/modules/incidentModule.js";
// repositories
@@ -76,6 +74,7 @@ import {
MongoNotificationsRepository,
MongoIncidentRepository,
MongoTeamsRepository,
MongoMaintenanceWindowsRepository,
IMonitorsRepository,
IChecksRepository,
IMonitorStatsRepository,
@@ -87,6 +86,7 @@ import {
INotificationsRepository,
IIncidentsRepository,
ITeamsRepository,
IMaintenanceWindowsRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
@@ -122,6 +122,7 @@ export type InitializedServices = {
notificationsRepository: INotificationsRepository;
incidentsRepository: IIncidentsRepository;
teamsRepository: ITeamsRepository;
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
};
export const initializeServices = async ({
@@ -137,16 +138,12 @@ export const initializeServices = async ({
}): Promise<InitializedServices> => {
// Create DB
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor });
const incidentModule = new IncidentModule({ logger, Incident, Monitor, User });
const db = new MongoDB({
logger,
envSettings,
statusPageModule,
maintenanceWindowModule,
notificationModule,
incidentModule,
});
@@ -163,6 +160,7 @@ export const initializeServices = async ({
const notificationsRepository = new MongoNotificationsRepository();
const incidentsRepository = new MongoIncidentRepository();
const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
const networkService = new NetworkService({
axios,
@@ -218,7 +216,6 @@ export const initializeServices = async ({
);
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
db,
logger,
networkService,
statusService,
@@ -226,6 +223,7 @@ export const initializeServices = async ({
checkService,
buffer: bufferService,
incidentService,
maintenanceWindowsRepository,
});
const superSimpleQueue = await SuperSimpleQueue.create({
@@ -259,9 +257,9 @@ export const initializeServices = async ({
errorService,
});
const maintenanceWindowService = new MaintenanceWindowService({
db,
errorService,
monitorsRepository,
maintenanceWindowsRepository,
});
const monitorService = new MonitorService({
jobQueue: superSimpleQueue,
@@ -307,6 +305,7 @@ export const initializeServices = async ({
notificationsRepository,
incidentsRepository,
teamsRepository,
maintenanceWindowsRepository,
};
return services;
@@ -1,5 +1,4 @@
import { Request, Response, NextFunction } from "express";
import { AppError } from "@/utils/AppError.js";
import {
createMaintenanceWindowBodyValidation,
editMaintenanceWindowByIdParamValidation,
@@ -9,6 +8,7 @@ import {
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "@/validation/joi.js";
import { requireTeamId } from "@/controllers/controllerUtils.js";
const SERVICE_NAME = "maintenanceWindowController";
@@ -26,11 +26,7 @@ class MaintenanceWindowController {
createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => {
try {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
@@ -46,10 +42,7 @@ class MaintenanceWindowController {
try {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId);
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId });
@@ -67,11 +60,7 @@ class MaintenanceWindowController {
try {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
@@ -89,10 +78,7 @@ class MaintenanceWindowController {
try {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId });
@@ -109,10 +95,7 @@ class MaintenanceWindowController {
try {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
const teamId = req?.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req?.user?.teamId);
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId });
@@ -130,10 +113,7 @@ class MaintenanceWindowController {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
const teamId = req.user?.teamId;
if (!teamId) {
throw new AppError({ message: "Team ID is required", status: 400 });
}
const teamId = requireTeamId(req.user?.teamId);
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId });
return res.status(200).json({
+1 -3
View File
@@ -4,11 +4,9 @@ import { runMigrations } from "./migration/index.js";
class MongoDB {
static SERVICE_NAME = "MongoDB";
constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, notificationModule, incidentModule }) {
constructor({ logger, envSettings, statusPageModule, incidentModule }) {
this.logger = logger;
this.envSettings = envSettings;
this.maintenanceWindowModule = maintenanceWindowModule;
this.notificationModule = notificationModule;
this.statusPageModule = statusPageModule;
this.incidentModule = incidentModule;
}
-68
View File
@@ -1,68 +0,0 @@
import mongoose from "mongoose";
/**
* MaintenanceWindow Schema
* @module MaintenanceWindow
* @typedef {Object} MaintenanceWindow
* @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable.
* @property {Boolean} active - Indicates whether the maintenance window is active.
* @property {Number} repeat - Indicates how often this maintenance window should repeat.
* @property {Date} start - The start date and time of the maintenance window.
* @property {Date} end - The end date and time of the maintenance window.
* @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`.
*
* @example
*
* let maintenanceWindow = new MaintenanceWindow({
* monitorId: monitorId,
* active: active,
* repeat: repeat,
* start: start,
* end: end,
* });
*
* if (repeat === 0) {
* maintenanceWindow.expiry = end;
* }
*
*/
const MaintenanceWindow = mongoose.Schema(
{
monitorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
},
teamId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Team",
immutable: true,
},
active: {
type: Boolean,
default: true,
},
name: {
type: String,
},
repeat: {
type: Number,
},
start: {
type: Date,
},
end: {
type: Date,
},
expiry: {
type: Date,
index: { expires: "0s" },
},
},
{
timestamps: true,
}
);
export default mongoose.model("MaintenanceWindow", MaintenanceWindow);
+60
View File
@@ -0,0 +1,60 @@
import { Schema, model, type Types } from "mongoose";
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js";
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
monitorId: Types.ObjectId;
teamId: Types.ObjectId;
start: Date;
end: Date;
expiry?: Date;
createdAt: Date;
updatedAt: Date;
};
interface MaintenanceWindowDocument extends MaintenanceWindowDocumentBase {
_id: Types.ObjectId;
}
const MaintenanceWindowSchema = new Schema<MaintenanceWindowDocument>(
{
monitorId: {
type: Schema.Types.ObjectId,
ref: "Monitor",
immutable: true,
},
teamId: {
type: Schema.Types.ObjectId,
ref: "Team",
immutable: true,
},
active: {
type: Boolean,
default: true,
},
name: {
type: String,
},
repeat: {
type: Number,
},
start: {
type: Date,
},
end: {
type: Date,
},
expiry: {
type: Date,
index: { expires: "0s" },
},
},
{
timestamps: true,
}
);
const MaintenanceWindowModel = model<MaintenanceWindowDocument>("MaintenanceWindow", MaintenanceWindowSchema);
export type { MaintenanceWindowDocument };
export { MaintenanceWindowModel };
export default MaintenanceWindowModel;
+3
View File
@@ -30,3 +30,6 @@ export { default as IncidentModel } from "@/db/models/Incident.js";
export * from "@/db/models/Team.js";
export { default as TeamModel } from "@/db/models/Team.js";
export * from "@/db/models/MaintenanceWindow.js";
export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js";
@@ -1,106 +0,0 @@
const SERVICE_NAME = "maintenanceWindowModule";
class MaintenanceWindowModule {
constructor({ MaintenanceWindow }) {
this.MaintenanceWindow = MaintenanceWindow;
}
createMaintenanceWindow = async (maintenanceWindowData) => {
try {
const maintenanceWindow = new this.MaintenanceWindow({
...maintenanceWindowData,
});
// If the maintenance window is a one time window, set the expiry to the end date
if (maintenanceWindowData.oneTime) {
maintenanceWindow.expiry = maintenanceWindowData.end;
}
const result = await maintenanceWindow.save();
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMaintenanceWindow";
throw error;
}
};
getMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOne({
_id: id,
teamId: teamId,
});
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowById";
throw error;
}
};
getMaintenanceWindowsByTeamId = async (teamId, query) => {
try {
let { active, page, rowsPerPage, field, order } = query || {};
const maintenanceQuery = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery);
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
return { maintenanceWindows, maintenanceWindowCount };
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowByUserId";
throw error;
}
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => {
try {
const maintenanceWindows = await this.MaintenanceWindow.find({
monitorId: monitorId,
teamId: teamId,
});
return maintenanceWindows;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMaintenanceWindowsByMonitorId";
throw error;
}
};
deleteMaintenanceWindowById = async ({ id, teamId }) => {
try {
const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId });
return maintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteMaintenanceWindowById";
throw error;
}
};
editMaintenanceWindowById = async ({ id, body }) => {
try {
const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true });
return editedMaintenanceWindow;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editMaintenanceWindowById";
throw error;
}
};
}
export default MaintenanceWindowModule;
@@ -1,98 +0,0 @@
const SERVICE_NAME = "notificationModule";
class NotificationModule {
constructor({ Notification, Monitor }) {
this.Notification = Notification;
this.Monitor = Monitor;
}
createNotification = async (notificationData) => {
try {
const notification = await new this.Notification({ ...notificationData }).save();
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createNotification";
throw error;
}
};
getNotificationsByTeamId = async (teamId) => {
try {
const notifications = await this.Notification.find({ teamId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByTeamId";
throw error;
}
};
getNotificationsByIds = async (notificationIds) => {
try {
const notifications = await this.Notification.find({ _id: { $in: notificationIds } });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByIds";
throw error;
}
};
getNotificationsByMonitorId = async (monitorId) => {
try {
const notifications = await this.Notification.find({ monitorId });
return notifications;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationsByMonitorId";
throw error;
}
};
deleteNotificationsByMonitorId = async (monitorId) => {
try {
const result = await this.Notification.deleteMany({ monitorId });
return result.deletedCount;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationsByMonitorId";
throw error;
}
};
deleteNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
if (!notification) {
throw new Error("Notification not found");
}
const result = await this.Notification.findByIdAndDelete(id);
await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
return result;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "deleteNotificationById";
throw error;
}
};
getNotificationById = async (id) => {
try {
const notification = await this.Notification.findById(id);
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getNotificationById";
throw error;
}
};
editNotification = async (id, notificationData) => {
try {
const notification = await this.Notification.findByIdAndUpdate(id, notificationData, {
new: true,
});
return notification;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "editNotification";
throw error;
}
};
}
export default NotificationModule;
+3
View File
@@ -30,3 +30,6 @@ export { default as MongoIncidentRepository } from "@/repositories/incidents/Mon
export * from "@/repositories/teams/ITeamsRepository.js";
export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js";
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
@@ -0,0 +1,16 @@
import type { MaintenanceWindow } from "@/types/index.js";
export interface IMaintenanceWindowsRepository {
// create
create(data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// fetch
findById(id: string, teamId: string): Promise<MaintenanceWindow>;
findByMonitorId(monitorId: string, teamId: string): Promise<MaintenanceWindow[]>;
findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise<MaintenanceWindow[]>;
// update
updateById(id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow>;
// delete
deleteById(id: string, teamId: string): Promise<MaintenanceWindow>;
// other
countByTeamId(teamId: string, active: boolean): Promise<number>;
}
@@ -0,0 +1,136 @@
import type { MaintenanceWindow } from "@/types/index.js";
import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js";
import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
import mongoose from "mongoose";
import { AppError } from "@/utils/AppError.js";
class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => {
if (!value) {
return "";
}
return value instanceof mongoose.Types.ObjectId ? value.toString() : String(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();
};
private mapDocuments = (documents: MaintenanceWindowDocument[]): MaintenanceWindow[] => {
if (!documents?.length) {
return [];
}
return documents.map((doc) => this.toEntity(doc));
};
private toEntity = (doc: MaintenanceWindowDocument): MaintenanceWindow => {
return {
id: this.toStringId(doc._id),
monitorId: this.toStringId(doc.monitorId),
teamId: this.toStringId(doc.teamId),
active: doc.active,
name: doc.name,
repeat: doc.repeat,
start: this.toDateString(doc.start),
end: this.toDateString(doc.end),
createdAt: this.toDateString(doc.createdAt),
updatedAt: this.toDateString(doc.updatedAt),
};
};
create = async (data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow> => {
const maintenanceWindow = new MaintenanceWindowModel(data);
// If the maintenance window is a one time window, set the expiry to the end date
if (maintenanceWindow.repeat === 0) {
maintenanceWindow.expiry = maintenanceWindow.end;
}
const result = await maintenanceWindow.save();
return this.toEntity(result);
};
findById = async (id: string, teamId: string): Promise<MaintenanceWindow> => {
const maintenanceWindow = await MaintenanceWindowModel.findOne({
_id: id,
teamId: teamId,
});
if (!maintenanceWindow) {
throw new AppError({ message: "Maintenance Window not found", status: 404 });
}
return this.toEntity(maintenanceWindow);
};
findByMonitorId = async (monitorId: string, teamId: string): Promise<MaintenanceWindow[]> => {
const maintenanceWindows = await MaintenanceWindowModel.find({
monitorId: monitorId,
teamId: teamId,
});
return this.mapDocuments(maintenanceWindows);
};
findByTeamId = async (
teamId: string,
active: boolean,
page: number,
rowsPerPage: number,
field: string,
order: string
): Promise<MaintenanceWindow[]> => {
const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
// Pagination
let skip = 0;
if (page && rowsPerPage) {
skip = page * rowsPerPage;
}
// Sorting
let sort: Record<string, any> = {};
if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1;
}
const maintenanceWindows = await MaintenanceWindowModel.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort);
return this.mapDocuments(maintenanceWindows);
};
updateById = async (id: string, teamId: string, data: Partial<MaintenanceWindow>): Promise<MaintenanceWindow> => {
const updated = await MaintenanceWindowModel.findOneAndUpdate(
{
_id: new mongoose.Types.ObjectId(id),
teamId: new mongoose.Types.ObjectId(teamId),
},
{ $set: data },
{ new: true, runValidators: true }
);
if (!updated) {
throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 });
}
return this.toEntity(updated);
};
deleteById = async (id: string, teamId: string): Promise<MaintenanceWindow> => {
const deleted = await MaintenanceWindowModel.findOneAndDelete({
_id: new mongoose.Types.ObjectId(id),
teamId: new mongoose.Types.ObjectId(teamId),
});
if (!deleted) {
throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 });
}
return this.toEntity(deleted);
};
countByTeamId = async (teamId: string, active: boolean) => {
const maintenanceQuery: Record<string, any> = { teamId };
if (active !== undefined) maintenanceQuery.active = active;
return await MaintenanceWindowModel.countDocuments(maintenanceQuery);
};
}
export default MongoMaintenanceWindowsRepository;
@@ -1,17 +1,26 @@
import { IMonitorsRepository } from "@/repositories/index.js";
import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js";
import { ParseBoolean } from "@/utils/utils.js";
const SERVICE_NAME = "maintenanceWindowService";
class MaintenanceWindowService {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private errorService: any;
private monitorsRepository: IMonitorsRepository;
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
constructor({ db, errorService, monitorsRepository }: { db: any; errorService: any; monitorsRepository: IMonitorsRepository }) {
this.db = db;
constructor({
errorService,
monitorsRepository,
maintenanceWindowsRepository,
}: {
errorService: any;
monitorsRepository: IMonitorsRepository;
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
}) {
this.errorService = errorService;
this.monitorsRepository = monitorsRepository;
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
}
get serviceName() {
@@ -29,7 +38,7 @@ class MaintenanceWindowService {
}
const dbTransactions = monitorIds.map((monitorId: string) => {
return this.db.maintenanceWindowModule.createMaintenanceWindow({
return this.maintenanceWindowsRepository.create({
teamId,
monitorId,
name: body.name,
@@ -43,27 +52,31 @@ class MaintenanceWindowService {
};
getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => {
const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId });
return maintenanceWindow;
return await this.maintenanceWindowsRepository.findById(id, teamId);
};
getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query);
return maintenanceWindows;
let { active, page, rowsPerPage, field, order } = query || {};
active = typeof active === "undefined" ? undefined : ParseBoolean(active);
page = parseInt(page) || 0;
rowsPerPage = parseInt(rowsPerPage) || 10;
const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order);
const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active);
return { maintenanceWindows, maintenanceWindowCount };
};
getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId });
return maintenanceWindows;
return await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
};
deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => {
await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId });
return await this.maintenanceWindowsRepository.deleteById(id, teamId);
};
editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => {
const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId });
return editedMaintenanceWindow;
return await this.maintenanceWindowsRepository.updateById(id, teamId, body);
};
}
@@ -3,11 +3,11 @@ import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
import IncidentService from "@/service/business/incidentService.js";
import { IMaintenanceWindowsRepository } from "@/repositories/index.js";
class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME;
private db: any;
private logger: any;
private networkService: INetworkService;
private statusService: IStatusService;
@@ -15,9 +15,9 @@ class SuperSimpleQueueHelper {
private checkService: any;
private buffer: any;
private incidentService: IncidentService;
private maintenanceWindowsRepository: IMaintenanceWindowsRepository;
constructor({
db,
logger,
networkService,
statusService,
@@ -25,8 +25,8 @@ class SuperSimpleQueueHelper {
checkService,
buffer,
incidentService,
maintenanceWindowsRepository,
}: {
db: any;
logger: any;
networkService: INetworkService;
statusService: IStatusService;
@@ -34,8 +34,8 @@ class SuperSimpleQueueHelper {
checkService: any;
buffer: any;
incidentService: IncidentService;
maintenanceWindowsRepository: IMaintenanceWindowsRepository;
}) {
this.db = db;
this.logger = logger;
this.networkService = networkService;
this.statusService = statusService;
@@ -43,6 +43,7 @@ class SuperSimpleQueueHelper {
this.buffer = buffer;
this.notificationsService = notificationsService;
this.incidentService = incidentService;
this.maintenanceWindowsRepository = maintenanceWindowsRepository;
}
get serviceName() {
@@ -122,10 +123,7 @@ class SuperSimpleQueueHelper {
};
async isInMaintenanceWindow(monitorId: string, teamId: string) {
const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({
monitorId: monitorId,
teamId: teamId,
});
const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => {
if (window.active) {
+1
View File
@@ -12,3 +12,4 @@ export * from "@/types/alert.js";
export * from "@/types/incident.js";
export * from "@/types/email.js";
export * from "@/types/team.js";
export * from "@/types/maintenanceWindow.js";
+12
View File
@@ -0,0 +1,12 @@
export interface MaintenanceWindow {
id: string;
monitorId: String;
teamId: string;
active: boolean;
name: string;
repeat: number;
start: string;
end: string;
createdAt: string;
updatedAt: string;
}
+14 -15
View File
@@ -5,21 +5,24 @@ import type { Monitor } from "../src/types/monitor.ts";
const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() });
const createHelper = (overrides?: Partial<ConstructorParameters<typeof SuperSimpleQueueHelper>[0]>) => {
const maintenanceWindowModule = {
getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]),
const maintenanceWindowsRepository = {
findByMonitorId: jest.fn().mockResolvedValue([]),
};
const statusServiceMock = {
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1" }, statusChanged: true, prevStatus: false }),
};
const helper = new SuperSimpleQueueHelper({
db: { maintenanceWindowModule },
logger: createLogger(),
networkService: { requestStatus: jest.fn() },
statusService: statusServiceMock,
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
checkService: { buildCheck: jest.fn().mockResolvedValue({}) },
buffer: { addToBuffer: jest.fn() },
incidentService: { handleIncident: jest.fn().mockResolvedValue(undefined) },
maintenanceWindowsRepository,
...overrides,
});
return { helper, maintenanceWindowModule };
return { helper, maintenanceWindowsRepository };
};
describe("SuperSimpleQueueHelper", () => {
@@ -42,19 +45,15 @@ describe("SuperSimpleQueueHelper", () => {
const { helper } = createHelper({
networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) },
statusService: {
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }),
updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false, code: 200 }),
},
notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) },
});
jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false);
const job = helper.getMonitorJob();
const monitor = { id: "m1", teamId: "team" } as Monitor;
await job(monitor);
expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor);
expect(helper["statusService"].updateMonitorStatus).toHaveBeenCalledWith(networkResponse);
expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith(
expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false })
);
});
it("throws when monitor id is missing", async () => {
@@ -68,8 +67,8 @@ describe("SuperSimpleQueueHelper", () => {
describe("isInMaintenanceWindow", () => {
it("returns true when an active window spans now", async () => {
const now = new Date();
const { helper, maintenanceWindowModule } = createHelper();
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
const { helper, maintenanceWindowsRepository } = createHelper();
maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([
{
active: true,
start: new Date(now.getTime() - 1000).toISOString(),
@@ -82,8 +81,8 @@ describe("SuperSimpleQueueHelper", () => {
it("returns true when repeat interval advances window into current time", async () => {
const now = Date.now();
const { helper, maintenanceWindowModule } = createHelper();
maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([
const { helper, maintenanceWindowsRepository } = createHelper();
maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([
{
active: true,
start: new Date(now - 7200000).toISOString(),