@oneuptime/common 11.3.26 → 11.3.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/IncidentEpisodeRoleMember.ts +1 -1
- package/Models/DatabaseModels/IncidentMember.ts +1 -1
- package/Models/DatabaseModels/IncomingCallPolicy.ts +13 -0
- package/Models/DatabaseModels/Index.ts +3 -0
- package/Models/DatabaseModels/MigrationFailure.ts +170 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +51 -2
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +44 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.ts +6 -0
- package/Models/DatabaseModels/OnCallDutyPolicyTimeLog.ts +1 -1
- package/Models/DatabaseModels/OnCallDutyPolicyUserOverride.ts +2 -2
- package/Server/API/AlertAPI.ts +1 -1
- package/Server/API/IncidentAPI.ts +2 -2
- package/Server/API/IncidentEpisodeAPI.ts +1 -1
- package/Server/API/LlmProviderAPI.ts +1 -0
- package/Server/API/ScheduledMaintenanceAPI.ts +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.ts +81 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Infrastructure/PostgresDatabase.ts +11 -0
- package/Server/Services/AIService.ts +74 -2
- package/Server/Services/AnalyticsDatabaseService.ts +10 -1
- package/Server/Services/DatabaseService.ts +10 -1
- package/Server/Services/IncidentService.ts +1 -1
- package/Server/Services/IncomingCallPolicyEscalationRuleService.ts +75 -2
- package/Server/Services/IncomingCallPolicyService.ts +42 -1
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/MigrationFailureService.ts +9 -0
- package/Server/Services/MonitorProbeService.ts +33 -7
- package/Server/Services/MonitorService.ts +19 -7
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +65 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleService.ts +185 -41
- package/Server/Services/OnCallDutyPolicyEscalationRuleUserService.ts +23 -15
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +39 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +148 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +11 -2
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +502 -26
- package/Server/Services/OnCallDutyPolicyTimeLogService.ts +49 -7
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +218 -4
- package/Server/Services/ProjectCallSMSConfigService.ts +82 -1
- package/Server/Services/TeamMemberService.ts +6 -0
- package/Server/Services/UserNotificationRuleService.ts +13 -42
- package/Server/Services/UserOnCallLogService.ts +90 -0
- package/Server/Services/UserOnCallLogTimelineService.ts +43 -1
- package/Server/Types/Database/QueryUtil.ts +57 -0
- package/Server/Utils/AI/AlertAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +14 -2
- package/Server/Utils/AI/IncidentAIContextBuilder.ts +18 -3
- package/Server/Utils/AI/IncidentEpisodeAIContextBuilder.ts +6 -1
- package/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.ts +12 -2
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +183 -1
- package/Server/Utils/AI/Toolbox/Index.ts +4 -1
- package/Server/Utils/AI/Toolbox/RecentChangesTools.ts +208 -0
- package/Server/Utils/AI/Toolbox/Serializer.ts +42 -0
- package/Server/Utils/Database/MigrationFailureLog.ts +240 -0
- package/Server/Utils/IncomingCallPhoneNumber.ts +41 -0
- package/Server/Utils/LLM/LLMService.ts +110 -11
- package/Server/Utils/Telemetry/IoTSnapshotScan.ts +12 -9
- package/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.ts +12 -9
- package/Tests/Server/Services/AnalyticsDatabaseService.test.ts +0 -8
- package/Tests/Server/Services/OnCallDutyPolicyScheduleLayerUserReorder.test.ts +233 -0
- package/Tests/Server/Services/OnCallDutyPolicyTimeLogServiceScoping.test.ts +242 -0
- package/Tests/Server/Services/OnCallDutyPolicyUserOverrideEdit.test.ts +198 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +495 -0
- package/Tests/Server/Types/Database/QueryUtil.test.ts +74 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +78 -0
- package/Tests/Server/Utils/AI/ToolResultSerializer.test.ts +68 -0
- package/Tests/Types/DateExhaustiveTimezone.test.ts +700 -0
- package/Tests/Types/DateTimezoneWallClock.test.ts +99 -0
- package/Tests/Types/Events/Recurring.test.ts +22 -9
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixes.test.ts +291 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilAuditFixesRound2.test.ts +398 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeekendGap.test.ts +114 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDSTWeeklyDayShift.test.ts +76 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyMutation.test.ts +128 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDailyProbe.test.ts +203 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilDiffFuzz.test.ts +322 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilEdgeCases.test.ts +280 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveCurrentUser.test.ts +863 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveDaily.test.ts +900 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveMultiLayer.test.ts +1104 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveRotation.test.ts +957 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilExhaustiveWeekly.test.ts +1095 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilInvariants.test.ts +232 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +227 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMonthYearAudit.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerAudit.test.ts +388 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMultiLayerEdge.test.ts +300 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilOvernightDSTAudit.test.ts +158 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapFix.test.ts +253 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRestrictedGapRepro.test.ts +157 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilRotationFixes.test.ts +414 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilTimezone.test.ts +243 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilWeekendGapRepro.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/OverrideLensAudit.test.ts +117 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +8 -6
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesDefaultDayAudit.test.ts +112 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesExhaustive.test.ts +821 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimesMutationInvariant.test.ts +886 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtil.test.ts +255 -0
- package/Tests/Types/OnCallDutyPolicy/UserOverrideUtilExhaustive.test.ts +1126 -0
- package/Types/Date.ts +200 -15
- package/Types/OnCallDutyPolicy/Layer.ts +790 -260
- package/Types/OnCallDutyPolicy/RestrictionTimes.ts +39 -13
- package/Types/OnCallDutyPolicy/UserOverrideUtil.ts +21 -5
- package/UI/Components/Events/RecurringFieldElement.tsx +16 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +8 -0
- package/UI/Components/RadioButtons/BasicRadioButtons.tsx +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeRoleMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js +10 -0
- package/build/dist/Models/DatabaseModels/IncomingCallPolicy.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MigrationFailure.js +196 -0
- package/build/dist/Models/DatabaseModels/MigrationFailure.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +52 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +45 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayer.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js +6 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleLayerUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyTimeLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js +2 -2
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyUserOverride.js.map +1 -1
- package/build/dist/Server/API/AlertAPI.js +1 -1
- package/build/dist/Server/API/IncidentAPI.js +2 -2
- package/build/dist/Server/API/IncidentEpisodeAPI.js +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +1 -0
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/API/ScheduledMaintenanceAPI.js +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783470000000-AddIncomingCallPolicyRoutingPhoneNumberUnique.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783510935686-FixNotNullForeignKeysOnDelete.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js +16 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783515836148-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1783523076215-AddMigrationFailureTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/PostgresDatabase.js +9 -0
- package/build/dist/Server/Infrastructure/PostgresDatabase.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +53 -2
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +10 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js +55 -2
- package/build/dist/Server/Services/IncomingCallPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/IncomingCallPolicyService.js +40 -0
- package/build/dist/Server/Services/IncomingCallPolicyService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MigrationFailureService.js +9 -0
- package/build/dist/Server/Services/MigrationFailureService.js.map +1 -0
- package/build/dist/Server/Services/MonitorProbeService.js +33 -7
- package/build/dist/Server/Services/MonitorProbeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +12 -5
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +81 -22
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js +134 -31
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js +21 -13
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +36 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +121 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +11 -2
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +409 -36
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js +52 -8
- package/build/dist/Server/Services/OnCallDutyPolicyTimeLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +170 -3
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js +72 -0
- package/build/dist/Server/Services/ProjectCallSMSConfigService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +6 -0
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleService.js +12 -30
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +75 -0
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js +31 -1
- package/build/dist/Server/Services/UserOnCallLogTimelineService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryUtil.js +45 -0
- package/build/dist/Server/Types/Database/QueryUtil.js.map +1 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/AlertAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +13 -2
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js +18 -3
- package/build/dist/Server/Utils/AI/IncidentAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js +6 -1
- package/build/dist/Server/Utils/AI/IncidentEpisodeAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js +12 -2
- package/build/dist/Server/Utils/AI/ScheduledMaintenanceAIContextBuilder.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +152 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +4 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js +165 -0
- package/build/dist/Server/Utils/AI/Toolbox/RecentChangesTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js +40 -0
- package/build/dist/Server/Utils/AI/Toolbox/Serializer.js.map +1 -1
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js +174 -0
- package/build/dist/Server/Utils/Database/MigrationFailureLog.js.map +1 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js +30 -0
- package/build/dist/Server/Utils/IncomingCallPhoneNumber.js.map +1 -0
- package/build/dist/Server/Utils/LLM/LLMService.js +81 -5
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/IoTSnapshotScan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js +11 -8
- package/build/dist/Server/Utils/Telemetry/ProxmoxCephSnapshotScan.js.map +1 -1
- package/build/dist/Types/Date.js +153 -15
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +602 -171
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js +27 -13
- package/build/dist/Types/OnCallDutyPolicy/RestrictionTimes.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js +20 -5
- package/build/dist/Types/OnCallDutyPolicy/UserOverrideUtil.js.map +1 -1
- package/build/dist/UI/Components/Events/RecurringFieldElement.js +13 -0
- package/build/dist/UI/Components/Events/RecurringFieldElement.js.map +1 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js +1 -1
- package/build/dist/UI/Components/RadioButtons/BasicRadioButtons.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@ import OnCallDutyPolicyEscalationRuleScheduleService from "./OnCallDutyPolicyEsc
|
|
|
10
10
|
import OnCallDutyPolicyEscalationRuleTeamService from "./OnCallDutyPolicyEscalationRuleTeamService";
|
|
11
11
|
import OnCallDutyPolicyEscalationRuleUserService from "./OnCallDutyPolicyEscalationRuleUserService";
|
|
12
12
|
import OnCallDutyPolicyExecutionLogService from "./OnCallDutyPolicyExecutionLogService";
|
|
13
|
+
import OnCallDutyPolicyExecutionLog from "../../Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
|
|
13
14
|
import OnCallDutyPolicyExecutionLogTimelineService from "./OnCallDutyPolicyExecutionLogTimelineService";
|
|
14
15
|
import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
|
|
15
16
|
import TeamMemberService from "./TeamMemberService";
|
|
@@ -315,31 +316,66 @@ export class Service extends DatabaseService<Model> {
|
|
|
315
316
|
);
|
|
316
317
|
|
|
317
318
|
type StartUserNotificationRuleExecutionFunction = (
|
|
318
|
-
|
|
319
|
+
originalUserId: ObjectID,
|
|
320
|
+
alertSentToUserId: ObjectID,
|
|
319
321
|
teamId: ObjectID | null,
|
|
320
322
|
scheduleId: ObjectID | null,
|
|
321
323
|
) => Promise<void>;
|
|
322
324
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
325
|
+
/*
|
|
326
|
+
* Resolve the FINAL alert recipient for a target, applying a user override
|
|
327
|
+
* (getRouteAlertToUserId) exactly ONCE per paging decision.
|
|
328
|
+
*
|
|
329
|
+
* Schedule targets are already override-resolved: getCurrentUserIdInSchedule
|
|
330
|
+
* applies overrides via UserOverrideUtil. Applying getRouteAlertToUserId to
|
|
331
|
+
* them again re-substituted an already-substituted user (transitive), so the
|
|
332
|
+
* schedule paging path could page a different person than the roster,
|
|
333
|
+
* dashboard, handoff notification, and a rule that lists the same user
|
|
334
|
+
* directly — all of which apply overrides only once (audit F5). So team and
|
|
335
|
+
* direct-user targets (the RAW roster user) get the single override hop here;
|
|
336
|
+
* schedule targets do not.
|
|
337
|
+
*/
|
|
338
|
+
const resolveAlertRecipientUserId: (
|
|
339
|
+
userId: ObjectID,
|
|
340
|
+
isFromSchedule: boolean,
|
|
341
|
+
) => Promise<ObjectID> = async (
|
|
342
|
+
userId: ObjectID,
|
|
343
|
+
isFromSchedule: boolean,
|
|
344
|
+
): Promise<ObjectID> => {
|
|
345
|
+
if (isFromSchedule) {
|
|
346
|
+
return userId;
|
|
347
|
+
}
|
|
333
348
|
|
|
334
|
-
|
|
335
|
-
|
|
349
|
+
if (options.onCallPolicyId) {
|
|
350
|
+
const routeAlertToUserId: ObjectID | null =
|
|
351
|
+
await this.getRouteAlertToUserId({
|
|
336
352
|
userId,
|
|
337
353
|
onCallDutyPolicyId: options.onCallPolicyId,
|
|
338
354
|
projectId: options.projectId,
|
|
339
355
|
});
|
|
356
|
+
|
|
357
|
+
if (routeAlertToUserId) {
|
|
358
|
+
return routeAlertToUserId;
|
|
340
359
|
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return userId;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const startUserNotificationRuleExecution: StartUserNotificationRuleExecutionFunction =
|
|
366
|
+
async (
|
|
367
|
+
originalUserId: ObjectID,
|
|
368
|
+
alertSentToUserId: ObjectID,
|
|
369
|
+
teamId: ObjectID | null,
|
|
370
|
+
scheduleId: ObjectID | null,
|
|
371
|
+
): Promise<void> => {
|
|
372
|
+
/*
|
|
373
|
+
* This is where user is notified. alertSentToUserId is already resolved
|
|
374
|
+
* (override applied at most once by resolveAlertRecipientUserId).
|
|
375
|
+
*/
|
|
341
376
|
|
|
342
|
-
const
|
|
377
|
+
const wasOverridden: boolean =
|
|
378
|
+
alertSentToUserId.toString() !== originalUserId.toString();
|
|
343
379
|
|
|
344
380
|
logger.debug(
|
|
345
381
|
`Starting notification rule execution for userId: ${alertSentToUserId.toString()}`,
|
|
@@ -354,8 +390,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
354
390
|
log.status = OnCallDutyExecutionLogTimelineStatus.Executing;
|
|
355
391
|
log.alertSentToUserId = alertSentToUserId;
|
|
356
392
|
|
|
357
|
-
if (
|
|
358
|
-
log.overridedByUserId =
|
|
393
|
+
if (wasOverridden) {
|
|
394
|
+
log.overridedByUserId = originalUserId;
|
|
359
395
|
}
|
|
360
396
|
|
|
361
397
|
if (teamId) {
|
|
@@ -390,12 +426,36 @@ export class Service extends DatabaseService<Model> {
|
|
|
390
426
|
onCallDutyPolicyExecutionLogTimelineId: log.id!,
|
|
391
427
|
projectId: options.projectId,
|
|
392
428
|
onCallScheduleId: scheduleId || undefined,
|
|
393
|
-
overridedByUserId:
|
|
429
|
+
overridedByUserId: wasOverridden ? originalUserId : undefined,
|
|
394
430
|
},
|
|
395
431
|
);
|
|
396
432
|
};
|
|
397
433
|
|
|
434
|
+
/*
|
|
435
|
+
* Dedup on the RESOLVED recipient (post-override), not the pre-route id.
|
|
436
|
+
* Keying on the pre-route id let two different targets that both reroute to
|
|
437
|
+
* the same backup each fire a full notification chain for the same person in
|
|
438
|
+
* one rule (audit F6).
|
|
439
|
+
*/
|
|
398
440
|
const uniqueUserIds: Array<ObjectID> = [];
|
|
441
|
+
const alreadyNotified: (recipientId: ObjectID) => boolean = (
|
|
442
|
+
recipientId: ObjectID,
|
|
443
|
+
): boolean => {
|
|
444
|
+
return Boolean(
|
|
445
|
+
uniqueUserIds.find((userId: ObjectID) => {
|
|
446
|
+
return recipientId.toString() === userId.toString();
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/*
|
|
452
|
+
* M-5: tracks whether any schedule target for this rule momentarily had no
|
|
453
|
+
* on-call user (a restriction gap / future start / empty layer). If a rule
|
|
454
|
+
* reaches NO ONE solely because of such a gap, we re-sample the same rule on
|
|
455
|
+
* the next cron ticks (bounded) instead of permanently skipping it, so a
|
|
456
|
+
* user coming on-call moments later still gets paged.
|
|
457
|
+
*/
|
|
458
|
+
let hadScheduleGap: boolean = false;
|
|
399
459
|
|
|
400
460
|
for (const teamInRule of teamsInRule) {
|
|
401
461
|
const usersInTeam: Array<User> = await TeamMemberService.getUsersInTeam(
|
|
@@ -403,14 +463,16 @@ export class Service extends DatabaseService<Model> {
|
|
|
403
463
|
);
|
|
404
464
|
|
|
405
465
|
for (const user of usersInTeam) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
466
|
+
const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
|
|
467
|
+
user.id!,
|
|
468
|
+
false,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (!alreadyNotified(recipientUserId)) {
|
|
472
|
+
uniqueUserIds.push(recipientUserId);
|
|
412
473
|
await startUserNotificationRuleExecution(
|
|
413
474
|
user.id!,
|
|
475
|
+
recipientUserId,
|
|
414
476
|
teamInRule.teamId!,
|
|
415
477
|
null,
|
|
416
478
|
);
|
|
@@ -419,7 +481,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
419
481
|
log.statusMessage =
|
|
420
482
|
"Skipped because notification sent to this user already.";
|
|
421
483
|
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
|
422
|
-
log.alertSentToUserId =
|
|
484
|
+
log.alertSentToUserId = recipientUserId;
|
|
423
485
|
log.userBelongsToTeamId = teamInRule.teamId!;
|
|
424
486
|
|
|
425
487
|
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
|
@@ -433,19 +495,25 @@ export class Service extends DatabaseService<Model> {
|
|
|
433
495
|
}
|
|
434
496
|
|
|
435
497
|
for (const userRule of usersInRule) {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
498
|
+
const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
|
|
499
|
+
userRule.userId!,
|
|
500
|
+
false,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (!alreadyNotified(recipientUserId)) {
|
|
504
|
+
uniqueUserIds.push(recipientUserId);
|
|
505
|
+
await startUserNotificationRuleExecution(
|
|
506
|
+
userRule.userId!,
|
|
507
|
+
recipientUserId,
|
|
508
|
+
null,
|
|
509
|
+
null,
|
|
510
|
+
);
|
|
443
511
|
} else {
|
|
444
512
|
const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
|
|
445
513
|
log.statusMessage =
|
|
446
514
|
"Skipped because notification sent to this user already.";
|
|
447
515
|
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
|
448
|
-
log.alertSentToUserId =
|
|
516
|
+
log.alertSentToUserId = recipientUserId;
|
|
449
517
|
|
|
450
518
|
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
|
451
519
|
data: log,
|
|
@@ -464,6 +532,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
464
532
|
);
|
|
465
533
|
|
|
466
534
|
if (!userIdInSchedule) {
|
|
535
|
+
hadScheduleGap = true;
|
|
536
|
+
|
|
467
537
|
const log: OnCallDutyPolicyExecutionLogTimeline = getNewLog();
|
|
468
538
|
log.statusMessage =
|
|
469
539
|
"Skipped because no active users are found in this schedule.";
|
|
@@ -480,14 +550,17 @@ export class Service extends DatabaseService<Model> {
|
|
|
480
550
|
continue;
|
|
481
551
|
}
|
|
482
552
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
)
|
|
488
|
-
|
|
553
|
+
// Schedule users are already override-resolved; no second override hop (F5).
|
|
554
|
+
const recipientUserId: ObjectID = await resolveAlertRecipientUserId(
|
|
555
|
+
userIdInSchedule,
|
|
556
|
+
true,
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
if (!alreadyNotified(recipientUserId)) {
|
|
560
|
+
uniqueUserIds.push(recipientUserId);
|
|
489
561
|
await startUserNotificationRuleExecution(
|
|
490
562
|
userIdInSchedule,
|
|
563
|
+
recipientUserId,
|
|
491
564
|
null,
|
|
492
565
|
scheduleRule.onCallDutyPolicyScheduleId!,
|
|
493
566
|
);
|
|
@@ -496,7 +569,7 @@ export class Service extends DatabaseService<Model> {
|
|
|
496
569
|
log.statusMessage =
|
|
497
570
|
"Skipped because notification sent to this user already.";
|
|
498
571
|
log.status = OnCallDutyExecutionLogTimelineStatus.Skipped;
|
|
499
|
-
log.alertSentToUserId =
|
|
572
|
+
log.alertSentToUserId = recipientUserId;
|
|
500
573
|
log.onCallDutyScheduleId = scheduleRule.onCallDutyPolicyScheduleId!;
|
|
501
574
|
|
|
502
575
|
await OnCallDutyPolicyExecutionLogTimelineService.create({
|
|
@@ -521,6 +594,77 @@ export class Service extends DatabaseService<Model> {
|
|
|
521
594
|
});
|
|
522
595
|
}
|
|
523
596
|
|
|
597
|
+
/*
|
|
598
|
+
* M-5: if this rule reached NO ONE and the only reason was a schedule gap
|
|
599
|
+
* (a schedule target with no on-call user right now), re-sample the SAME
|
|
600
|
+
* rule on the next cron ticks — bounded — instead of permanently skipping.
|
|
601
|
+
* ExecutePendingExecutions selects the rule with order =
|
|
602
|
+
* lastExecutedEscalationRuleOrder + 1, so rewinding the order by one makes
|
|
603
|
+
* it re-run this rule. No notifications were sent (uniqueUserIds is empty),
|
|
604
|
+
* so a re-sample cannot double-page anyone.
|
|
605
|
+
*/
|
|
606
|
+
const maxScheduleGapRetries: number = 5;
|
|
607
|
+
const scheduleGapRetryIntervalMinutes: number = 1;
|
|
608
|
+
const reachedNoOne: boolean = uniqueUserIds.length === 0;
|
|
609
|
+
|
|
610
|
+
if (reachedNoOne && hadScheduleGap && rule.order) {
|
|
611
|
+
const executionLog: OnCallDutyPolicyExecutionLog | null =
|
|
612
|
+
await OnCallDutyPolicyExecutionLogService.findOneById({
|
|
613
|
+
id: options.onCallPolicyExecutionLogId,
|
|
614
|
+
select: {
|
|
615
|
+
scheduleGapRetryCount: true,
|
|
616
|
+
},
|
|
617
|
+
props: {
|
|
618
|
+
isRoot: true,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const retryCount: number = executionLog?.scheduleGapRetryCount || 0;
|
|
623
|
+
|
|
624
|
+
if (retryCount < maxScheduleGapRetries) {
|
|
625
|
+
await OnCallDutyPolicyExecutionLogService.updateOneById({
|
|
626
|
+
id: options.onCallPolicyExecutionLogId,
|
|
627
|
+
data: {
|
|
628
|
+
lastExecutedEscalationRuleOrder: rule.order - 1,
|
|
629
|
+
executeNextEscalationRuleInMinutes: scheduleGapRetryIntervalMinutes,
|
|
630
|
+
scheduleGapRetryCount: retryCount + 1,
|
|
631
|
+
},
|
|
632
|
+
props: {
|
|
633
|
+
isRoot: true,
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
logger.debug(
|
|
638
|
+
`Rule ${ruleId.toString()} reached no one due to a schedule gap; re-sampling ${retryCount + 1}/${maxScheduleGapRetries}.`,
|
|
639
|
+
{
|
|
640
|
+
onCallDutyPolicyEscalationRuleId: ruleId.toString(),
|
|
641
|
+
} as LogAttributes,
|
|
642
|
+
);
|
|
643
|
+
} else {
|
|
644
|
+
// Retries exhausted — advance normally next tick and reset the counter.
|
|
645
|
+
await OnCallDutyPolicyExecutionLogService.updateOneById({
|
|
646
|
+
id: options.onCallPolicyExecutionLogId,
|
|
647
|
+
data: {
|
|
648
|
+
scheduleGapRetryCount: 0,
|
|
649
|
+
},
|
|
650
|
+
props: {
|
|
651
|
+
isRoot: true,
|
|
652
|
+
},
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
} else {
|
|
656
|
+
// Reached someone (or no schedule gap): clear retry state for the next rule.
|
|
657
|
+
await OnCallDutyPolicyExecutionLogService.updateOneById({
|
|
658
|
+
id: options.onCallPolicyExecutionLogId,
|
|
659
|
+
data: {
|
|
660
|
+
scheduleGapRetryCount: 0,
|
|
661
|
+
},
|
|
662
|
+
props: {
|
|
663
|
+
isRoot: true,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
524
668
|
logger.debug(`Completed rule execution for ruleId: ${ruleId.toString()}`, {
|
|
525
669
|
projectId: options.projectId.toString(),
|
|
526
670
|
onCallDutyPolicyEscalationRuleId: ruleId.toString(),
|
|
@@ -912,8 +1056,8 @@ export class Service extends DatabaseService<Model> {
|
|
|
912
1056
|
// moving down.
|
|
913
1057
|
|
|
914
1058
|
for (const resource of resources) {
|
|
915
|
-
if (resource.order! <= newOrder) {
|
|
916
|
-
//
|
|
1059
|
+
if (resource.order! > currentOrder && resource.order! <= newOrder) {
|
|
1060
|
+
// decrement order to fill the gap left by the moved rule.
|
|
917
1061
|
await this.updateOneBy({
|
|
918
1062
|
query: {
|
|
919
1063
|
_id: resource._id!,
|
|
@@ -261,22 +261,30 @@ export class Service extends DatabaseService<Model> {
|
|
|
261
261
|
notifyUserId: userId || undefined,
|
|
262
262
|
},
|
|
263
263
|
});
|
|
264
|
-
|
|
265
|
-
// also remove on-call duty time log.
|
|
266
|
-
OnCallDutyPolicyTimeLogService.endTimeLogForUser({
|
|
267
|
-
projectId: projectId,
|
|
268
|
-
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
269
|
-
onCallDutyPolicyEscalationRuleId:
|
|
270
|
-
item.onCallDutyPolicyEscalationRule!.id!,
|
|
271
|
-
userId: userId,
|
|
272
|
-
endsAt: OneUptimeDate.getCurrentDate(),
|
|
273
|
-
}).catch((error: Error) => {
|
|
274
|
-
logger.error(`Error ending time log for user ${userId}: ${error}`, {
|
|
275
|
-
projectId: projectId?.toString(),
|
|
276
|
-
userId: userId?.toString(),
|
|
277
|
-
} as LogAttributes);
|
|
278
|
-
});
|
|
279
264
|
}
|
|
265
|
+
|
|
266
|
+
/*
|
|
267
|
+
* Close the removed user's open on-call time log. This MUST run
|
|
268
|
+
* independently of whether the user has a display name — invited users
|
|
269
|
+
* who have not finished signup commonly have a null/empty name, and the
|
|
270
|
+
* ADD path (onCreateSuccess) opens their time log regardless of name.
|
|
271
|
+
* Nesting this inside `if (user && user.name)` left those users' logs
|
|
272
|
+
* open forever, showing them as perpetually on-call in reporting
|
|
273
|
+
* (audit F19).
|
|
274
|
+
*/
|
|
275
|
+
OnCallDutyPolicyTimeLogService.endTimeLogForUser({
|
|
276
|
+
projectId: projectId,
|
|
277
|
+
onCallDutyPolicyId: onCallDutyPolicyId,
|
|
278
|
+
onCallDutyPolicyEscalationRuleId:
|
|
279
|
+
item.onCallDutyPolicyEscalationRule!.id!,
|
|
280
|
+
userId: userId,
|
|
281
|
+
endsAt: OneUptimeDate.getCurrentDate(),
|
|
282
|
+
}).catch((error: Error) => {
|
|
283
|
+
logger.error(`Error ending time log for user ${userId}: ${error}`, {
|
|
284
|
+
projectId: projectId?.toString(),
|
|
285
|
+
userId: userId?.toString(),
|
|
286
|
+
} as LogAttributes);
|
|
287
|
+
});
|
|
280
288
|
}
|
|
281
289
|
}
|
|
282
290
|
|
|
@@ -26,6 +26,7 @@ import IncidentService from "./IncidentService";
|
|
|
26
26
|
import AlertService from "./AlertService";
|
|
27
27
|
import AlertEpisodeService from "./AlertEpisodeService";
|
|
28
28
|
import IncidentEpisodeService from "./IncidentEpisodeService";
|
|
29
|
+
import { IsNull, UpdateResult } from "typeorm";
|
|
29
30
|
|
|
30
31
|
export class Service extends DatabaseService<Model> {
|
|
31
32
|
public constructor() {
|
|
@@ -35,6 +36,44 @@ export class Service extends DatabaseService<Model> {
|
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Atomically claim the right to advance this execution to its next escalation
|
|
41
|
+
* rule on the current tick. Implemented as a single conditional
|
|
42
|
+
* `UPDATE ... WHERE lastEscalationRuleExecutedAt = <the value just read>`
|
|
43
|
+
* (via TypeORM's repository.update, which emits one atomic statement and
|
|
44
|
+
* returns the affected-row count), so that when two overlapping cron runs read
|
|
45
|
+
* the same lastEscalationRuleExecutedAt, exactly one wins — the loser sees
|
|
46
|
+
* `affected === 0` and bows out instead of double-paging responders (audit
|
|
47
|
+
* F13). `updateOneBy` could NOT provide this: it does a non-locking SELECT and
|
|
48
|
+
* then `save()` keyed only on `_id`, and returns the SELECT match count, so
|
|
49
|
+
* both overlapping runs would "win".
|
|
50
|
+
*/
|
|
51
|
+
@CaptureSpan()
|
|
52
|
+
public async claimEscalationAdvance(data: {
|
|
53
|
+
executionLogId: ObjectID;
|
|
54
|
+
previousLastEscalationRuleExecutedAt: Date | null;
|
|
55
|
+
newLastEscalationRuleExecutedAt: Date;
|
|
56
|
+
}): Promise<boolean> {
|
|
57
|
+
const whereClause: Record<string, unknown> = {
|
|
58
|
+
_id: data.executionLogId.toString(),
|
|
59
|
+
status: OnCallDutyPolicyStatus.Executing,
|
|
60
|
+
lastEscalationRuleExecutedAt: data.previousLastEscalationRuleExecutedAt
|
|
61
|
+
? data.previousLastEscalationRuleExecutedAt
|
|
62
|
+
: IsNull(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const result: UpdateResult = await this.getRepository().update(
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
whereClause as any,
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
69
|
+
{
|
|
70
|
+
lastEscalationRuleExecutedAt: data.newLastEscalationRuleExecutedAt,
|
|
71
|
+
} as any,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return (result.affected || 0) > 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
38
77
|
@CaptureSpan()
|
|
39
78
|
protected override async onBeforeCreate(
|
|
40
79
|
createBy: CreateBy<Model>,
|
|
@@ -8,6 +8,12 @@ import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
|
8
8
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
9
|
import ObjectID from "../../Types/ObjectID";
|
|
10
10
|
import PositiveNumber from "../../Types/PositiveNumber";
|
|
11
|
+
import Recurring from "../../Types/Events/Recurring";
|
|
12
|
+
import RestrictionTimes, {
|
|
13
|
+
RestrictionType,
|
|
14
|
+
} from "../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
15
|
+
import OneUptimeDate from "../../Types/Date";
|
|
16
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
11
17
|
import Model from "../../Models/DatabaseModels/OnCallDutyPolicyScheduleLayer";
|
|
12
18
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
13
19
|
import OnCallDutyPolicyScheduleService from "./OnCallDutyPolicyScheduleService";
|
|
@@ -17,6 +23,90 @@ export class Service extends DatabaseService<Model> {
|
|
|
17
23
|
super(Model);
|
|
18
24
|
}
|
|
19
25
|
|
|
26
|
+
/*
|
|
27
|
+
* A rotation interval count of 0 / NaN / negative breaks the handoff math
|
|
28
|
+
* (division by zero, Invalid Date), so reject it at the persistence boundary.
|
|
29
|
+
* The client also guards this, and the engine defensively clamps, but a raw
|
|
30
|
+
* API write must not be able to store an invalid rotation.
|
|
31
|
+
*/
|
|
32
|
+
private validateRotationInterval(rotation: unknown): void {
|
|
33
|
+
if (!rotation || typeof rotation === "function") {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let count: number;
|
|
38
|
+
try {
|
|
39
|
+
const recurring: Recurring =
|
|
40
|
+
rotation instanceof Recurring
|
|
41
|
+
? rotation
|
|
42
|
+
: Recurring.fromJSON(rotation as any);
|
|
43
|
+
count = recurring.intervalCount.toNumber();
|
|
44
|
+
} catch {
|
|
45
|
+
throw new BadDataException("Invalid rotation configuration.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!Number.isFinite(count) ||
|
|
50
|
+
isNaN(count) ||
|
|
51
|
+
!Number.isInteger(count) ||
|
|
52
|
+
count < 1
|
|
53
|
+
) {
|
|
54
|
+
throw new BadDataException(
|
|
55
|
+
"Rotation interval must be a whole number greater than or equal to 1.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/*
|
|
61
|
+
* Reject a zero-length Daily restriction window (From == To time-of-day) at the
|
|
62
|
+
* persistence boundary. Such a window is active 0 seconds/day, so the layer
|
|
63
|
+
* silently pages nobody for its coverage. The dashboard already blocks this
|
|
64
|
+
* (audit F22), but a raw API write bypasses the form — so guard server-side
|
|
65
|
+
* too. Compares UTC time-of-day, which is stable regardless of server zone.
|
|
66
|
+
*/
|
|
67
|
+
private validateRestrictionTimes(restrictionTimes: unknown): void {
|
|
68
|
+
if (!restrictionTimes || typeof restrictionTimes === "function") {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let parsed: RestrictionTimes;
|
|
73
|
+
try {
|
|
74
|
+
parsed =
|
|
75
|
+
restrictionTimes instanceof RestrictionTimes
|
|
76
|
+
? restrictionTimes
|
|
77
|
+
: RestrictionTimes.fromJSON(restrictionTimes as any);
|
|
78
|
+
} catch {
|
|
79
|
+
// Malformed input is handled elsewhere; nothing to validate here.
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
parsed.restictionType === RestrictionType.Daily &&
|
|
85
|
+
parsed.dayRestrictionTimes &&
|
|
86
|
+
parsed.dayRestrictionTimes.startTime &&
|
|
87
|
+
parsed.dayRestrictionTimes.endTime
|
|
88
|
+
) {
|
|
89
|
+
const start: Date = OneUptimeDate.fromString(
|
|
90
|
+
parsed.dayRestrictionTimes.startTime as any,
|
|
91
|
+
);
|
|
92
|
+
const end: Date = OneUptimeDate.fromString(
|
|
93
|
+
parsed.dayRestrictionTimes.endTime as any,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const timeOfDay: (d: Date) => number = (d: Date): number => {
|
|
97
|
+
return (
|
|
98
|
+
d.getUTCHours() * 3600 + d.getUTCMinutes() * 60 + d.getUTCSeconds()
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (timeOfDay(start) === timeOfDay(end)) {
|
|
103
|
+
throw new BadDataException(
|
|
104
|
+
"Daily restriction 'From' and 'To' times cannot be the same. Choose a window with a positive duration, or set restrictions to None for 24/7 coverage.",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
20
110
|
@CaptureSpan()
|
|
21
111
|
protected override async onBeforeCreate(
|
|
22
112
|
createBy: CreateBy<Model>,
|
|
@@ -25,6 +115,9 @@ export class Service extends DatabaseService<Model> {
|
|
|
25
115
|
throw new BadDataException("onCallDutyPolicyScheduleId is required");
|
|
26
116
|
}
|
|
27
117
|
|
|
118
|
+
this.validateRotationInterval(createBy.data.rotation);
|
|
119
|
+
this.validateRestrictionTimes(createBy.data.restrictionTimes);
|
|
120
|
+
|
|
28
121
|
if (!createBy.data.order) {
|
|
29
122
|
// count number of users in this layer.
|
|
30
123
|
|
|
@@ -77,6 +170,19 @@ export class Service extends DatabaseService<Model> {
|
|
|
77
170
|
return createdItem;
|
|
78
171
|
}
|
|
79
172
|
|
|
173
|
+
@CaptureSpan()
|
|
174
|
+
protected override async onBeforeUpdate(
|
|
175
|
+
updateBy: UpdateBy<Model>,
|
|
176
|
+
): Promise<OnUpdate<Model>> {
|
|
177
|
+
this.validateRotationInterval(updateBy.data.rotation);
|
|
178
|
+
this.validateRestrictionTimes(updateBy.data.restrictionTimes);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
updateBy,
|
|
182
|
+
carryForward: null,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
80
186
|
protected override async onUpdateSuccess(
|
|
81
187
|
_onUpdate: OnUpdate<Model>,
|
|
82
188
|
updatedItemIds: Array<ObjectID>,
|
|
@@ -107,6 +213,48 @@ export class Service extends DatabaseService<Model> {
|
|
|
107
213
|
};
|
|
108
214
|
}
|
|
109
215
|
|
|
216
|
+
@CaptureSpan()
|
|
217
|
+
protected override async onBeforeDelete(
|
|
218
|
+
deleteBy: DeleteBy<Model>,
|
|
219
|
+
): Promise<OnDelete<Model>> {
|
|
220
|
+
/*
|
|
221
|
+
* Capture the layer's order + schedule id as carryForward so onDeleteSuccess
|
|
222
|
+
* can re-sequence the remaining layers AND refresh the schedule roster after
|
|
223
|
+
* a layer is removed. Without this hook, onDeleteSuccess (which gates all of
|
|
224
|
+
* its work on onDelete.carryForward) was dead code: deleting a layer never
|
|
225
|
+
* re-sequenced order and never called
|
|
226
|
+
* refreshCurrentUserIdAndHandoffTimeInSchedule, so the schedule kept showing
|
|
227
|
+
* the removed layer's user as on-call and the genuinely-now-on-call user got
|
|
228
|
+
* no handoff notification for up to a full rotation period (audit F3).
|
|
229
|
+
* Mirrors OnCallDutyPolicyScheduleLayerUserService.onBeforeDelete.
|
|
230
|
+
*/
|
|
231
|
+
if (!deleteBy.query._id && !deleteBy.props.isRoot) {
|
|
232
|
+
throw new BadDataException(
|
|
233
|
+
"_id should be present when deleting a schedule layer. Please try the delete with objectId",
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let resource: Model | null = null;
|
|
238
|
+
|
|
239
|
+
if (!deleteBy.props.isRoot) {
|
|
240
|
+
resource = await this.findOneBy({
|
|
241
|
+
query: deleteBy.query,
|
|
242
|
+
props: {
|
|
243
|
+
isRoot: true,
|
|
244
|
+
},
|
|
245
|
+
select: {
|
|
246
|
+
order: true,
|
|
247
|
+
onCallDutyPolicyScheduleId: true,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
deleteBy,
|
|
254
|
+
carryForward: resource,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
110
258
|
@CaptureSpan()
|
|
111
259
|
protected override async onDeleteSuccess(
|
|
112
260
|
onDelete: OnDelete<Model>,
|
|
@@ -247,8 +247,17 @@ export class Service extends DatabaseService<Model> {
|
|
|
247
247
|
// moving down.
|
|
248
248
|
|
|
249
249
|
for (const resource of resources) {
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
/*
|
|
251
|
+
* Only shift rows strictly BETWEEN the old and new position. The lower
|
|
252
|
+
* bound (order > currentOrder) was missing, so every row above the
|
|
253
|
+
* moved user was also decremented — driving the top row's order to 0
|
|
254
|
+
* (and negative after repeated down-drags) and opening a gap at 1,
|
|
255
|
+
* breaking the 1-based contiguous invariant that create-default
|
|
256
|
+
* (count+1) and delete re-sequencing rely on (audit L3). Mirrors the
|
|
257
|
+
* double-bounded moving-up branch above.
|
|
258
|
+
*/
|
|
259
|
+
if (resource.order! <= newOrder && resource.order! > currentOrder) {
|
|
260
|
+
// decrement order.
|
|
252
261
|
await this.updateOneBy({
|
|
253
262
|
query: {
|
|
254
263
|
_id: resource._id!,
|