@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
|
@@ -2,6 +2,7 @@ import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
|
2
2
|
import ObjectID from "../../Types/ObjectID";
|
|
3
3
|
import DatabaseService from "./DatabaseService";
|
|
4
4
|
import Model from "../../Models/DatabaseModels/OnCallDutyPolicyTimeLog";
|
|
5
|
+
import QueryHelper from "../Types/Database/QueryHelper";
|
|
5
6
|
|
|
6
7
|
export class Service extends DatabaseService<Model> {
|
|
7
8
|
public constructor() {
|
|
@@ -25,16 +26,33 @@ export class Service extends DatabaseService<Model> {
|
|
|
25
26
|
startsAt,
|
|
26
27
|
} = data;
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
/*
|
|
30
|
+
* Check if an OPEN (not-yet-ended) time log already exists for the user.
|
|
31
|
+
* The endsAt IS NULL filter is essential: without it, a user who rotated
|
|
32
|
+
* off and later rotates back on would match their already-CLOSED prior log
|
|
33
|
+
* and skip creating a new one, so their later on-call stints would never be
|
|
34
|
+
* recorded (and a "who is on call now" query filtering endsAt IS NULL would
|
|
35
|
+
* miss them). Each distinct on-call stint must be its own row.
|
|
36
|
+
*/
|
|
30
37
|
const existingTimeLog: Model | null = await this.findOneBy({
|
|
31
38
|
query: {
|
|
32
39
|
projectId: data.projectId,
|
|
33
40
|
onCallDutyPolicyId,
|
|
34
41
|
onCallDutyPolicyEscalationRuleId: data.onCallDutyPolicyEscalationRuleId,
|
|
35
42
|
userId,
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
/*
|
|
44
|
+
* Scope the context columns explicitly so a direct-user log dedups only
|
|
45
|
+
* against other direct logs, a team log only against team logs, and a
|
|
46
|
+
* schedule log only against schedule logs. The old `...(x && {x})`
|
|
47
|
+
* OMITTED the column when absent, so an absent column matched ANY value:
|
|
48
|
+
* a direct-user start then deduped against an already-open schedule/team
|
|
49
|
+
* log and never created the direct log, leaving the user with zero open
|
|
50
|
+
* logs once that schedule/team log closed (audit L4).
|
|
51
|
+
*/
|
|
52
|
+
teamId: teamId ?? QueryHelper.isNull(),
|
|
53
|
+
onCallDutyPolicyScheduleId:
|
|
54
|
+
onCallDutyPolicyScheduleId ?? QueryHelper.isNull(),
|
|
55
|
+
endsAt: QueryHelper.isNull(),
|
|
38
56
|
},
|
|
39
57
|
props: {
|
|
40
58
|
isRoot: true,
|
|
@@ -74,9 +92,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
74
92
|
}): Promise<void> {
|
|
75
93
|
const { endsAt, onCallDutyPolicyScheduleId } = data;
|
|
76
94
|
await this.updateBy({
|
|
95
|
+
// only close still-open logs; never overwrite historical closed intervals.
|
|
77
96
|
query: {
|
|
78
97
|
projectId: data.projectId,
|
|
79
98
|
onCallDutyPolicyScheduleId,
|
|
99
|
+
endsAt: QueryHelper.isNull(),
|
|
80
100
|
},
|
|
81
101
|
data: {
|
|
82
102
|
endsAt: endsAt,
|
|
@@ -96,9 +116,11 @@ export class Service extends DatabaseService<Model> {
|
|
|
96
116
|
}): Promise<void> {
|
|
97
117
|
const { endsAt, teamId } = data;
|
|
98
118
|
await this.updateBy({
|
|
119
|
+
// only close still-open logs; never overwrite historical closed intervals.
|
|
99
120
|
query: {
|
|
100
121
|
projectId: data.projectId,
|
|
101
122
|
teamId,
|
|
123
|
+
endsAt: QueryHelper.isNull(),
|
|
102
124
|
},
|
|
103
125
|
|
|
104
126
|
limit: LIMIT_PER_PROJECT,
|
|
@@ -115,13 +137,22 @@ export class Service extends DatabaseService<Model> {
|
|
|
115
137
|
public async endTimeForUser(data: {
|
|
116
138
|
projectId: ObjectID;
|
|
117
139
|
userId: ObjectID;
|
|
140
|
+
/*
|
|
141
|
+
* When set, only close logs derived from this team. Leaving one team must
|
|
142
|
+
* not close a user's still-active logs from other teams, direct escalation
|
|
143
|
+
* assignments, or schedule rosters (audit F17).
|
|
144
|
+
*/
|
|
145
|
+
teamId?: ObjectID | undefined;
|
|
118
146
|
endsAt: Date;
|
|
119
147
|
}): Promise<void> {
|
|
120
|
-
const { endsAt, userId } = data;
|
|
148
|
+
const { endsAt, userId, teamId } = data;
|
|
121
149
|
await this.updateBy({
|
|
150
|
+
// only close still-open logs; never overwrite historical closed intervals.
|
|
122
151
|
query: {
|
|
123
152
|
projectId: data.projectId,
|
|
124
153
|
userId,
|
|
154
|
+
...(teamId && { teamId }),
|
|
155
|
+
endsAt: QueryHelper.isNull(),
|
|
125
156
|
},
|
|
126
157
|
limit: LIMIT_PER_PROJECT,
|
|
127
158
|
skip: 0,
|
|
@@ -152,13 +183,24 @@ export class Service extends DatabaseService<Model> {
|
|
|
152
183
|
} = data;
|
|
153
184
|
|
|
154
185
|
await this.updateBy({
|
|
186
|
+
// only close the still-open stint; leave prior closed stints intact.
|
|
155
187
|
query: {
|
|
156
188
|
projectId: data.projectId,
|
|
157
189
|
onCallDutyPolicyId,
|
|
158
190
|
onCallDutyPolicyEscalationRuleId: data.onCallDutyPolicyEscalationRuleId,
|
|
159
191
|
userId,
|
|
160
|
-
|
|
161
|
-
|
|
192
|
+
/*
|
|
193
|
+
* Constrain the context columns to IS NULL when not supplied so each
|
|
194
|
+
* context closes ONLY its own stint. The old `...(x && {x})` omitted the
|
|
195
|
+
* column, so ending a direct-user assignment (no teamId/scheduleId)
|
|
196
|
+
* matched — and closed — that user's still-open SCHEDULE and TEAM logs
|
|
197
|
+
* for the same rule too, leaving a permanent hole in the "who is on call
|
|
198
|
+
* now" view until the next roster change (audit M3).
|
|
199
|
+
*/
|
|
200
|
+
teamId: teamId ?? QueryHelper.isNull(),
|
|
201
|
+
onCallDutyPolicyScheduleId:
|
|
202
|
+
onCallDutyPolicyScheduleId ?? QueryHelper.isNull(),
|
|
203
|
+
endsAt: QueryHelper.isNull(),
|
|
162
204
|
},
|
|
163
205
|
limit: LIMIT_PER_PROJECT,
|
|
164
206
|
skip: 0,
|
|
@@ -4,7 +4,8 @@ import DatabaseConfig from "../DatabaseConfig";
|
|
|
4
4
|
import URL from "../../Types/API/URL";
|
|
5
5
|
import OnCallDutyPolicyUserOverride from "../../Models/DatabaseModels/OnCallDutyPolicyUserOverride";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
|
-
import
|
|
7
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
8
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
8
9
|
import OneUptimeDate from "../../Types/Date";
|
|
9
10
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
10
11
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
@@ -16,12 +17,50 @@ import OnCallDutyPolicyService from "./OnCallDutyPolicyService";
|
|
|
16
17
|
import Timezone from "../../Types/Timezone";
|
|
17
18
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
18
19
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
20
|
+
import logger from "../Utils/Logger";
|
|
19
21
|
|
|
20
22
|
export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
21
23
|
public constructor() {
|
|
22
24
|
super(OnCallDutyPolicyUserOverride);
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Re-resolve the persisted roster of every schedule in the project that
|
|
29
|
+
* contains the override user, so a created / updated / deleted override is
|
|
30
|
+
* reflected in the dashboard roster, handoff notifications and on-call time
|
|
31
|
+
* logs mid-period instead of only at the next natural handoff (audit F4).
|
|
32
|
+
* Best-effort: never throws into the CRUD path. Lazy require avoids a static
|
|
33
|
+
* circular import (OnCallDutyPolicyScheduleService imports this service).
|
|
34
|
+
*/
|
|
35
|
+
private async refreshRostersForOverrideUser(data: {
|
|
36
|
+
projectId: ObjectID | null | undefined;
|
|
37
|
+
overrideUserId: ObjectID | null | undefined;
|
|
38
|
+
}): Promise<void> {
|
|
39
|
+
if (!data.projectId || !data.overrideUserId) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const scheduleService: {
|
|
45
|
+
refreshRostersForUserInProject: (d: {
|
|
46
|
+
projectId: ObjectID;
|
|
47
|
+
userId: ObjectID;
|
|
48
|
+
}) => Promise<void>;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
50
|
+
} = require("./OnCallDutyPolicyScheduleService").default;
|
|
51
|
+
|
|
52
|
+
await scheduleService.refreshRostersForUserInProject({
|
|
53
|
+
projectId: data.projectId,
|
|
54
|
+
userId: data.overrideUserId,
|
|
55
|
+
});
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.error(
|
|
58
|
+
"Error refreshing rosters after a user override change (best-effort).",
|
|
59
|
+
);
|
|
60
|
+
logger.error(err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
25
64
|
@CaptureSpan()
|
|
26
65
|
protected override async onBeforeCreate(
|
|
27
66
|
createBy: CreateBy<OnCallDutyPolicyUserOverride>,
|
|
@@ -30,8 +69,14 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
|
30
69
|
throw new BadDataException("Start time and end time are required");
|
|
31
70
|
}
|
|
32
71
|
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
/*
|
|
73
|
+
* make sure start time is STRICTLY before end time. Using !isBefore (rather
|
|
74
|
+
* than isAfter) also rejects equal start/end and same-second reversed
|
|
75
|
+
* sub-second values, which the downstream event splitter would otherwise
|
|
76
|
+
* turn into a zero-length / inverted substitution segment and a spurious
|
|
77
|
+
* "you are next on-call" notification.
|
|
78
|
+
*/
|
|
79
|
+
if (!OneUptimeDate.isBefore(createBy.data.startsAt, createBy.data.endsAt)) {
|
|
35
80
|
throw new BadDataException("Start time must be before end time");
|
|
36
81
|
}
|
|
37
82
|
|
|
@@ -139,9 +184,164 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
|
139
184
|
});
|
|
140
185
|
}
|
|
141
186
|
|
|
187
|
+
// Reflect the new override in the persisted roster immediately (audit F4).
|
|
188
|
+
await this.refreshRostersForOverrideUser({
|
|
189
|
+
projectId,
|
|
190
|
+
overrideUserId,
|
|
191
|
+
});
|
|
192
|
+
|
|
142
193
|
return createdItem;
|
|
143
194
|
}
|
|
144
195
|
|
|
196
|
+
@CaptureSpan()
|
|
197
|
+
protected override async onBeforeUpdate(
|
|
198
|
+
updateBy: UpdateBy<OnCallDutyPolicyUserOverride>,
|
|
199
|
+
): Promise<OnUpdate<OnCallDutyPolicyUserOverride>> {
|
|
200
|
+
/*
|
|
201
|
+
* Capture each affected override's PRE-update project + overrideUserId so
|
|
202
|
+
* onUpdateSuccess can also refresh the OLD user's schedules. onUpdateSuccess
|
|
203
|
+
* otherwise only sees the post-update overrideUserId; when an edit changes
|
|
204
|
+
* overrideUserId from A to A2, schedules that contained A (but not A2) would
|
|
205
|
+
* keep showing the substitute forever — until the next natural handoff the
|
|
206
|
+
* cron happens to re-select — because refreshRostersForUserInProject selects
|
|
207
|
+
* schedules by the NEW user only (audit M2). Mirrors the onBeforeDelete
|
|
208
|
+
* carry-forward pattern used by the delete path.
|
|
209
|
+
*/
|
|
210
|
+
const previous: Array<{
|
|
211
|
+
projectId: ObjectID;
|
|
212
|
+
overrideUserId: ObjectID;
|
|
213
|
+
}> = [];
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const rows: Array<OnCallDutyPolicyUserOverride> = await this.findBy({
|
|
217
|
+
query: updateBy.query,
|
|
218
|
+
select: {
|
|
219
|
+
projectId: true,
|
|
220
|
+
overrideUserId: true,
|
|
221
|
+
},
|
|
222
|
+
props: {
|
|
223
|
+
isRoot: true,
|
|
224
|
+
},
|
|
225
|
+
skip: 0,
|
|
226
|
+
limit: LIMIT_PER_PROJECT,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
for (const row of rows) {
|
|
230
|
+
const projectId: ObjectID | undefined | null =
|
|
231
|
+
row.projectId || row.project?.id;
|
|
232
|
+
const overrideUserId: ObjectID | undefined | null =
|
|
233
|
+
row.overrideUserId || row.overrideUser?.id;
|
|
234
|
+
if (projectId && overrideUserId) {
|
|
235
|
+
previous.push({ projectId, overrideUserId });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
logger.error(err);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
updateBy,
|
|
244
|
+
carryForward: previous,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@CaptureSpan()
|
|
249
|
+
protected override async onUpdateSuccess(
|
|
250
|
+
onUpdate: OnUpdate<OnCallDutyPolicyUserOverride>,
|
|
251
|
+
updatedItemIds: Array<ObjectID>,
|
|
252
|
+
): Promise<OnUpdate<OnCallDutyPolicyUserOverride>> {
|
|
253
|
+
/*
|
|
254
|
+
* An override edit (times, route target, or the override user) must be
|
|
255
|
+
* reflected in the persisted roster (audit F4). Refresh the schedules of
|
|
256
|
+
* BOTH the NEW override user (post-update) and the OLD override user captured
|
|
257
|
+
* in onBeforeUpdate, so a change of overrideUserId also restores the roster
|
|
258
|
+
* of schedules that only contained the previous user (audit M2). Deduped so
|
|
259
|
+
* an unchanged overrideUserId refreshes each schedule set only once.
|
|
260
|
+
*/
|
|
261
|
+
const refreshed: Set<string> = new Set<string>();
|
|
262
|
+
|
|
263
|
+
const refreshPair: (
|
|
264
|
+
projectId: ObjectID | null | undefined,
|
|
265
|
+
overrideUserId: ObjectID | null | undefined,
|
|
266
|
+
) => Promise<void> = async (
|
|
267
|
+
projectId: ObjectID | null | undefined,
|
|
268
|
+
overrideUserId: ObjectID | null | undefined,
|
|
269
|
+
): Promise<void> => {
|
|
270
|
+
if (!projectId || !overrideUserId) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const key: string = `${projectId.toString()}:${overrideUserId.toString()}`;
|
|
274
|
+
if (refreshed.has(key)) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
refreshed.add(key);
|
|
278
|
+
await this.refreshRostersForOverrideUser({ projectId, overrideUserId });
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// OLD override users captured before the update.
|
|
282
|
+
const previous: Array<{ projectId: ObjectID; overrideUserId: ObjectID }> =
|
|
283
|
+
(onUpdate.carryForward as Array<{
|
|
284
|
+
projectId: ObjectID;
|
|
285
|
+
overrideUserId: ObjectID;
|
|
286
|
+
}>) || [];
|
|
287
|
+
|
|
288
|
+
for (const entry of previous) {
|
|
289
|
+
await refreshPair(entry.projectId, entry.overrideUserId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// NEW override users (post-update).
|
|
293
|
+
for (const id of updatedItemIds) {
|
|
294
|
+
const item: OnCallDutyPolicyUserOverride | null = await this.findOneById({
|
|
295
|
+
id: id,
|
|
296
|
+
select: {
|
|
297
|
+
projectId: true,
|
|
298
|
+
overrideUserId: true,
|
|
299
|
+
},
|
|
300
|
+
props: {
|
|
301
|
+
isRoot: true,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!item) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
await refreshPair(
|
|
310
|
+
item.projectId || item.project?.id,
|
|
311
|
+
item.overrideUserId || item.overrideUser?.id,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return onUpdate;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
@CaptureSpan()
|
|
319
|
+
protected override async onDeleteSuccess(
|
|
320
|
+
onDelete: OnDelete<OnCallDutyPolicyUserOverride>,
|
|
321
|
+
_itemIdsBeforeDelete: ObjectID[],
|
|
322
|
+
): Promise<OnDelete<OnCallDutyPolicyUserOverride>> {
|
|
323
|
+
/*
|
|
324
|
+
* After an override is removed, recompute the roster so the original user
|
|
325
|
+
* (or whoever the schedule now resolves to) is reflected immediately, rather
|
|
326
|
+
* than waiting for the next natural handoff (audit F4). The affected
|
|
327
|
+
* project/user pairs were captured in onBeforeDelete (the rows are gone now).
|
|
328
|
+
*/
|
|
329
|
+
const affected: Array<{ projectId: ObjectID; overrideUserId: ObjectID }> =
|
|
330
|
+
(onDelete.carryForward as Array<{
|
|
331
|
+
projectId: ObjectID;
|
|
332
|
+
overrideUserId: ObjectID;
|
|
333
|
+
}>) || [];
|
|
334
|
+
|
|
335
|
+
for (const entry of affected) {
|
|
336
|
+
await this.refreshRostersForOverrideUser({
|
|
337
|
+
projectId: entry.projectId,
|
|
338
|
+
overrideUserId: entry.overrideUserId,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return onDelete;
|
|
343
|
+
}
|
|
344
|
+
|
|
145
345
|
protected override async onBeforeDelete(
|
|
146
346
|
deleteBy: DeleteBy<OnCallDutyPolicyUserOverride>,
|
|
147
347
|
): Promise<OnDelete<OnCallDutyPolicyUserOverride>> {
|
|
@@ -163,6 +363,16 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
|
163
363
|
limit: LIMIT_PER_PROJECT,
|
|
164
364
|
});
|
|
165
365
|
|
|
366
|
+
/*
|
|
367
|
+
* Capture the project + override-user of each row being deleted so
|
|
368
|
+
* onDeleteSuccess can refresh those schedules' rosters AFTER the rows are
|
|
369
|
+
* gone (audit F4). Collected for global overrides too (no policy id).
|
|
370
|
+
*/
|
|
371
|
+
const affectedRosters: Array<{
|
|
372
|
+
projectId: ObjectID;
|
|
373
|
+
overrideUserId: ObjectID;
|
|
374
|
+
}> = [];
|
|
375
|
+
|
|
166
376
|
for (const item of itemsToDelete) {
|
|
167
377
|
const onCallDutyPolicyId: ObjectID | undefined | null =
|
|
168
378
|
item.onCallDutyPolicyId || item.onCallDutyPolicy?.id;
|
|
@@ -176,6 +386,10 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
|
176
386
|
const routeAlertsToUserId: ObjectID | undefined | null =
|
|
177
387
|
item.routeAlertsToUserId || item.routeAlertsToUser?.id;
|
|
178
388
|
|
|
389
|
+
if (projectId && overrideUserId) {
|
|
390
|
+
affectedRosters.push({ projectId, overrideUserId });
|
|
391
|
+
}
|
|
392
|
+
|
|
179
393
|
if (
|
|
180
394
|
onCallDutyPolicyId &&
|
|
181
395
|
projectId &&
|
|
@@ -233,7 +447,7 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
|
|
|
233
447
|
|
|
234
448
|
return {
|
|
235
449
|
deleteBy,
|
|
236
|
-
carryForward:
|
|
450
|
+
carryForward: affectedRosters,
|
|
237
451
|
};
|
|
238
452
|
}
|
|
239
453
|
|
|
@@ -5,10 +5,14 @@ import Model from "../../Models/DatabaseModels/ProjectCallSMSConfig";
|
|
|
5
5
|
import Phone from "../../Types/Phone";
|
|
6
6
|
import CreateBy from "../Types/Database/CreateBy";
|
|
7
7
|
import UpdateBy from "../Types/Database/UpdateBy";
|
|
8
|
-
import
|
|
8
|
+
import DeleteBy from "../Types/Database/DeleteBy";
|
|
9
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
9
10
|
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
10
11
|
import ObjectID from "../../Types/ObjectID";
|
|
11
12
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
13
|
+
import IncomingCallPolicyService from "./IncomingCallPolicyService";
|
|
14
|
+
import IncomingCallPolicy from "../../Models/DatabaseModels/IncomingCallPolicy";
|
|
15
|
+
import releaseIncomingCallPhoneNumber from "../Utils/IncomingCallPhoneNumber";
|
|
12
16
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
13
17
|
|
|
14
18
|
export class Service extends DatabaseService<Model> {
|
|
@@ -92,6 +96,83 @@ export class Service extends DatabaseService<Model> {
|
|
|
92
96
|
return { updateBy, carryForward: null };
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
@CaptureSpan()
|
|
100
|
+
protected override async onBeforeDelete(
|
|
101
|
+
deleteBy: DeleteBy<Model>,
|
|
102
|
+
): Promise<OnDelete<Model>> {
|
|
103
|
+
/*
|
|
104
|
+
* When a Call/SMS config is deleted, release any incoming-call numbers
|
|
105
|
+
* provisioned through it (so they don't keep billing on the provider) and
|
|
106
|
+
* clear the now-dangling number fields on those policies. The config still
|
|
107
|
+
* exists at this point, so the provider can be built to release the numbers.
|
|
108
|
+
*/
|
|
109
|
+
const configs: Array<Model> = await this.findBy({
|
|
110
|
+
query: deleteBy.query,
|
|
111
|
+
select: {
|
|
112
|
+
_id: true,
|
|
113
|
+
},
|
|
114
|
+
limit: LIMIT_MAX,
|
|
115
|
+
skip: 0,
|
|
116
|
+
props: {
|
|
117
|
+
isRoot: true,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
for (const config of configs) {
|
|
122
|
+
if (!config.id) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const policies: Array<IncomingCallPolicy> =
|
|
127
|
+
await IncomingCallPolicyService.findBy({
|
|
128
|
+
query: {
|
|
129
|
+
projectCallSMSConfigId: config.id,
|
|
130
|
+
},
|
|
131
|
+
select: {
|
|
132
|
+
_id: true,
|
|
133
|
+
callProviderPhoneNumberId: true,
|
|
134
|
+
projectCallSMSConfigId: true,
|
|
135
|
+
},
|
|
136
|
+
limit: LIMIT_MAX,
|
|
137
|
+
skip: 0,
|
|
138
|
+
props: {
|
|
139
|
+
isRoot: true,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
for (const policy of policies) {
|
|
144
|
+
if (
|
|
145
|
+
!policy.callProviderPhoneNumberId ||
|
|
146
|
+
!policy.projectCallSMSConfigId
|
|
147
|
+
) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await releaseIncomingCallPhoneNumber({
|
|
152
|
+
projectCallSMSConfigId: policy.projectCallSMSConfigId,
|
|
153
|
+
callProviderPhoneNumberId: policy.callProviderPhoneNumberId,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
await IncomingCallPolicyService.updateOneById({
|
|
157
|
+
id: policy.id!,
|
|
158
|
+
data: {
|
|
159
|
+
routingPhoneNumber: null,
|
|
160
|
+
callProviderPhoneNumberId: null,
|
|
161
|
+
phoneNumberCountryCode: null,
|
|
162
|
+
phoneNumberAreaCode: null,
|
|
163
|
+
phoneNumberPurchasedAt: null,
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
} as any,
|
|
166
|
+
props: {
|
|
167
|
+
isRoot: true,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { deleteBy, carryForward: null };
|
|
174
|
+
}
|
|
175
|
+
|
|
95
176
|
@CaptureSpan()
|
|
96
177
|
public async getProjectDefaultTwilioConfig(
|
|
97
178
|
projectId: ObjectID | undefined,
|
|
@@ -383,6 +383,12 @@ export class TeamMemberService extends DatabaseService<TeamMember> {
|
|
|
383
383
|
OnCallDutyPolicyTimeLogService.endTimeForUser({
|
|
384
384
|
projectId: member.projectId!,
|
|
385
385
|
userId: member.userId!,
|
|
386
|
+
/*
|
|
387
|
+
* scope to the team being left so the user's still-active logs from
|
|
388
|
+
* other teams, direct escalation assignments, and schedule rosters stay
|
|
389
|
+
* open (audit F17).
|
|
390
|
+
*/
|
|
391
|
+
teamId: member.teamId!,
|
|
386
392
|
endsAt: OneUptimeDate.getCurrentDate(),
|
|
387
393
|
}).catch((err: Error) => {
|
|
388
394
|
logger.error(err, {
|
|
@@ -24,7 +24,6 @@ import URL from "../../Types/API/URL";
|
|
|
24
24
|
import CallRequest from "../../Types/Call/CallRequest";
|
|
25
25
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
26
26
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
27
|
-
import OneUptimeDate from "../../Types/Date";
|
|
28
27
|
import Dictionary from "../../Types/Dictionary";
|
|
29
28
|
import Email from "../../Types/Email";
|
|
30
29
|
import EmailMessage from "../../Types/Email/EmailMessage";
|
|
@@ -109,52 +108,24 @@ export class Service extends DatabaseService<Model> {
|
|
|
109
108
|
onCallScheduleId?: ObjectID | undefined;
|
|
110
109
|
},
|
|
111
110
|
): Promise<void> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
111
|
+
/*
|
|
112
|
+
* Atomically claim this rule for this on-call log BEFORE sending, so two
|
|
113
|
+
* overlapping cron runs cannot both mark the rule un-executed and both
|
|
114
|
+
* notify — double-paging the responder for one escalation (audit F7). The
|
|
115
|
+
* previous read-check-then-blind-save was a non-atomic TOCTOU. If the claim
|
|
116
|
+
* was already taken (or the log is gone), skip.
|
|
117
|
+
*/
|
|
118
|
+
const claimedRuleExecution: boolean =
|
|
119
|
+
await UserOnCallLogService.claimNotificationRuleExecution({
|
|
120
|
+
userOnCallLogId: options.userNotificationLogId,
|
|
121
|
+
userNotificationRuleId: userNotificationRuleId,
|
|
124
122
|
});
|
|
125
123
|
|
|
126
|
-
if (!
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
Object.keys(userOnCallLog.executedNotificationRules || {}).includes(
|
|
132
|
-
userNotificationRuleId.toString(),
|
|
133
|
-
)
|
|
134
|
-
) {
|
|
135
|
-
// already executed.
|
|
124
|
+
if (!claimedRuleExecution) {
|
|
125
|
+
// already executed by this or a concurrent run.
|
|
136
126
|
return;
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
if (!userOnCallLog.executedNotificationRules) {
|
|
140
|
-
userOnCallLog.executedNotificationRules = {};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
userOnCallLog.executedNotificationRules[userNotificationRuleId.toString()] =
|
|
144
|
-
OneUptimeDate.getCurrentDate();
|
|
145
|
-
|
|
146
|
-
await UserOnCallLogService.updateOneById({
|
|
147
|
-
id: userOnCallLog.id!,
|
|
148
|
-
data: {
|
|
149
|
-
executedNotificationRules: {
|
|
150
|
-
...userOnCallLog.executedNotificationRules,
|
|
151
|
-
},
|
|
152
|
-
} as any,
|
|
153
|
-
props: {
|
|
154
|
-
isRoot: true,
|
|
155
|
-
},
|
|
156
|
-
});
|
|
157
|
-
|
|
158
129
|
// find notification rule item.
|
|
159
130
|
const notificationRuleItem: Model | null = await this.findOneById({
|
|
160
131
|
id: userNotificationRuleId!,
|
|
@@ -23,6 +23,9 @@ import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
|
|
|
23
23
|
import AlertEpisodeService from "./AlertEpisodeService";
|
|
24
24
|
import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
|
|
25
25
|
import IncidentEpisodeService from "./IncidentEpisodeService";
|
|
26
|
+
import OneUptimeDate from "../../Types/Date";
|
|
27
|
+
import { JSONObject } from "../../Types/JSON";
|
|
28
|
+
import logger from "../Utils/Logger";
|
|
26
29
|
|
|
27
30
|
export class Service extends DatabaseService<Model> {
|
|
28
31
|
public constructor() {
|
|
@@ -32,6 +35,93 @@ export class Service extends DatabaseService<Model> {
|
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Atomically claim the right to execute ONE notification rule for this on-call
|
|
40
|
+
* log. Overlapping UserOnCallLog:ExecutePendingExecutions runs (a tick that
|
|
41
|
+
* exceeds the 1-minute cadence, a stalled-job re-delivery, or a burst release
|
|
42
|
+
* across worker replicas) could both read executedNotificationRules without a
|
|
43
|
+
* given delayed rule's key, both write it, and both send — double-paging the
|
|
44
|
+
* responder for a single escalation (audit F7). This mirrors
|
|
45
|
+
* OnCallDutyPolicyExecutionLogService.claimEscalationAdvance: a single
|
|
46
|
+
* conditional UPDATE that sets executedNotificationRules[ruleId] only when the
|
|
47
|
+
* key is absent and RETURNs the affected row, so exactly one concurrent run
|
|
48
|
+
* wins and the loser skips.
|
|
49
|
+
*
|
|
50
|
+
* Fail-safe: if the atomic statement errors for any reason, fall back to the
|
|
51
|
+
* previous non-atomic read-modify-write so the rule is still marked and sent.
|
|
52
|
+
* For an on-call system a rare duplicate page (the pre-fix behavior) is far
|
|
53
|
+
* better than a missed page, so this never makes paging less reliable.
|
|
54
|
+
*
|
|
55
|
+
* Returns true when THIS call claimed the rule (caller should send), false when
|
|
56
|
+
* it was already executed/claimed (caller should skip) or the log is missing.
|
|
57
|
+
*/
|
|
58
|
+
@CaptureSpan()
|
|
59
|
+
public async claimNotificationRuleExecution(data: {
|
|
60
|
+
userOnCallLogId: ObjectID;
|
|
61
|
+
userNotificationRuleId: ObjectID;
|
|
62
|
+
}): Promise<boolean> {
|
|
63
|
+
const ruleKey: string = data.userNotificationRuleId.toString();
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const nowIso: string = OneUptimeDate.getCurrentDate().toISOString();
|
|
67
|
+
|
|
68
|
+
const rows: Array<{ _id: string }> =
|
|
69
|
+
await this.getRepository().manager.query(
|
|
70
|
+
`UPDATE "UserOnCallLog"
|
|
71
|
+
SET "executedNotificationRules" = jsonb_set(
|
|
72
|
+
COALESCE("executedNotificationRules", '{}')::jsonb,
|
|
73
|
+
ARRAY[$2::text],
|
|
74
|
+
to_jsonb($3::text),
|
|
75
|
+
true
|
|
76
|
+
)::json
|
|
77
|
+
WHERE "_id" = $1::uuid
|
|
78
|
+
AND NOT (COALESCE("executedNotificationRules", '{}')::jsonb ? $2::text)
|
|
79
|
+
RETURNING "_id"`,
|
|
80
|
+
[data.userOnCallLogId.toString(), ruleKey, nowIso],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
return Array.isArray(rows) && rows.length > 0;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
logger.error(
|
|
86
|
+
"claimNotificationRuleExecution atomic claim failed; falling back to non-atomic marking.",
|
|
87
|
+
);
|
|
88
|
+
logger.error(err);
|
|
89
|
+
|
|
90
|
+
// Fallback: previous non-atomic read-modify-write (reliable marking).
|
|
91
|
+
const log: Model | null = await this.findOneById({
|
|
92
|
+
id: data.userOnCallLogId,
|
|
93
|
+
props: { isRoot: true },
|
|
94
|
+
select: {
|
|
95
|
+
_id: true,
|
|
96
|
+
executedNotificationRules: true,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!log) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const executed: JSONObject = log.executedNotificationRules || {};
|
|
105
|
+
|
|
106
|
+
if (Object.keys(executed).includes(ruleKey)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
executed[ruleKey] = OneUptimeDate.getCurrentDate();
|
|
111
|
+
|
|
112
|
+
await this.updateOneById({
|
|
113
|
+
id: log.id!,
|
|
114
|
+
data: {
|
|
115
|
+
executedNotificationRules: { ...executed },
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
} as any,
|
|
118
|
+
props: { isRoot: true },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
35
125
|
@CaptureSpan()
|
|
36
126
|
protected override async onBeforeCreate(
|
|
37
127
|
createBy: CreateBy<Model>,
|