From c42932a7b3f55c39fa53daad85c5387c992e71db Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 8 Jan 2026 17:27:02 +0000 Subject: [PATCH] refactor --- server/src/db/models/index.ts | 5 +- .../NotificationChannel.ts | 17 +-- server/src/init/services.ts | 7 +- server/src/repositories/index.ts | 3 + .../monitors/IMonitorRepoistory.ts | 5 + .../monitors/MongoMonitorRepository.ts | 9 ++ .../INotificationChannelRepository.ts | 17 +++ .../MongoNotificationChannelRepository.ts | 67 +++++++++- .../recovery-token/IRecoveryTokenRepistory.ts | 1 + .../MongoRecoveryTokenRepository.ts | 5 + .../business/NotificationChannelService.ts | 122 +++++++++--------- server/src/types/domain/index.ts | 3 + .../src/types/domain/notificationChannel.ts | 21 +++ server/src/types/domain/recoveryToken.ts | 8 ++ server/src/validation/notifications.ts | 3 +- 15 files changed, 217 insertions(+), 76 deletions(-) create mode 100644 server/src/repositories/recovery-token/IRecoveryTokenRepistory.ts create mode 100644 server/src/repositories/recovery-token/MongoRecoveryTokenRepository.ts create mode 100644 server/src/types/domain/notificationChannel.ts create mode 100644 server/src/types/domain/recoveryToken.ts diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 29f78dd11..0e66526c0 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -9,10 +9,7 @@ export type { IMonitorStats } from "./monitors/MonitorStats.js"; export { User } from "./auth/User.js"; export type { IUser, IUserProfile } from "./auth/User.js"; -export { - NotificationChannel, - ChannelTypes, -} from "./notification-channel/NotificationChannel.js"; +export { NotificationChannel } from "./notification-channel/NotificationChannel.js"; export type { INotificationChannel } from "./notification-channel/NotificationChannel.js"; export { Invite } from "./auth/Invite.js"; diff --git a/server/src/db/models/notification-channel/NotificationChannel.ts b/server/src/db/models/notification-channel/NotificationChannel.ts index 1cd0bed1d..54be6e643 100644 --- a/server/src/db/models/notification-channel/NotificationChannel.ts +++ b/server/src/db/models/notification-channel/NotificationChannel.ts @@ -1,19 +1,16 @@ import mongoose, { Schema, Types } from "mongoose"; -export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; -export type ChannelType = (typeof ChannelTypes)[number]; - -export interface INotificationChannelConfig { - url?: string; // For webhook, slack, discord - emailAddress?: string; // For email -} - +import { + type ChannelType, + ChannelTypes, + NotificationChannelConfig, +} from "@/types/domain/index.js"; export interface INotificationChannel { _id: Types.ObjectId; orgId: Types.ObjectId; teamId: Types.ObjectId; name: string; type: ChannelType; - config: INotificationChannelConfig; + config: NotificationChannelConfig; isActive: boolean; createdBy: Types.ObjectId; updatedBy: Types.ObjectId; @@ -21,7 +18,7 @@ export interface INotificationChannel { updatedAt: Date; } -const NotificationChannelConfigSchema = new Schema( +const NotificationChannelConfigSchema = new Schema( { url: { type: String, required: false }, emailAddress: { type: String, required: false }, diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 29c121a75..d9b5cbbd1 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -46,6 +46,7 @@ import { MongoOrgRepository, MongoOrgMembershipRepository, MongoMaintenanceRepository, + MongoNotificationChannelRepository, } from "@/repositories/index.js"; export const initServices = async () => { @@ -61,6 +62,7 @@ export const initServices = async () => { const orgRepository = new MongoOrgRepository(); const orgMembershipRepository = new MongoOrgMembershipRepository(); const maintenanceRepository = new MongoMaintenanceRepository(); + const notificationChannelRepository = new MongoNotificationChannelRepository(); const checkService = new CheckService(checksRepository, monitorRepository); const inviteService = new InviteService( @@ -78,7 +80,10 @@ export const initServices = async () => { monitorRepository, monitorStatsRepository ); - const notificationChannelService = new NotificationChannelService(); + const notificationChannelService = new NotificationChannelService( + notificationChannelRepository, + monitorRepository + ); const userService = new UserService(); const discordService = new DiscordService(); const slackService = new SlackService(); diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 8ad9ec686..3bf00ef81 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -39,3 +39,6 @@ export { default as MongoMaintenanceRepository } from "@/repositories/maintenanc export type { INotificationChannelRepository } from "@/repositories/notification-channels/INotificationChannelRepository.js"; export { default as MongoNotificationChannelRepository } from "@/repositories/notification-channels/MongoNotificationChannelRepository.js"; + +export type { IRecoveryTokenRepistory } from "@/repositories/recovery-token/IRecoveryTokenRepistory.js"; +export { default as MongoRecoveryTokenRepository } from "@/repositories/recovery-token/MongoRecoveryTokenRepository.js"; diff --git a/server/src/repositories/monitors/IMonitorRepoistory.ts b/server/src/repositories/monitors/IMonitorRepoistory.ts index 086be2706..b44dc2b19 100644 --- a/server/src/repositories/monitors/IMonitorRepoistory.ts +++ b/server/src/repositories/monitors/IMonitorRepoistory.ts @@ -85,4 +85,9 @@ export interface IMonitorRepository { // Counts countByOrgId(orgId: string): Promise; countByIdAndTeamId(monitorIds: string[], teamId: string): Promise; + + // Other + removeNotificationChannelFromMonitors( + notificationId: string + ): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorRepository.ts b/server/src/repositories/monitors/MongoMonitorRepository.ts index d2b1a18a8..07f721a1a 100644 --- a/server/src/repositories/monitors/MongoMonitorRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorRepository.ts @@ -239,6 +239,15 @@ class MongoMonitorRepository implements IMonitorRepository { teamId: teamId, }); }; + + // Other + removeNotificationChannelFromMonitors = async (notificationId: string) => { + const result = await Monitor.updateMany( + { notificationChannels: notificationId }, + { $pull: { notificationChannels: notificationId } } + ); + return result.modifiedCount > 0; + }; } export default MongoMonitorRepository; diff --git a/server/src/repositories/notification-channels/INotificationChannelRepository.ts b/server/src/repositories/notification-channels/INotificationChannelRepository.ts index 048525eb0..19ceaa837 100644 --- a/server/src/repositories/notification-channels/INotificationChannelRepository.ts +++ b/server/src/repositories/notification-channels/INotificationChannelRepository.ts @@ -1,7 +1,24 @@ +import type { NotificationChannel } from "@/types/domain/index.js"; + export interface INotificationChannelRepository { // create + create: (data: Partial) => Promise; // single fetch + findById: ( + notificationChannelId: string, + teamId: string + ) => Promise; // collection fetch + findByTeamId: (teamId: string) => Promise; // update + update: ( + notificationChannelId: string, + teamid: string, + data: Partial + ) => Promise; // delete + deleteById: ( + notificationChannelId: string, + teamId: string + ) => Promise; } diff --git a/server/src/repositories/notification-channels/MongoNotificationChannelRepository.ts b/server/src/repositories/notification-channels/MongoNotificationChannelRepository.ts index ef68ffd36..42dd71d38 100644 --- a/server/src/repositories/notification-channels/MongoNotificationChannelRepository.ts +++ b/server/src/repositories/notification-channels/MongoNotificationChannelRepository.ts @@ -1,5 +1,70 @@ +import type { INotificationChannel } from "@/db/models/index.js"; +import { NotificationChannel } from "@/db/models/index.js"; +import type { NotificationChannel as NotificationChannelEntity } from "@/types/domain/index.js"; + import { INotificationChannelRepository } from "@/repositories/index.js"; -class MongoNotificationRepository implements INotificationChannelRepository {} +class MongoNotificationRepository implements INotificationChannelRepository { + private toEntity = (doc: INotificationChannel): NotificationChannelEntity => { + return { + id: doc._id.toString(), + orgId: doc.orgId.toString(), + teamId: doc.teamId.toString(), + name: doc.name, + type: doc.type, + config: doc.config, + isActive: doc.isActive, + createdBy: doc.createdBy.toString(), + updatedBy: doc.updatedBy.toString(), + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + }; + }; + create = async (data: Partial) => { + const created = await NotificationChannel.create(data); + return this.toEntity(created); + }; + + findById = async (notificationChannelId: string, teamId: string) => { + const channel = await NotificationChannel.findOne({ + _id: notificationChannelId, + teamId, + }); + + if (!channel) return null; + return this.toEntity(channel); + }; + + findByTeamId = async (teamId: string) => { + const channels = await NotificationChannel.find({ teamId }); + return channels.map(this.toEntity); + }; + + update = async ( + notificationChannelId: string, + teamId: string, + data: Partial + ) => { + const updatedChannel = await NotificationChannel.findOneAndUpdate( + { _id: notificationChannelId, teamId }, + [ + { + $set: data, + }, + ], + { new: true, runValidators: true } + ); + + if (!updatedChannel) return null; + return this.toEntity(updatedChannel); + }; + deleteById = async (notificationChannelId: string, teamId: string) => { + const result = await NotificationChannel.deleteOne({ + _id: notificationChannelId, + teamId, + }); + return result.deletedCount === 1; + }; +} export default MongoNotificationRepository; diff --git a/server/src/repositories/recovery-token/IRecoveryTokenRepistory.ts b/server/src/repositories/recovery-token/IRecoveryTokenRepistory.ts new file mode 100644 index 000000000..de988e4d6 --- /dev/null +++ b/server/src/repositories/recovery-token/IRecoveryTokenRepistory.ts @@ -0,0 +1 @@ +export interface IRecoveryTokenRepistory {} diff --git a/server/src/repositories/recovery-token/MongoRecoveryTokenRepository.ts b/server/src/repositories/recovery-token/MongoRecoveryTokenRepository.ts new file mode 100644 index 000000000..bede6bff4 --- /dev/null +++ b/server/src/repositories/recovery-token/MongoRecoveryTokenRepository.ts @@ -0,0 +1,5 @@ +import { IRecoveryTokenRepistory } from "@/repositories/index.js"; + +class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {} + +export default MongoRecoveryTokenRepository; diff --git a/server/src/services/business/NotificationChannelService.ts b/server/src/services/business/NotificationChannelService.ts index eeb774607..913c021ea 100644 --- a/server/src/services/business/NotificationChannelService.ts +++ b/server/src/services/business/NotificationChannelService.ts @@ -1,61 +1,70 @@ -import mongoose from "mongoose"; - -import { - INotificationChannel, - NotificationChannel, - Monitor, -} from "@/db/models/index.js"; import ApiError from "@/utils/ApiError.js"; -import type { UserContext } from "@/types/domain/index.js"; +import { + INotificationChannelRepository, + IMonitorRepository, +} from "@/repositories/index.js"; +import { + type UserContext, + NotificationChannel as NotificationChannelEntity, +} from "@/types/domain/index.js"; const SERVICE_NAME = "NotificationChannelService"; export interface INotificationChannelService { create: ( tokenizedUser: UserContext, - notificationChannel: INotificationChannel - ) => Promise; - getAll: (teamId: string) => Promise; - get: (teamId: string, id: string) => Promise; + notificationChannel: NotificationChannelEntity + ) => Promise; + getAll: (teamId: string) => Promise; + get: (teamId: string, id: string) => Promise; toggleActive: ( teamId: string, tokenizedUser: UserContext, id: string - ) => Promise; + ) => Promise; update: ( teamId: string, tokenizedUser: UserContext, id: string, - updateData: Partial - ) => Promise; + updateData: Partial + ) => Promise; delete: (teamId: string, id: string) => Promise; } class NotificationChannelService implements INotificationChannelService { public SERVICE_NAME: string; - - constructor() { + private notificationChannelRepository: INotificationChannelRepository; + private monitorRepository: IMonitorRepository; + constructor( + notificationChannelRepository: INotificationChannelRepository, + monitorRepository: IMonitorRepository + ) { this.SERVICE_NAME = SERVICE_NAME; + this.notificationChannelRepository = notificationChannelRepository; + this.monitorRepository = monitorRepository; } create = async ( userContext: UserContext, - notificationChannelData: INotificationChannel + notificationChannelData: NotificationChannelEntity ) => { - const data: INotificationChannel = { - ...notificationChannelData, - orgId: new mongoose.Types.ObjectId(userContext.orgId), - teamId: new mongoose.Types.ObjectId(userContext.currentTeamId), - createdBy: new mongoose.Types.ObjectId(userContext.sub), - updatedBy: new mongoose.Types.ObjectId(userContext.sub), - }; - - const notificationChannel = await NotificationChannel.create(data); + const notificationChannel = await this.notificationChannelRepository.create( + { + ...notificationChannelData, + orgId: userContext.orgId, + teamId: userContext.currentTeamId || "", + createdBy: userContext.sub, + updatedBy: userContext.sub, + } + ); return notificationChannel; }; get = async (teamId: string, id: string) => { - const channel = await NotificationChannel.findOne({ _id: id, teamId }); + const channel = await this.notificationChannelRepository.findById( + id, + teamId + ); if (!channel) { throw new ApiError("Notification channel not found", 404); } @@ -63,7 +72,7 @@ class NotificationChannelService implements INotificationChannelService { }; getAll = async (teamId: string) => { - return NotificationChannel.find({ teamId }); + return await this.notificationChannelRepository.findByTeamId(teamId); }; toggleActive = async ( @@ -71,18 +80,14 @@ class NotificationChannelService implements INotificationChannelService { userContext: UserContext, id: string ) => { - const updatedChannel = await NotificationChannel.findOneAndUpdate( - { _id: id, teamId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - updatedBy: userContext.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } + const updatedChannel = await this.notificationChannelRepository.update( + id, + teamId, + { + isActive: { $not: "$isActive" }, + updatedBy: userContext.sub, + updatedAt: new Date(), + } ); if (!updatedChannel) { throw new ApiError("Notification channel not found", 404); @@ -94,18 +99,16 @@ class NotificationChannelService implements INotificationChannelService { teamId: string, userContext: UserContext, id: string, - updateData: Partial + updateData: Partial ) => { - const updatedChannel = await NotificationChannel.findOneAndUpdate( - { _id: id, teamId }, + const updatedChannel = await this.notificationChannelRepository.update( + id, + teamId, { - $set: { - ...updateData, - updatedAt: new Date(), - updatedBy: userContext.sub, - }, - }, - { new: true, runValidators: true } + ...updateData, + updatedAt: new Date(), + updatedBy: userContext.sub, + } ); if (!updatedChannel) { @@ -115,17 +118,20 @@ class NotificationChannelService implements INotificationChannelService { return updatedChannel; }; - delete = async (teamId: string, id: string) => { - const result = await NotificationChannel.deleteOne({ _id: id, teamId }); - if (!result.deletedCount) { + delete = async (teamId: string, notificationChannelId: string) => { + const didDelete = await this.notificationChannelRepository.deleteById( + notificationChannelId, + teamId + ); + if (!didDelete) { throw new ApiError("Notification channel not found", 404); } - await Monitor.updateMany( - { notificationChannels: id }, - { $pull: { notificationChannels: id } } + await this.monitorRepository.removeNotificationChannelFromMonitors( + notificationChannelId ); - return result.deletedCount === 1; + + return didDelete; }; } diff --git a/server/src/types/domain/index.ts b/server/src/types/domain/index.ts index dba07ee81..994692079 100644 --- a/server/src/types/domain/index.ts +++ b/server/src/types/domain/index.ts @@ -11,3 +11,6 @@ export * from "@/types/domain/org.js"; export * from "@/types/domain/orgMembership.js"; export * from "@/types/domain/maintenance.js"; export * from "@/types/domain/statsHourly.js"; +export * from "@/types/domain/notificationChannel.js"; +export * from "@/types/domain/statsDaily.js"; +export * from "@/types/domain/recoveryToken.js"; diff --git a/server/src/types/domain/notificationChannel.ts b/server/src/types/domain/notificationChannel.ts new file mode 100644 index 000000000..127d88285 --- /dev/null +++ b/server/src/types/domain/notificationChannel.ts @@ -0,0 +1,21 @@ +export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; +export type ChannelType = (typeof ChannelTypes)[number]; + +export interface NotificationChannelConfig { + url?: string; + emailAddress?: string; +} + +export interface NotificationChannel { + id: string; + orgId: string; + teamId: string; + name: string; + type: ChannelType; + config: NotificationChannelConfig; + isActive: boolean | unknown; + createdBy: string; + updatedBy: string; + createdAt: Date; + updatedAt: Date; +} diff --git a/server/src/types/domain/recoveryToken.ts b/server/src/types/domain/recoveryToken.ts new file mode 100644 index 000000000..6808f870d --- /dev/null +++ b/server/src/types/domain/recoveryToken.ts @@ -0,0 +1,8 @@ +export interface RecoveryToken { + id: string; + userId: string; + tokenHash: string; + expiry: Date; + createdAt: Date; + updatedAt: Date; +} diff --git a/server/src/validation/notifications.ts b/server/src/validation/notifications.ts index 77a884c71..ce430e0c3 100644 --- a/server/src/validation/notifications.ts +++ b/server/src/validation/notifications.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { ChannelTypes } from "@/db/models/index.js"; +import { ChannelTypes } from "@/types/domain/index.js"; import { urlRegex } from "./common.js"; export const notificationChannelSchema = z @@ -101,4 +101,3 @@ export const notificationPatchSchema = z }); } }); -