mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
482 lines
15 KiB
TypeScript
482 lines
15 KiB
TypeScript
import DatabaseService from "./DatabaseService";
|
|
import OnCallDutyPolicyExecutionLogService from "./OnCallDutyPolicyExecutionLogService";
|
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
import ObjectID from "../../Types/ObjectID";
|
|
import OnCallDutyPolicyStatus from "../../Types/OnCallDutyPolicy/OnCallDutyPolicyStatus";
|
|
import UserNotificationEventType from "../../Types/UserNotification/UserNotificationEventType";
|
|
import OnCallDutyPolicy from "../../Models/DatabaseModels/OnCallDutyPolicy";
|
|
import OnCallDutyPolicyExecutionLog from "../../Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
|
|
import DatabaseConfig from "../DatabaseConfig";
|
|
import URL from "../../Types/API/URL";
|
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
import OnCallDutyPolicySchedule from "../../Models/DatabaseModels/OnCallDutyPolicySchedule";
|
|
import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
|
|
import TeamService from "./TeamService";
|
|
import Team from "../../Models/DatabaseModels/Team";
|
|
import OnCallDutyPolicyEscalationRuleUser from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleUser";
|
|
import OnCallDutyPolicyEscalationRuleUserService from "./OnCallDutyPolicyEscalationRuleUserService";
|
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
import OnCallDutyPolicyEscalationRuleTeam from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleTeam";
|
|
import OnCallDutyPolicyEscalationRuleTeamService from "./OnCallDutyPolicyEscalationRuleTeamService";
|
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
import OnCallDutyPolicyEscalationRuleSchedule from "../../Models/DatabaseModels/OnCallDutyPolicyEscalationRuleSchedule";
|
|
import OnCallDutyPolicyEscalationRuleScheduleService from "./OnCallDutyPolicyEscalationRuleScheduleService";
|
|
import WorkspaceType from "../../Types/Workspace/WorkspaceType";
|
|
import NotificationRuleWorkspaceChannel from "../../Types/Workspace/NotificationRules/NotificationRuleWorkspaceChannel";
|
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
|
import { FindWhere } from "../../Types/BaseDatabase/Query";
|
|
import QueryOperator from "../../Types/BaseDatabase/QueryOperator";
|
|
import WorkspaceNotificationRuleService, {
|
|
MessageBlocksByWorkspaceType,
|
|
} from "./WorkspaceNotificationRuleService";
|
|
import logger from "../Utils/Logger";
|
|
import OnCallDutyPolicyWorkspaceMessages from "../Utils/Workspace/WorkspaceMessages/OnCallDutyPolicy";
|
|
import OnCallDutyPolicyFeedService from "./OnCallDutyPolicyFeedService";
|
|
import { OnCallDutyPolicyFeedEventType } from "../../Models/DatabaseModels/OnCallDutyPolicyFeed";
|
|
import { Green500 } from "../../Types/BrandColors";
|
|
|
|
export class Service extends DatabaseService<OnCallDutyPolicy> {
|
|
public constructor() {
|
|
super(OnCallDutyPolicy);
|
|
}
|
|
|
|
protected override async onCreateSuccess(
|
|
_onCreate: OnCreate<OnCallDutyPolicy>,
|
|
createdItem: OnCallDutyPolicy,
|
|
): Promise<OnCallDutyPolicy> {
|
|
if (!createdItem.id) {
|
|
throw new BadDataException("On Call Policy id not found.");
|
|
}
|
|
|
|
const onCallPolicy: OnCallDutyPolicy | null = await this.findOneById({
|
|
id: createdItem.id,
|
|
select: {
|
|
projectId: true,
|
|
name: true,
|
|
description: true,
|
|
labels: {
|
|
name: true,
|
|
},
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!onCallPolicy) {
|
|
throw new BadDataException("On Call Policy not found.");
|
|
}
|
|
|
|
const createdByUserId: ObjectID | undefined | null =
|
|
createdItem.createdByUserId || createdItem.createdByUser?.id;
|
|
|
|
let feedInfoInMarkdown: string = `#### 📞 On Call Policy Created:
|
|
|
|
**${onCallPolicy.name || "No name provided."}**:
|
|
|
|
${onCallPolicy.description || "No description provided."}
|
|
|
|
`;
|
|
|
|
if (onCallPolicy?.labels && onCallPolicy.labels.length > 0) {
|
|
feedInfoInMarkdown += `🏷️ **Labels**:\n`;
|
|
|
|
for (const label of onCallPolicy.labels) {
|
|
feedInfoInMarkdown += `- ${label.name}\n`;
|
|
}
|
|
|
|
feedInfoInMarkdown += `\n\n`;
|
|
}
|
|
|
|
// send message to workspaces - slack, teams, etc.
|
|
const workspaceResult: {
|
|
channelsCreated: Array<NotificationRuleWorkspaceChannel>;
|
|
} | null =
|
|
await OnCallDutyPolicyWorkspaceMessages.createChannelsAndInviteUsersToChannels(
|
|
{
|
|
projectId: onCallPolicy.projectId!,
|
|
onCallDutyPolicyId: onCallPolicy.id!,
|
|
onCallDutyPolicyName: onCallPolicy.name!,
|
|
},
|
|
);
|
|
|
|
if (workspaceResult && workspaceResult.channelsCreated?.length > 0) {
|
|
// update incident with these channels.
|
|
await this.updateOneById({
|
|
id: createdItem.id!,
|
|
data: {
|
|
postUpdatesToWorkspaceChannels: workspaceResult.channelsCreated || [],
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
const onCallDutyPolicyCreateMessageBlocks: Array<MessageBlocksByWorkspaceType> =
|
|
await OnCallDutyPolicyWorkspaceMessages.getOnCallDutyPolicyCreateMessageBlocks(
|
|
{
|
|
onCallDutyPolicyId: createdItem.id!,
|
|
projectId: createdItem.projectId!,
|
|
},
|
|
);
|
|
|
|
await OnCallDutyPolicyFeedService.createOnCallDutyPolicyFeedItem({
|
|
onCallDutyPolicyId: createdItem.id!,
|
|
projectId: createdItem.projectId!,
|
|
onCallDutyPolicyFeedEventType:
|
|
OnCallDutyPolicyFeedEventType.OnCallDutyPolicyCreated,
|
|
displayColor: Green500,
|
|
feedInfoInMarkdown: feedInfoInMarkdown,
|
|
userId: createdByUserId || undefined,
|
|
workspaceNotification: {
|
|
appendMessageBlocks: onCallDutyPolicyCreateMessageBlocks,
|
|
sendWorkspaceNotification: true,
|
|
},
|
|
});
|
|
|
|
return createdItem;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
protected override async onBeforeDelete(
|
|
deleteBy: DeleteBy<OnCallDutyPolicy>,
|
|
): Promise<OnDelete<OnCallDutyPolicy>> {
|
|
if (deleteBy.query._id) {
|
|
let projectId: FindWhere<ObjectID> | QueryOperator<ObjectID> | undefined =
|
|
deleteBy.query.projectId || deleteBy.props.tenantId;
|
|
|
|
if (!projectId) {
|
|
// fetch this onCallDutyPolicy from the database to get the projectId.
|
|
const onCallDutyPolicy: OnCallDutyPolicy | null =
|
|
await this.findOneById({
|
|
id: new ObjectID(deleteBy.query._id as string) as ObjectID,
|
|
select: {
|
|
projectId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!onCallDutyPolicy) {
|
|
throw new BadDataException("OnCallDutyPolicy not found.");
|
|
}
|
|
|
|
if (!onCallDutyPolicy.id) {
|
|
throw new BadDataException("OnCallDutyPolicy id not found.");
|
|
}
|
|
|
|
projectId = onCallDutyPolicy.projectId!;
|
|
}
|
|
|
|
try {
|
|
await WorkspaceNotificationRuleService.archiveWorkspaceChannels({
|
|
projectId: projectId as ObjectID,
|
|
notificationFor: {
|
|
onCallDutyPolicyId: new ObjectID(
|
|
deleteBy.query._id as string,
|
|
) as ObjectID,
|
|
},
|
|
sendMessageBeforeArchiving: {
|
|
_type: "WorkspacePayloadMarkdown",
|
|
text: `🗑️ This on-call policy is deleted. The channel is being archived.`,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error(
|
|
`Error while archiving workspace channels for onCallDutyPolicy ${deleteBy.query._id}: ${error}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
return { deleteBy, carryForward: null };
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getWorkspaceChannelForOnCallDutyPolicy(data: {
|
|
onCallDutyPolicyId: ObjectID;
|
|
workspaceType?: WorkspaceType | null;
|
|
}): Promise<Array<NotificationRuleWorkspaceChannel>> {
|
|
const onCallDutyPolicy: OnCallDutyPolicy | null = await this.findOneById({
|
|
id: data.onCallDutyPolicyId,
|
|
select: {
|
|
postUpdatesToWorkspaceChannels: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!onCallDutyPolicy) {
|
|
throw new BadDataException("OnCallDutyPolicy not found.");
|
|
}
|
|
|
|
return (onCallDutyPolicy.postUpdatesToWorkspaceChannels || []).filter(
|
|
(channel: NotificationRuleWorkspaceChannel) => {
|
|
if (!data.workspaceType) {
|
|
return true;
|
|
}
|
|
|
|
return channel.workspaceType === data.workspaceType;
|
|
},
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getOnCallDutyPolicyLinkInDashboard(
|
|
projectId: ObjectID,
|
|
onCallDutyPolicyId: ObjectID,
|
|
): Promise<URL> {
|
|
const dashboardUrl: URL = await DatabaseConfig.getDashboardUrl();
|
|
|
|
return URL.fromString(dashboardUrl.toString()).addRoute(
|
|
`/${projectId.toString()}/on-call-duty/policies/${onCallDutyPolicyId.toString()}`,
|
|
);
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async getOnCallDutyPolicyName(data: {
|
|
onCallDutyPolicyId: ObjectID;
|
|
}): Promise<string | null> {
|
|
const { onCallDutyPolicyId } = data;
|
|
|
|
const onCallDutyPolicy: OnCallDutyPolicy | null = await this.findOneById({
|
|
id: onCallDutyPolicyId,
|
|
select: {
|
|
name: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return onCallDutyPolicy && onCallDutyPolicy.name
|
|
? onCallDutyPolicy.name.toString()
|
|
: null;
|
|
}
|
|
|
|
@CaptureSpan()
|
|
public async executePolicy(
|
|
policyId: ObjectID,
|
|
options: {
|
|
triggeredByIncidentId?: ObjectID | undefined;
|
|
triggeredByAlertId?: ObjectID | undefined;
|
|
triggeredByAlertEpisodeId?: ObjectID | undefined;
|
|
triggeredByIncidentEpisodeId?: ObjectID | undefined;
|
|
userNotificationEventType: UserNotificationEventType;
|
|
},
|
|
): Promise<void> {
|
|
// execute this policy
|
|
|
|
if (
|
|
UserNotificationEventType.IncidentCreated ===
|
|
options.userNotificationEventType &&
|
|
!options.triggeredByIncidentId
|
|
) {
|
|
throw new BadDataException(
|
|
"triggeredByIncidentId is required when userNotificationEventType is IncidentCreated",
|
|
);
|
|
}
|
|
|
|
if (
|
|
UserNotificationEventType.AlertCreated ===
|
|
options.userNotificationEventType &&
|
|
!options.triggeredByAlertId
|
|
) {
|
|
throw new BadDataException(
|
|
"triggeredByAlertId is required when userNotificationEventType is AlertCreated",
|
|
);
|
|
}
|
|
|
|
if (
|
|
UserNotificationEventType.AlertEpisodeCreated ===
|
|
options.userNotificationEventType &&
|
|
!options.triggeredByAlertEpisodeId
|
|
) {
|
|
throw new BadDataException(
|
|
"triggeredByAlertEpisodeId is required when userNotificationEventType is AlertEpisodeCreated",
|
|
);
|
|
}
|
|
|
|
if (
|
|
UserNotificationEventType.IncidentEpisodeCreated ===
|
|
options.userNotificationEventType &&
|
|
!options.triggeredByIncidentEpisodeId
|
|
) {
|
|
throw new BadDataException(
|
|
"triggeredByIncidentEpisodeId is required when userNotificationEventType is IncidentEpisodeCreated",
|
|
);
|
|
}
|
|
|
|
const policy: OnCallDutyPolicy | null = await this.findOneById({
|
|
id: policyId,
|
|
select: {
|
|
_id: true,
|
|
projectId: true,
|
|
},
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
if (!policy) {
|
|
throw new BadDataException(
|
|
`On-Call Duty Policy with id ${policyId.toString()} not found`,
|
|
);
|
|
}
|
|
|
|
// add policy log.
|
|
const log: OnCallDutyPolicyExecutionLog =
|
|
new OnCallDutyPolicyExecutionLog();
|
|
|
|
log.projectId = policy.projectId!;
|
|
log.onCallDutyPolicyId = policyId;
|
|
log.userNotificationEventType = options.userNotificationEventType;
|
|
log.statusMessage = "Scheduled.";
|
|
log.status = OnCallDutyPolicyStatus.Scheduled;
|
|
|
|
if (options.triggeredByIncidentId) {
|
|
log.triggeredByIncidentId = options.triggeredByIncidentId;
|
|
}
|
|
|
|
if (options.triggeredByAlertId) {
|
|
log.triggeredByAlertId = options.triggeredByAlertId;
|
|
}
|
|
|
|
if (options.triggeredByAlertEpisodeId) {
|
|
log.triggeredByAlertEpisodeId = options.triggeredByAlertEpisodeId;
|
|
}
|
|
|
|
if (options.triggeredByIncidentEpisodeId) {
|
|
log.triggeredByIncidentEpisodeId = options.triggeredByIncidentEpisodeId;
|
|
}
|
|
|
|
await OnCallDutyPolicyExecutionLogService.create({
|
|
data: log,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
public async getOnCallPoliciesWhereUserIsOnCallDuty(data: {
|
|
projectId: ObjectID;
|
|
userId: ObjectID;
|
|
}): Promise<{
|
|
escalationRulesByUser: Array<OnCallDutyPolicyEscalationRuleUser>;
|
|
escalationRulesByTeam: Array<OnCallDutyPolicyEscalationRuleTeam>;
|
|
escalationRulesBySchedule: Array<OnCallDutyPolicyEscalationRuleSchedule>;
|
|
}> {
|
|
// get all schedules where user is on call duty.
|
|
const onCallSchedules: Array<OnCallDutyPolicySchedule> =
|
|
await OnCallDutyPolicyScheduleService.getOnCallSchedulesWhereUserIsOnCallDuty(
|
|
data,
|
|
);
|
|
|
|
const teams: Array<Team> = await TeamService.getTeamsUserIsAPartOf({
|
|
userId: data.userId,
|
|
projectId: data.projectId,
|
|
});
|
|
|
|
// get escalationPolicies by user, team and schedule.
|
|
const escalationRulesByUser: Array<OnCallDutyPolicyEscalationRuleUser> =
|
|
await OnCallDutyPolicyEscalationRuleUserService.findBy({
|
|
query: {
|
|
userId: data.userId!,
|
|
projectId: data.projectId!,
|
|
},
|
|
select: {
|
|
onCallDutyPolicyEscalationRule: {
|
|
name: true,
|
|
_id: true,
|
|
order: true,
|
|
},
|
|
onCallDutyPolicy: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// do the same for teams.
|
|
const escalationRulesByTeam: Array<OnCallDutyPolicyEscalationRuleTeam> =
|
|
await OnCallDutyPolicyEscalationRuleTeamService.findBy({
|
|
query: {
|
|
teamId: QueryHelper.any(
|
|
teams.map((team: Team) => {
|
|
return team.id!;
|
|
}),
|
|
),
|
|
projectId: data.projectId!,
|
|
},
|
|
select: {
|
|
onCallDutyPolicy: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
onCallDutyPolicyEscalationRule: {
|
|
name: true,
|
|
_id: true,
|
|
order: true,
|
|
},
|
|
team: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
// do the same for schedules.
|
|
const escalationRulesBySchedule: Array<OnCallDutyPolicyEscalationRuleSchedule> =
|
|
await OnCallDutyPolicyEscalationRuleScheduleService.findBy({
|
|
query: {
|
|
onCallDutyPolicyScheduleId: QueryHelper.any(
|
|
onCallSchedules.map((schedule: OnCallDutyPolicySchedule) => {
|
|
return schedule.id!;
|
|
}),
|
|
),
|
|
projectId: data.projectId!,
|
|
},
|
|
select: {
|
|
onCallDutyPolicy: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
onCallDutyPolicyEscalationRule: {
|
|
name: true,
|
|
_id: true,
|
|
order: true,
|
|
},
|
|
onCallDutyPolicySchedule: {
|
|
name: true,
|
|
_id: true,
|
|
},
|
|
},
|
|
limit: LIMIT_PER_PROJECT,
|
|
skip: 0,
|
|
props: {
|
|
isRoot: true,
|
|
},
|
|
});
|
|
|
|
return {
|
|
escalationRulesByUser: escalationRulesByUser,
|
|
escalationRulesByTeam: escalationRulesByTeam,
|
|
escalationRulesBySchedule: escalationRulesBySchedule,
|
|
};
|
|
}
|
|
}
|
|
export default new Service();
|