This commit is contained in:
Alex Holliday committed 2026-01-07 21:11:44 +00:00
1 parent 9248870716
commit 77f943a7c7
9 files changed
+169 -64

No files matched your search

+1 -1
View File
@@ -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
+3
View File
@@ -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";
@@ -0,0 +1,18 @@
import type { Maintenance } from "@/types/domain/index.js";
export interface IMaintenanceRepository {
// create
create(maintenanceData: Partial<Maintenance>): Promise<Maintenance>;
// single fetch
findById(maintenanceId: string, teamId: string): Promise<Maintenance | null>;
// collection fetch
findByTeamId(teamId: string): Promise<Maintenance[]>;
findActive(): Promise<Maintenance[]>;
// update
updateById(
maintenanceId: string,
teamId: string,
patchData: Partial<Maintenance>
): Promise<Maintenance | null>;
// delete
deleteById(maintenanceId: string, teamId: string): Promise<boolean>;
}
@@ -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<MaintenanceEntity>) => {
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<MaintenanceEntity>
) => {
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;
@@ -84,4 +84,5 @@ export interface IMonitorRepository {
// Counts
countByOrgId(orgId: string): Promise<number>;
countByIdAndTeamId(monitorIds: string[], teamId: string): Promise<number>;
}
@@ -232,6 +232,13 @@ class MongoMonitorRepository implements IMonitorRepository {
countByOrgId(orgId: string): Promise<number> {
return Monitor.countDocuments({ orgId });
}
countByIdAndTeamId = async (monitorIds: string[], teamId: string) => {
return await Monitor.countDocuments({
_id: { $in: monitorIds },
teamId: teamId,
});
};
}
export default MongoMonitorRepository;
@@ -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 {
@@ -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<IMaintenance>;
getAll: (teamId: string) => Promise<IMaintenance[]>;
get: (teamId: string, id: string) => Promise<IMaintenance>;
maintenance: Maintenance
) => Promise<Maintenance>;
get: (teamId: string, id: string) => Promise<Maintenance>;
getAll: (teamId: string) => Promise<Maintenance[]>;
toggleActive: (
teamId: string,
userContext: UserContext,
id: string
) => Promise<IMaintenance>;
) => Promise<Maintenance>;
update: (
teamId: string,
userContext: UserContext,
id: string,
updateData: Partial<IMaintenance>
) => Promise<IMaintenance>;
updateData: Partial<Maintenance>
) => Promise<Maintenance>;
delete: (teamId: string, id: string) => Promise<boolean>;
isInMaintenance: (monitorId: string) => Promise<boolean>;
}
type MaintenanceCache = Map<string, IMaintenance[]>;
type MaintenanceCache = Map<string, Maintenance[]>;
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<IMaintenance>
updateData: Partial<Maintenance>
) => {
// 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<IMaintenance> = {};
const safeUpdate: Partial<Maintenance> = {};
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();
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Maintenance {
orgId: string;
teamId: string;
name: string;
isActive: boolean;
isActive: boolean | unknown;
repeat: MaintenanceRepeat;
monitors: string[];
startTime: Date;