@oneuptime/common 11.7.1 → 11.7.2

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 (237) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +1 -0
  2. package/Models/DatabaseModels/Incident.ts +2 -0
  3. package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
  4. package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
  5. package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
  6. package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
  7. package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
  8. package/Models/DatabaseModels/Monitor.ts +1 -0
  9. package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
  10. package/Models/DatabaseModels/NetworkSite.ts +1 -0
  11. package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
  12. package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
  13. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
  14. package/Models/DatabaseModels/Probe.ts +13 -13
  15. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
  16. package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
  17. package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
  18. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
  19. package/Models/DatabaseModels/StatusPage.ts +1 -0
  20. package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
  21. package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
  22. package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
  23. package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
  24. package/Server/API/BaseAPI.ts +17 -1
  25. package/Server/API/CommonAPI.ts +32 -0
  26. package/Server/API/MicrosoftTeamsAPI.ts +99 -2
  27. package/Server/API/ProbeAPI.ts +2 -0
  28. package/Server/API/SlackAPI.ts +62 -0
  29. package/Server/API/StatusPageAPI.ts +157 -192
  30. package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
  31. package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
  32. package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
  33. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  34. package/Server/Services/DatabaseService.ts +11 -3
  35. package/Server/Services/MonitorGroupService.ts +145 -0
  36. package/Server/Services/MonitorService.ts +231 -30
  37. package/Server/Services/ProbeService.ts +319 -16
  38. package/Server/Services/StatusPageService.ts +72 -42
  39. package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
  40. package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
  41. package/Server/Utils/Monitor/MonitorResource.ts +88 -35
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
  44. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
  45. package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
  46. package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
  47. package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
  48. package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
  49. package/Tests/Server/API/ProbeAPI.test.ts +6 -0
  50. package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
  51. package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
  52. package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
  53. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
  54. package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
  55. package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
  56. package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
  57. package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
  58. package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
  59. package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
  60. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
  61. package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
  62. package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
  63. package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
  64. package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
  65. package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
  66. package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
  67. package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
  68. package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
  69. package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
  70. package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
  71. package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
  72. package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
  73. package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
  74. package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
  75. package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
  76. package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
  77. package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
  78. package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
  79. package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
  80. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
  81. package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
  82. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
  83. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
  84. package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
  85. package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
  86. package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
  87. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  88. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  89. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  90. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  91. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  92. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  93. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  94. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  95. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  96. package/Tests/UI/Utils/Project.test.ts +809 -0
  97. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  98. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  99. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  100. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  101. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  102. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  103. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  104. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  105. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  106. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  107. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  108. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  109. package/UI/Components/Forms/ModelForm.tsx +41 -18
  110. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  111. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  112. package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
  113. package/UI/Components/ModelTable/Column.ts +12 -0
  114. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  115. package/UI/Components/Navbar/NavBar.tsx +66 -1
  116. package/UI/Components/Table/Table.tsx +52 -13
  117. package/UI/Components/Table/Types/Column.ts +18 -0
  118. package/UI/Utils/Project.ts +214 -18
  119. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  120. package/UI/esbuild-config.js +34 -0
  121. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  122. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  123. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  124. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  125. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  126. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  127. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  128. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  129. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  130. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  131. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  133. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  135. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  137. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  139. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  140. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  141. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  142. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  143. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  144. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  145. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  146. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  147. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  148. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  149. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  150. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  151. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  152. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  153. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  155. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  157. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  159. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  161. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  163. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  164. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  165. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  166. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  167. package/build/dist/Server/API/BaseAPI.js +12 -1
  168. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  169. package/build/dist/Server/API/CommonAPI.js +21 -0
  170. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  171. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  172. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  173. package/build/dist/Server/API/ProbeAPI.js +2 -0
  174. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  175. package/build/dist/Server/API/SlackAPI.js +44 -0
  176. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  177. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  178. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  179. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  180. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  181. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  182. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  183. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  184. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  185. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  186. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  187. package/build/dist/Server/Services/DatabaseService.js +10 -2
  188. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  189. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  190. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  191. package/build/dist/Server/Services/MonitorService.js +198 -24
  192. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  193. package/build/dist/Server/Services/ProbeService.js +238 -9
  194. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  195. package/build/dist/Server/Services/StatusPageService.js +46 -39
  196. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  197. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  198. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  199. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  200. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  201. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  202. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  203. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  204. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  205. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  206. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  207. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  208. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  209. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  210. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  211. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  212. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  213. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  214. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  215. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  216. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  217. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  218. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  219. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  220. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  221. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  222. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  223. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
  224. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  225. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  226. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  227. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  228. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  229. package/build/dist/UI/Components/Table/Table.js +31 -11
  230. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  231. package/build/dist/UI/Utils/Project.js +161 -15
  232. package/build/dist/UI/Utils/Project.js.map +1 -1
  233. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  234. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  235. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  236. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  237. package/package.json +4 -3
