diff --git a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx index 09c69dd98..709bca212 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx @@ -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) => { diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx index 004bbeb3a..765858050 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -203,7 +203,7 @@ const MaintenanceTable = ({ }, }, onRowClick: (row) => { - navigate(`/maintenance/create/${row._id}`); + navigate(`/maintenance/create/${row.id}`); }, }} headers={headers} diff --git a/server/src/config/services.ts b/server/src/config/services.ts index cdee721b2..fff56f9fb 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -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 => { // 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; diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 3d212769f..0cc9610ea 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -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({ diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index eacd2383e..07d3d31c8 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -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; } diff --git a/server/src/db/models/MaintenanceWindow.js b/server/src/db/models/MaintenanceWindow.js deleted file mode 100755 index b68abb413..000000000 --- a/server/src/db/models/MaintenanceWindow.js +++ /dev/null @@ -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); diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts new file mode 100644 index 000000000..b68de206e --- /dev/null +++ b/server/src/db/models/MaintenanceWindow.ts @@ -0,0 +1,60 @@ +import { Schema, model, type Types } from "mongoose"; +import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; + +type MaintenanceWindowDocumentBase = Omit & { + 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( + { + 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("MaintenanceWindow", MaintenanceWindowSchema); + +export type { MaintenanceWindowDocument }; +export { MaintenanceWindowModel }; +export default MaintenanceWindowModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index f762027ec..fee369e2b 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -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"; diff --git a/server/src/db/modules/maintenanceWindowModule.js b/server/src/db/modules/maintenanceWindowModule.js deleted file mode 100755 index 61523c9f0..000000000 --- a/server/src/db/modules/maintenanceWindowModule.js +++ /dev/null @@ -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; diff --git a/server/src/db/modules/notificationModule.js b/server/src/db/modules/notificationModule.js deleted file mode 100755 index f45900184..000000000 --- a/server/src/db/modules/notificationModule.js +++ /dev/null @@ -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; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index ac5125af9..da31a8c75 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -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"; diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..1bcb02347 --- /dev/null +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -0,0 +1,16 @@ +import type { MaintenanceWindow } from "@/types/index.js"; +export interface IMaintenanceWindowsRepository { + // create + create(data: Partial): Promise; + // fetch + findById(id: string, teamId: string): Promise; + findByMonitorId(monitorId: string, teamId: string): Promise; + findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise; + + // update + updateById(id: string, teamId: string, data: Partial): Promise; + // delete + deleteById(id: string, teamId: string): Promise; + // other + countByTeamId(teamId: string, active: boolean): Promise; +} diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..e85af5cb9 --- /dev/null +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -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): Promise => { + 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 => { + 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 => { + 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 => { + const maintenanceQuery: Record = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + // Sorting + let sort: Record = {}; + 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): Promise => { + 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 => { + 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 = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + return await MaintenanceWindowModel.countDocuments(maintenanceQuery); + }; +} + +export default MongoMaintenanceWindowsRepository; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index c1ba5b8d7..561d97480 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -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); }; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 9a2957ee9..cf3824d60 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -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) { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9464622ac..ad92ea1fe 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -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"; diff --git a/server/src/types/maintenanceWindow.ts b/server/src/types/maintenanceWindow.ts new file mode 100644 index 000000000..33285b8e4 --- /dev/null +++ b/server/src/types/maintenanceWindow.ts @@ -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; +} diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 19caef876..3b179bd91 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -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[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(),