feat: enhance workspace notification handling in various services

This commit is contained in:
Nawaz Dhandala
2026-02-11 12:39:48 +00:00
parent 7c672e14a1
commit 3e48a706bd
8 changed files with 926 additions and 0 deletions

View File

@@ -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<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
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<Model>,
_itemIdsBeforeDelete: Array<ObjectID>,
): Promise<OnDelete<Model>> {
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<Model>,
createdItem: Model,
): Promise<Model> {
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<WorkspaceNotificationRule> =
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();

View File

@@ -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<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
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<Model>,
_itemIdsBeforeDelete: Array<ObjectID>,
): Promise<OnDelete<Model>> {
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<Model>,
createdItem: Model,
): Promise<Model> {
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<WorkspaceNotificationRule> =
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();

View File

@@ -374,6 +374,11 @@ export class Service extends DatabaseService<AlertEpisodeStateTimeline> {
? `**Cause:** \n${createdItem.rootCause}`
: undefined,
userId: createdItem.createdByUserId || onCreate.createBy.props.userId,
workspaceNotification: {
sendWorkspaceNotification: true,
notifyUserId:
createdItem.createdByUserId || onCreate.createBy.props.userId,
},
});
return createdItem;

View File

@@ -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<Model> {
public constructor() {
@@ -66,6 +74,161 @@ export class Service extends DatabaseService<Model> {
return existingNote !== null;
}
@CaptureSpan()
public override async onCreateSuccess(
_onCreate: OnCreate<Model>,
createdItem: Model,
): Promise<Model> {
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<Model>,
_updatedItemIds: Array<ObjectID>,
): Promise<OnUpdate<Model>> {
if (onUpdate.updateBy.data.note) {
const updatedItems: Array<Model> = 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<string> {
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<ObjectID> = 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();

View File

@@ -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<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
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<Model>,
_itemIdsBeforeDelete: Array<ObjectID>,
): Promise<OnDelete<Model>> {
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<Model>,
createdItem: Model,
): Promise<Model> {
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<WorkspaceNotificationRule> =
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();

View File

@@ -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<Model> {
public constructor() {
super(Model);
}
@CaptureSpan()
protected override async onBeforeDelete(
deleteBy: DeleteBy<Model>,
): Promise<OnDelete<Model>> {
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<Model>,
_itemIdsBeforeDelete: Array<ObjectID>,
): Promise<OnDelete<Model>> {
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<Model>,
createdItem: Model,
): Promise<Model> {
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<WorkspaceNotificationRule> =
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();

View File

@@ -140,6 +140,10 @@ export class Service extends DatabaseService<Model> {
${(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,
},
});
}
}

View File

@@ -383,6 +383,11 @@ export class Service extends DatabaseService<IncidentEpisodeStateTimeline> {
? `**Cause:** \n${createdItem.rootCause}`
: undefined,
userId: createdItem.createdByUserId || onCreate.createBy.props.userId,
workspaceNotification: {
sendWorkspaceNotification: true,
notifyUserId:
createdItem.createdByUserId || onCreate.createBy.props.userId,
},
});
return createdItem;