@@ -0,0 +1,331 @@
1
+ /*
2
+ * StatusPageService monitor-group batching.
3
+ *
4
+ * Public status-page reads (overview, badge, subscriber reports) used to
5
+ * issue one MonitorGroupResource query PER STATUS PAGE RESOURCE (not even per
6
+ * distinct group) and re-fetch the page's StatusPageResource rows the caller
7
+ * already had in hand. These tests pin the two batched entry points:
8
+ * - getMonitorIdsOnStatusPage: skips the StatusPageResource query entirely
9
+ * when the caller supplies the rows, resolves ALL groups through ONE
10
+ * MonitorGroupService.getMonitorIdsInMonitorGroups call, and de-dupes a
11
+ * monitor that is both directly on the page and inside a group,
12
+ * - getMonitorGroupCurrentStatuses: one batched group-resource fetch for
13
+ * any number of resources (including repeated references to the same
14
+ * group), while keeping its OWN worst-status semantics (see below).
15
+ *
16
+ * SEMANTICS NOTE (deliberate, preserved as-is by the batching change):
17
+ * StatusPageService.getMonitorGroupCurrentStatuses picks the LOWEST
18
+ * priority-number status as "worst" and produces NO entry for a group whose
19
+ * member statuses cannot be resolved — the exact OPPOSITE of
20
+ * MonitorGroupService.getCurrentStatus(es), where the HIGHEST priority number
21
+ * wins on top of an operational default. Both behaviors predate the batching
22
+ * and both are pinned so neither can silently converge on the other.
23
+ *
24
+ * All service reads are spied on — no database is touched.
25
+ */
26
+
27
+ import StatusPageService from "../../../Server/Services/StatusPageService";
28
+ import StatusPageResourceService from "../../../Server/Services/StatusPageResourceService";
29
+ import MonitorGroupService from "../../../Server/Services/MonitorGroupService";
30
+ import StatusPageResource from "../../../Models/DatabaseModels/StatusPageResource";
31
+ import MonitorGroupResource from "../../../Models/DatabaseModels/MonitorGroupResource";
32
+ import MonitorStatus from "../../../Models/DatabaseModels/MonitorStatus";
33
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
34
+ import Dictionary from "../../../Types/Dictionary";
35
+ import ObjectID from "../../../Types/ObjectID";
36
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
37
+
38
+ function makeStatusPageResource(data: {
39
+ monitorId?: ObjectID | undefined;
40
+ monitorGroupId?: ObjectID | undefined;
41
+ }): StatusPageResource {
42
+ const resource: StatusPageResource = new StatusPageResource();
43
+ if (data.monitorId) {
44
+ resource.monitorId = data.monitorId;
45
+ }
46
+ if (data.monitorGroupId) {
47
+ resource.monitorGroupId = data.monitorGroupId;
48
+ }
49
+ return resource;
50
+ }
51
+
52
+ function makeGroupResource(data: {
53
+ monitorGroupId: ObjectID;
54
+ monitorId?: ObjectID | undefined;
55
+ currentMonitorStatusId?: ObjectID | undefined;
56
+ omitMonitor?: boolean;
57
+ }): MonitorGroupResource {
58
+ const resource: MonitorGroupResource = new MonitorGroupResource();
59
+ resource.monitorGroupId = data.monitorGroupId;
60
+ if (data.monitorId) {
61
+ resource.monitorId = data.monitorId;
62
+ }
63
+ if (!data.omitMonitor) {
64
+ const monitor: Monitor = new Monitor();
65
+ if (data.currentMonitorStatusId) {
66
+ monitor.currentMonitorStatusId = data.currentMonitorStatusId;
67
+ }
68
+ resource.monitor = monitor;
69
+ }
70
+ return resource;
71
+ }
72
+
73
+ function makeStatus(data: { priority: number; name: string }): MonitorStatus {
74
+ const status: MonitorStatus = new MonitorStatus();
75
+ status.id = ObjectID.generate();
76
+ status.priority = data.priority;
77
+ status.name = data.name;
78
+ return status;
79
+ }
80
+
81
+ function toStrings(ids: Array<ObjectID>): Array<string> {
82
+ return ids.map((id: ObjectID) => {
83
+ return id.toString();
84
+ });
85
+ }
86
+
87
+ describe("StatusPageService monitor-group batching", () => {
88
+ afterEach(() => {
89
+ jest.restoreAllMocks();
90
+ });
91
+
92
+ describe("getMonitorIdsOnStatusPage", () => {
93
+ test("supplied resources skip the StatusPageResource query; groups resolve via ONE batched call; direct+group monitor de-dupes", async () => {
94
+ const statusPageId: ObjectID = ObjectID.generate();
95
+ const groupA: ObjectID = ObjectID.generate();
96
+ const groupB: ObjectID = ObjectID.generate();
97
+ const directMonitor: ObjectID = ObjectID.generate();
98
+ const groupOnlyMonitorA: ObjectID = ObjectID.generate();
99
+ const groupOnlyMonitorB: ObjectID = ObjectID.generate();
100
+
101
+ const statusPageResources: Array<StatusPageResource> = [
102
+ // Direct monitor, no group: must contribute no group lookup.
103
+ makeStatusPageResource({ monitorId: directMonitor }),
104
+ makeStatusPageResource({ monitorGroupId: groupA }),
105
+ makeStatusPageResource({ monitorGroupId: groupB }),
106
+ ];
107
+
108
+ const getResourcesSpy: jest.SpyInstance = jest.spyOn(
109
+ StatusPageService,
110
+ "getStatusPageResources",
111
+ ) as unknown as jest.SpyInstance;
112
+ const findBySpy: jest.SpyInstance = jest.spyOn(
113
+ StatusPageResourceService,
114
+ "findBy",
115
+ ) as unknown as jest.SpyInstance;
116
+ const batchedSpy: jest.SpyInstance = (
117
+ jest.spyOn(
118
+ MonitorGroupService,
119
+ "getMonitorIdsInMonitorGroups",
120
+ ) as unknown as jest.SpyInstance
121
+ ).mockResolvedValue({
122
+ // groupA contains the direct monitor too — it must appear only once.
123
+ [groupA.toString()]: [directMonitor, groupOnlyMonitorA],
124
+ [groupB.toString()]: [groupOnlyMonitorB],
125
+ } as never);
126
+
127
+ const result: {
128
+ monitorsOnStatusPage: Array<ObjectID>;
129
+ monitorsInGroup: Dictionary<Array<ObjectID>>;
130
+ } = await StatusPageService.getMonitorIdsOnStatusPage({
131
+ statusPageId: statusPageId,
132
+ statusPageResources: statusPageResources,
133
+ });
134
+
135
+ // Supplied rows mean the (identical) StatusPageResource query never runs.
136
+ expect(getResourcesSpy).not.toHaveBeenCalled();
137
+ expect(findBySpy).not.toHaveBeenCalled();
138
+
139
+ // ONE batched group lookup for all groups; null groupIds filtered out.
140
+ expect(batchedSpy).toHaveBeenCalledTimes(1);
141
+ expect(
142
+ toStrings(batchedSpy.mock.calls[0]![0] as Array<ObjectID>),
143
+ ).toEqual(toStrings([groupA, groupB]));
144
+
145
+ // Union of direct + group monitors, without duplicating directMonitor.
146
+ expect(toStrings(result.monitorsOnStatusPage)).toEqual(
147
+ toStrings([directMonitor, groupOnlyMonitorA, groupOnlyMonitorB]),
148
+ );
149
+
150
+ // The per-group membership dict is passed through exactly.
151
+ expect(Object.keys(result.monitorsInGroup).sort()).toEqual(
152
+ [groupA.toString(), groupB.toString()].sort(),
153
+ );
154
+ expect(toStrings(result.monitorsInGroup[groupA.toString()]!)).toEqual(
155
+ toStrings([directMonitor, groupOnlyMonitorA]),
156
+ );
157
+ expect(toStrings(result.monitorsInGroup[groupB.toString()]!)).toEqual(
158
+ toStrings([groupOnlyMonitorB]),
159
+ );
160
+ });
161
+
162
+ test("without supplied resources it fetches them once itself", async () => {
163
+ const statusPageId: ObjectID = ObjectID.generate();
164
+ const directMonitor: ObjectID = ObjectID.generate();
165
+
166
+ const getResourcesSpy: jest.SpyInstance = (
167
+ jest.spyOn(
168
+ StatusPageService,
169
+ "getStatusPageResources",
170
+ ) as unknown as jest.SpyInstance
171
+ ).mockResolvedValue([
172
+ makeStatusPageResource({ monitorId: directMonitor }),
173
+ ] as never);
174
+ const batchedSpy: jest.SpyInstance = (
175
+ jest.spyOn(
176
+ MonitorGroupService,
177
+ "getMonitorIdsInMonitorGroups",
178
+ ) as unknown as jest.SpyInstance
179
+ ).mockResolvedValue({} as never);
180
+
181
+ const result: {
182
+ monitorsOnStatusPage: Array<ObjectID>;
183
+ monitorsInGroup: Dictionary<Array<ObjectID>>;
184
+ } = await StatusPageService.getMonitorIdsOnStatusPage({
185
+ statusPageId: statusPageId,
186
+ });
187
+
188
+ expect(getResourcesSpy).toHaveBeenCalledTimes(1);
189
+ expect(getResourcesSpy).toHaveBeenCalledWith({
190
+ statusPageId: statusPageId,
191
+ });
192
+ expect(batchedSpy).toHaveBeenCalledTimes(1);
193
+ expect(batchedSpy).toHaveBeenCalledWith([]);
194
+ expect(toStrings(result.monitorsOnStatusPage)).toEqual(
195
+ toStrings([directMonitor]),
196
+ );
197
+ expect(result.monitorsInGroup).toEqual({});
198
+ });
199
+ });
200
+
201
+ describe("getMonitorGroupCurrentStatuses", () => {
202
+ const statusOperational: MonitorStatus = makeStatus({
203
+ priority: 1,
204
+ name: "Operational",
205
+ });
206
+ const statusDegraded: MonitorStatus = makeStatus({
207
+ priority: 2,
208
+ name: "Degraded",
209
+ });
210
+ const statusOffline: MonitorStatus = makeStatus({
211
+ priority: 3,
212
+ name: "Offline",
213
+ });
214
+ const allStatuses: Array<MonitorStatus> = [
215
+ statusOperational,
216
+ statusDegraded,
217
+ statusOffline,
218
+ ];
219
+
220
+ test("one batched fetch for many resources; LOWEST priority number wins; direct/unresolvable groups get no entry", async () => {
221
+ const groupA: ObjectID = ObjectID.generate();
222
+ const groupB: ObjectID = ObjectID.generate();
223
+
224
+ const statusPageResources: Array<StatusPageResource> = [
225
+ makeStatusPageResource({ monitorGroupId: groupA }),
226
+ makeStatusPageResource({ monitorGroupId: groupB }),
227
+ // The same group referenced twice must not trigger a second fetch.
228
+ makeStatusPageResource({ monitorGroupId: groupA }),
229
+ // Direct monitor resource: contributes no group, gets no entry.
230
+ makeStatusPageResource({ monitorId: ObjectID.generate() }),
231
+ ];
232
+
233
+ const batchedSpy: jest.SpyInstance = (
234
+ jest.spyOn(
235
+ MonitorGroupService,
236
+ "getMonitorGroupResourcesByGroupIds",
237
+ ) as unknown as jest.SpyInstance
238
+ ).mockResolvedValue({
239
+ [groupA.toString()]: [
240
+ makeGroupResource({
241
+ monitorGroupId: groupA,
242
+ monitorId: ObjectID.generate(),
243
+ currentMonitorStatusId: statusDegraded.id!,
244
+ }),
245
+ makeGroupResource({
246
+ monitorGroupId: groupA,
247
+ monitorId: ObjectID.generate(),
248
+ currentMonitorStatusId: statusOffline.id!,
249
+ }),
250
+ /*
251
+ * Would be the "worst" under these semantics (lowest number), but
252
+ * the row has no monitorId, so the filter must drop it.
253
+ */
254
+ makeGroupResource({
255
+ monitorGroupId: groupA,
256
+ currentMonitorStatusId: statusOperational.id!,
257
+ }),
258
+ // No monitor relation loaded: filtered out.
259
+ makeGroupResource({
260
+ monitorGroupId: groupA,
261
+ monitorId: ObjectID.generate(),
262
+ omitMonitor: true,
263
+ }),
264
+ ],
265
+ // groupB's only member points at a status that no longer exists.
266
+ [groupB.toString()]: [
267
+ makeGroupResource({
268
+ monitorGroupId: groupB,
269
+ monitorId: ObjectID.generate(),
270
+ currentMonitorStatusId: ObjectID.generate(),
271
+ }),
272
+ ],
273
+ } as never);
274
+
275
+ const result: Dictionary<ObjectID> =
276
+ await StatusPageService.getMonitorGroupCurrentStatuses({
277
+ statusPageResources: statusPageResources,
278
+ monitorStatuses: allStatuses,
279
+ });
280
+
281
+ // ONE batched query serves every resource on the page.
282
+ expect(batchedSpy).toHaveBeenCalledTimes(1);
283
+ /*
284
+ * The raw (non-deduped, null-filtered) group id list is handed to the
285
+ * helper — deduping is the helper's job, pinned in its own suite.
286
+ */
287
+ expect(
288
+ toStrings(batchedSpy.mock.calls[0]![0] as Array<ObjectID>),
289
+ ).toEqual(toStrings([groupA, groupB, groupA]));
290
+
291
+ /*
292
+ * StatusPageService semantics: the LOWEST priority number is "worst",
293
+ * so Degraded (2) beats Offline (3). This is the OPPOSITE of
294
+ * MonitorGroupService.getCurrentStatus(es), where the HIGHEST number
295
+ * wins — both pre-existing behaviors are preserved as-is by the
296
+ * batching change, and this assertion keeps them from converging.
297
+ */
298
+ expect(result[groupA.toString()]).toBeInstanceOf(ObjectID);
299
+ expect(result[groupA.toString()]!.toString()).toBe(
300
+ statusDegraded.id!.toString(),
301
+ );
302
+
303
+ // No resolvable member status -> NO dict entry (no operational default).
304
+ expect(result[groupB.toString()]).toBeUndefined();
305
+ // Direct-monitor resources never produce group entries.
306
+ expect(Object.keys(result)).toEqual([groupA.toString()]);
307
+ });
308
+
309
+ test("no group resources at all yields an empty dictionary", async () => {
310
+ const batchedSpy: jest.SpyInstance = (
311
+ jest.spyOn(
312
+ MonitorGroupService,
313
+ "getMonitorGroupResourcesByGroupIds",
314
+ ) as unknown as jest.SpyInstance
315
+ ).mockResolvedValue({} as never);
316
+
317
+ const result: Dictionary<ObjectID> =
318
+ await StatusPageService.getMonitorGroupCurrentStatuses({
319
+ statusPageResources: [
320
+ makeStatusPageResource({ monitorId: ObjectID.generate() }),
321
+ ],
322
+ monitorStatuses: allStatuses,
323
+ });
324
+
325
+ expect(result).toEqual({});
326
+ // Still exactly one batched call (with an empty id list).
327
+ expect(batchedSpy).toHaveBeenCalledTimes(1);
328
+ expect(batchedSpy).toHaveBeenCalledWith([]);
329
+ });
330
+ });
331
+ });
@@ -261,8 +261,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
261
261
  .mockResolvedValue(fakeLog(logId, {}));
