diff --git a/Common/Server/Services/AlertEpisodeOwnerTeamService.ts b/Common/Server/Services/AlertEpisodeOwnerTeamService.ts index 6ee3790148..4bc6b1d735 100644 --- a/Common/Server/Services/AlertEpisodeOwnerTeamService.ts +++ b/Common/Server/Services/AlertEpisodeOwnerTeamService.ts @@ -1,10 +1,199 @@ +import ObjectID from "../../Types/ObjectID"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/AlertEpisodeOwnerTeam"; +import AlertEpisodeFeedService from "./AlertEpisodeFeedService"; +import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; +import TeamService from "./TeamService"; +import Team from "../../Models/DatabaseModels/Team"; +import DeleteBy from "../Types/Database/DeleteBy"; +import AlertEpisodeService from "./AlertEpisodeService"; +import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode"; +import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService"; +import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType"; +import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule"; +import logger from "../Utils/Logger"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; export class Service extends DatabaseService { public constructor() { super(Model); } + + @CaptureSpan() + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + alertEpisodeId: true, + projectId: true, + teamId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + @CaptureSpan() + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const alertEpisodeId: ObjectID | undefined = item.alertEpisodeId; + const projectId: ObjectID | undefined = item.projectId; + const teamId: ObjectID | undefined = item.teamId; + + if (alertEpisodeId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await AlertEpisodeService.getEpisodeNumber({ + episodeId: alertEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + if (team && team.name) { + await AlertEpisodeFeedService.createAlertEpisodeFeedItem({ + alertEpisodeId: alertEpisodeId, + projectId: projectId, + alertEpisodeFeedEventType: + AlertEpisodeFeedEventType.OwnerTeamRemoved, + displayColor: Red500, + feedInfoInMarkdown: `👨🏻‍👩🏻‍👦🏻 Removed team **${team.name}** from the [Episode ${episodeNumberDisplay}](${(await AlertEpisodeService.getEpisodeLinkInDashboard(projectId!, alertEpisodeId!)).toString()}) as the owner.`, + userId: deleteByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: deleteByUserId || undefined, + }, + }); + } + } + } + + return onDelete; + } + + @CaptureSpan() + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + const alertEpisodeId: ObjectID | undefined = createdItem.alertEpisodeId; + const projectId: ObjectID | undefined = createdItem.projectId; + const teamId: ObjectID | undefined = createdItem.teamId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (alertEpisodeId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + if (team && team.name) { + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await AlertEpisodeService.getEpisodeNumber({ + episodeId: alertEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + await AlertEpisodeFeedService.createAlertEpisodeFeedItem({ + alertEpisodeId: alertEpisodeId, + projectId: projectId, + alertEpisodeFeedEventType: + AlertEpisodeFeedEventType.OwnerTeamAdded, + displayColor: Gray500, + feedInfoInMarkdown: `👨🏻‍👩🏻‍👦🏻 Added team **${team.name}** to the [Episode ${episodeNumberDisplay}](${(await AlertEpisodeService.getEpisodeLinkInDashboard(projectId!, alertEpisodeId!)).toString()}) as the owner.`, + userId: createdByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: createdByUserId || undefined, + }, + }); + } + + // get notification rule where inviteOwners is true. + const notificationRules: Array = + await WorkspaceNotificationRuleService.getNotificationRulesWhereInviteOwnersIsTrue( + { + projectId: projectId, + notificationFor: { + alertEpisodeId: alertEpisodeId, + }, + notificationRuleEventType: + NotificationRuleEventType.AlertEpisode, + }, + ); + + // Fetch episode to get workspace channels + const episode: AlertEpisode | null = + await AlertEpisodeService.findOneById({ + id: alertEpisodeId, + select: { + postUpdatesToWorkspaceChannels: true, + }, + props: { + isRoot: true, + }, + }); + + if (episode) { + WorkspaceNotificationRuleService.inviteTeamsBasedOnRulesAndWorkspaceChannels( + { + notificationRules: notificationRules, + projectId: projectId, + workspaceChannels: + episode.postUpdatesToWorkspaceChannels || [], + teamIds: [teamId], + }, + ).catch((error: Error) => { + logger.error(error); + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/AlertEpisodeOwnerUserService.ts b/Common/Server/Services/AlertEpisodeOwnerUserService.ts index e1b3a92798..7a27b95782 100644 --- a/Common/Server/Services/AlertEpisodeOwnerUserService.ts +++ b/Common/Server/Services/AlertEpisodeOwnerUserService.ts @@ -1,10 +1,192 @@ +import ObjectID from "../../Types/ObjectID"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/AlertEpisodeOwnerUser"; +import AlertEpisodeFeedService from "./AlertEpisodeFeedService"; +import { AlertEpisodeFeedEventType } from "../../Models/DatabaseModels/AlertEpisodeFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; +import User from "../../Models/DatabaseModels/User"; +import UserService from "./UserService"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; +import DeleteBy from "../Types/Database/DeleteBy"; +import AlertEpisodeService from "./AlertEpisodeService"; +import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode"; +import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService"; +import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType"; +import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule"; +import logger from "../Utils/Logger"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; export class Service extends DatabaseService { public constructor() { super(Model); } + + @CaptureSpan() + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + alertEpisodeId: true, + projectId: true, + userId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + @CaptureSpan() + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const alertEpisodeId: ObjectID | undefined = item.alertEpisodeId; + const projectId: ObjectID | undefined = item.projectId; + const userId: ObjectID | undefined = item.userId; + + if (alertEpisodeId && userId && projectId) { + const user: User | null = await UserService.findOneById({ + id: userId, + select: { + name: true, + email: true, + }, + props: { + isRoot: true, + }, + }); + + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await AlertEpisodeService.getEpisodeNumber({ + episodeId: alertEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + if (user && user.name) { + await AlertEpisodeFeedService.createAlertEpisodeFeedItem({ + alertEpisodeId: alertEpisodeId, + projectId: projectId, + alertEpisodeFeedEventType: + AlertEpisodeFeedEventType.OwnerUserRemoved, + displayColor: Red500, + feedInfoInMarkdown: `👨🏻‍💻 Removed **${user.name.toString()}** (${user.email?.toString()}) from the [Episode ${episodeNumberDisplay}](${(await AlertEpisodeService.getEpisodeLinkInDashboard(projectId!, alertEpisodeId!)).toString()}) as the owner.`, + userId: deleteByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + } + } + } + + return onDelete; + } + + @CaptureSpan() + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + const alertEpisodeId: ObjectID | undefined = createdItem.alertEpisodeId; + const projectId: ObjectID | undefined = createdItem.projectId; + const userId: ObjectID | undefined = createdItem.userId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (alertEpisodeId && userId && projectId) { + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await AlertEpisodeService.getEpisodeNumber({ + episodeId: alertEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + await AlertEpisodeFeedService.createAlertEpisodeFeedItem({ + alertEpisodeId: alertEpisodeId, + projectId: projectId, + alertEpisodeFeedEventType: AlertEpisodeFeedEventType.OwnerUserAdded, + displayColor: Gray500, + feedInfoInMarkdown: `👨🏻‍💻 Added **${await UserService.getUserMarkdownString( + { + userId: userId, + projectId: projectId, + }, + )}** to the [Episode ${episodeNumberDisplay}](${(await AlertEpisodeService.getEpisodeLinkInDashboard(projectId!, alertEpisodeId!)).toString()}) as the owner.`, + userId: createdByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + + // get notification rule where inviteOwners is true. + const notificationRules: Array = + await WorkspaceNotificationRuleService.getNotificationRulesWhereInviteOwnersIsTrue( + { + projectId: projectId, + notificationFor: { + alertEpisodeId: alertEpisodeId, + }, + notificationRuleEventType: + NotificationRuleEventType.AlertEpisode, + }, + ); + + // Fetch episode to get workspace channels + const episode: AlertEpisode | null = + await AlertEpisodeService.findOneById({ + id: alertEpisodeId, + select: { + postUpdatesToWorkspaceChannels: true, + }, + props: { + isRoot: true, + }, + }); + + if (episode) { + WorkspaceNotificationRuleService.inviteUsersBasedOnRulesAndWorkspaceChannels( + { + notificationRules: notificationRules, + projectId: projectId, + workspaceChannels: + episode.postUpdatesToWorkspaceChannels || [], + userIds: [userId], + }, + ).catch((error: Error) => { + logger.error(error); + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/AlertEpisodeStateTimelineService.ts b/Common/Server/Services/AlertEpisodeStateTimelineService.ts index 9c31385070..c8b207b526 100644 --- a/Common/Server/Services/AlertEpisodeStateTimelineService.ts +++ b/Common/Server/Services/AlertEpisodeStateTimelineService.ts @@ -374,6 +374,11 @@ export class Service extends DatabaseService { ? `**Cause:** \n${createdItem.rootCause}` : undefined, userId: createdItem.createdByUserId || onCreate.createBy.props.userId, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: + createdItem.createdByUserId || onCreate.createBy.props.userId, + }, }); return createdItem; diff --git a/Common/Server/Services/IncidentEpisodeInternalNoteService.ts b/Common/Server/Services/IncidentEpisodeInternalNoteService.ts index fdd95d1677..a171f4eae8 100644 --- a/Common/Server/Services/IncidentEpisodeInternalNoteService.ts +++ b/Common/Server/Services/IncidentEpisodeInternalNoteService.ts @@ -1,8 +1,16 @@ import ObjectID from "../../Types/ObjectID"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/IncidentEpisodeInternalNote"; +import { OnCreate, OnUpdate } from "../Types/Database/Hooks"; +import IncidentEpisodeFeedService from "./IncidentEpisodeFeedService"; +import { IncidentEpisodeFeedEventType } from "../../Models/DatabaseModels/IncidentEpisodeFeed"; +import { Blue500 } from "../../Types/BrandColors"; +import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax"; +import IncidentEpisodeService from "./IncidentEpisodeService"; +import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode"; import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; import File from "../../Models/DatabaseModels/File"; +import FileAttachmentMarkdownUtil from "../Utils/FileAttachmentMarkdownUtil"; export class Service extends DatabaseService { public constructor() { @@ -66,6 +74,161 @@ export class Service extends DatabaseService { return existingNote !== null; } + + @CaptureSpan() + public override async onCreateSuccess( + _onCreate: OnCreate, + createdItem: Model, + ): Promise { + const userId: ObjectID | null | undefined = + createdItem.createdByUserId || createdItem.createdByUser?.id; + + const incidentEpisodeId: ObjectID = createdItem.incidentEpisodeId!; + + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await IncidentEpisodeService.getEpisodeNumber({ + episodeId: incidentEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + const attachmentsMarkdown: string = await this.getAttachmentsMarkdown( + createdItem.id!, + "/incident-episode-internal-note/attachment", + ); + + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: createdItem.incidentEpisodeId!, + projectId: createdItem.projectId!, + incidentEpisodeFeedEventType: IncidentEpisodeFeedEventType.PrivateNote, + displayColor: Blue500, + userId: userId || undefined, + feedInfoInMarkdown: `📄 posted **private note** for this [Episode ${episodeNumberDisplay}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(createdItem.projectId!, incidentEpisodeId)).toString()}): + +${(createdItem.note || "") + attachmentsMarkdown} + `, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + + return createdItem; + } + + @CaptureSpan() + public override async onUpdateSuccess( + onUpdate: OnUpdate, + _updatedItemIds: Array, + ): Promise> { + if (onUpdate.updateBy.data.note) { + const updatedItems: Array = await this.findBy({ + query: onUpdate.updateBy.query, + limit: LIMIT_PER_PROJECT, + skip: 0, + props: { + isRoot: true, + }, + select: { + incidentEpisodeId: true, + incidentEpisode: { + projectId: true, + episodeNumber: true, + episodeNumberWithPrefix: true, + }, + projectId: true, + note: true, + createdByUserId: true, + createdByUser: { + _id: true, + }, + }, + }); + + const userId: ObjectID | null | undefined = + onUpdate.updateBy.props.userId; + + for (const updatedItem of updatedItems) { + const episode: IncidentEpisode = updatedItem.incidentEpisode!; + + const attachmentsMarkdown: string = await this.getAttachmentsMarkdown( + updatedItem.id!, + "/incident-episode-internal-note/attachment", + ); + + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: updatedItem.incidentEpisodeId!, + projectId: updatedItem.projectId!, + incidentEpisodeFeedEventType: IncidentEpisodeFeedEventType.PrivateNote, + displayColor: Blue500, + userId: userId || undefined, + feedInfoInMarkdown: `📄 updated **Private Note** for this [Episode ${episode.episodeNumberWithPrefix || "#" + episode.episodeNumber}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(episode.projectId!, episode.id!)).toString()}) + +${(updatedItem.note || "") + attachmentsMarkdown} + `, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + } + } + return onUpdate; + } + + private async getAttachmentsMarkdown( + modelId: ObjectID, + attachmentApiPath: string, + ): Promise { + if (!modelId) { + return ""; + } + + const noteWithAttachments: Model | null = await this.findOneById({ + id: modelId, + select: { + attachments: { + _id: true, + }, + }, + props: { + isRoot: true, + }, + }); + + if (!noteWithAttachments || !noteWithAttachments.attachments) { + return ""; + } + + const attachmentIds: Array = noteWithAttachments.attachments + .map((file: File) => { + if (file.id) { + return file.id; + } + + if (file._id) { + return new ObjectID(file._id); + } + + return null; + }) + .filter((id: ObjectID | null): id is ObjectID => { + return Boolean(id); + }); + + if (!attachmentIds.length) { + return ""; + } + + return await FileAttachmentMarkdownUtil.buildAttachmentMarkdown({ + modelId, + attachmentIds, + attachmentApiPath, + }); + } } export default new Service(); diff --git a/Common/Server/Services/IncidentEpisodeOwnerTeamService.ts b/Common/Server/Services/IncidentEpisodeOwnerTeamService.ts index a88cf45cc2..b13178627b 100644 --- a/Common/Server/Services/IncidentEpisodeOwnerTeamService.ts +++ b/Common/Server/Services/IncidentEpisodeOwnerTeamService.ts @@ -1,10 +1,200 @@ +import ObjectID from "../../Types/ObjectID"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/IncidentEpisodeOwnerTeam"; +import IncidentEpisodeFeedService from "./IncidentEpisodeFeedService"; +import { IncidentEpisodeFeedEventType } from "../../Models/DatabaseModels/IncidentEpisodeFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; +import TeamService from "./TeamService"; +import Team from "../../Models/DatabaseModels/Team"; +import DeleteBy from "../Types/Database/DeleteBy"; +import IncidentEpisodeService from "./IncidentEpisodeService"; +import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode"; +import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService"; +import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType"; +import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule"; +import logger from "../Utils/Logger"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; export class Service extends DatabaseService { public constructor() { super(Model); } + + @CaptureSpan() + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + incidentEpisodeId: true, + projectId: true, + teamId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + @CaptureSpan() + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const incidentEpisodeId: ObjectID | undefined = item.incidentEpisodeId; + const projectId: ObjectID | undefined = item.projectId; + const teamId: ObjectID | undefined = item.teamId; + + if (incidentEpisodeId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await IncidentEpisodeService.getEpisodeNumber({ + episodeId: incidentEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + if (team && team.name) { + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: incidentEpisodeId, + projectId: projectId, + incidentEpisodeFeedEventType: + IncidentEpisodeFeedEventType.OwnerTeamRemoved, + displayColor: Red500, + feedInfoInMarkdown: `👨🏻‍👩🏻‍👦🏻 Removed team **${team.name}** from the [Episode ${episodeNumberDisplay}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(projectId!, incidentEpisodeId!)).toString()}) as the owner.`, + userId: deleteByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: deleteByUserId || undefined, + }, + }); + } + } + } + + return onDelete; + } + + @CaptureSpan() + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + const incidentEpisodeId: ObjectID | undefined = + createdItem.incidentEpisodeId; + const projectId: ObjectID | undefined = createdItem.projectId; + const teamId: ObjectID | undefined = createdItem.teamId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (incidentEpisodeId && teamId && projectId) { + const team: Team | null = await TeamService.findOneById({ + id: teamId, + select: { + name: true, + }, + props: { + isRoot: true, + }, + }); + + if (team && team.name) { + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await IncidentEpisodeService.getEpisodeNumber({ + episodeId: incidentEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: incidentEpisodeId, + projectId: projectId, + incidentEpisodeFeedEventType: + IncidentEpisodeFeedEventType.OwnerTeamAdded, + displayColor: Gray500, + feedInfoInMarkdown: `👨🏻‍👩🏻‍👦🏻 Added team **${team.name}** to the [Episode ${episodeNumberDisplay}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(projectId!, incidentEpisodeId!)).toString()}) as the owner.`, + userId: createdByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: createdByUserId || undefined, + }, + }); + } + + // get notification rule where inviteOwners is true. + const notificationRules: Array = + await WorkspaceNotificationRuleService.getNotificationRulesWhereInviteOwnersIsTrue( + { + projectId: projectId, + notificationFor: { + incidentEpisodeId: incidentEpisodeId, + }, + notificationRuleEventType: + NotificationRuleEventType.IncidentEpisode, + }, + ); + + // Fetch episode to get workspace channels + const episode: IncidentEpisode | null = + await IncidentEpisodeService.findOneById({ + id: incidentEpisodeId, + select: { + postUpdatesToWorkspaceChannels: true, + }, + props: { + isRoot: true, + }, + }); + + if (episode) { + WorkspaceNotificationRuleService.inviteTeamsBasedOnRulesAndWorkspaceChannels( + { + notificationRules: notificationRules, + projectId: projectId, + workspaceChannels: + episode.postUpdatesToWorkspaceChannels || [], + teamIds: [teamId], + }, + ).catch((error: Error) => { + logger.error(error); + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/IncidentEpisodeOwnerUserService.ts b/Common/Server/Services/IncidentEpisodeOwnerUserService.ts index beb125b683..3b854fd3b4 100644 --- a/Common/Server/Services/IncidentEpisodeOwnerUserService.ts +++ b/Common/Server/Services/IncidentEpisodeOwnerUserService.ts @@ -1,10 +1,194 @@ +import ObjectID from "../../Types/ObjectID"; import DatabaseService from "./DatabaseService"; import Model from "../../Models/DatabaseModels/IncidentEpisodeOwnerUser"; +import IncidentEpisodeFeedService from "./IncidentEpisodeFeedService"; +import { IncidentEpisodeFeedEventType } from "../../Models/DatabaseModels/IncidentEpisodeFeed"; +import { Gray500, Red500 } from "../../Types/BrandColors"; +import User from "../../Models/DatabaseModels/User"; +import UserService from "./UserService"; +import { OnCreate, OnDelete } from "../Types/Database/Hooks"; +import DeleteBy from "../Types/Database/DeleteBy"; +import IncidentEpisodeService from "./IncidentEpisodeService"; +import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode"; +import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService"; +import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType"; +import WorkspaceNotificationRule from "../../Models/DatabaseModels/WorkspaceNotificationRule"; +import logger from "../Utils/Logger"; +import CaptureSpan from "../Utils/Telemetry/CaptureSpan"; export class Service extends DatabaseService { public constructor() { super(Model); } + + @CaptureSpan() + protected override async onBeforeDelete( + deleteBy: DeleteBy, + ): Promise> { + const itemsToDelete: Model[] = await this.findBy({ + query: deleteBy.query, + limit: deleteBy.limit, + skip: deleteBy.skip, + props: { + isRoot: true, + }, + select: { + incidentEpisodeId: true, + projectId: true, + userId: true, + }, + }); + + return { + carryForward: { + itemsToDelete: itemsToDelete, + }, + deleteBy: deleteBy, + }; + } + + @CaptureSpan() + protected override async onDeleteSuccess( + onDelete: OnDelete, + _itemIdsBeforeDelete: Array, + ): Promise> { + const deleteByUserId: ObjectID | undefined = + onDelete.deleteBy.deletedByUser?.id || onDelete.deleteBy.props.userId; + + const itemsToDelete: Model[] = onDelete.carryForward.itemsToDelete; + + for (const item of itemsToDelete) { + const incidentEpisodeId: ObjectID | undefined = item.incidentEpisodeId; + const projectId: ObjectID | undefined = item.projectId; + const userId: ObjectID | undefined = item.userId; + + if (incidentEpisodeId && userId && projectId) { + const user: User | null = await UserService.findOneById({ + id: userId, + select: { + name: true, + email: true, + }, + props: { + isRoot: true, + }, + }); + + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await IncidentEpisodeService.getEpisodeNumber({ + episodeId: incidentEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + if (user && user.name) { + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: incidentEpisodeId, + projectId: projectId, + incidentEpisodeFeedEventType: + IncidentEpisodeFeedEventType.OwnerUserRemoved, + displayColor: Red500, + feedInfoInMarkdown: `👨🏻‍💻 Removed **${user.name.toString()}** (${user.email?.toString()}) from the [Episode ${episodeNumberDisplay}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(projectId!, incidentEpisodeId!)).toString()}) as the owner.`, + userId: deleteByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + } + } + } + + return onDelete; + } + + @CaptureSpan() + public override async onCreateSuccess( + onCreate: OnCreate, + createdItem: Model, + ): Promise { + const incidentEpisodeId: ObjectID | undefined = + createdItem.incidentEpisodeId; + const projectId: ObjectID | undefined = createdItem.projectId; + const userId: ObjectID | undefined = createdItem.userId; + const createdByUserId: ObjectID | undefined = + createdItem.createdByUserId || onCreate.createBy.props.userId; + + if (incidentEpisodeId && userId && projectId) { + const episodeNumberResult: { + number: number | null; + numberWithPrefix: string | null; + } = await IncidentEpisodeService.getEpisodeNumber({ + episodeId: incidentEpisodeId, + }); + const episodeNumberDisplay: string = + episodeNumberResult.numberWithPrefix || + "#" + episodeNumberResult.number; + + await IncidentEpisodeFeedService.createIncidentEpisodeFeedItem({ + incidentEpisodeId: incidentEpisodeId, + projectId: projectId, + incidentEpisodeFeedEventType: + IncidentEpisodeFeedEventType.OwnerUserAdded, + displayColor: Gray500, + feedInfoInMarkdown: `👨🏻‍💻 Added **${await UserService.getUserMarkdownString( + { + userId: userId, + projectId: projectId, + }, + )}** to the [Episode ${episodeNumberDisplay}](${(await IncidentEpisodeService.getEpisodeLinkInDashboard(projectId!, incidentEpisodeId!)).toString()}) as the owner.`, + userId: createdByUserId || undefined, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, + }); + + // get notification rule where inviteOwners is true. + const notificationRules: Array = + await WorkspaceNotificationRuleService.getNotificationRulesWhereInviteOwnersIsTrue( + { + projectId: projectId, + notificationFor: { + incidentEpisodeId: incidentEpisodeId, + }, + notificationRuleEventType: + NotificationRuleEventType.IncidentEpisode, + }, + ); + + // Fetch episode to get workspace channels + const episode: IncidentEpisode | null = + await IncidentEpisodeService.findOneById({ + id: incidentEpisodeId, + select: { + postUpdatesToWorkspaceChannels: true, + }, + props: { + isRoot: true, + }, + }); + + if (episode) { + WorkspaceNotificationRuleService.inviteUsersBasedOnRulesAndWorkspaceChannels( + { + notificationRules: notificationRules, + projectId: projectId, + workspaceChannels: + episode.postUpdatesToWorkspaceChannels || [], + userIds: [userId], + }, + ).catch((error: Error) => { + logger.error(error); + }); + } + } + + return createdItem; + } } export default new Service(); diff --git a/Common/Server/Services/IncidentEpisodePublicNoteService.ts b/Common/Server/Services/IncidentEpisodePublicNoteService.ts index 23e146940d..522d5fa53f 100644 --- a/Common/Server/Services/IncidentEpisodePublicNoteService.ts +++ b/Common/Server/Services/IncidentEpisodePublicNoteService.ts @@ -140,6 +140,10 @@ export class Service extends DatabaseService { ${(createdItem.note || "") + attachmentsMarkdown} `, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, }); return createdItem; @@ -196,6 +200,10 @@ ${(createdItem.note || "") + attachmentsMarkdown} ${(updatedItem.note || "") + attachmentsMarkdown} `, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: userId || undefined, + }, }); } } diff --git a/Common/Server/Services/IncidentEpisodeStateTimelineService.ts b/Common/Server/Services/IncidentEpisodeStateTimelineService.ts index 566fecfe32..632a273967 100644 --- a/Common/Server/Services/IncidentEpisodeStateTimelineService.ts +++ b/Common/Server/Services/IncidentEpisodeStateTimelineService.ts @@ -383,6 +383,11 @@ export class Service extends DatabaseService { ? `**Cause:** \n${createdItem.rootCause}` : undefined, userId: createdItem.createdByUserId || onCreate.createBy.props.userId, + workspaceNotification: { + sendWorkspaceNotification: true, + notifyUserId: + createdItem.createdByUserId || onCreate.createBy.props.userId, + }, }); return createdItem;