refactor notifications service

This commit is contained in:
Alex Holliday
2026-01-21 13:51:04 -08:00
parent 5b43d9d173
commit d627a25b40
13 changed files with 132 additions and 47 deletions
@@ -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 = ({
<Stack
direction="row"
alignItems="center"
key={notification._id}
key={notification.id}
width="100%"
>
<Typography
@@ -106,7 +106,7 @@ const NotificationConfig = ({
name="Trash2"
size={20}
onClick={() => {
handleDelete(notification._id);
handleDelete(notification.id);
}}
style={{ cursor: "pointer" }}
/>
+1 -1
View File
@@ -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);
@@ -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();
};
@@ -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) =>
+1 -1
View File
@@ -100,7 +100,7 @@ const Notifications = () => {
<Typography variant="h1">{t("notifications.createTitle")}</Typography>
<DataTable
config={{
onRowClick: (row) => navigate(`/notifications/${row._id}`),
onRowClick: (row) => navigate(`/notifications/${row.id}`),
rowSX: {
cursor: "pointer",
"&:hover td": {
+6 -6
View File
@@ -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 = {
+1
View File
@@ -207,6 +207,7 @@ export const initializeServices = async ({
const notificationsService = new NotificationsService(
notificationsRepository,
monitorsRepository,
webhookProvider,
emailProvider,
slackProvider,
@@ -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",
@@ -39,4 +39,5 @@ export interface IMonitorsRepository {
// other
findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise<MonitorsSummary>;
findGroupsByTeamId(teamId: string): Promise<string[]>;
removeNotificationFromMonitors(notificationId: string): Promise<void>;
}
@@ -189,6 +189,10 @@ class MongoMonitorsRepository implements IMonitorsRepository {
return groups.sort();
};
removeNotificationFromMonitors = async (notificationId: string): Promise<void> => {
await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } });
};
private mapDocuments = (documents: MonitorDocument[]): Monitor[] => {
if (!documents?.length) {
return [];
@@ -1,8 +1,13 @@
import type { Notification } from "@/types/index.js";
export interface INotificationsRepository {
// create
create(notificationData: Partial<Notification>): Promise<Notification>;
// fetch
findById(id: string, teamId: string): Promise<Notification>;
findNotificationsByIds(ids: string[]): Promise<Notification[]>;
findByTeamId(teamId: string): Promise<Notification[]>;
// update
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
// delete
deleteById(id: string, teamId: string): Promise<Notification>;
}
@@ -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<Notification>) => {
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<Notification> => {
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<Notification[]> => {
const documents = await NotificationModel.find({ teamId });
return this.mapDocuments(documents);
};
updateById = async (id: string, teamId: string, patch: Partial<Notification>): Promise<Notification> => {
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<Notification> => {
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;
@@ -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<Notification>) => Promise<Notification>;
findById: (id: string, teamId: string) => Promise<Notification>;
findNotificationsByTeamId: (teamId: string) => Promise<Notification[]>;
updateById(id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification>;
deleteById: (id: string, teamId: string) => Promise<Notification>;
handleNotifications: (
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
prevStatus: boolean | undefined,
statusChanged: boolean
) => Promise<boolean>;
sendTestNotification: (notification: Notification) => Promise<boolean>;
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
}
@@ -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<Notification>): Promise<Notification> => {
return await this.notificationsRepository.create(notificationData);
};
findById = async (id: string, teamId: string): Promise<Notification> => {
return await this.notificationsRepository.findById(id, teamId);
};
findNotificationsByTeamId = async (teamId: string): Promise<Notification[]> => {
return await this.notificationsRepository.findByTeamId(teamId);
};
updateById = async (id: string, teamId: string, updateData: Partial<Notification>): Promise<Notification> => {
return await this.notificationsRepository.updateById(id, teamId, updateData);
};
deleteById = async (id: string, teamId: string): Promise<Notification> => {
const deleted = await this.notificationsRepository.deleteById(id, teamId);
await this.monitorsRepository.removeNotificationFromMonitors(id);
return deleted;
};
}