This commit is contained in:
Alex Holliday committed 2026-01-08 17:27:02 +00:00
1 parent c1bb1fd29d
commit c42932a7b3
15 files changed
+217 -76

No files matched your search

+1 -4
View File
@@ -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";
@@ -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<INotificationChannelConfig>(
const NotificationChannelConfigSchema = new Schema<NotificationChannelConfig>(
{
url: { type: String, required: false },
emailAddress: { type: String, required: false },
+6 -1
View File
@@ -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();
+3
View File
@@ -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";
@@ -85,4 +85,9 @@ export interface IMonitorRepository {
// Counts
countByOrgId(orgId: string): Promise<number>;
countByIdAndTeamId(monitorIds: string[], teamId: string): Promise<number>;
// Other
removeNotificationChannelFromMonitors(
notificationId: string
): Promise<boolean>;
}
@@ -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;
@@ -1,7 +1,24 @@
import type { NotificationChannel } from "@/types/domain/index.js";
export interface INotificationChannelRepository {
// create
create: (data: Partial<NotificationChannel>) => Promise<NotificationChannel>;
// single fetch
findById: (
notificationChannelId: string,
teamId: string
) => Promise<NotificationChannel | null>;
// collection fetch
findByTeamId: (teamId: string) => Promise<NotificationChannel[]>;
// update
update: (
notificationChannelId: string,
teamid: string,
data: Partial<NotificationChannel>
) => Promise<NotificationChannel | null>;
// delete
deleteById: (
notificationChannelId: string,
teamId: string
) => Promise<boolean>;
}
@@ -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<NotificationChannelEntity>) => {
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<NotificationChannelEntity>
) => {
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;
@@ -0,0 +1 @@
export interface IRecoveryTokenRepistory {}
@@ -0,0 +1,5 @@
import { IRecoveryTokenRepistory } from "@/repositories/index.js";
class MongoRecoveryTokenRepository implements IRecoveryTokenRepistory {}
export default MongoRecoveryTokenRepository;
@@ -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<INotificationChannel>;
getAll: (teamId: string) => Promise<INotificationChannel[]>;
get: (teamId: string, id: string) => Promise<INotificationChannel>;
notificationChannel: NotificationChannelEntity
) => Promise<NotificationChannelEntity>;
getAll: (teamId: string) => Promise<NotificationChannelEntity[]>;
get: (teamId: string, id: string) => Promise<NotificationChannelEntity>;
toggleActive: (
teamId: string,
tokenizedUser: UserContext,
id: string
) => Promise<INotificationChannel>;
) => Promise<NotificationChannelEntity>;
update: (
teamId: string,
tokenizedUser: UserContext,
id: string,
updateData: Partial<INotificationChannel>
) => Promise<INotificationChannel>;
updateData: Partial<NotificationChannelEntity>
) => Promise<NotificationChannelEntity>;
delete: (teamId: string, id: string) => Promise<boolean>;
}
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<INotificationChannel>
updateData: Partial<NotificationChannelEntity>
) => {
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;
};
}
+3
View File
@@ -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";
@@ -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;
}
+8
View File
@@ -0,0 +1,8 @@
export interface RecoveryToken {
id: string;
userId: string;
tokenHash: string;
expiry: Date;
createdAt: Date;
updatedAt: Date;
}
+1 -2
View File
@@ -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
});
}
});