@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.
Files changed (189) hide show
  1. package/Models/DatabaseModels/Index.ts +11 -0
  2. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  3. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  4. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  5. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  6. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  7. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  8. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  9. package/Server/EnvironmentConfig.ts +138 -0
  10. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  13. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  14. package/Server/Services/Index.ts +12 -0
  15. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  16. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  17. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  18. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  19. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  20. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  21. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  22. package/Server/Services/TeamMemberService.ts +489 -1
  23. package/Server/Services/TeamService.ts +50 -0
  24. package/Server/Services/UserNotificationSettingService.ts +44 -1
  25. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  26. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  27. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  28. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  29. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  30. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  31. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  32. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  33. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  34. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  35. package/Server/Utils/Response.ts +134 -0
  36. package/Server/Utils/StartServer.ts +10 -0
  37. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  38. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  39. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  40. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  41. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  42. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  43. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  44. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  45. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  46. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  47. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  48. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  49. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  50. package/Tests/Server/API/Helpers.ts +8 -0
  51. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  52. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  53. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  54. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  55. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  56. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  57. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  58. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  59. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  60. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  61. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  62. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  63. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  64. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  65. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  66. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  67. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  68. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  69. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  70. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  71. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  72. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  73. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  74. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  75. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  76. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  77. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  78. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  79. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  80. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  81. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  82. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  83. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  84. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  85. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  86. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  87. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  88. package/Types/Calendar/ICalendar.ts +442 -0
  89. package/Types/Email/EmailTemplateType.ts +2 -0
  90. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  91. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  92. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  93. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  94. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  95. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  96. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  97. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  100. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  102. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  103. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  104. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  105. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  106. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  107. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  108. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  109. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  110. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  111. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  112. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  113. package/build/dist/Server/EnvironmentConfig.js +102 -0
  114. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  115. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  116. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  117. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  118. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  119. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  120. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  121. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  122. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  123. package/build/dist/Server/Services/Index.js +12 -0
  124. package/build/dist/Server/Services/Index.js.map +1 -1
  125. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  126. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  127. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  128. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  129. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  130. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  131. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  132. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  133. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  134. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  135. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  136. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  137. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  138. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  139. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  140. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  141. package/build/dist/Server/Services/TeamService.js +43 -0
  142. package/build/dist/Server/Services/TeamService.js.map +1 -1
  143. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  144. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  145. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  146. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  147. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  148. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  149. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  150. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  151. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  152. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  153. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  154. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  155. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  156. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  157. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  158. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  159. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  160. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  161. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  162. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  163. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  164. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  165. package/build/dist/Server/Utils/Response.js +97 -0
  166. package/build/dist/Server/Utils/Response.js.map +1 -1
  167. package/build/dist/Server/Utils/StartServer.js +9 -1
  168. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  169. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  170. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  171. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  172. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  173. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  174. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  175. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  176. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  177. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  178. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  179. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  180. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  181. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  182. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  183. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  184. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  185. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  186. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  187. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  188. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  189. package/package.json +1 -1
@@ -18,12 +18,124 @@ import Timezone from "../../Types/Timezone";
18
18
  import DeleteBy from "../Types/Database/DeleteBy";
19
19
  import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
20
20
  import logger from "../Utils/Logger";
21
+ import { OnCallShiftChangeReason } from "../Utils/OnCall/OnCallShiftChangeListeners";
22
+
23
+ /*
24
+ * The (project, overridden user, substitute) triple an override names; the
25
+ * hooks capture it before an update / delete so the schedules of the OLD
26
+ * users can be refreshed and re-versioned once the row has changed.
27
+ */
28
+ interface OverrideUserPair {
29
+ projectId: ObjectID;
30
+ overrideUserId: ObjectID;
31
+ routeAlertsToUserId?: ObjectID | undefined;
32
+ }
33
+
34
+ /*
35
+ * The subset of OnCallDutyPolicyScheduleService the override hooks call.
36
+ * Lazily required (see refreshRostersForOverrideUser) because that service
37
+ * imports this one.
38
+ */
39
+ interface ScheduleServiceForOverrides {
40
+ refreshRostersForUserInProject: (d: {
41
+ projectId: ObjectID;
42
+ userId: ObjectID;
43
+ }) => Promise<void>;
44
+ getScheduleIdsForUsersInProject: (d: {
45
+ projectId: ObjectID;
46
+ userIds: Array<ObjectID>;
47
+ }) => Promise<Array<ObjectID>>;
48
+ propagateShiftConfigChange: (d: {
49
+ scheduleIds: Array<ObjectID>;
50
+ projectId?: ObjectID | null | undefined;
51
+ userIds?: Array<ObjectID> | undefined;
52
+ reason: OnCallShiftChangeReason;
53
+ }) => Promise<void>;
54
+ }
21
55
 