262
262
  const updateOneById: jest.SpyInstance = jest
263
263
  .spyOn(UserOnCallLogService, "updateOneById")
264
- .mockImplementation((): Promise<void> => {
265
- return Promise.resolve();
264
+ .mockImplementation((): Promise<number> => {
265
+ return Promise.resolve(1);
266
266
  });
267
267
 
268
268
  const claimed: boolean =
@@ -319,8 +319,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
319
319
  .mockResolvedValue(fakeLog(logId, undefined));
320
320
  const updateOneById: jest.SpyInstance = jest
321
321
  .spyOn(UserOnCallLogService, "updateOneById")
322
- .mockImplementation((): Promise<void> => {
323
- return Promise.resolve();
322
+ .mockImplementation((): Promise<number> => {
323
+ return Promise.resolve(1);
324
324
  });
325
325
 
326
326
  const claimed: boolean =
@@ -355,8 +355,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
355
355
  );
356
356
  const updateOneById: jest.SpyInstance = jest
357
357
  .spyOn(UserOnCallLogService, "updateOneById")
358
- .mockImplementation((): Promise<void> => {
359
- return Promise.resolve();
358
+ .mockImplementation((): Promise<number> => {
359
+ return Promise.resolve(1);
360
360
  });
361
361
 
362
362
  const claimed: boolean =
@@ -394,8 +394,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
394
394
  );
395
395
  const updateOneById: jest.SpyInstance = jest
396
396
  .spyOn(UserOnCallLogService, "updateOneById")
397
- .mockImplementation((): Promise<void> => {
398
- return Promise.resolve();
397
+ .mockImplementation((): Promise<number> => {
398
+ return Promise.resolve(1);
399
399
  });
400
400
 
401
401
  const claimed: boolean =
@@ -419,8 +419,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
419
419
  .mockResolvedValue(null);
420
420
  const updateOneById: jest.SpyInstance = jest
421
421
  .spyOn(UserOnCallLogService, "updateOneById")
422
- .mockImplementation((): Promise<void> => {
423
- return Promise.resolve();
422
+ .mockImplementation((): Promise<number> => {
423
+ return Promise.resolve(1);
424
424
  });
425
425
 
426
426
  const claimed: boolean =
@@ -445,8 +445,8 @@ describe("UserOnCallLogService.claimNotificationRuleExecution", () => {
445
445
  .mockResolvedValue(fakeLog(ObjectID.generate(), {}));
446
446
  jest
447
447
  .spyOn(UserOnCallLogService, "updateOneById")
448
- .mockImplementation((): Promise<void> => {
449
- return Promise.resolve();
448
+ .mockImplementation((): Promise<number> => {
449
+ return Promise.resolve(1);
450
450
  });
451
451
 
452
452
  await UserOnCallLogService.claimNotificationRuleExecution({