Files
oneuptime/Common/Server/Services/AlertEpisodeMemberService.ts

316 lines
9.2 KiB
TypeScript

import CreateBy from "../Types/Database/CreateBy";
import DeleteBy from "../Types/Database/DeleteBy";
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
import DatabaseService from "./DatabaseService";
import BadDataException from "../../Types/Exception/BadDataException";
import ObjectID from "../../Types/ObjectID";
import Model from "../../Models/DatabaseModels/AlertEpisodeMember";
import Alert from "../../Models/DatabaseModels/Alert";
import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
import { IsBillingEnabled } from "../EnvironmentConfig";
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
import logger from "../Utils/Logger";
import AlertEpisodeFeedService from "./AlertEpisodeFeedService";
import AlertFeedService from "./AlertFeedService";
import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed";
import { AlertFeedEventType } from "../../Models/DatabaseModels/AlertFeed";
import { Yellow500, Green500 } from "../../Types/BrandColors";
import OneUptimeDate from "../../Types/Date";
import AlertService from "./AlertService";
import AlertEpisodeService from "./AlertEpisodeService";
export class Service extends DatabaseService<Model> {
public constructor() {
super(Model);
if (IsBillingEnabled) {
this.hardDeleteItemsOlderThanInDays("createdAt", 3 * 365); // 3 years
}
}
@CaptureSpan()
protected override async onBeforeCreate(
createBy: CreateBy<Model>,
): Promise<OnCreate<Model>> {
if (!createBy.data.alertEpisodeId) {
throw new BadDataException("alertEpisodeId is required");
}
if (!createBy.data.alertId) {
throw new BadDataException("alertId is required");
}
// Check if this alert is already in the episode
const existingMember: Model | null = await this.findOneBy({
query: {
alertEpisodeId: createBy.data.alertEpisodeId,
alertId: createBy.data.alertId,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
if (existingMember) {
throw new BadDataException("Alert is already a member of this episode");
}
// Set addedAt if not provided
if (!createBy.data.addedAt) {
createBy.data.addedAt = OneUptimeDate.getCurrentDate();
}
return { createBy, carryForward: null };
}
@CaptureSpan()
protected override async onCreateSuccess(
_onCreate: OnCreate<Model>,
createdItem: Model,
): Promise<Model> {
if (!createdItem.alertEpisodeId) {
throw new BadDataException("alertEpisodeId is required");
}
if (!createdItem.alertId) {
throw new BadDataException("alertId is required");
}
if (!createdItem.projectId) {
throw new BadDataException("projectId is required");
}
// Update alert's episode reference
await AlertService.updateOneById({
id: createdItem.alertId,
data: {
alertEpisodeId: createdItem.alertEpisodeId,
},
props: {
isRoot: true,
},
});
// Update episode's alertCount and lastAlertAddedAt
try {
await AlertEpisodeService.updateAlertCount(createdItem.alertEpisodeId!);
await AlertEpisodeService.updateLastAlertAddedAt(
createdItem.alertEpisodeId!,
);
} catch (error) {
logger.error(
`Error updating episode counts in AlertEpisodeMemberService.onCreateSuccess: ${error}`,
);
}
// Get alert details for feed
const alert: Alert | null = await AlertService.findOneById({
id: createdItem.alertId,
select: {
alertNumber: true,
alertNumberWithPrefix: true,
title: true,
},
props: {
isRoot: true,
},
});
// Get episode details for feed
const episode: AlertEpisode | null = await AlertEpisodeService.findOneById({
id: createdItem.alertEpisodeId,
select: {
episodeNumber: true,
episodeNumberWithPrefix: true,
title: true,
},
props: {
isRoot: true,
},
});
// Create feed item on episode
await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
alertEpisodeId: createdItem.alertEpisodeId,
projectId: createdItem.projectId,
alertEpisodeFeedEventType: AlertEpisodeFeedEventType.AlertAdded,
displayColor: Yellow500,
feedInfoInMarkdown: `**Alert ${alert?.alertNumberWithPrefix || "#" + (alert?.alertNumber || "N/A")}** added to episode: ${alert?.title || "No title"}`,
userId: createdItem.addedByUserId || undefined,
workspaceNotification: {
sendWorkspaceNotification: true,
notifyUserId: createdItem.addedByUserId || undefined,
},
});
// Create feed item on alert
await AlertFeedService.createAlertFeedItem({
alertId: createdItem.alertId,
projectId: createdItem.projectId,
alertFeedEventType: AlertFeedEventType.AddedToEpisode,
displayColor: Yellow500,
feedInfoInMarkdown: `Added to **Episode ${episode?.episodeNumberWithPrefix || "#" + (episode?.episodeNumber || "N/A")}**: ${episode?.title || "No title"}`,
userId: createdItem.addedByUserId || undefined,
});
return createdItem;
}
@CaptureSpan()
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
// Get the member records before deletion
const membersToDelete: Model[] = await this.findBy({
query: deleteBy.query,
props: {
isRoot: true,
},
select: {
alertEpisodeId: true,
alertId: true,
projectId: true,
},
limit: 100,
skip: 0,
});
return {
deleteBy,
carryForward: membersToDelete,
};
}
@CaptureSpan()
protected override async onDeleteSuccess(
onDelete: OnDelete<Model>,
_itemIdsBeforeDelete: ObjectID[],
): Promise<OnDelete<Model>> {
const membersDeleted: Model[] = onDelete.carryForward as Model[];
if (membersDeleted && membersDeleted.length > 0) {
for (const member of membersDeleted) {
if (member.alertId) {
// Clear the episode reference from the alert
await AlertService.updateOneById({
id: member.alertId,
data: {
alertEpisodeId: undefined as any,
},
props: {
isRoot: true,
},
});
// Get alert details for feed
const alert: Alert | null = await AlertService.findOneById({
id: member.alertId,
select: {
alertNumber: true,
title: true,
},
props: {
isRoot: true,
},
});
// Create feed item for removal
if (member.alertEpisodeId && member.projectId) {
// Get episode details for feed
const episode: AlertEpisode | null =
await AlertEpisodeService.findOneById({
id: member.alertEpisodeId,
select: {
episodeNumber: true,
episodeNumberWithPrefix: true,
title: true,
},
props: {
isRoot: true,
},
});
// Create feed item on episode
await AlertEpisodeFeedService.createAlertEpisodeFeedItem({
alertEpisodeId: member.alertEpisodeId,
projectId: member.projectId,
alertEpisodeFeedEventType: AlertEpisodeFeedEventType.AlertRemoved,
displayColor: Green500,
feedInfoInMarkdown: `**Alert #${alert?.alertNumber || "N/A"}** removed from episode: ${alert?.title || "No title"}`,
workspaceNotification: {
sendWorkspaceNotification: true,
},
});
// Create feed item on alert
await AlertFeedService.createAlertFeedItem({
alertId: member.alertId,
projectId: member.projectId,
alertFeedEventType: AlertFeedEventType.RemovedFromEpisode,
displayColor: Green500,
feedInfoInMarkdown: `Removed from **Episode ${episode?.episodeNumberWithPrefix || "#" + (episode?.episodeNumber || "N/A")}**: ${episode?.title || "No title"}`,
});
}
}
if (member.alertEpisodeId) {
// Update episode's alertCount
await AlertEpisodeService.updateAlertCount(member.alertEpisodeId);
}
}
}
return onDelete;
}
@CaptureSpan()
public async getAlertsInEpisode(episodeId: ObjectID): Promise<ObjectID[]> {
const members: Model[] = await this.findBy({
query: {
alertEpisodeId: episodeId,
},
props: {
isRoot: true,
},
select: {
alertId: true,
},
limit: 1000,
skip: 0,
});
return members
.filter((m: Model) => {
return m.alertId;
})
.map((m: Model) => {
return m.alertId!;
});
}
@CaptureSpan()
public async isAlertInEpisode(
alertId: ObjectID,
episodeId: ObjectID,
): Promise<boolean> {
const member: Model | null = await this.findOneBy({
query: {
alertId: alertId,
alertEpisodeId: episodeId,
},
props: {
isRoot: true,
},
select: {
_id: true,
},
});
return member !== null;
}
}
export default new Service();