22
56
  export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
23
57
  public constructor() {
24
58
  super(OnCallDutyPolicyUserOverride);
25
59
  }
26
60
 
61
+ private getScheduleService(): ScheduleServiceForOverrides {
62
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
63
+ return require("./OnCallDutyPolicyScheduleService").default;
64
+ }
65
+
66
+ /**
67
+ * An override changes who is on call on every schedule that contains the
68
+ * overridden user, and it changes the SUBSTITUTE's personal calendar too.
69
+ * Find those schedules with the same layer-user query the roster refresh
70
+ * uses (for every overridden user, old and new), then bump their
71
+ * shiftConfigVersion, drop the feed caches — the substitutes' personal
72
+ * caches included — and let the shift-change listeners re-plan reminders.
73
+ * One call per project; best-effort, never throws into the CRUD path.
74
+ */
75
+ private async propagateShiftChangeForOverrides(
76
+ pairs: Array<OverrideUserPair>,
77
+ ): Promise<void> {
78
+ const byProject: Map<
79
+ string,
80
+ {
81
+ projectId: ObjectID;
82
+ overrideUserIds: Array<ObjectID>;
83
+ allUserIds: Array<ObjectID>;
84
+ }
85
+ > = new Map();
86
+
87
+ for (const pair of pairs) {
88
+ if (!pair.projectId || !pair.overrideUserId) {
89
+ continue;
90
+ }
91
+
92
+ const key: string = pair.projectId.toString();
93
+ const entry: {
94
+ projectId: ObjectID;
95
+ overrideUserIds: Array<ObjectID>;
96
+ allUserIds: Array<ObjectID>;
97
+ } = byProject.get(key) || {
98
+ projectId: pair.projectId,
99
+ overrideUserIds: [],
100
+ allUserIds: [],
101
+ };
102
+
103
+ entry.overrideUserIds.push(pair.overrideUserId);
104
+ entry.allUserIds.push(pair.overrideUserId);
105
+
106
+ if (pair.routeAlertsToUserId) {
107
+ entry.allUserIds.push(pair.routeAlertsToUserId);
108
+ }
109
+
110
+ byProject.set(key, entry);
111
+ }
112
+
113
+ for (const entry of byProject.values()) {
114
+ try {
115
+ const scheduleService: ScheduleServiceForOverrides =
116
+ this.getScheduleService();
117
+
118
+ const scheduleIds: Array<ObjectID> =
119
+ await scheduleService.getScheduleIdsForUsersInProject({
120
+ projectId: entry.projectId,
121
+ userIds: entry.overrideUserIds,
122
+ });
123
+
124
+ await scheduleService.propagateShiftConfigChange({
125
+ scheduleIds,
126
+ projectId: entry.projectId,
127
+ userIds: entry.allUserIds,
128
+ reason: OnCallShiftChangeReason.OverrideChanged,
129
+ });
130
+ } catch (err) {
131
+ logger.error(
132
+ "Error propagating a user override change to the calendar feeds (best-effort).",
133
+ );
134
+ logger.error(err);
135
+ }
136
+ }
137
+ }
138
+
27
139
  /**
28
140
  * Re-resolve the persisted roster of every schedule in the project that
29
141
  * contains the override user, so a created / updated / deleted override is
@@ -41,13 +153,8 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
41
153
  }
42
154
 
43
155
  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;
156
+ const scheduleService: ScheduleServiceForOverrides =
157
+ this.getScheduleService();
51
158
 
52
159
  await scheduleService.refreshRostersForUserInProject({
53
160
  projectId: data.projectId,
@@ -190,6 +297,16 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
190
297
  overrideUserId,
191
298
  });
192
299
 
300
+ if (projectId && overrideUserId) {
301
+ await this.propagateShiftChangeForOverrides([
302
+ {
303
+ projectId,
304
+ overrideUserId,
305
+ routeAlertsToUserId: routeAlertsToUserId || undefined,
306
+ },
307
+ ]);
308
+ }
309
+
193
310
  return createdItem;
194
311
  }
195
312
 
@@ -207,10 +324,7 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
207
324
  * schedules by the NEW user only (audit M2). Mirrors the onBeforeDelete
208
325
  * carry-forward pattern used by the delete path.
209
326
  */
