@oneuptime/common 12.0.29 → 12.0.30
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/Index.ts +11 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/package.json +1 -1
|
@@ -23,6 +23,15 @@ export interface LayerProps {
|
|
|
23
23
|
* time exactly as before — fully backward compatible.
|
|
24
24
|
*/
|
|
25
25
|
timezone?: string | undefined;
|
|
26
|
+
/*
|
|
27
|
+
* Identity of the layer these props were built from. Optional and purely
|
|
28
|
+
* informational: the engine never reads them, it only stamps them onto every
|
|
29
|
+
* event it emits for this layer (see PriorityCalendarEvents) so consumers
|
|
30
|
+
* that render or group by layer — the on-call calendar feed — can tell which
|
|
31
|
+
* layer a merged segment came from without re-deriving it from priority.
|
|
32
|
+
*/
|
|
33
|
+
layerId?: string;
|
|
34
|
+
layerName?: string;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export interface EventProps extends LayerProps {
|
|
@@ -36,8 +45,61 @@ export interface MultiLayerProps {
|
|
|
36
45
|
calendarEndDate: Date;
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/*
|
|
49
|
+
* Key stamped on every event getEvents emits, holding the epoch milliseconds
|
|
50
|
+
* of the TRUE (un-clamped) start of the rotation period the event belongs to.
|
|
51
|
+
*
|
|
52
|
+
* Two consecutive rotation periods held by the SAME user (a single-user layer,
|
|
53
|
+
* or any rotation where one person holds two turns in a row) are otherwise
|
|
54
|
+
* indistinguishable once expanded: the engine's 1 s seam makes them contiguous
|
|
55
|
+
* and every other property matches, so a consumer that groups contiguous
|
|
56
|
+
* same-user segments folds them into one block whose start and end depend on
|
|
57
|
+
* how far the expansion window reached — an unstable identity for anything
|
|
58
|
+
* that must be the same object across two different windows (the calendar
|
|
59
|
+
* feed's UID/DTSTART). The period start is window-independent, so grouping on
|
|
60
|
+
* it keeps one shift per rotation turn.
|
|
61
|
+
*
|
|
62
|
+
* Read it through ScheduleShiftUtil.getEventRotationPeriodStart, which
|
|
63
|
+
* tolerates events that never carried it (hand-built fixtures, JSON round
|
|
64
|
+
* trips) rather than reaching into the index signature here.
|
|
65
|
+
*/
|
|
66
|
+
export const ROTATION_PERIOD_START_KEY: string = "rotationPeriodStartsAt";
|
|
67
|
+
|
|
39
68
|
export interface PriorityCalendarEvents extends CalendarEvent {
|
|
40
69
|
priority: number;
|
|
70
|
+
/*
|
|
71
|
+
* Copied from LayerProps.layerId / layerName when the caller supplied them.
|
|
72
|
+
* Absent (not undefined) otherwise, so callers that never set them see
|
|
73
|
+
* exactly the events they always did.
|
|
74
|
+
*/
|
|
75
|
+
layerId?: string;
|
|
76
|
+
layerName?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface LayerExpansionOptions {
|
|
80
|
+
getNumberOfEvents?: number;
|
|
81
|
+
/*
|
|
82
|
+
* Upper bound on the number of rotation periods the engine will step
|
|
83
|
+
* through for ONE layer, both while locating the rotation position at the
|
|
84
|
+
* window start (the per-period simulation restricted layers need) and while
|
|
85
|
+
* expanding periods inside the window. Defaults to the engine's own caps
|
|
86
|
+
* (5,000,000 / 1,000,000), so omitting it changes nothing. When the bound is
|
|
87
|
+
* hit the expansion stops and the result is flagged `truncated`: a
|
|
88
|
+
* pre-window hit contributes NO events for that layer (its rotation
|
|
89
|
+
* position is unknown, and emitting the wrong person is worse than emitting
|
|
90
|
+
* nobody), an in-window hit keeps the events produced so far.
|
|
91
|
+
*/
|
|
92
|
+
maxSimulationIterations?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface LayerEventsResult {
|
|
96
|
+
events: Array<CalendarEvent>;
|
|
97
|
+
/*
|
|
98
|
+
* True when any expansion stopped at maxSimulationIterations (or the
|
|
99
|
+
* engine's own ceiling) before reaching the end of the window, i.e. the
|
|
100
|
+
* events may be incomplete.
|
|
101
|
+
*/
|
|
102
|
+
truncated: boolean;
|
|
41
103
|
}
|
|
42
104
|
|
|
43
105
|
export default class LayerUtil {
|
|
@@ -52,16 +114,24 @@ export default class LayerUtil {
|
|
|
52
114
|
|
|
53
115
|
public getEvents(
|
|
54
116
|
data: EventProps,
|
|
55
|
-
options?:
|
|
56
|
-
| {
|
|
57
|
-
getNumberOfEvents?: number;
|
|
58
|
-
}
|
|
59
|
-
| undefined,
|
|
117
|
+
options?: LayerExpansionOptions | undefined,
|
|
60
118
|
): Array<CalendarEvent> {
|
|
119
|
+
return this.getEventsWithMeta(data, options).events;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/*
|
|
123
|
+
* Same expansion as getEvents, plus a `truncated` flag telling the caller
|
|
124
|
+
* whether an iteration cap cut the expansion short. getEvents discards the
|
|
125
|
+
* flag so its existing callers are untouched.
|
|
126
|
+
*/
|
|
127
|
+
public getEventsWithMeta(
|
|
128
|
+
data: EventProps,
|
|
129
|
+
options?: LayerExpansionOptions | undefined,
|
|
130
|
+
): LayerEventsResult {
|
|
61
131
|
let events: Array<CalendarEvent> = [];
|
|
62
132
|
|
|
63
133
|
if (!this.isDataValid(data)) {
|
|
64
|
-
return [];
|
|
134
|
+
return { events: [], truncated: false };
|
|
65
135
|
}
|
|
66
136
|
|
|
67
137
|
data = this.sanitizeData(data);
|
|
@@ -85,9 +155,16 @@ export default class LayerUtil {
|
|
|
85
155
|
let handOffTime: Date = data.handOffTime;
|
|
86
156
|
|
|
87
157
|
if (!handOffTime) {
|
|
88
|
-
return [];
|
|
158
|
+
return { events: [], truncated: false };
|
|
89
159
|
}
|
|
90
160
|
|
|
161
|
+
const maxSimulationIterations: number | undefined =
|
|
162
|
+
options?.maxSimulationIterations !== undefined &&
|
|
163
|
+
Number.isFinite(options.maxSimulationIterations) &&
|
|
164
|
+
options.maxSimulationIterations > 0
|
|
165
|
+
? Math.floor(options.maxSimulationIterations)
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
91
168
|
// Looop vars
|
|
92
169
|
let currentUserIndex: number = 0;
|
|
93
170
|
let currentEventStartTime: Date = start;
|
|
@@ -99,6 +176,7 @@ export default class LayerUtil {
|
|
|
99
176
|
const currentUserResolution: {
|
|
100
177
|
currentUserIndex: number;
|
|
101
178
|
currentPeriodStart: Date;
|
|
179
|
+
truncated: boolean;
|
|
102
180
|
} = this.getCurrentUserIndexBasedOnHandoffTime({
|
|
103
181
|
rotation,
|
|
104
182
|
handOffTime,
|
|
@@ -107,7 +185,18 @@ export default class LayerUtil {
|
|
|
107
185
|
users: data.users,
|
|
108
186
|
currentEventStartTime,
|
|
109
187
|
restrictionTimes: data.restrictionTimes,
|
|
188
|
+
maxSimulationIterations,
|
|
110
189
|
});
|
|
190
|
+
|
|
191
|
+
/*
|
|
192
|
+
* The pre-window simulation gave up before reaching the window start, so
|
|
193
|
+
* the rotation position is unknown. Emit nothing for this layer rather
|
|
194
|
+
* than shifts attributed to the wrong person, and tell the caller.
|
|
195
|
+
*/
|
|
196
|
+
if (currentUserResolution.truncated) {
|
|
197
|
+
return { events: [], truncated: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
111
200
|
currentUserIndex = currentUserResolution.currentUserIndex;
|
|
112
201
|
|
|
113
202
|
/*
|
|
@@ -146,10 +235,11 @@ export default class LayerUtil {
|
|
|
146
235
|
trimmedStartAndEndTimes,
|
|
147
236
|
data.users,
|
|
148
237
|
currentUserIndex,
|
|
238
|
+
firstPeriodTrueStart,
|
|
149
239
|
),
|
|
150
240
|
];
|
|
151
241
|
|
|
152
|
-
return events;
|
|
242
|
+
return { events, truncated: false };
|
|
153
243
|
}
|
|
154
244
|
|
|
155
245
|
/*
|
|
@@ -175,10 +265,12 @@ export default class LayerUtil {
|
|
|
175
265
|
rotation.intervalType,
|
|
176
266
|
);
|
|
177
267
|
const maxLoopCount: number = Math.min(
|
|
268
|
+
maxSimulationIterations ?? 1000000,
|
|
178
269
|
1000000,
|
|
179
270
|
Math.max(100, Math.ceil(windowUnits / periodUnitsForBound) + 10),
|
|
180
271
|
);
|
|
181
272
|
let loopCount: number = 0;
|
|
273
|
+
let truncated: boolean = false;
|
|
182
274
|
|
|
183
275
|
/*
|
|
184
276
|
* The first loop iteration expands the rotation period that CONTAINS the
|
|
@@ -191,6 +283,7 @@ export default class LayerUtil {
|
|
|
191
283
|
while (!hasReachedTheEndOfTheCalendar) {
|
|
192
284
|
loopCount++;
|
|
193
285
|
if (loopCount > maxLoopCount) {
|
|
286
|
+
truncated = true;
|
|
194
287
|
break;
|
|
195
288
|
}
|
|
196
289
|
currentEventEndTime = handOffTime;
|
|
@@ -242,12 +335,18 @@ export default class LayerUtil {
|
|
|
242
335
|
trimmedStartAndEndTimes,
|
|
243
336
|
data.users,
|
|
244
337
|
currentUserIndex,
|
|
338
|
+
/*
|
|
339
|
+
* The first iteration expands the period that CONTAINS the window
|
|
340
|
+
* start, whose currentEventStartTime may be clamped to it; stamp the
|
|
341
|
+
* period's TRUE start so the identity does not move with the window.
|
|
342
|
+
*/
|
|
343
|
+
isFirstPeriod ? firstPeriodTrueStart : currentEventStartTime,
|
|
245
344
|
),
|
|
246
345
|
);
|
|
247
346
|
|
|
248
347
|
if (options?.getNumberOfEvents !== undefined) {
|
|
249
348
|
if (events.length >= options.getNumberOfEvents) {
|
|
250
|
-
return events;
|
|
349
|
+
return { events, truncated: false };
|
|
251
350
|
}
|
|
252
351
|
}
|
|
253
352
|
|
|
@@ -328,7 +427,7 @@ export default class LayerUtil {
|
|
|
328
427
|
}
|
|
329
428
|
}
|
|
330
429
|
|
|
331
|
-
return events;
|
|
430
|
+
return { events, truncated };
|
|
332
431
|
}
|
|
333
432
|
|
|
334
433
|
private sanitizeData(data: EventProps): EventProps {
|
|
@@ -524,7 +623,12 @@ export default class LayerUtil {
|
|
|
524
623
|
users: Array<UserModel>;
|
|
525
624
|
currentEventStartTime: Date;
|
|
526
625
|
restrictionTimes: RestrictionTimes;
|
|
527
|
-
|
|
626
|
+
maxSimulationIterations?: number | undefined;
|
|
627
|
+
}): {
|
|
628
|
+
currentUserIndex: number;
|
|
629
|
+
currentPeriodStart: Date;
|
|
630
|
+
truncated: boolean;
|
|
631
|
+
} {
|
|
528
632
|
/*
|
|
529
633
|
* Returns both the on-call user index for the rotation period that CONTAINS
|
|
530
634
|
* currentEventStartTime AND the true (un-clamped) start of that period.
|
|
@@ -546,6 +650,7 @@ export default class LayerUtil {
|
|
|
546
650
|
return {
|
|
547
651
|
currentUserIndex,
|
|
548
652
|
currentPeriodStart: data.currentEventStartTime,
|
|
653
|
+
truncated: false,
|
|
549
654
|
};
|
|
550
655
|
}
|
|
551
656
|
|
|
@@ -558,6 +663,7 @@ export default class LayerUtil {
|
|
|
558
663
|
return {
|
|
559
664
|
currentUserIndex,
|
|
560
665
|
currentPeriodStart: data.startDateTimeOfLayer,
|
|
666
|
+
truncated: false,
|
|
561
667
|
};
|
|
562
668
|
}
|
|
563
669
|
|
|
@@ -599,6 +705,7 @@ export default class LayerUtil {
|
|
|
599
705
|
currentUserIndex:
|
|
600
706
|
(((currentUserIndex + periodsElapsed) % length) + length) % length,
|
|
601
707
|
currentPeriodStart: data.currentEventStartTime,
|
|
708
|
+
truncated: false,
|
|
602
709
|
};
|
|
603
710
|
}
|
|
604
711
|
|
|
@@ -639,15 +746,23 @@ export default class LayerUtil {
|
|
|
639
746
|
data.rotation.intervalType,
|
|
640
747
|
);
|
|
641
748
|
const maxIterations: number = Math.min(
|
|
749
|
+
data.maxSimulationIterations ?? 5000000,
|
|
642
750
|
5000000,
|
|
643
751
|
Math.max(10000, Math.ceil(simUnitsBetween / simPeriodUnits) + 10),
|
|
644
752
|
);
|
|
645
753
|
let iterations: number = 0;
|
|
754
|
+
let truncated: boolean = false;
|
|
646
755
|
|
|
647
|
-
while (
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
756
|
+
while (OneUptimeDate.isBefore(simulatedTime, data.currentEventStartTime)) {
|
|
757
|
+
/*
|
|
758
|
+
* Cap hit before the target was reached: the rotation position for the
|
|
759
|
+
* window is unknown. Report it instead of silently returning the index
|
|
760
|
+
* at the cap (audit F9 explains why that is the wrong on-call person).
|
|
761
|
+
*/
|
|
762
|
+
if (iterations >= maxIterations) {
|
|
763
|
+
truncated = true;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
651
766
|
iterations++;
|
|
652
767
|
|
|
653
768
|
const eventEnd: Date = simulatedHandOff;
|
|
@@ -685,7 +800,7 @@ export default class LayerUtil {
|
|
|
685
800
|
* next period start each covered iteration and breaks once a period would
|
|
686
801
|
* extend past the target, so it holds the current period's real start.
|
|
687
802
|
*/
|
|
688
|
-
return { currentUserIndex, currentPeriodStart: simulatedTime };
|
|
803
|
+
return { currentUserIndex, currentPeriodStart: simulatedTime, truncated };
|
|
689
804
|
}
|
|
690
805
|
|
|
691
806
|
/*
|
|
@@ -1432,6 +1547,8 @@ export default class LayerUtil {
|
|
|
1432
1547
|
trimmedStartAndEndTimes: Array<StartAndEndTime>,
|
|
1433
1548
|
users: Array<UserModel>,
|
|
1434
1549
|
currentUserIndex: number,
|
|
1550
|
+
// True (un-clamped) start of the rotation period these segments came from.
|
|
1551
|
+
rotationPeriodStartsAt: Date,
|
|
1435
1552
|
): Array<CalendarEvent> {
|
|
1436
1553
|
const events: Array<CalendarEvent> = [];
|
|
1437
1554
|
|
|
@@ -1446,6 +1563,10 @@ export default class LayerUtil {
|
|
|
1446
1563
|
end: trimmedStartAndEndTime.endTime,
|
|
1447
1564
|
};
|
|
1448
1565
|
|
|
1566
|
+
// See ROTATION_PERIOD_START_KEY.
|
|
1567
|
+
(event as unknown as Record<string, unknown>)[ROTATION_PERIOD_START_KEY] =
|
|
1568
|
+
rotationPeriodStartsAt.getTime();
|
|
1569
|
+
|
|
1449
1570
|
events.push(event);
|
|
1450
1571
|
}
|
|
1451
1572
|
|
|
@@ -1454,14 +1575,29 @@ export default class LayerUtil {
|
|
|
1454
1575
|
|
|
1455
1576
|
public getMultiLayerEvents(
|
|
1456
1577
|
data: MultiLayerProps,
|
|
1457
|
-
options?:
|
|
1458
|
-
| {
|
|
1459
|
-
getNumberOfEvents?: number;
|
|
1460
|
-
}
|
|
1461
|
-
| undefined,
|
|
1578
|
+
options?: LayerExpansionOptions | undefined,
|
|
1462
1579
|
): Array<CalendarEvent> {
|
|
1580
|
+
return this.getMultiLayerEventsWithMeta(data, options).events;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/*
|
|
1584
|
+
* Same priority merge as getMultiLayerEvents, plus a `truncated` flag that
|
|
1585
|
+
* is true when ANY layer's expansion hit an iteration cap (see
|
|
1586
|
+
* LayerExpansionOptions.maxSimulationIterations). getMultiLayerEvents
|
|
1587
|
+
* discards the flag so its existing callers are untouched.
|
|
1588
|
+
*/
|
|
1589
|
+
public getMultiLayerEventsWithMeta(
|
|
1590
|
+
data: MultiLayerProps,
|
|
1591
|
+
options?: LayerExpansionOptions | undefined,
|
|
1592
|
+
): LayerEventsResult {
|
|
1463
1593
|
const events: Array<PriorityCalendarEvents> = [];
|
|
1464
1594
|
let layerPriority: number = 1;
|
|
1595
|
+
let truncated: boolean = false;
|
|
1596
|
+
|
|
1597
|
+
const layerOptions: LayerExpansionOptions | undefined =
|
|
1598
|
+
options?.maxSimulationIterations !== undefined
|
|
1599
|
+
? { maxSimulationIterations: options.maxSimulationIterations }
|
|
1600
|
+
: undefined;
|
|
1465
1601
|
|
|
1466
1602
|
for (const layer of data.layers) {
|
|
1467
1603
|
/*
|
|
@@ -1473,25 +1609,41 @@ export default class LayerUtil {
|
|
|
1473
1609
|
* is never generated, corrupting the merged "next" roster. The cap is
|
|
1474
1610
|
* applied only once, after the merge, below.
|
|
1475
1611
|
*/
|
|
1476
|
-
const
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1612
|
+
const layerResult: LayerEventsResult = this.getEventsWithMeta(
|
|
1613
|
+
{
|
|
1614
|
+
users: layer.users,
|
|
1615
|
+
startDateTimeOfLayer: layer.startDateTimeOfLayer,
|
|
1616
|
+
restrictionTimes: layer.restrictionTimes,
|
|
1617
|
+
handOffTime: layer.handOffTime,
|
|
1618
|
+
rotation: layer.rotation,
|
|
1619
|
+
timezone: layer.timezone,
|
|
1620
|
+
calendarStartDate: data.calendarStartDate,
|
|
1621
|
+
calendarEndDate: data.calendarEndDate,
|
|
1622
|
+
},
|
|
1623
|
+
layerOptions,
|
|
1624
|
+
);
|
|
1486
1625
|
|
|
1487
|
-
|
|
1626
|
+
truncated = truncated || layerResult.truncated;
|
|
1488
1627
|
|
|
1489
|
-
|
|
1628
|
+
/*
|
|
1629
|
+
* Add priority (and, when the caller identified the layer, its id and
|
|
1630
|
+
* name) to each event. The keys are only added when present so callers
|
|
1631
|
+
* that never set them get exactly the event shape they always did.
|
|
1632
|
+
*/
|
|
1633
|
+
for (const layerEvent of layerResult.events) {
|
|
1490
1634
|
const priorityEvent: PriorityCalendarEvents = {
|
|
1491
1635
|
...layerEvent,
|
|
1492
1636
|
priority: layerPriority,
|
|
1493
1637
|
};
|
|
1494
1638
|
|
|
1639
|
+
if (layer.layerId !== undefined) {
|
|
1640
|
+
priorityEvent.layerId = layer.layerId;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (layer.layerName !== undefined) {
|
|
1644
|
+
priorityEvent.layerName = layer.layerName;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1495
1647
|
events.push(priorityEvent);
|
|
1496
1648
|
}
|
|
1497
1649
|
|
|
@@ -1506,11 +1658,14 @@ export default class LayerUtil {
|
|
|
1506
1658
|
|
|
1507
1659
|
if (options?.getNumberOfEvents !== undefined) {
|
|
1508
1660
|
if (nonOverlappingEvents.length > options.getNumberOfEvents) {
|
|
1509
|
-
return
|
|
1661
|
+
return {
|
|
1662
|
+
events: nonOverlappingEvents.slice(0, options.getNumberOfEvents),
|
|
1663
|
+
truncated,
|
|
1664
|
+
};
|
|
1510
1665
|
}
|
|
1511
1666
|
}
|
|
1512
1667
|
|
|
1513
|
-
return nonOverlappingEvents;
|
|
1668
|
+
return { events: nonOverlappingEvents, truncated };
|
|
1514
1669
|
}
|
|
1515
1670
|
|
|
1516
1671
|
public removeOverlappingEvents(
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import OneUptimeDate from "../Date";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* A fully resolved on-call shift: one person, one schedule, one contiguous
|
|
5
|
+
* block of time, with everything a calendar entry or a reminder needs already
|
|
6
|
+
* looked up (names, policies, override provenance). Produced by the server's
|
|
7
|
+
* OnCallShiftMaterializer from LayerUtil -> overrides -> groupEventsIntoShifts
|
|
8
|
+
* -> seam normalisation, cached per schedule, and consumed by the calendar
|
|
9
|
+
* feed mapper (OnCallCalendarFeedUtil), the /my-shifts API and the shift
|
|
10
|
+
* reminder runner.
|
|
11
|
+
*
|
|
12
|
+
* Identity is (schedule, seam-normalised start) — NOT the user — so an
|
|
13
|
+
* override that swaps who is on call updates the same calendar event in place
|
|
14
|
+
* instead of deleting one and creating another. Policy-variant shifts (an
|
|
15
|
+
* extra shift that exists only for one escalation policy because of a
|
|
16
|
+
* policy-scoped override) add the policy id to that identity.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface MaterializedShiftOverride {
|
|
20
|
+
originalUserId: string;
|
|
21
|
+
originalUserName: string;
|
|
22
|
+
overrideStartsAt: Date;
|
|
23
|
+
overrideEndsAt: Date;
|
|
24
|
+
// Absent for a global override; set when the override is policy-scoped.
|
|
25
|
+
onCallDutyPolicyId?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MaterializedShiftPolicyVariant {
|
|
29
|
+
policyId: string;
|
|
30
|
+
policyName: string;
|
|
31
|
+
// Who the GLOBAL resolution puts on call for the same interval.
|
|
32
|
+
globalUserId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MaterializedShiftPolicy {
|
|
36
|
+
policyId: string;
|
|
37
|
+
policyName: string;
|
|
38
|
+
ruleId: string;
|
|
39
|
+
ruleName: string;
|
|
40
|
+
ruleOrder: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MaterializedShift {
|
|
44
|
+
// `${scheduleId}:${normalizedStartEpochSeconds}` (+ `:${policyId}` for policy variants).
|
|
45
|
+
shiftKey: string;
|
|
46
|
+
// sha256 of {end, userId, summary, description, transparency, policies, override, layer}.
|
|
47
|
+
contentHash: string;
|
|
48
|
+
projectId: string;
|
|
49
|
+
projectName?: string;
|
|
50
|
+
scheduleId: string;
|
|
51
|
+
scheduleName: string;
|
|
52
|
+
// IANA zone the schedule is authored in; absent for legacy schedules.
|
|
53
|
+
scheduleTimezone?: string;
|
|
54
|
+
userId: string;
|
|
55
|
+
userName: string;
|
|
56
|
+
// Seam-normalised (see ShiftSeamUtil); DTEND-style exclusive end.
|
|
57
|
+
start: Date;
|
|
58
|
+
end: Date;
|
|
59
|
+
coverageSeconds: number;
|
|
60
|
+
layerId?: string;
|
|
61
|
+
layerName?: string;
|
|
62
|
+
override?: MaterializedShiftOverride;
|
|
63
|
+
// Set only on the extra, policy-specific shifts.
|
|
64
|
+
policyVariantOf?: MaterializedShiftPolicyVariant;
|
|
65
|
+
policies: Array<MaterializedShiftPolicy>;
|
|
66
|
+
// start < now at materialisation time: "reflects current configuration".
|
|
67
|
+
isPast: boolean;
|
|
68
|
+
lastModifiedAt: Date;
|
|
69
|
+
shiftConfigVersion: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* Wire shape (dates as ISO strings, optional strings as null) used by the
|
|
74
|
+
* /my-shifts endpoint and the schedule-level cache.
|
|
75
|
+
*/
|
|
76
|
+
export interface MaterializedShiftOverrideJson {
|
|
77
|
+
originalUserId: string;
|
|
78
|
+
originalUserName: string;
|
|
79
|
+
overrideStartsAt: string;
|
|
80
|
+
overrideEndsAt: string;
|
|
81
|
+
onCallDutyPolicyId?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface MaterializedShiftJson {
|
|
85
|
+
shiftKey: string;
|
|
86
|
+
contentHash: string;
|
|
87
|
+
projectId: string;
|
|
88
|
+
projectName?: string;
|
|
89
|
+
scheduleId: string;
|
|
90
|
+
scheduleName: string;
|
|
91
|
+
scheduleTimezone: string | null;
|
|
92
|
+
userId: string;
|
|
93
|
+
userName: string;
|
|
94
|
+
start: string;
|
|
95
|
+
end: string;
|
|
96
|
+
coverageSeconds: number;
|
|
97
|
+
layerId?: string;
|
|
98
|
+
layerName?: string;
|
|
99
|
+
override?: MaterializedShiftOverrideJson;
|
|
100
|
+
policyVariantOf?: MaterializedShiftPolicyVariant;
|
|
101
|
+
policies: Array<MaterializedShiftPolicy>;
|
|
102
|
+
isPast: boolean;
|
|
103
|
+
lastModifiedAt: string;
|
|
104
|
+
shiftConfigVersion: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default class MaterializedShiftUtil {
|
|
108
|
+
public static toJSON(shift: MaterializedShift): MaterializedShiftJson {
|
|
109
|
+
const json: MaterializedShiftJson = {
|
|
110
|
+
shiftKey: shift.shiftKey,
|
|
111
|
+
contentHash: shift.contentHash,
|
|
112
|
+
projectId: shift.projectId,
|
|
113
|
+
scheduleId: shift.scheduleId,
|
|
114
|
+
scheduleName: shift.scheduleName,
|
|
115
|
+
scheduleTimezone: shift.scheduleTimezone ?? null,
|
|
116
|
+
userId: shift.userId,
|
|
117
|
+
userName: shift.userName,
|
|
118
|
+
start: shift.start.toISOString(),
|
|
119
|
+
end: shift.end.toISOString(),
|
|
120
|
+
coverageSeconds: shift.coverageSeconds,
|
|
121
|
+
policies: shift.policies.map((policy: MaterializedShiftPolicy) => {
|
|
122
|
+
return { ...policy };
|
|
123
|
+
}),
|
|
124
|
+
isPast: shift.isPast,
|
|
125
|
+
lastModifiedAt: shift.lastModifiedAt.toISOString(),
|
|
126
|
+
shiftConfigVersion: shift.shiftConfigVersion,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (shift.projectName !== undefined) {
|
|
130
|
+
json.projectName = shift.projectName;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (shift.layerId !== undefined) {
|
|
134
|
+
json.layerId = shift.layerId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (shift.layerName !== undefined) {
|
|
138
|
+
json.layerName = shift.layerName;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (shift.override) {
|
|
142
|
+
const override: MaterializedShiftOverrideJson = {
|
|
143
|
+
originalUserId: shift.override.originalUserId,
|
|
144
|
+
originalUserName: shift.override.originalUserName,
|
|
145
|
+
overrideStartsAt: shift.override.overrideStartsAt.toISOString(),
|
|
146
|
+
overrideEndsAt: shift.override.overrideEndsAt.toISOString(),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (shift.override.onCallDutyPolicyId !== undefined) {
|
|
150
|
+
override.onCallDutyPolicyId = shift.override.onCallDutyPolicyId;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
json.override = override;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (shift.policyVariantOf) {
|
|
157
|
+
json.policyVariantOf = { ...shift.policyVariantOf };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return json;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
public static fromJSON(json: MaterializedShiftJson): MaterializedShift {
|
|
164
|
+
const shift: MaterializedShift = {
|
|
165
|
+
shiftKey: json.shiftKey,
|
|
166
|
+
contentHash: json.contentHash,
|
|
167
|
+
projectId: json.projectId,
|
|
168
|
+
scheduleId: json.scheduleId,
|
|
169
|
+
scheduleName: json.scheduleName,
|
|
170
|
+
userId: json.userId,
|
|
171
|
+
userName: json.userName,
|
|
172
|
+
start: OneUptimeDate.fromString(json.start),
|
|
173
|
+
end: OneUptimeDate.fromString(json.end),
|
|
174
|
+
coverageSeconds: json.coverageSeconds,
|
|
175
|
+
policies: (json.policies || []).map((policy: MaterializedShiftPolicy) => {
|
|
176
|
+
return { ...policy };
|
|
177
|
+
}),
|
|
178
|
+
isPast: Boolean(json.isPast),
|
|
179
|
+
lastModifiedAt: OneUptimeDate.fromString(json.lastModifiedAt),
|
|
180
|
+
shiftConfigVersion: json.shiftConfigVersion,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
if (json.projectName !== undefined) {
|
|
184
|
+
shift.projectName = json.projectName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (json.scheduleTimezone !== null && json.scheduleTimezone !== undefined) {
|
|
188
|
+
shift.scheduleTimezone = json.scheduleTimezone;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (json.layerId !== undefined) {
|
|
192
|
+
shift.layerId = json.layerId;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (json.layerName !== undefined) {
|
|
196
|
+
shift.layerName = json.layerName;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (json.override) {
|
|
200
|
+
const override: MaterializedShiftOverride = {
|
|
201
|
+
originalUserId: json.override.originalUserId,
|
|
202
|
+
originalUserName: json.override.originalUserName,
|
|
203
|
+
overrideStartsAt: OneUptimeDate.fromString(
|
|
204
|
+
json.override.overrideStartsAt,
|
|
205
|
+
),
|
|
206
|
+
overrideEndsAt: OneUptimeDate.fromString(json.override.overrideEndsAt),
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
if (json.override.onCallDutyPolicyId !== undefined) {
|
|
210
|
+
override.onCallDutyPolicyId = json.override.onCallDutyPolicyId;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
shift.override = override;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (json.policyVariantOf) {
|
|
217
|
+
shift.policyVariantOf = { ...json.policyVariantOf };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return shift;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
public static toJSONArray(
|
|
224
|
+
shifts: Array<MaterializedShift>,
|
|
225
|
+
): Array<MaterializedShiftJson> {
|
|
226
|
+
return shifts.map((shift: MaterializedShift) => {
|
|
227
|
+
return MaterializedShiftUtil.toJSON(shift);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public static fromJSONArray(
|
|
232
|
+
json: Array<MaterializedShiftJson>,
|
|
233
|
+
): Array<MaterializedShift> {
|
|
234
|
+
return json.map((item: MaterializedShiftJson) => {
|
|
235
|
+
return MaterializedShiftUtil.fromJSON(item);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/*
|
|
240
|
+
* Deterministic order for rendering and hashing: by start, then schedule,
|
|
241
|
+
* then shift key. Returns a sorted copy; never mutates the input.
|
|
242
|
+
*/
|
|
243
|
+
public static sortByStart(
|
|
244
|
+
shifts: Array<MaterializedShift>,
|
|
245
|
+
): Array<MaterializedShift> {
|
|
246
|
+
return [...shifts].sort((a: MaterializedShift, b: MaterializedShift) => {
|
|
247
|
+
const byStart: number = a.start.getTime() - b.start.getTime();
|
|
248
|
+
if (byStart !== 0) {
|
|
249
|
+
return byStart;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (a.scheduleId !== b.scheduleId) {
|
|
253
|
+
return a.scheduleId < b.scheduleId ? -1 : 1;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (a.shiftKey === b.shiftKey) {
|
|
257
|
+
return 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return a.shiftKey < b.shiftKey ? -1 : 1;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// `${scheduleId}:${startEpochSeconds}` (+ `:${policyId}` for variants).
|
|
265
|
+
public static buildShiftKey(data: {
|
|
266
|
+
scheduleId: string;
|
|
267
|
+
start: Date;
|
|
268
|
+
policyId?: string | undefined;
|
|
269
|
+
}): string {
|
|
270
|
+
const epochSeconds: number = Math.floor(data.start.getTime() / 1000);
|
|
271
|
+
const base: string = `${data.scheduleId}:${epochSeconds}`;
|
|
272
|
+
return data.policyId ? `${base}:${data.policyId}` : base;
|
|
273
|
+
}
|
|
274
|
+
}
|