@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,280 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ /*
4
+ * Monitors can end up holding references to another project's MonitorStatus,
5
+ * IncidentSeverity or AlertSeverity — nothing validated that the ids embedded
6
+ * in `monitorSteps` belong to the monitor's own project, so a status id picked
7
+ * up from a second project got persisted and then copied onto every
8
+ * MonitorStatusTimeline row the monitor wrote.
9
+ *
10
+ * That makes the *referenced* project undeletable: deleting a Project cascades
11
+ * into its MonitorStatus rows, but MonitorStatusTimeline rows owned by a
12
+ * different project still point at them and the FK is ON DELETE NO ACTION, so
13
+ * Postgres raises 23503 and the API answers "Server Error".
14
+ *
15
+ * This migration:
16
+ * 1. repoints every cross-project reference at the equivalent record in the
17
+ * row's own project (matched by name, then by operational/offline role,
18
+ * then by nearest priority), and
19
+ * 2. switches the two status-timeline FKs to ON DELETE CASCADE, which is
20
+ * what they should always have been — a status timeline row is
21
+ * meaningless once its status is gone.
22
+ *
23
+ * Step 1 runs before step 2 on purpose: repairing first means the timeline
24
+ * history stays attached to the surviving project instead of being swept up by
25
+ * the new cascade.
26
+ */
27
+ export class RepairCrossProjectMonitorStatusReferences1785240000000
28
+ implements MigrationInterface
29
+ {
30
+ public name: string =
31
+ "RepairCrossProjectMonitorStatusReferences1785240000000";
32
+
33
+ public async up(queryRunner: QueryRunner): Promise<void> {
34
+ /*
35
+ * The repair below scans MonitorStatusTimeline, which is in the millions of
36
+ * rows. Measured at ~4.5s on a production-sized table, but the connection
37
+ * carries the app's 30s statement_timeout (DATABASE_STATEMENT_TIMEOUT_MS)
38
+ * and a loaded database should not turn this into a failed deploy.
39
+ */
40
+ await queryRunner.query(`SET LOCAL statement_timeout = '600s'`);
41
+
42
+ // 1. MonitorStatusTimeline rows pointing at another project's status.
43
+ await queryRunner.query(`
44
+ WITH bad AS (
45
+ SELECT t."_id" AS row_id,
46
+ t."projectId" AS project_id,
47
+ ms."name" AS status_name,
48
+ ms."isOperationalState" AS is_operational,
49
+ ms."isOfflineState" AS is_offline,
50
+ ms."priority" AS priority
51
+ FROM "MonitorStatusTimeline" t
52
+ JOIN "MonitorStatus" ms ON ms."_id" = t."monitorStatusId"
53
+ WHERE t."projectId" IS DISTINCT FROM ms."projectId"
54
+ ), resolved AS (
55
+ SELECT b.row_id, COALESCE(
56
+ (SELECT s."_id" FROM "MonitorStatus" s
57
+ WHERE s."projectId" = b.project_id AND lower(s."name") = lower(b.status_name)
58
+ ORDER BY s."priority" LIMIT 1),
59
+ (SELECT s."_id" FROM "MonitorStatus" s
60
+ WHERE s."projectId" = b.project_id
61
+ AND s."isOperationalState" = b.is_operational
62
+ AND s."isOfflineState" = b.is_offline
63
+ ORDER BY s."priority" LIMIT 1),
64
+ (SELECT s."_id" FROM "MonitorStatus" s
65
+ WHERE s."projectId" = b.project_id
66
+ ORDER BY abs(s."priority" - b.priority), s."priority" LIMIT 1)
67
+ ) AS good_status_id
68
+ FROM bad b
69
+ )
70
+ UPDATE "MonitorStatusTimeline" t
71
+ SET "monitorStatusId" = r.good_status_id
72
+ FROM resolved r
73
+ WHERE t."_id" = r.row_id AND r.good_status_id IS NOT NULL
74
+ `);
75
+
76
+ // 2. NetworkSiteStatusTimeline rows pointing at another project's status.
77
+ await queryRunner.query(`
78
+ WITH bad AS (
79
+ SELECT t."_id" AS row_id,
80
+ t."projectId" AS project_id,
81
+ ms."name" AS status_name,
82
+ ms."isOperationalState" AS is_operational,
83
+ ms."isOfflineState" AS is_offline,
84
+ ms."priority" AS priority
85
+ FROM "NetworkSiteStatusTimeline" t
86
+ JOIN "MonitorStatus" ms ON ms."_id" = t."monitorStatusId"
87
+ WHERE t."projectId" IS DISTINCT FROM ms."projectId"
88
+ ), resolved AS (
89
+ SELECT b.row_id, COALESCE(
90
+ (SELECT s."_id" FROM "MonitorStatus" s
91
+ WHERE s."projectId" = b.project_id AND lower(s."name") = lower(b.status_name)
92
+ ORDER BY s."priority" LIMIT 1),
93
+ (SELECT s."_id" FROM "MonitorStatus" s
94
+ WHERE s."projectId" = b.project_id
95
+ AND s."isOperationalState" = b.is_operational
96
+ AND s."isOfflineState" = b.is_offline
97
+ ORDER BY s."priority" LIMIT 1),
98
+ (SELECT s."_id" FROM "MonitorStatus" s
99
+ WHERE s."projectId" = b.project_id
100
+ ORDER BY abs(s."priority" - b.priority), s."priority" LIMIT 1)
101
+ ) AS good_status_id
102
+ FROM bad b
103
+ )
104
+ UPDATE "NetworkSiteStatusTimeline" t
105
+ SET "monitorStatusId" = r.good_status_id
106
+ FROM resolved r
107
+ WHERE t."_id" = r.row_id AND r.good_status_id IS NOT NULL
108
+ `);
109
+
110
+ /*
111
+ * 3. Monitor.currentMonitorStatusId pointing at another project's status.
112
+ * This FK stays NO ACTION — deleting a status that monitors are currently
113
+ * in should be blocked — so a cross-project value here blocks deletion the
114
+ * same way. The column is NOT NULL, hence a repoint rather than a null.
115
+ */
116
+ await queryRunner.query(`
117
+ WITH bad AS (
118
+ SELECT m."_id" AS row_id,
119
+ m."projectId" AS project_id,
120
+ ms."name" AS status_name,
121
+ ms."isOperationalState" AS is_operational,
122
+ ms."isOfflineState" AS is_offline,
123
+ ms."priority" AS priority
124
+ FROM "Monitor" m
125
+ JOIN "MonitorStatus" ms ON ms."_id" = m."currentMonitorStatusId"
126
+ WHERE m."projectId" IS DISTINCT FROM ms."projectId"
127
+ ), resolved AS (
128
+ SELECT b.row_id, COALESCE(
129
+ (SELECT s."_id" FROM "MonitorStatus" s
130
+ WHERE s."projectId" = b.project_id AND lower(s."name") = lower(b.status_name)
131
+ ORDER BY s."priority" LIMIT 1),
132
+ (SELECT s."_id" FROM "MonitorStatus" s
133
+ WHERE s."projectId" = b.project_id
134
+ AND s."isOperationalState" = b.is_operational
135
+ AND s."isOfflineState" = b.is_offline
136
+ ORDER BY s."priority" LIMIT 1),
137
+ (SELECT s."_id" FROM "MonitorStatus" s
138
+ WHERE s."projectId" = b.project_id
139
+ ORDER BY abs(s."priority" - b.priority), s."priority" LIMIT 1)
140
+ ) AS good_status_id
141
+ FROM bad b
142
+ )
143
+ UPDATE "Monitor" m
144
+ SET "currentMonitorStatusId" = r.good_status_id
145
+ FROM resolved r
146
+ WHERE m."_id" = r.row_id AND r.good_status_id IS NOT NULL
147
+ `);
148
+
149
+ /*
150
+ * 4. Cross-project ids embedded in Monitor.monitorSteps. These carry no FK,
151
+ * so they would silently dangle once the other project is deleted (the
152
+ * monitor's default status, and the severity used to open incidents/alerts,
153
+ * would both point at nothing). Rewrite them onto the monitor's own
154
+ * project's equivalents.
155
+ */
156
+ await queryRunner.query(`
157
+ DO $$
158
+ DECLARE
159
+ monitor_row record;
160
+ remap_row record;
161
+ steps_text text;
162
+ BEGIN
163
+ CREATE TEMP TABLE _cross_project_step_refs ON COMMIT DROP AS
164
+ SELECT m."_id" AS monitor_id,
165
+ m."projectId" AS project_id,
166
+ matched.arr[1]::uuid AS ref_id
167
+ FROM "Monitor" m
168
+ CROSS JOIN LATERAL regexp_matches(
169
+ m."monitorSteps"::text,
170
+ '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}',
171
+ 'g'
172
+ ) AS matched(arr)
173
+ WHERE m."monitorSteps" IS NOT NULL;
174
+
175
+ CREATE TEMP TABLE _cross_project_step_remap ON COMMIT DROP AS
176
+ SELECT DISTINCT sr.monitor_id, sr.ref_id AS bad_id, COALESCE(
177
+ (SELECT x."_id" FROM "MonitorStatus" x
178
+ WHERE x."projectId" = sr.project_id AND lower(x."name") = lower(ms."name")
179
+ ORDER BY x."priority" LIMIT 1),
180
+ (SELECT x."_id" FROM "MonitorStatus" x
181
+ WHERE x."projectId" = sr.project_id
182
+ AND x."isOperationalState" = ms."isOperationalState"
183
+ AND x."isOfflineState" = ms."isOfflineState"
184
+ ORDER BY x."priority" LIMIT 1)
185
+ ) AS good_id
186
+ FROM _cross_project_step_refs sr
187
+ JOIN "MonitorStatus" ms ON ms."_id" = sr.ref_id
188
+ WHERE ms."projectId" IS DISTINCT FROM sr.project_id
189
+
190
+ UNION ALL
191
+
192
+ SELECT DISTINCT sr.monitor_id, sr.ref_id, (
193
+ SELECT x."_id" FROM "IncidentSeverity" x
194
+ WHERE x."projectId" = sr.project_id AND lower(x."name") = lower(sev."name")
195
+ ORDER BY x."order" LIMIT 1
196
+ )
197
+ FROM _cross_project_step_refs sr
198
+ JOIN "IncidentSeverity" sev ON sev."_id" = sr.ref_id
199
+ WHERE sev."projectId" IS DISTINCT FROM sr.project_id
200
+
201
+ UNION ALL
202
+
203
+ SELECT DISTINCT sr.monitor_id, sr.ref_id, (
204
+ SELECT x."_id" FROM "AlertSeverity" x
205
+ WHERE x."projectId" = sr.project_id AND lower(x."name") = lower(sev."name")
206
+ ORDER BY x."order" LIMIT 1
207
+ )
208
+ FROM _cross_project_step_refs sr
209
+ JOIN "AlertSeverity" sev ON sev."_id" = sr.ref_id
210
+ WHERE sev."projectId" IS DISTINCT FROM sr.project_id;
211
+
212
+ FOR monitor_row IN
213
+ SELECT DISTINCT monitor_id FROM _cross_project_step_remap WHERE good_id IS NOT NULL
214
+ LOOP
215
+ SELECT "monitorSteps"::text INTO steps_text
216
+ FROM "Monitor" WHERE "_id" = monitor_row.monitor_id;
217
+
218
+ FOR remap_row IN
219
+ SELECT bad_id, good_id FROM _cross_project_step_remap
220
+ WHERE monitor_id = monitor_row.monitor_id AND good_id IS NOT NULL
221
+ LOOP
222
+ steps_text := replace(steps_text, remap_row.bad_id::text, remap_row.good_id::text);
223
+ END LOOP;
224
+
225
+ UPDATE "Monitor" SET "monitorSteps" = steps_text::jsonb
226
+ WHERE "_id" = monitor_row.monitor_id;
227
+ END LOOP;
228
+ END $$;
229
+ `);
230
+
231
+ /*
232
+ * 5. A status timeline row cannot outlive its status, so cascade instead of
233
+ * blocking. Without this, a leftover cross-project row (one we could not
234
+ * resolve above, e.g. a project with no statuses of its own) would still
235
+ * make the referenced project undeletable.
236
+ *
237
+ * Swapping a foreign key needs ACCESS EXCLUSIVE on MonitorStatusTimeline —
238
+ * the busiest write table there is — and the lock is held until this
239
+ * migration commits. The DDL itself measures ~1.3s, but the server has no
240
+ * lock_timeout, so without the line below a single slow transaction holding
241
+ * the table would make this statement queue *and* park every reader and
242
+ * writer behind it until the 30s statement_timeout fired. Failing fast and
243
+ * retrying the deploy is much cheaper than stalling the table.
244
+ */
245
+ await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
246
+
247
+ await queryRunner.query(
248
+ `ALTER TABLE "MonitorStatusTimeline" DROP CONSTRAINT "FK_574feb4161c5216c2c7ee0faaf8"`,
249
+ );
250
+ await queryRunner.query(
251
+ `ALTER TABLE "MonitorStatusTimeline" ADD CONSTRAINT "FK_574feb4161c5216c2c7ee0faaf8" FOREIGN KEY ("monitorStatusId") REFERENCES "MonitorStatus"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
252
+ );
253
+ await queryRunner.query(
254
+ `ALTER TABLE "NetworkSiteStatusTimeline" DROP CONSTRAINT "FK_8886e21e28e5287abb5b26475a1"`,
255
+ );
256
+ await queryRunner.query(
257
+ `ALTER TABLE "NetworkSiteStatusTimeline" ADD CONSTRAINT "FK_8886e21e28e5287abb5b26475a1" FOREIGN KEY ("monitorStatusId") REFERENCES "MonitorStatus"("_id") ON DELETE CASCADE ON UPDATE NO ACTION`,
258
+ );
259
+ }
260
+
261
+ /*
262
+ * Only the constraints are reversible. The repaired references are not:
263
+ * the previous values pointed at another project's records and there is
264
+ * nothing worth restoring them to.
265
+ */
266
+ public async down(queryRunner: QueryRunner): Promise<void> {
267
+ await queryRunner.query(
268
+ `ALTER TABLE "NetworkSiteStatusTimeline" DROP CONSTRAINT "FK_8886e21e28e5287abb5b26475a1"`,
269
+ );
270
+ await queryRunner.query(
271
+ `ALTER TABLE "NetworkSiteStatusTimeline" ADD CONSTRAINT "FK_8886e21e28e5287abb5b26475a1" FOREIGN KEY ("monitorStatusId") REFERENCES "MonitorStatus"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
272
+ );
273
+ await queryRunner.query(
274
+ `ALTER TABLE "MonitorStatusTimeline" DROP CONSTRAINT "FK_574feb4161c5216c2c7ee0faaf8"`,
275
+ );
276
+ await queryRunner.query(
277
+ `ALTER TABLE "MonitorStatusTimeline" ADD CONSTRAINT "FK_574feb4161c5216c2c7ee0faaf8" FOREIGN KEY ("monitorStatusId") REFERENCES "MonitorStatus"("_id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
278
+ );
279
+ }
280
+ }
@@ -470,6 +470,9 @@ import { AddNetworkDevicePollingColumns1784970388777 } from "./1784970388777-Add
470
470
  import { AddNetworkSiteTypeTable1784986826214 } from "./1784986826214-AddNetworkSiteTypeTable";
471
471
  import { AddServiceLevelObjective1784987015619 } from "./1784987015619-AddServiceLevelObjective";
472
472
  import { MigrationName1785066759532 } from "./1785066759532-MigrationName";
473
+ import { AddHotQueryIndexes1785140242697 } from "./1785140242697-AddHotQueryIndexes";
474
+ import { AddHotQueryIndexesSecondPass1785148065137 } from "./1785148065137-AddHotQueryIndexesSecondPass";
475
+ import { RepairCrossProjectMonitorStatusReferences1785240000000 } from "./1785240000000-RepairCrossProjectMonitorStatusReferences";
473
476
 
474
477
  export default [
475
478
  InitialMigration,
@@ -944,4 +947,7 @@ export default [
944
947
  AddNetworkSiteTypeTable1784986826214,
945
948
  AddServiceLevelObjective1784987015619,
946
949
  MigrationName1785066759532,
950
+ AddHotQueryIndexes1785140242697,
951
+ AddHotQueryIndexesSecondPass1785148065137,
952
+ RepairCrossProjectMonitorStatusReferences1785240000000,
947
953
  ];
@@ -30,6 +30,7 @@ import UpdateByID from "../Types/Database/UpdateByID";
30
30
  import UpdateByIDAndFetch from "../Types/Database/UpdateByIDAndFetch";
31
31
  import UpdateOneBy from "../Types/Database/UpdateOneBy";
32
32
  import Encryption from "../Utils/Encryption";
33
+ import PostgresErrorTranslator from "../Utils/Database/PostgresErrorTranslator";
33
34
  import logger, { LogAttributes } from "../Utils/Logger";
34
35
  import BaseService from "./BaseService";
35
36
  import BaseModel from "../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
@@ -453,7 +454,7 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
453
454
  * propagates the original exception on the synchronous throw path.
454
455
  */
455
456
  protected getException(error: Exception): never {
456
- throw error;
457
+ throw PostgresErrorTranslator.translateException(error);
457
458
  }
458
459
 
459
460
  private generateSlug(createBy: CreateBy<TBaseModel>): CreateBy<TBaseModel> {
@@ -2015,10 +2016,17 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
2015
2016
  return await this._updateBy(updateBy);
2016
2017
  }
2017
2018
 
2019
+ /*
2020
+ * Returns how many rows the update actually matched. Callers that need to
2021
+ * tell "saved" apart from "matched nothing" must check it: the permission
2022
+ * layer narrows the query after the caller builds it (tenant scope, access
2023
+ * control labels, owned scope), so an update can legitimately reach here
2024
+ * and write nothing at all.
2025
+ */
2018
2026
  @CaptureSpan()
2019
2027
  public async updateOneById(
2020
2028
  updateById: UpdateByID<TBaseModel>,
2021
- ): Promise<void> {
2029
+ ): Promise<number> {
2022
2030
  if (!updateById.id) {
2023
2031
  throw new BadDataException("updateById.id is required");
2024
2032
  }
@@ -2048,7 +2056,7 @@ class DatabaseService<TBaseModel extends BaseModel> extends BaseService {
2048
2056
  props: updateById.props,
2049
2057
  });
2050
2058
 
2051
- await this.updateOneBy({
2059
+ return await this.updateOneBy({
2052
2060
  query: {
2053
2061
  _id: updateById.id.toString() as any,
2054
2062
  },
@@ -4,6 +4,7 @@ import MonitorGroupResourceService from "./MonitorGroupResourceService";
4
4
  import MonitorStatusService from "./MonitorStatusService";
5
5
  import MonitorStatusTimelineService from "./MonitorStatusTimelineService";
6
6
  import DatabaseCommonInteractionProps from "../../Types/BaseDatabase/DatabaseCommonInteractionProps";
7
+ import Dictionary from "../../Types/Dictionary";
7
8
  import SortOrder from "../../Types/BaseDatabase/SortOrder";
8
9
  import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
9
10
  import BadDataException from "../../Types/Exception/BadDataException";
@@ -221,5 +222,149 @@ export class Service extends DatabaseService<MonitorGroup> {
221
222
 
222
223
  return currentStatus;
223
224
  }
225
+
226
+ /*
227
+ * Batched fetch of MonitorGroupResource rows for many groups in ONE query
228
+ * (paged only if the total exceeds LIMIT_MAX). The public status-page read
229
+ * paths used to issue one query per monitor group per page view; on a page
230
+ * with G groups that was an N+1 of G index lookups per request. Every
231
+ * requested group id gets an entry in the returned dictionary (empty array
232
+ * when the group has no resources).
233
+ */
234
+ @CaptureSpan()
235
+ public async getMonitorGroupResourcesByGroupIds(
236
+ monitorGroupIds: Array<ObjectID>,
237
+ ): Promise<Dictionary<Array<MonitorGroupResource>>> {
238
+ const resourcesByGroupId: Dictionary<Array<MonitorGroupResource>> = {};
239
+
240
+ const dedupedGroupIds: Array<ObjectID> = [];
241
+ for (const monitorGroupId of monitorGroupIds) {
242
+ if (monitorGroupId && !resourcesByGroupId[monitorGroupId.toString()]) {
243
+ resourcesByGroupId[monitorGroupId.toString()] = [];
244
+ dedupedGroupIds.push(monitorGroupId);
245
+ }
246
+ }
247
+
248
+ if (dedupedGroupIds.length === 0) {
249
+ return resourcesByGroupId;
250
+ }
251
+
252
+ const monitorGroupResources: Array<MonitorGroupResource> =
253
+ await MonitorGroupResourceService.findAllBy({
254
+ query: {
255
+ monitorGroupId: QueryHelper.any(dedupedGroupIds),
256
+ },
257
+ select: {
258
+ monitorGroupId: true,
259
+ monitorId: true,
260
+ monitor: {
261
+ currentMonitorStatusId: true,
262
+ },
263
+ },
264
+ props: {
265
+ isRoot: true,
266
+ },
267
+ });
268
+
269
+ for (const monitorGroupResource of monitorGroupResources) {
270
+ if (!monitorGroupResource.monitorGroupId) {
271
+ continue;
272
+ }
273
+
274
+ resourcesByGroupId[monitorGroupResource.monitorGroupId.toString()]?.push(
275
+ monitorGroupResource,
276
+ );
277
+ }
278
+
279
+ return resourcesByGroupId;
280
+ }
281
+
282
+ // Batched twin of the per-group monitor-id lookup: ONE query for all groups.
283
+ @CaptureSpan()
284
+ public async getMonitorIdsInMonitorGroups(
285
+ monitorGroupIds: Array<ObjectID>,
286
+ ): Promise<Dictionary<Array<ObjectID>>> {
287
+ const resourcesByGroupId: Dictionary<Array<MonitorGroupResource>> =
288
+ await this.getMonitorGroupResourcesByGroupIds(monitorGroupIds);
289
+
290
+ const monitorIdsByGroupId: Dictionary<Array<ObjectID>> = {};
291
+
292
+ for (const groupId of Object.keys(resourcesByGroupId)) {
293
+ monitorIdsByGroupId[groupId] = (resourcesByGroupId[groupId] || [])
294
+ .map((resource: MonitorGroupResource) => {
295
+ return resource.monitorId!;
296
+ })
297
+ .filter((id: ObjectID) => {
298
+ return Boolean(id); // remove nulls
299
+ });
300
+ }
301
+
302
+ return monitorIdsByGroupId;
303
+ }
304
+
305
+ /*
306
+ * Batched twin of getCurrentStatus, preserving its exact semantics: each
307
+ * group's status starts at the project's operational state and is replaced
308
+ * by the highest-priority-number status among its monitors. Callers supply
309
+ * the project's monitor statuses (they already have them in hand on every
310
+ * path that renders a status page), so this issues at most ONE query — the
311
+ * group-resource fetch — instead of three per group.
312
+ */
313
+ @CaptureSpan()
314
+ public async getCurrentStatusesForMonitorGroups(data: {
315
+ monitorGroupIds: Array<ObjectID>;
316
+ monitorStatuses: Array<MonitorStatus>;
317
+ monitorGroupResources?: Dictionary<Array<MonitorGroupResource>> | undefined;
318
+ }): Promise<Dictionary<MonitorStatus>> {
319
+ const currentStatuses: Dictionary<MonitorStatus> = {};
320
+
321
+ if (data.monitorGroupIds.length === 0) {
322
+ return currentStatuses;
323
+ }
324
+
325
+ const operationalStatus: MonitorStatus | undefined =
326
+ data.monitorStatuses.find((monitorStatus: MonitorStatus) => {
327
+ return monitorStatus.isOperationalState;
328
+ });
329
+
330
+ if (!operationalStatus) {
331
+ throw new BadDataException("Operational state not found.");
332
+ }
333
+
334
+ const resourcesByGroupId: Dictionary<Array<MonitorGroupResource>> =
335
+ data.monitorGroupResources ||
336
+ (await this.getMonitorGroupResourcesByGroupIds(data.monitorGroupIds));
337
+
338
+ for (const monitorGroupId of data.monitorGroupIds) {
339
+ let currentStatus: MonitorStatus = operationalStatus;
340
+
341
+ for (const monitorGroupResource of resourcesByGroupId[
342
+ monitorGroupId.toString()
343
+ ] || []) {
344
+ if (!monitorGroupResource.monitor) {
345
+ continue;
346
+ }
347
+
348
+ const monitorStatus: MonitorStatus | undefined =
349
+ data.monitorStatuses.find((monitorStatus: MonitorStatus) => {
350
+ return (
351
+ monitorStatus.id?.toString() ===
352
+ monitorGroupResource.monitor!.currentMonitorStatusId?.toString()
353
+ );
354
+ });
355
+
356
+ if (
357
+ monitorStatus &&
358
+ currentStatus.priority! < monitorStatus.priority!
359
+ ) {
360
+ currentStatus = monitorStatus;
361
+ }
362
+ }
363
+
364
+ currentStatuses[monitorGroupId.toString()] = currentStatus;
365
+ }
366
+
367
+ return currentStatuses;
368
+ }
224
369
  }
225
370
  export default new Service();