210
- const previous: Array<{
211
- projectId: ObjectID;
212
- overrideUserId: ObjectID;
213
- }> = [];
327
+ const previous: Array<OverrideUserPair> = [];
214
328
 
215
329
  try {
216
330
  const rows: Array<OnCallDutyPolicyUserOverride> = await this.findBy({
@@ -218,6 +332,7 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
218
332
  select: {
219
333
  projectId: true,
220
334
  overrideUserId: true,
335
+ routeAlertsToUserId: true,
221
336
  },
222
337
  props: {
223
338
  isRoot: true,
@@ -231,8 +346,14 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
231
346
  row.projectId || row.project?.id;
232
347
  const overrideUserId: ObjectID | undefined | null =
233
348
  row.overrideUserId || row.overrideUser?.id;
349
+ const routeAlertsToUserId: ObjectID | undefined | null =
350
+ row.routeAlertsToUserId || row.routeAlertsToUser?.id;
234
351
  if (projectId && overrideUserId) {
235
- previous.push({ projectId, overrideUserId });
352
+ previous.push({
353
+ projectId,
354
+ overrideUserId,
355
+ routeAlertsToUserId: routeAlertsToUserId || undefined,
356
+ });
236
357
  }
237
358
  }
238
359
  } catch (err) {
@@ -279,11 +400,11 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
279
400
  };
280
401
 
281
402
  // 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
- }>) || [];
403
+ const previous: Array<OverrideUserPair> =
404
+ (onUpdate.carryForward as Array<OverrideUserPair>) || [];
405
+
406
+ // Everyone the edit touched, old and new, for the feed / reminder pass.
407
+ const affectedPairs: Array<OverrideUserPair> = [...previous];
287
408
 
288
409
  for (const entry of previous) {
289
410
  await refreshPair(entry.projectId, entry.overrideUserId);
@@ -296,6 +417,7 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
296
417
  select: {
297
418
  projectId: true,
298
419
  overrideUserId: true,
420
+ routeAlertsToUserId: true,
299
421
  },
300
422
  props: {
301
423
  isRoot: true,
@@ -306,12 +428,26 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
306
428
  continue;
307
429
  }
308
430
 
309
- await refreshPair(
310
- item.projectId || item.project?.id,
311
- item.overrideUserId || item.overrideUser?.id,
312
- );
431
+ const newProjectId: ObjectID | undefined | null =
432
+ item.projectId || item.project?.id;
433
+ const newOverrideUserId: ObjectID | undefined | null =
434
+ item.overrideUserId || item.overrideUser?.id;
435
+ const newRouteAlertsToUserId: ObjectID | undefined | null =
436
+ item.routeAlertsToUserId || item.routeAlertsToUser?.id;
437
+
438
+ await refreshPair(newProjectId, newOverrideUserId);
439
+
440
+ if (newProjectId && newOverrideUserId) {
441
+ affectedPairs.push({
442
+ projectId: newProjectId,
443
+ overrideUserId: newOverrideUserId,
444
+ routeAlertsToUserId: newRouteAlertsToUserId || undefined,
445
+ });
446
+ }
313
447
  }
314
448
 
449
+ await this.propagateShiftChangeForOverrides(affectedPairs);
450
+
315
451
  return onUpdate;
316
452
  }
317
453
 
@@ -326,11 +462,8 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
326
462
  * than waiting for the next natural handoff (audit F4). The affected
327
463
  * project/user pairs were captured in onBeforeDelete (the rows are gone now).
328
464
  */
329
- const affected: Array<{ projectId: ObjectID; overrideUserId: ObjectID }> =
330
- (onDelete.carryForward as Array<{
331
- projectId: ObjectID;
332
- overrideUserId: ObjectID;
333
- }>) || [];
465
+ const affected: Array<OverrideUserPair> =
466
+ (onDelete.carryForward as Array<OverrideUserPair>) || [];
334
467
 
335
468
  for (const entry of affected) {
336
469
  await this.refreshRostersForOverrideUser({
@@ -339,6 +472,8 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
339
472
  });
340
473
  }
341
474
 
