diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 18608adc6..785d24e21 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -68,7 +68,7 @@ export const initServices = async () => { inviteRepository, teamMembershipRepository ); - const maintenanceService = new MaintenanceService(); + const maintenanceService = new MaintenanceService(monitorRepository); const monitorStatsService = new MonitorStatsService( monitorRepository, monitorStatsRepository diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 05d45d6ca..08e943270 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -33,3 +33,6 @@ export { default as MongoOrgRepository } from "@/repositories/org/MongoOrgReposi export type { IOrgMembershipRepository } from "@/repositories/org-membership/IOrgMembershopRepository.js"; export { default as MongoOrgMembershipRepository } from "@/repositories/org-membership/MongoOrgMembershipRepository.js"; + +export type { IMaintenanceRepository } from "@/repositories/maintenance/IMaintenanceRepository.js"; +export { default as MongoMaintenanceRepository } from "@/repositories/maintenance/MongoMaintenanceRepository.js"; diff --git a/server/src/repositories/maintenance/IMaintenanceRepository.ts b/server/src/repositories/maintenance/IMaintenanceRepository.ts new file mode 100644 index 000000000..fb9e2cf4f --- /dev/null +++ b/server/src/repositories/maintenance/IMaintenanceRepository.ts @@ -0,0 +1,18 @@ +import type { Maintenance } from "@/types/domain/index.js"; +export interface IMaintenanceRepository { + // create + create(maintenanceData: Partial): Promise; + // single fetch + findById(maintenanceId: string, teamId: string): Promise; + // collection fetch + findByTeamId(teamId: string): Promise; + findActive(): Promise; + // update + updateById( + maintenanceId: string, + teamId: string, + patchData: Partial + ): Promise; + // delete + deleteById(maintenanceId: string, teamId: string): Promise; +} diff --git a/server/src/repositories/maintenance/MongoMaintenanceRepository.ts b/server/src/repositories/maintenance/MongoMaintenanceRepository.ts new file mode 100644 index 000000000..754d7c957 --- /dev/null +++ b/server/src/repositories/maintenance/MongoMaintenanceRepository.ts @@ -0,0 +1,81 @@ +import type { IMaintenanceRepository } from "@/repositories/maintenance/IMaintenanceRepository.js"; +import { type IMaintenance, Maintenance } from "@/db/models/index.js"; +import type { Maintenance as MaintenanceEntity } from "@/types/domain/index.js"; +import { MaintenanceRepeats } from "@/types/domain/index.js"; + +class MongoMaintenanceRepository implements IMaintenanceRepository { + private toEntity = (doc: IMaintenance): MaintenanceEntity => { + return { + id: doc._id.toString(), + orgId: doc.orgId.toString(), + teamId: doc.teamId.toString(), + name: doc.name, + isActive: doc.isActive, + repeat: doc.repeat, + monitors: doc.monitors?.map((m) => m.toString()) ?? [], + startTime: doc.startTime, + endTime: doc.endTime, + createdBy: doc.createdBy.toString(), + updatedBy: doc.updatedBy.toString(), + createdAt: doc.createdAt, + updatedAt: doc.updatedAt, + }; + }; + + create = async (data: Partial) => { + const maintenance = await Maintenance.create(data); + return this.toEntity(maintenance); + }; + + findById = async (maintenanceId: string, teamId: string) => { + const maintenance = await Maintenance.findOne({ + _id: maintenanceId, + teamId, + }); + if (!maintenance) return null; + return this.toEntity(maintenance); + }; + + findByTeamId = async (teamId: string) => { + const maintennaces = await Maintenance.find({ teamId }); + return maintennaces.map(this.toEntity); + }; + + findActive = async () => { + const now = new Date(); + + const activeMaintenances = await Maintenance.find({ + isActive: true, + $or: [ + { repeat: { $in: MaintenanceRepeats } }, + { startTime: { $lte: now }, endTime: { $gte: now } }, + ], + }); + return activeMaintenances.map(this.toEntity); + }; + + updateById = async ( + maintenanceId: string, + teamId: string, + patchData: Partial + ) => { + const updatedMaintenance = await Maintenance.findOneAndUpdate( + { _id: maintenanceId, teamId }, + [ + { + $set: patchData, + }, + ], + { new: true, runValidators: true } + ); + if (!updatedMaintenance) return null; + return this.toEntity(updatedMaintenance); + }; + + deleteById = async (maintenanceId: string, teamId: string) => { + const result = await Maintenance.deleteOne({ _id: maintenanceId, teamId }); + return result.deletedCount === 1; + }; +} + +export default MongoMaintenanceRepository; diff --git a/server/src/repositories/monitors/IMonitorRepoistory.ts b/server/src/repositories/monitors/IMonitorRepoistory.ts index 305b47c1a..086be2706 100644 --- a/server/src/repositories/monitors/IMonitorRepoistory.ts +++ b/server/src/repositories/monitors/IMonitorRepoistory.ts @@ -84,4 +84,5 @@ export interface IMonitorRepository { // Counts countByOrgId(orgId: string): Promise; + countByIdAndTeamId(monitorIds: string[], teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorRepository.ts b/server/src/repositories/monitors/MongoMonitorRepository.ts index 78cc2c2de..d2b1a18a8 100644 --- a/server/src/repositories/monitors/MongoMonitorRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorRepository.ts @@ -232,6 +232,13 @@ class MongoMonitorRepository implements IMonitorRepository { countByOrgId(orgId: string): Promise { return Monitor.countDocuments({ orgId }); } + + countByIdAndTeamId = async (monitorIds: string[], teamId: string) => { + return await Monitor.countDocuments({ + _id: { $in: monitorIds }, + teamId: teamId, + }); + }; } export default MongoMonitorRepository; diff --git a/server/src/services/business/InviteService.ts b/server/src/services/business/InviteService.ts index e24612346..7b75b761b 100644 --- a/server/src/services/business/InviteService.ts +++ b/server/src/services/business/InviteService.ts @@ -7,8 +7,7 @@ import type { IInviteRepository, ITeamMembershipRepository, } from "@/repositories/index.js"; -import { User } from "@/types/domain/index.js"; -import { Invite } from "@/types/domain/index.js"; +import { User, Invite } from "@/types/domain/index.js"; const SERVICE_NAME = "InviteService"; export interface IInviteService { diff --git a/server/src/services/business/MaintenanceService.ts b/server/src/services/business/MaintenanceService.ts index a04882936..2cb9d89fd 100644 --- a/server/src/services/business/MaintenanceService.ts +++ b/server/src/services/business/MaintenanceService.ts @@ -1,6 +1,9 @@ -import { Monitor, IMaintenance, Maintenance } from "@/db/models/index.js"; +import { + IMonitorRepository, + IMaintenanceRepository, +} from "@/repositories/index.js"; import ApiError from "@/utils/ApiError.js"; -import { MaintenanceRepeats, UserContext } from "@/types/domain/index.js"; +import { UserContext, Maintenance } from "@/types/domain/index.js"; const SERVICE_NAME = "MaintenanceService"; const DAY_MS = 24 * 60 * 60 * 1000; @@ -9,26 +12,26 @@ const WEEK_MS = 7 * DAY_MS; export interface IMaintenanceService { create: ( userContext: UserContext, - maintenance: IMaintenance - ) => Promise; - getAll: (teamId: string) => Promise; - get: (teamId: string, id: string) => Promise; + maintenance: Maintenance + ) => Promise; + get: (teamId: string, id: string) => Promise; + getAll: (teamId: string) => Promise; toggleActive: ( teamId: string, userContext: UserContext, id: string - ) => Promise; + ) => Promise; update: ( teamId: string, userContext: UserContext, id: string, - updateData: Partial - ) => Promise; + updateData: Partial + ) => Promise; delete: (teamId: string, id: string) => Promise; isInMaintenance: (monitorId: string) => Promise; } -type MaintenanceCache = Map; +type MaintenanceCache = Map; class MaintenanceService implements IMaintenanceService { public SERVICE_NAME: string; @@ -36,19 +39,27 @@ class MaintenanceService implements IMaintenanceService { private lastRefresh: number; private CACHE_TTL_MS = 60 * 1000; - constructor() { + // Repositories + private maintenanceRepository: IMaintenanceRepository; + private monitorRepository: IMonitorRepository; + constructor( + maintenanceRepository: IMaintenanceRepository, + monitorRepository: IMonitorRepository + ) { this.SERVICE_NAME = SERVICE_NAME; this.maintenanceCache = new Map(); this.lastRefresh = 0; + this.monitorRepository = monitorRepository; + this.maintenanceRepository = maintenanceRepository; } - create = async (userContext: UserContext, maintenanceData: IMaintenance) => { + create = async (userContext: UserContext, maintenanceData: Maintenance) => { // Make sure monitors belong to current team const monitorIds = maintenanceData.monitors || []; - const count = await Monitor.countDocuments({ - _id: { $in: monitorIds }, - teamId: userContext.currentTeamId, - }); + const count = await this.monitorRepository.countByIdAndTeamId( + monitorIds, + userContext.currentTeamId || "" + ); if (count !== monitorIds.length) { throw new ApiError( @@ -57,7 +68,7 @@ class MaintenanceService implements IMaintenanceService { ); } - const maintenance = await Maintenance.create({ + const maintenance = await this.maintenanceRepository.create({ ...maintenanceData, orgId: userContext.orgId, teamId: userContext.currentTeamId, @@ -68,7 +79,7 @@ class MaintenanceService implements IMaintenanceService { }; get = async (teamId: string, id: string) => { - const maintenance = await Maintenance.findOne({ _id: id, teamId }); + const maintenance = await this.maintenanceRepository.findById(id, teamId); if (!maintenance) { throw new ApiError("Maintenance not found", 404); } @@ -76,7 +87,7 @@ class MaintenanceService implements IMaintenanceService { }; getAll = async (teamId: string) => { - return Maintenance.find({ teamId }); + return this.maintenanceRepository.findByTeamId(teamId); }; toggleActive = async ( @@ -84,18 +95,14 @@ class MaintenanceService implements IMaintenanceService { userContext: UserContext, id: string ) => { - const updatedMaintenance = await Maintenance.findOneAndUpdate( - { _id: id, teamId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - updatedBy: userContext.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } + const updatedMaintenance = await this.maintenanceRepository.updateById( + id, + teamId, + { + isActive: { $not: "$isActive" }, + updatedBy: userContext.sub, + updatedAt: new Date(), + } ); if (!updatedMaintenance) { throw new ApiError("Maintenance not found", 404); @@ -107,14 +114,14 @@ class MaintenanceService implements IMaintenanceService { teamId: string, userContext: UserContext, id: string, - updateData: Partial + updateData: Partial ) => { // Make sure monitors belong to current team const monitorIds = updateData.monitors || []; - const count = await Monitor.countDocuments({ - _id: { $in: monitorIds }, - teamId: userContext.currentTeamId, - }); + const count = await this.monitorRepository.countByIdAndTeamId( + monitorIds, + userContext.currentTeamId || "" + ); if (count !== monitorIds.length) { throw new ApiError( @@ -123,7 +130,7 @@ class MaintenanceService implements IMaintenanceService { ); } - const allowedFields: (keyof IMaintenance)[] = [ + const allowedFields: (keyof Maintenance)[] = [ "name", "monitors", "repeat", @@ -131,25 +138,22 @@ class MaintenanceService implements IMaintenanceService { "endTime", "isActive", ]; - const safeUpdate: Partial = {}; + const safeUpdate: Partial = {}; for (const field of allowedFields) { if (updateData[field] !== undefined) { (safeUpdate as any)[field] = updateData[field]; } } - const updatedMaintenance = await Maintenance.findOneAndUpdate( - { _id: id, teamId }, + const updatedMaintenance = await this.maintenanceRepository.updateById( + id, + teamId, { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: userContext.sub, - }, - }, - { new: true, runValidators: true } + ...safeUpdate, + updatedAt: new Date(), + updatedBy: userContext.sub, + } ); - if (!updatedMaintenance) { throw new ApiError("Failed to update maintenance", 500); } @@ -158,23 +162,15 @@ class MaintenanceService implements IMaintenanceService { }; delete = async (teamId: string, id: string) => { - const result = await Maintenance.deleteOne({ _id: id, teamId }); - if (!result.deletedCount) { + const didDelete = await this.maintenanceRepository.deleteById(id, teamId); + if (!didDelete) { throw new ApiError("Maintenance not found", 404); } - return result.deletedCount === 1; + return didDelete; }; private refreshCache = async () => { - const now = new Date(); - - const activeMaintenances = await Maintenance.find({ - isActive: true, - $or: [ - { repeat: { $in: MaintenanceRepeats } }, - { startTime: { $lte: now }, endTime: { $gte: now } }, - ], - }).lean(); + const activeMaintenances = await this.maintenanceRepository.findActive(); // Reset cache const newCache = new Map(); diff --git a/server/src/types/domain/maintenance.ts b/server/src/types/domain/maintenance.ts index 07837e661..8829dbe63 100644 --- a/server/src/types/domain/maintenance.ts +++ b/server/src/types/domain/maintenance.ts @@ -6,7 +6,7 @@ export interface Maintenance { orgId: string; teamId: string; name: string; - isActive: boolean; + isActive: boolean | unknown; repeat: MaintenanceRepeat; monitors: string[]; startTime: Date;