From d627a25b409a5a5efcb7e821e53266a69833d40a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:51:04 -0800 Subject: [PATCH 1/2] refactor notifications service --- .../v1/NotificationConfig/index.jsx | 12 ++--- client/src/Hooks/useNotifications.js | 2 +- .../Notifications/components/ActionMenu.jsx | 4 +- .../src/Pages/Notifications/create/index.jsx | 4 +- client/src/Pages/Notifications/index.jsx | 2 +- client/src/Pages/Notifications/utils.js | 12 ++--- server/src/config/services.ts | 1 + .../src/controllers/notificationController.ts | 47 +++++++---------- .../monitors/IMonitorsRepository.ts | 1 + .../monitors/MongoMonitorsRepository.ts | 4 ++ .../notifications/INotificationsRepository.ts | 5 ++ .../MongoNotificationsRepository.ts | 52 +++++++++++++++++++ .../infrastructure/notificationsService.ts | 33 +++++++++++- 13 files changed, 132 insertions(+), 47 deletions(-) diff --git a/client/src/Components/v1/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx index d76fd9fb6..f1ed30ad1 100644 --- a/client/src/Components/v1/NotificationConfig/index.jsx +++ b/client/src/Components/v1/NotificationConfig/index.jsx @@ -31,7 +31,7 @@ const NotificationConfig = ({ setMonitor((prev) => { return { ...prev, - notifications: value.map((notification) => notification._id), + notifications: value.map((notification) => notification.id), }; }); }; @@ -39,14 +39,14 @@ const NotificationConfig = ({ // Handlers const handleDelete = (id) => { const updatedNotifications = selectedNotifications.filter( - (notification) => notification._id !== id + (notification) => notification.id !== id ); setSelectedNotifications(updatedNotifications); setMonitor((prev) => { return { ...prev, - notifications: updatedNotifications.map((notification) => notification._id), + notifications: updatedNotifications.map((notification) => notification.id), }; }); }; @@ -57,7 +57,7 @@ const NotificationConfig = ({ useEffect(() => { if (setNotifications) { const toSet = setNotifications.map((notification) => { - return notifications.find((n) => n._id === notification); + return notifications.find((n) => n.id === notification); }); setSelectedNotifications(toSet); } @@ -94,7 +94,7 @@ const NotificationConfig = ({ { - handleDelete(notification._id); + handleDelete(notification.id); }} style={{ cursor: "pointer" }} /> diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 5f2058b6a..6c5919fac 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -102,7 +102,7 @@ const useGetNotificationById = (id, setNotification) => { const notificationData = { address: notification?.address, notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, + type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, }; setNotification(notificationData); diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx index 52516a737..a3ccc4bbb 100644 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ b/client/src/Pages/Notifications/components/ActionMenu.jsx @@ -32,13 +32,13 @@ const ActionMenu = ({ notification, onDelete }) => { const handleRemove = (e) => { e.stopPropagation(); - onDelete(notification._id); + onDelete(notification.id); handleClose(); }; const handleConfigure = (e) => { e.stopPropagation(); - navigate(`/notifications/${notification._id}`); + navigate(`/notifications/${notification.id}`); handleClose(); }; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 11bf2a95d..14b72ab61 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -56,7 +56,7 @@ const CreateNotifications = () => { const [notification, setNotification] = useState({ notificationName: "", address: "", - type: NOTIFICATION_TYPES[0]._id, + type: NOTIFICATION_TYPES[0].id, }); const [errors, setErrors] = useState({}); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const CreateNotifications = () => { const [isOpen, setIsOpen] = useState(false); const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type._id === typeId)?.value || "email"; + return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; }; const extractError = (error, field) => diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx index 8ebdb22ec..70ed3ef96 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -100,7 +100,7 @@ const Notifications = () => { {t("notifications.createTitle")} navigate(`/notifications/${row._id}`), + onRowClick: (row) => navigate(`/notifications/${row.id}`), rowSX: { cursor: "pointer", "&:hover td": { diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 2a3d32f4d..9ad422101 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -1,10 +1,10 @@ export const NOTIFICATION_TYPES = [ - { _id: 1, name: "E-mail", value: "email" }, - { _id: 2, name: "Slack", value: "slack" }, - { _id: 3, name: "PagerDuty", value: "pager_duty" }, - { _id: 4, name: "Webhook", value: "webhook" }, - { _id: 5, name: "Discord", value: "discord" }, - { _id: 6, name: "Matrix", value: "matrix" }, + { id: 1, name: "E-mail", value: "email" }, + { id: 2, name: "Slack", value: "slack" }, + { id: 3, name: "PagerDuty", value: "pager_duty" }, + { id: 4, name: "Webhook", value: "webhook" }, + { id: 5, name: "Discord", value: "discord" }, + { id: 6, name: "Matrix", value: "matrix" }, ]; export const TITLE_MAP = { diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 664194d82..cdee721b2 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -207,6 +207,7 @@ export const initializeServices = async ({ const notificationsService = new NotificationsService( notificationsRepository, + monitorsRepository, webhookProvider, emailProvider, slackProvider, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 1569f0986..1aa10a82c 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -5,17 +5,16 @@ import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; +import { requireTeamId } from "./controllerUtils.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; - private db: any; private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { this.notificationsService = notificationsService; - this.db = db; this.monitorsRepository = monitorsRepository; } @@ -61,7 +60,7 @@ class NotificationController { body.userId = userId; body.teamId = teamId; - const notification = await this.db.notificationModule.createNotification(body); + const notification = await this.notificationsService.createNotification(body); return res.status(200).json({ success: true, msg: "Notification created successfully", @@ -79,7 +78,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); + const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); return res.status(200).json({ success: true, @@ -98,12 +97,12 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - await this.db.notificationModule.deleteNotificationById(req.params.id); + await this.notificationsService.deleteById(notificationId, teamId); return res.status(200).json({ success: true, msg: "Notification deleted successfully", @@ -115,16 +114,14 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } + const notification = await this.notificationsService.findById(notificationId, teamId); + return res.status(200).json({ success: true, msg: "Notification fetched successfully", @@ -141,18 +138,12 @@ class NotificationController { abortEarly: false, }); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } - - const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); return res.status(200).json({ success: true, msg: "Notification updated successfully", diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index f29cf9d15..8ead5a688 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -39,4 +39,5 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; + removeNotificationFromMonitors(notificationId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 17b9ce243..ad7e8577d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -189,6 +189,10 @@ class MongoMonitorsRepository implements IMonitorsRepository { return groups.sort(); }; + removeNotificationFromMonitors = async (notificationId: string): Promise => { + await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 8bb65769a..0236c3a89 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -1,8 +1,13 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create + create(notificationData: Partial): Promise; // fetch + findById(id: string, teamId: string): Promise; findNotificationsByIds(ids: string[]): Promise; + findByTeamId(teamId: string): Promise; // update + updateById(id: string, teamId: string, updateData: Partial): Promise; // delete + deleteById(id: string, teamId: string): Promise; } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 01a514f1c..730072d8c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -2,6 +2,8 @@ import mongoose from "mongoose"; import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; import { INotificationsRepository } from "@/repositories/index.js"; import type { Notification } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; +import { not } from "joi"; class MongoNotificationsRepository implements INotificationsRepository { private mapDocuments = (documents: NotificationDocument[]): Notification[] => { @@ -36,11 +38,61 @@ class MongoNotificationsRepository implements INotificationsRepository { }; }; + create = async (notificationData: Partial) => { + const notification = await NotificationModel.create({ ...notificationData }); + if (!notification) { + throw new AppError({ message: "Failed to create notification", status: 500 }); + } + return this.toEntity(notification); + }; + + findById = async (id: string, teamId: string): Promise => { + const notification = await NotificationModel.findOne({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!notification) { + throw new AppError({ message: "Notification not found", status: 404 }); + } + return this.toEntity(notification); + }; + findNotificationsByIds = async (ids: string[]) => { const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); return this.mapDocuments(documents); }; + + findByTeamId = async (teamId: string): Promise => { + const documents = await NotificationModel.find({ teamId }); + return this.mapDocuments(documents); + }; + + updateById = async (id: string, teamId: string, patch: Partial): Promise => { + const notification = await NotificationModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: patch }, + { new: true, runValidators: true } + ); + if (!notification) { + throw new AppError({ message: "Notification not found or could not be updated", status: 404 }); + } + return this.toEntity(notification); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await NotificationModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Notification not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoNotificationsRepository; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..4e0c9d842 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,14 +1,20 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; -import { INotificationsRepository } from "@/repositories/index.js"; +import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { + createNotification: (notificationData: Partial) => Promise; + findById: (id: string, teamId: string) => Promise; + findNotificationsByTeamId: (teamId: string) => Promise; + updateById(id: string, teamId: string, updateData: Partial): Promise; + deleteById: (id: string, teamId: string) => Promise; handleNotifications: ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; } @@ -19,6 +25,7 @@ export class NotificationsService implements INotificationsService { static SERVICE_NAME = SERVICE_NAME; private notificationsRepository: INotificationsRepository; + private monitorsRepository: IMonitorsRepository; private webhookProvider: INotificationProvider; private emailProvider: INotificationProvider; private slackProvider: INotificationProvider; @@ -29,6 +36,7 @@ export class NotificationsService implements INotificationsService { constructor( notificationsRepository: INotificationsRepository, + monitorsRepository: IMonitorsRepository, webhookProvider: INotificationProvider, emailProvider: INotificationProvider, slackProvider: INotificationProvider, @@ -38,6 +46,7 @@ export class NotificationsService implements INotificationsService { logger: any ) { this.notificationsRepository = notificationsRepository; + this.monitorsRepository = monitorsRepository; this.webhookProvider = webhookProvider; this.emailProvider = emailProvider; this.slackProvider = slackProvider; @@ -147,4 +156,26 @@ export class NotificationsService implements INotificationsService { } return true; }; + + createNotification = async (notificationData: Partial): Promise => { + return await this.notificationsRepository.create(notificationData); + }; + + findById = async (id: string, teamId: string): Promise => { + return await this.notificationsRepository.findById(id, teamId); + }; + + findNotificationsByTeamId = async (teamId: string): Promise => { + return await this.notificationsRepository.findByTeamId(teamId); + }; + + updateById = async (id: string, teamId: string, updateData: Partial): Promise => { + return await this.notificationsRepository.updateById(id, teamId, updateData); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await this.notificationsRepository.deleteById(id, teamId); + await this.monitorsRepository.removeNotificationFromMonitors(id); + return deleted; + }; } From 155a2577d0890f5646054ce4eee98cbac540342e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:55:22 -0800 Subject: [PATCH 2/2] remove unused dep --- server/src/config/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index d72e4f76f..3b81d27db 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -37,7 +37,7 @@ export const initializeControllers = (services: InitializedServices): Initialize queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), statusPageController: new StatusPageController(services.db), - notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository), + notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), };