475
+ await this.propagateShiftChangeForOverrides(affected);
476
+
342
477
  return onDelete;
343
478
  }
344
479
 
@@ -368,10 +503,7 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
368
503
  * onDeleteSuccess can refresh those schedules' rosters AFTER the rows are
369
504
  * gone (audit F4). Collected for global overrides too (no policy id).
370
505
  */
371
- const affectedRosters: Array<{
372
- projectId: ObjectID;
373
- overrideUserId: ObjectID;
374
- }> = [];
506
+ const affectedRosters: Array<OverrideUserPair> = [];
375
507
 
376
508
  for (const item of itemsToDelete) {
377
509
  const onCallDutyPolicyId: ObjectID | undefined | null =
@@ -387,7 +519,11 @@ export class Service extends DatabaseService<OnCallDutyPolicyUserOverride> {
387
519
  item.routeAlertsToUserId || item.routeAlertsToUser?.id;
388
520
 
389
521
  if (projectId && overrideUserId) {
390
- affectedRosters.push({ projectId, overrideUserId });
522
+ affectedRosters.push({
523
+ projectId,
524
+ overrideUserId,
525
+ routeAlertsToUserId: routeAlertsToUserId || undefined,
526
+ });
391
527
  }
392
528
 
393
529
  if (
@@ -0,0 +1,192 @@
1
+ import CreateBy from "../Types/Database/CreateBy";
2
+ import UpdateBy from "../Types/Database/UpdateBy";
3
+ import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
4
+ import DatabaseService from "./DatabaseService";
5
+ import {
6
+ MAX_MINIMUM_GAP_MINUTES,
7
+ MIN_MINIMUM_GAP_MINUTES,
8
+ } from "./OnCallDutyPolicyScheduleCalendarFeedService";
9
+ import CalendarFeedToken, {
10
+ CalendarFeedRotation,
11
+ } from "../Utils/OnCall/CalendarFeedToken";
12
+ import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
13
+ import Model from "../../Models/DatabaseModels/ProjectOnCallCalendarFeed";
14
+ import BadDataException from "../../Types/Exception/BadDataException";
15
+ import NotFoundException from "../../Types/Exception/NotFoundException";
16
+ import CalendarFeedWindow from "../../Types/OnCallDutyPolicy/CalendarFeedWindow";
17
+ import ObjectID from "../../Types/ObjectID";
18
+ import PositiveNumber from "../../Types/PositiveNumber";
19
+
20
+ /*
21
+ * The project-wide shared calendar feed: at most one row per project.
22
+ *
23
+ * The same guarantees as OnCallDutyPolicyScheduleCalendarFeedService minus
24
+ * the schedule check: one feed per project (unique index + a readable
25
+ * message), the token minted here on every create whatever the request
26
+ * carried, window and gap settings bounded on every write, root-only
27
+ * rotation with the 30-day grace hash.
28
+ */
29
+ export class Service extends DatabaseService<Model> {
30
+ public constructor() {
31
+ super(Model);
32
+ }
33
+
34
+ @CaptureSpan()
35
+ protected override async onBeforeCreate(
36
+ createBy: CreateBy<Model>,
37
+ ): Promise<OnCreate<Model>> {
38
+ const projectId: ObjectID | undefined =
39
+ createBy.data.projectId || createBy.props.tenantId;
40
+
41
+ if (!projectId) {
42
+ throw new BadDataException("projectId is required");
43
+ }
44
+
45
+ createBy.data.projectId = projectId;
46
+
47
+ const existing: PositiveNumber = await this.countBy({
48
+ query: {
49
+ projectId: projectId,
50
+ },
51
+ props: {
52
+ isRoot: true,
53
+ },
54
+ });
55
+
56
+ if (existing.toNumber() > 0) {
57
+ throw new BadDataException(
58
+ "This project already has a shared calendar feed. Regenerate or edit the existing one instead.",
59
+ );
60
+ }
61
+
62
+ // A request body never chooses the secret; only root may pre-mint one.
63
+ CalendarFeedToken.applyTokenColumnsOnCreate(createBy.data, {
64
+ trustSuppliedToken: createBy.props.isRoot === true,
65
+ });
66
+
67
+ createBy.data.pastDays = CalendarFeedWindow.clampPastDays(
68
+ createBy.data.pastDays,
69
+ );
70
+ createBy.data.futureDays = CalendarFeedWindow.clampFutureDays(
71
+ createBy.data.futureDays,
72
+ );
73
+
74
+ if (createBy.data.minimumGapMinutes !== undefined) {
75
+ createBy.data.minimumGapMinutes = Service.validateMinimumGapMinutes(
76
+ createBy.data.minimumGapMinutes,
77
+ );
78
+ }
79
+
80
+ return { createBy, carryForward: null };
81
+ }
82
+
83
+ @CaptureSpan()
84
+ protected override async onBeforeUpdate(
85
+ updateBy: UpdateBy<Model>,
86
+ ): Promise<OnUpdate<Model>> {
87
+ if (updateBy.data.pastDays !== undefined) {
88
+ updateBy.data.pastDays = CalendarFeedWindow.clampPastDays(
89
+ updateBy.data.pastDays,
90
+ );
91
+ }
92
+
93
+ if (updateBy.data.futureDays !== undefined) {
94
+ updateBy.data.futureDays = CalendarFeedWindow.clampFutureDays(
95
+ updateBy.data.futureDays,
96
+ );
97
+ }
98
+
99
+ if (updateBy.data.minimumGapMinutes !== undefined) {
100
+ updateBy.data.minimumGapMinutes = Service.validateMinimumGapMinutes(
101
+ updateBy.data.minimumGapMinutes,
102
+ );
103
+ }
104
+
105
+ return { updateBy, carryForward: null };
106
+ }
107
+
108
+ public static validateMinimumGapMinutes(value: unknown): number {
109
+ const parsed: number =
110
+ typeof value === "string" && value.trim() !== ""
111
+ ? Number(value)
112
+ : (value as number);
113
+
114
+ if (
115
+ typeof parsed !== "number" ||
116
+ !Number.isInteger(parsed) ||
117
+ parsed < MIN_MINIMUM_GAP_MINUTES ||
118
+ parsed > MAX_MINIMUM_GAP_MINUTES
119
+ ) {
120
+ throw new BadDataException(
121
+ `Minimum gap must be a whole number of minutes between ${MIN_MINIMUM_GAP_MINUTES} and ${MAX_MINIMUM_GAP_MINUTES}.`,
122
+ );
123
+ }
124
+
125
+ return parsed;
126
+ }
127
+
128
+ /**
129
+ * Rotate the token of an existing feed. Returns the new plaintext token
130
+ * ONCE. Root write; the caller purges caches.
131
+ */
132
+ @CaptureSpan()
133
+ public async rotateTokenById(data: {
134
+ id: ObjectID;
135
+ }): Promise<CalendarFeedRotation> {
136
+ const existing: Model | null = await this.findOneBy({
137
+ query: { _id: data.id },
138
+ select: { _id: true, tokenHash: true },
139
+ props: { isRoot: true, ignoreHooks: true },
140
+ });
141
+
142
+ if (!existing) {
143
+ throw new NotFoundException("Calendar feed not found.");
144
+ }
145
+
146
+ const rotation: CalendarFeedRotation = CalendarFeedToken.buildRotation({
147
+ currentTokenHash: existing.tokenHash,
148
+ });
149
+
150
+ await this.updateOneById({
151
+ id: data.id,
152
+ data: CalendarFeedToken.toRotationUpdateData(rotation),
153
+ props: { isRoot: true },
154
+ });
155
+
156
+ return rotation;
157
+ }
158
+
159
+ /**
160
+ * Rotate the project's feed when a member leaves, if it is enabled and
161
+ * opted in. Returns the rotated id (at most one) for cache purging.
162
+ */
163
+ @CaptureSpan()
164
+ public async rotateFeedsForMemberLeave(data: {
165
+ projectId: ObjectID;
166
+ }): Promise<Array<ObjectID>> {
167
+ const feed: Model | null = await this.findOneBy({
168
+ query: {
169
+ projectId: data.projectId,
170
+ isEnabled: true,
171
+ rotateWhenMemberLeaves: true,
172
+ },
173
+ select: {
174
+ _id: true,
175
+ },
176
+ props: {
177
+ isRoot: true,
178
+ ignoreHooks: true,
179
+ },
180
+ });
181
+
182
+ if (!feed || !feed.id) {
183
+ return [];
184
+ }
185
+
186
+ await this.rotateTokenById({ id: feed.id });
187
+
188
+ return [feed.id];
189
+ }
190
+ }
191
+
192
+ export default new Service();