@oneuptime/common 11.7.0 → 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 (264) 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/DateUserTimezone.test.ts +205 -0
  88. package/Tests/Types/DateUserTimezoneOverridesBrowser.test.ts +73 -0
  89. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
  90. package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
  91. package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
  92. package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
  93. package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
  94. package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
  95. package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
  96. package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
  97. package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
  98. package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +11 -0
  99. package/Tests/UI/Utils/Project.test.ts +809 -0
  100. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
  101. package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
  102. package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
  103. package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
  104. package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
  105. package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
  106. package/Types/Date.ts +119 -50
  107. package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
  108. package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
  109. package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
  110. package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
  111. package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
  112. package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
  113. package/UI/Components/Forms/Fields/FormField.tsx +13 -3
  114. package/UI/Components/Forms/ModelForm.tsx +41 -18
  115. package/UI/Components/Input/Input.tsx +6 -1
  116. package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
  117. package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
  118. package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
  119. package/UI/Components/ModelTable/Column.ts +12 -0
  120. package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
  121. package/UI/Components/Navbar/NavBar.tsx +66 -1
  122. package/UI/Components/Table/Table.tsx +52 -13
  123. package/UI/Components/Table/Types/Column.ts +18 -0
  124. package/UI/Components/TimePicker/TimePicker.tsx +8 -4
  125. package/UI/Utils/Project.ts +214 -18
  126. package/UI/Utils/TableColumnsToCsv.ts +86 -6
  127. package/UI/Utils/User.ts +19 -0
  128. package/UI/esbuild-config.js +34 -0
  129. package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
  130. package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
  131. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  132. package/build/dist/Models/DatabaseModels/Incident.js +2 -0
  133. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  134. package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
  135. package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
  137. package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
  139. package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
  140. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
  141. package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
  142. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
  143. package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
  144. package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
  145. package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
  146. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
  147. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  148. package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
  149. package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
  150. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
  151. package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
  152. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
  153. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
  154. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
  155. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  156. package/build/dist/Models/DatabaseModels/Probe.js +13 -13
  157. package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
  158. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
  159. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  160. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
  161. package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
  162. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
  163. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
  164. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
  165. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  166. package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
  167. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  168. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
  169. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  170. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
  171. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  172. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
  173. package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
  174. package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
  175. package/build/dist/Server/API/BaseAPI.js +12 -1
  176. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  177. package/build/dist/Server/API/CommonAPI.js +21 -0
  178. package/build/dist/Server/API/CommonAPI.js.map +1 -1
  179. package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
  180. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  181. package/build/dist/Server/API/ProbeAPI.js +2 -0
  182. package/build/dist/Server/API/ProbeAPI.js.map +1 -1
  183. package/build/dist/Server/API/SlackAPI.js +44 -0
  184. package/build/dist/Server/API/SlackAPI.js.map +1 -1
  185. package/build/dist/Server/API/StatusPageAPI.js +98 -152
  186. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  187. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
  188. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
  189. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
  190. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
  191. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
  192. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
  193. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  194. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  195. package/build/dist/Server/Services/DatabaseService.js +10 -2
  196. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  197. package/build/dist/Server/Services/MonitorGroupService.js +118 -0
  198. package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
  199. package/build/dist/Server/Services/MonitorService.js +198 -24
  200. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  201. package/build/dist/Server/Services/ProbeService.js +238 -9
  202. package/build/dist/Server/Services/ProbeService.js.map +1 -1
  203. package/build/dist/Server/Services/StatusPageService.js +46 -39
  204. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  205. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
  206. package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
  207. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
  208. package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
  209. package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
  210. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  211. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
  212. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  213. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
  214. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
  215. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
  216. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  217. package/build/dist/Types/Date.js +106 -35
  218. package/build/dist/Types/Date.js.map +1 -1
  219. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
  220. package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
  221. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
  222. package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
  223. package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
  224. package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
  225. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
  226. package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
  227. package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -3
  228. package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
  229. package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
  230. package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
  231. package/build/dist/UI/Components/Input/Input.js +6 -1
  232. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  233. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
  234. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  235. package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
  236. package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
  237. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
  238. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  239. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
  240. package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
  241. package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
  242. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  243. package/build/dist/UI/Components/Table/Table.js +31 -11
  244. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  245. package/build/dist/UI/Components/TimePicker/TimePicker.js +4 -4
  246. package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
  247. package/build/dist/UI/Utils/Project.js +161 -15
  248. package/build/dist/UI/Utils/Project.js.map +1 -1
  249. package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
  250. package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
  251. package/build/dist/UI/Utils/User.js +19 -0
  252. package/build/dist/UI/Utils/User.js.map +1 -1
  253. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
  254. package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
  255. package/package.json +4 -3
  256. package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +0 -79
  257. package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +0 -41
  258. package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +0 -17
  259. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +0 -34
  260. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +0 -1
  261. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +0 -22
  262. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +0 -1
  263. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +0 -12
  264. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +0 -1
@@ -0,0 +1,228 @@
1
+ import { describe, expect, test, afterEach } from "@jest/globals";
2
+ import { JSONArray, JSONObject } from "../../../Types/JSON";
3
+ import MicrosoftTeamsAPI from "../../../Server/API/MicrosoftTeamsAPI";
4
+
5
+ const MOCK_TEAMS_CLIENT_ID: string = "11111111-2222-3333-4444-555555555555";
6
+
7
+ jest.mock("../../../Server/EnvironmentConfig", () => {
8
+ return {
9
+ ...(jest.requireActual("../../../Server/EnvironmentConfig") as Record<
10
+ string,
11
+ unknown
12
+ >),
13
+ MicrosoftTeamsAppClientId: "11111111-2222-3333-4444-555555555555",
14
+ };
15
+ });
16
+
17
+ type GetManifestFunction = () => JSONObject;
18
+
19
+ const getManifest: GetManifestFunction = (): JSONObject => {
20
+ return (
21
+ MicrosoftTeamsAPI as unknown as {
22
+ getTeamsAppManifest: GetManifestFunction;
23
+ }
24
+ ).getTeamsAppManifest();
25
+ };
26
+
27
+ type GetBotFunction = (manifest: JSONObject) => JSONObject;
28
+
29
+ const getBot: GetBotFunction = (manifest: JSONObject): JSONObject => {
30
+ const bots: JSONArray = manifest["bots"] as JSONArray;
31
+ return bots[0] as JSONObject;
32
+ };
33
+
34
+ type GetResourceSpecificPermissionsFunction = (
35
+ manifest: JSONObject,
36
+ ) => Array<JSONObject>;
37
+
38
+ const getResourceSpecificPermissions: GetResourceSpecificPermissionsFunction = (
39
+ manifest: JSONObject,
40
+ ): Array<JSONObject> => {
41
+ const authorization: JSONObject = manifest["authorization"] as JSONObject;
42
+ const permissions: JSONObject = authorization["permissions"] as JSONObject;
43
+ return permissions["resourceSpecific"] as Array<JSONObject>;
44
+ };
45
+
46
+ describe("MicrosoftTeamsAPI.getTeamsAppManifest", () => {
47
+ afterEach(() => {
48
+ jest.restoreAllMocks();
49
+ });
50
+
51
+ describe("bot scopes (chat support)", () => {
52
+ test("declares exactly one bot", () => {
53
+ const manifest: JSONObject = getManifest();
54
+ const bots: JSONArray = manifest["bots"] as JSONArray;
55
+ expect(bots).toHaveLength(1);
56
+ });
57
+
58
+ test("bot scopes contain exactly team, personal and groupChat", () => {
59
+ const bot: JSONObject = getBot(getManifest());
60
+ const scopes: Array<string> = bot["scopes"] as Array<string>;
61
+ expect([...scopes].sort()).toEqual(
62
+ ["groupChat", "personal", "team"].sort(),
63
+ );
64
+ });
65
+
66
+ test("bot scopes include groupChat so the bot can be added to group chats", () => {
67
+ const bot: JSONObject = getBot(getManifest());
68
+ expect(bot["scopes"] as Array<string>).toContain("groupChat");
69
+ });
70
+
71
+ test("bot scopes include personal so the bot can be added to 1:1 chats", () => {
72
+ const bot: JSONObject = getBot(getManifest());
73
+ expect(bot["scopes"] as Array<string>).toContain("personal");
74
+ });
75
+
76
+ test("bot is not notification-only (it must receive conversationUpdate events to capture chats)", () => {
77
+ const bot: JSONObject = getBot(getManifest());
78
+ expect(bot["isNotificationOnly"]).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe("identity", () => {
83
+ test("bot id equals the Microsoft Teams app client id", () => {
84
+ const bot: JSONObject = getBot(getManifest());
85
+ expect(bot["botId"]).toBe(MOCK_TEAMS_CLIENT_ID);
86
+ });
87
+
88
+ test("manifest id equals the Microsoft Teams app client id", () => {
89
+ const manifest: JSONObject = getManifest();
90
+ expect(manifest["id"]).toBe(MOCK_TEAMS_CLIENT_ID);
91
+ });
92
+
93
+ test("webApplicationInfo id equals the Microsoft Teams app client id", () => {
94
+ const manifest: JSONObject = getManifest();
95
+ const webApplicationInfo: JSONObject = manifest[
96
+ "webApplicationInfo"
97
+ ] as JSONObject;
98
+ expect(webApplicationInfo["id"]).toBe(MOCK_TEAMS_CLIENT_ID);
99
+ });
100
+ });
101
+
102
+ describe("resource-specific consent permissions", () => {
103
+ test.each([
104
+ "ChannelMessage.Send.Group",
105
+ "ChannelMessage.Read.Group",
106
+ "Channel.Create.Group",
107
+ "ChatMessage.Read.Chat",
108
+ "ChatMember.Read.Chat",
109
+ ])("includes the %s permission with type Application", (name: string) => {
110
+ const permissions: Array<JSONObject> =
111
+ getResourceSpecificPermissions(getManifest());
112
+ const match: JSONObject | undefined = permissions.find(
113
+ (permission: JSONObject) => {
114
+ return permission["name"] === name;
115
+ },
116
+ );
117
+ expect(match).toBeDefined();
118
+ expect(match!["type"]).toBe("Application");
119
+ });
120
+
121
+ test("does NOT include a ChatMessage.Send.Chat permission (not part of Microsoft's RSC catalog - chat sends go through the bot)", () => {
122
+ const permissions: Array<JSONObject> =
123
+ getResourceSpecificPermissions(getManifest());
124
+ const names: Array<string> = permissions.map((permission: JSONObject) => {
125
+ return permission["name"] as string;
126
+ });
127
+ expect(names).not.toContain("ChatMessage.Send.Chat");
128
+ });
129
+
130
+ test("declares exactly the five expected resource-specific permissions", () => {
131
+ const permissions: Array<JSONObject> =
132
+ getResourceSpecificPermissions(getManifest());
133
+ const names: Array<string> = permissions.map((permission: JSONObject) => {
134
+ return permission["name"] as string;
135
+ });
136
+ expect(names.sort()).toEqual(
137
+ [
138
+ "ChannelMessage.Send.Group",
139
+ "ChannelMessage.Read.Group",
140
+ "Channel.Create.Group",
141
+ "ChatMessage.Read.Chat",
142
+ "ChatMember.Read.Chat",
143
+ ].sort(),
144
+ );
145
+ });
146
+
147
+ test("every resource-specific permission is of type Application", () => {
148
+ const permissions: Array<JSONObject> =
149
+ getResourceSpecificPermissions(getManifest());
150
+ for (const permission of permissions) {
151
+ expect(permission["type"]).toBe("Application");
152
+ }
153
+ });
154
+
155
+ test("top-level permissions stay identity + messageTeamMembers", () => {
156
+ const manifest: JSONObject = getManifest();
157
+ expect(manifest["permissions"]).toEqual([
158
+ "identity",
159
+ "messageTeamMembers",
160
+ ]);
161
+ });
162
+ });
163
+
164
+ describe("command lists", () => {
165
+ test("commandLists cover all three scopes (team, groupChat, personal)", () => {
166
+ const bot: JSONObject = getBot(getManifest());
167
+ const commandLists: Array<JSONObject> = bot[
168
+ "commandLists"
169
+ ] as Array<JSONObject>;
170
+ const coveredScopes: Set<string> = new Set<string>();
171
+ for (const commandList of commandLists) {
172
+ for (const scope of commandList["scopes"] as Array<string>) {
173
+ coveredScopes.add(scope);
174
+ }
175
+ }
176
+ expect([...coveredScopes].sort()).toEqual(
177
+ ["groupChat", "personal", "team"].sort(),
178
+ );
179
+ });
180
+
181
+ test("every commandList scope is a declared bot scope", () => {
182
+ const bot: JSONObject = getBot(getManifest());
183
+ const botScopes: Array<string> = bot["scopes"] as Array<string>;
184
+ const commandLists: Array<JSONObject> = bot[
185
+ "commandLists"
186
+ ] as Array<JSONObject>;
187
+ for (const commandList of commandLists) {
188
+ for (const scope of commandList["scopes"] as Array<string>) {
189
+ expect(botScopes).toContain(scope);
190
+ }
191
+ }
192
+ });
193
+
194
+ test("commandLists include a help command", () => {
195
+ const bot: JSONObject = getBot(getManifest());
196
+ const commandLists: Array<JSONObject> = bot[
197
+ "commandLists"
198
+ ] as Array<JSONObject>;
199
+ const commandTitles: Array<string> = commandLists.flatMap(
200
+ (commandList: JSONObject) => {
201
+ return (commandList["commands"] as Array<JSONObject>).map(
202
+ (command: JSONObject) => {
203
+ return command["title"] as string;
204
+ },
205
+ );
206
+ },
207
+ );
208
+ expect(commandTitles).toContain("help");
209
+ });
210
+ });
211
+
212
+ describe("manifest schema basics", () => {
213
+ test("uses manifest schema version 1.23", () => {
214
+ const manifest: JSONObject = getManifest();
215
+ expect(manifest["manifestVersion"]).toBe("1.23");
216
+ expect(manifest["$schema"]).toBe(
217
+ "https://developer.microsoft.com/json-schemas/teams/v1.23/MicrosoftTeams.schema.json",
218
+ );
219
+ });
220
+
221
+ test("declares color and outline icons", () => {
222
+ const manifest: JSONObject = getManifest();
223
+ const icons: JSONObject = manifest["icons"] as JSONObject;
224
+ expect(icons["color"]).toBe("color.png");
225
+ expect(icons["outline"]).toBe("outline.png");
226
+ });
227
+ });
228
+ });
@@ -71,6 +71,12 @@ describe("ProbeAPI", () => {
71
71
  connectionStatus: true,
72
72
  lastAlive: true,
73
73
  iconFileId: true,
74
+ /*
75
+ * The monitor create form pre-selects the probes that auto-enable on
76
+ * new monitors, and global probes are not project rows - this endpoint
77
+ * is the only place the browser can learn the flag for them.
78
+ */
79
+ shouldAutoEnableProbeOnNewMonitors: true,
74
80
  },
75
81
  props: {
76
82
  isRoot: true,
@@ -0,0 +1,167 @@
1
+ import { describe, expect, test } from "@jest/globals";
2
+ import express from "express";
3
+ import http from "http";
4
+ import { AddressInfo } from "net";
5
+
6
+ jest.mock("../../../Server/Services/WorkspaceProjectAuthTokenService", () => {
7
+ return {
8
+ __esModule: true,
9
+ default: {
10
+ getProjectAuth: jest.fn(async () => {
11
+ return {
12
+ miscData: {
13
+ availableChats: {
14
+ "19:chat-abc": {
15
+ id: "19:chat-abc",
16
+ name: "Jane Doe",
17
+ chatType: "personal",
18
+ addedAt: "2026-07-01T00:00:00.000Z",
19
+ },
20
+ "19:chat-def": {
21
+ id: "19:chat-def",
22
+ name: "Alice, Bob + 2 more",
23
+ chatType: "group",
24
+ addedAt: "2026-07-02T00:00:00.000Z",
25
+ },
26
+ },
27
+ },
28
+ };
29
+ }),
30
+ },
31
+ };
32
+ });
33
+
34
+ jest.mock("../../../Server/Services/ProjectService", () => {
35
+ return {
36
+ __esModule: true,
37
+ default: {
38
+ updateLastActive: jest.fn(async () => {
39
+ return undefined;
40
+ }),
41
+ getCurrentPlan: jest.fn(async () => {
42
+ return { plan: null, isSubscriptionUnpaid: false };
43
+ }),
44
+ getRequireSsoForLogin: jest.fn(async () => {
45
+ return true;
46
+ }),
47
+ },
48
+ };
49
+ });
50
+
51
+ interface HttpProbeResult {
52
+ status: number;
53
+ body: unknown;
54
+ }
55
+
56
+ /*
57
+ * The jest test environment is jsdom, which does not expose a global fetch, so
58
+ * this probe talks to the server with Node's http client. Everything runs
59
+ * inside the caller's try/finally so the server is always closed - a leaked
60
+ * listener keeps the event loop alive and hangs the whole `jest` run at exit.
61
+ */
62
+ type HttpGetJsonFunction = (data: {
63
+ port: number;
64
+ path: string;
65
+ headers: http.OutgoingHttpHeaders;
66
+ }) => Promise<HttpProbeResult>;
67
+
68
+ const httpGetJson: HttpGetJsonFunction = (data: {
69
+ port: number;
70
+ path: string;
71
+ headers: http.OutgoingHttpHeaders;
72
+ }): Promise<HttpProbeResult> => {
73
+ return new Promise<HttpProbeResult>(
74
+ (
75
+ resolve: (result: HttpProbeResult) => void,
76
+ reject: (error: Error) => void,
77
+ ) => {
78
+ const request: http.ClientRequest = http.request(
79
+ {
80
+ host: "127.0.0.1",
81
+ port: data.port,
82
+ path: data.path,
83
+ method: "GET",
84
+ headers: data.headers,
85
+ },
86
+ (response: http.IncomingMessage) => {
87
+ const chunks: Array<Buffer> = [];
88
+
89
+ response.on("data", (chunk: Buffer) => {
90
+ chunks.push(chunk);
91
+ });
92
+
93
+ response.on("end", () => {
94
+ const raw: string = Buffer.concat(chunks).toString("utf8");
95
+
96
+ let parsed: unknown = null;
97
+ try {
98
+ parsed = raw ? JSON.parse(raw) : null;
99
+ } catch {
100
+ parsed = raw;
101
+ }
102
+
103
+ resolve({
104
+ status: response.statusCode || 0,
105
+ body: parsed,
106
+ });
107
+ });
108
+ },
109
+ );
110
+
111
+ request.on("error", (error: Error) => {
112
+ return reject(error);
113
+ });
114
+
115
+ request.end();
116
+ },
117
+ );
118
+ };
119
+
120
+ describe("ANON ACCESS PROBE /microsoft-teams/chats", () => {
121
+ test("unauthenticated GET with only a tenantid header", async () => {
122
+ const MicrosoftTeamsAPI: any = (
123
+ await import("../../../Server/API/MicrosoftTeamsAPI")
124
+ ).default;
125
+
126
+ const app: express.Express = express();
127
+ app.use("/api", new MicrosoftTeamsAPI().getRouter());
128
+
129
+ const server: http.Server = http.createServer(app);
130
+ await new Promise<void>((resolve: () => void) => {
131
+ server.listen(0, resolve);
132
+ });
133
+
134
+ try {
135
+ const port: number = (server.address() as AddressInfo).port;
136
+
137
+ const result: HttpProbeResult = await httpGetJson({
138
+ port,
139
+ path: "/api/microsoft-teams/chats",
140
+ headers: {
141
+ tenantid: "6f4e4b2c-1111-4c2a-9e3d-abcdefabcdef",
142
+ },
143
+ });
144
+
145
+ /*
146
+ * The hardening in this branch requires an authenticated project member:
147
+ * getUserMiddleware admits unauthenticated requests as Public, so the
148
+ * route must reject one carrying only a tenantid header. A 200 here would
149
+ * mean the captured chat list leaks to anonymous callers - the exact
150
+ * regression this probe guards against.
151
+ */
152
+ expect(result.status).toBeGreaterThanOrEqual(400);
153
+ expect(result.status).toBeLessThan(500);
154
+
155
+ const serializedBody: string = JSON.stringify(result.body);
156
+ expect(serializedBody).not.toContain("Jane Doe");
157
+ expect(serializedBody).not.toContain("19:chat-abc");
158
+ expect(serializedBody).not.toContain("19:chat-def");
159
+ } finally {
160
+ await new Promise<void>((resolve: () => void) => {
161
+ server.close(() => {
162
+ return resolve();
163
+ });
164
+ });
165
+ }
166
+ }, 30000);
167
+ });
@@ -0,0 +1,255 @@
1
+ import Incident from "../../../Models/DatabaseModels/Incident";
2
+ import IncidentEpisode from "../../../Models/DatabaseModels/IncidentEpisode";
3
+ import IncidentEpisodePublicNote from "../../../Models/DatabaseModels/IncidentEpisodePublicNote";
4
+ import IncidentEpisodeStateTimeline from "../../../Models/DatabaseModels/IncidentEpisodeStateTimeline";
5
+ import IncidentPublicNote from "../../../Models/DatabaseModels/IncidentPublicNote";
6
+ import IncidentStateTimeline from "../../../Models/DatabaseModels/IncidentStateTimeline";
7
+ import Monitor from "../../../Models/DatabaseModels/Monitor";
8
+ import OnCallDutyPolicyExecutionLog from "../../../Models/DatabaseModels/OnCallDutyPolicyExecutionLog";
9
+ import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
10
+ import ScheduledMaintenancePublicNote from "../../../Models/DatabaseModels/ScheduledMaintenancePublicNote";
11
+ import ScheduledMaintenanceStateTimeline from "../../../Models/DatabaseModels/ScheduledMaintenanceStateTimeline";
12
+ import StatusPageAnnouncement from "../../../Models/DatabaseModels/StatusPageAnnouncement";
13
+ import { AddHotQueryIndexes1785140242697 } from "../../../Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes";
14
+ import SchemaMigrations from "../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
15
+ import { QueryRunner, getMetadataArgsStorage } from "typeorm";
16
+ import type { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
17
+ import { describe, expect, test } from "@jest/globals";
18
+
19
+ /*
20
+ * The 13 columns below are the filter columns of ~13 EVERY_MINUTE worker
21
+ * queries (the on-call executor crons scan OnCallDutyPolicyExecutionLog.status,
22
+ * the every-30s heartbeat crons branch on Monitor.monitorType, and the 11
23
+ * subscriberNotificationStatus* columns map 1:1 to the EVERY_MINUTE
24
+ * SendNotificationToSubscribers jobs that poll ever-growing tables for
25
+ * usually-zero Pending rows). Before AddHotQueryIndexes1785140242697 every one
26
+ * of those queries was a full-table seq scan.
27
+ *
28
+ * These tests pin the three pieces that must all hold for the scans to stay
29
+ * gone — a regression in ANY one silently reintroduces them:
30
+ * 1. the @Index() decorator on each entity property (keeps future
31
+ * schema:generate runs from emitting a DROP INDEX as "drift"),
32
+ * 2. the migration's SQL contract: up() creates exactly these 13 indexes
33
+ * and nothing else (no ALTER TABLE drift statements from a regenerated
34
+ * migration), down() drops exactly the indexes up() created,
35
+ * 3. the migration's registration in SchemaMigrations/Index.ts (an
36
+ * unregistered migration is dead code and never runs on boot).
37
+ *
38
+ * Pure metadata/mocks — no Postgres connection anywhere.
39
+ */
40
+
41
+ type ModelClass = { new (): unknown };
42
+
43
+ // [entity class, table name (=== class name for all of these), indexed column]
44
+ const HOT_INDEX_PAIRS: Array<[ModelClass, string, string]> = [
45
+ [Monitor, "Monitor", "monitorType"],
46
+ [OnCallDutyPolicyExecutionLog, "OnCallDutyPolicyExecutionLog", "status"],
47
+ [
48
+ IncidentStateTimeline,
49
+ "IncidentStateTimeline",
50
+ "subscriberNotificationStatus",
51
+ ],
52
+ [Incident, "Incident", "subscriberNotificationStatusOnIncidentCreated"],
53
+ [Incident, "Incident", "subscriberNotificationStatusOnPostmortemPublished"],
54
+ [
55
+ IncidentPublicNote,
56
+ "IncidentPublicNote",
57
+ "subscriberNotificationStatusOnNoteCreated",
58
+ ],
59
+ [
60
+ IncidentEpisode,
61
+ "IncidentEpisode",
62
+ "subscriberNotificationStatusOnEpisodeCreated",
63
+ ],
64
+ [
65
+ IncidentEpisodeStateTimeline,
66
+ "IncidentEpisodeStateTimeline",
67
+ "subscriberNotificationStatus",
68
+ ],
69
+ [
70
+ IncidentEpisodePublicNote,
71
+ "IncidentEpisodePublicNote",
72
+ "subscriberNotificationStatusOnNoteCreated",
73
+ ],
74
+ [
75
+ ScheduledMaintenance,
76
+ "ScheduledMaintenance",
77
+ "subscriberNotificationStatusOnEventScheduled",
78
+ ],
79
+ [
80
+ ScheduledMaintenanceStateTimeline,
81
+ "ScheduledMaintenanceStateTimeline",
82
+ "subscriberNotificationStatus",
83
+ ],
84
+ [
85
+ ScheduledMaintenancePublicNote,
86
+ "ScheduledMaintenancePublicNote",
87
+ "subscriberNotificationStatusOnNoteCreated",
88
+ ],
89
+ [
90
+ StatusPageAnnouncement,
91
+ "StatusPageAnnouncement",
92
+ "subscriberNotificationStatus",
93
+ ],
94
+ ];
95
+
96
+ /*
97
+ * IndexMetadataArgs.columns is either an array of property names (property-
98
+ * level @Index()) or a function over the entity's properties map (class-level
99
+ * @Index((o) => [o.a, o.b])). Resolve both to plain property-name arrays; the
100
+ * function form is fed a proxy whose every property reads as its own name,
101
+ * which is exactly how TypeORM's propertiesMap behaves for flat columns.
102
+ */
103
+ function resolveIndexColumns(
104
+ columns: IndexMetadataArgs["columns"],
105
+ ): Array<string> {
106
+ if (!columns) {
107
+ return [];
108
+ }
109
+ if (Array.isArray(columns)) {
110
+ return columns.map(String);
111
+ }
112
+ const propertiesMap: Record<string, string> = new Proxy(
113
+ {},
114
+ {
115
+ get: (_target: Record<string, string>, property: string | symbol) => {
116
+ return String(property);
117
+ },
118
+ },
119
+ );
120
+ const resolved: Array<unknown> | { [key: string]: number } =
121
+ columns(propertiesMap);
122
+ if (Array.isArray(resolved)) {
123
+ return resolved.map(String);
124
+ }
125
+ return Object.keys(resolved);
126
+ }
127
+
128
+ interface CreateIndexStatement {
129
+ indexName: string;
130
+ tableName: string;
131
+ columnName: string;
132
+ }
133
+
134
+ const CREATE_INDEX_REGEX: RegExp =
135
+ /^CREATE INDEX "([^"]+)" ON "([^"]+)" \("([^"]+)"\)\s*$/;
136
+ const DROP_INDEX_REGEX: RegExp = /^DROP INDEX "public"\."([^"]+)"$/;
137
+
138
+ function makeQueryRunner(): { runner: QueryRunner; query: jest.Mock } {
139
+ const query: jest.Mock = jest.fn().mockResolvedValue(undefined);
140
+ return { runner: { query } as unknown as QueryRunner, query };
141
+ }
142
+
143
+ function executedSql(query: jest.Mock): Array<string> {
144
+ return query.mock.calls.map((call: Array<unknown>) => {
145
+ return String(call[0]);
146
+ });
147
+ }
148
+
149
+ describe("AddHotQueryIndexes: entity @Index() decorators", () => {
150
+ test.each(
151
+ HOT_INDEX_PAIRS.map(
152
+ ([model, table, column]: [ModelClass, string, string]) => {
153
+ return [table, column, model] as [string, string, ModelClass];
154
+ },
155
+ ),
156
+ )(
157
+ "%s.%s has a single-column index decorator",
158
+ (_table: string, column: string, model: ModelClass) => {
159
+ const singleColumnIndexes: Array<string> = getMetadataArgsStorage()
160
+ .indices.filter((index: IndexMetadataArgs) => {
161
+ return index.target === model;
162
+ })
163
+ .map((index: IndexMetadataArgs) => {
164
+ return resolveIndexColumns(index.columns);
165
+ })
166
+ .filter((columns: Array<string>) => {
167
+ return columns.length === 1;
168
+ })
169
+ .map((columns: Array<string>) => {
170
+ return columns[0] as string;
171
+ });
172
+
173
+ expect(singleColumnIndexes).toContain(column);
174
+ },
175
+ );
176
+ });
177
+
178
+ describe("AddHotQueryIndexes1785140242697 migration SQL contract", () => {
179
+ const migration: AddHotQueryIndexes1785140242697 =
180
+ new AddHotQueryIndexes1785140242697();
181
+
182
+ test("up() issues exactly 13 CREATE INDEX statements covering exactly the 13 hot columns — and nothing else", async () => {
183
+ const { runner, query } = makeQueryRunner();
184
+ await migration.up(runner);
185
+
186
+ const statements: Array<string> = executedSql(query);
187
+ expect(statements).toHaveLength(13);
188
+
189
+ /*
190
+ * A regenerated migration on a drifted schema would interleave ALTER
191
+ * TABLE / DROP statements with the index creates; none may sneak in.
192
+ */
193
+ for (const sql of statements) {
194
+ expect(sql).not.toContain("ALTER TABLE");
195
+ expect(sql).not.toContain("DROP");
196
+ expect(sql).toMatch(CREATE_INDEX_REGEX);
197
+ }
198
+
199
+ const created: Array<CreateIndexStatement> = statements.map(
200
+ (sql: string) => {
201
+ const match: RegExpMatchArray = sql.match(
202
+ CREATE_INDEX_REGEX,
203
+ ) as RegExpMatchArray;
204
+ return {
205
+ indexName: match[1] as string,
206
+ tableName: match[2] as string,
207
+ columnName: match[3] as string,
208
+ };
209
+ },
210
+ );
211
+
212
+ const createdPairs: Array<string> = created
213
+ .map((statement: CreateIndexStatement) => {
214
+ return `"${statement.tableName}"."${statement.columnName}"`;
215
+ })
216
+ .sort();
217
+ const expectedPairs: Array<string> = HOT_INDEX_PAIRS.map(
218
+ ([, table, column]: [ModelClass, string, string]) => {
219
+ return `"${table}"."${column}"`;
220
+ },
221
+ ).sort();
222
+
223
+ expect(createdPairs).toEqual(expectedPairs);
224
+ });
225
+
226
+ test("down() drops exactly the 13 indexes up() created", async () => {
227
+ const upRunner: { runner: QueryRunner; query: jest.Mock } =
228
+ makeQueryRunner();
229
+ await migration.up(upRunner.runner);
230
+ const createdNames: Array<string> = executedSql(upRunner.query).map(
231
+ (sql: string) => {
232
+ return (sql.match(CREATE_INDEX_REGEX) as RegExpMatchArray)[1] as string;
233
+ },
234
+ );
235
+
236
+ const downRunner: { runner: QueryRunner; query: jest.Mock } =
237
+ makeQueryRunner();
238
+ await migration.down(downRunner.runner);
239
+ const statements: Array<string> = executedSql(downRunner.query);
240
+ expect(statements).toHaveLength(13);
241
+
242
+ const droppedNames: Array<string> = statements.map((sql: string) => {
243
+ expect(sql).toMatch(DROP_INDEX_REGEX);
244
+ return (sql.match(DROP_INDEX_REGEX) as RegExpMatchArray)[1] as string;
245
+ });
246
+
247
+ expect([...droppedNames].sort()).toEqual([...createdNames].sort());
248
+ });
249
+ });
250
+
251
+ describe("AddHotQueryIndexes1785140242697 registration", () => {
252
+ test("is registered in SchemaMigrations/Index.ts so it actually runs on boot", () => {
253
+ expect(SchemaMigrations).toContain(AddHotQueryIndexes1785140242697);
254
+ });
255
+ });