@oneuptime/common 12.0.7 → 12.0.9

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 (873) hide show
  1. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
  2. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
  3. package/Models/DatabaseModels/AIConversation.ts +32 -0
  4. package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
  5. package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
  6. package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
  7. package/Models/DatabaseModels/Index.ts +12 -4
  8. package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +184 -9
  9. package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
  10. package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +33 -7
  11. package/Models/DatabaseModels/NetworkDevice.ts +141 -0
  12. package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
  13. package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
  14. package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
  15. package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
  16. package/Models/DatabaseModels/Project.ts +78 -0
  17. package/Models/DatabaseModels/UserCall.ts +42 -0
  18. package/Models/DatabaseModels/UserEmail.ts +44 -0
  19. package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
  20. package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
  21. package/Models/DatabaseModels/UserPush.ts +49 -0
  22. package/Models/DatabaseModels/UserSMS.ts +43 -0
  23. package/Models/DatabaseModels/UserTelegram.ts +59 -0
  24. package/Models/DatabaseModels/UserWebhook.ts +52 -0
  25. package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
  26. package/Models/DatabaseModels/WorkflowLog.ts +38 -0
  27. package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
  28. package/Server/API/AIChatAPI.ts +315 -1
  29. package/Server/API/DashboardAPI.ts +217 -1
  30. package/Server/API/OnCallReadinessAPI.ts +841 -0
  31. package/Server/API/TeamComplianceAPI.ts +69 -17
  32. package/Server/API/TelemetryAPI.ts +220 -12
  33. package/Server/API/UserAPI.ts +16 -1
  34. package/Server/EnvironmentConfig.ts +52 -0
  35. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
  36. package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
  37. package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
  38. package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
  39. package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
  40. package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
  41. package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
  42. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
  43. package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
  44. package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
  45. package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
  46. package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
  47. package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
  48. package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
  49. package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
  50. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +24 -0
  51. package/Server/Infrastructure/Queue.ts +78 -13
  52. package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
  53. package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
  54. package/Server/Services/AIService.ts +7 -0
  55. package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
  56. package/Server/Services/AlertSeverityService.ts +63 -0
  57. package/Server/Services/DashboardService.ts +9 -10
  58. package/Server/Services/DatabaseService.ts +32 -2
  59. package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
  60. package/Server/Services/IncidentSeverityService.ts +76 -0
  61. package/Server/Services/Index.ts +12 -4
  62. package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
  63. package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +7 -4
  64. package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +203 -21
  65. package/Server/Services/LogAggregationService.ts +45 -8
  66. package/Server/Services/MetricAggregationService.ts +121 -0
  67. package/Server/Services/MetricService.ts +7 -7
  68. package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
  69. package/Server/Services/NetworkDeviceLinkService.ts +84 -0
  70. package/Server/Services/NetworkDeviceService.ts +140 -0
  71. package/Server/Services/NetworkSiteService.ts +77 -25
  72. package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
  73. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
  74. package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
  75. package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
  76. package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
  77. package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
  78. package/Server/Services/OnCallReadinessService.ts +2803 -0
  79. package/Server/Services/OnCallSetupReminderService.ts +955 -0
  80. package/Server/Services/ProfileAggregationService.ts +123 -0
  81. package/Server/Services/StatusPageService.ts +9 -10
  82. package/Server/Services/TeamComplianceService.ts +429 -252
  83. package/Server/Services/UserCallService.ts +26 -1
  84. package/Server/Services/UserEmailService.ts +26 -1
  85. package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
  86. package/Server/Services/UserNotificationRuleService.ts +3812 -333
  87. package/Server/Services/UserOnCallLogService.ts +561 -48
  88. package/Server/Services/UserPushService.ts +29 -0
  89. package/Server/Services/UserService.ts +11 -0
  90. package/Server/Services/UserSmsService.ts +26 -1
  91. package/Server/Services/UserTelegramService.ts +24 -1
  92. package/Server/Services/UserWebhookService.ts +28 -1
  93. package/Server/Services/UserWhatsAppService.ts +24 -1
  94. package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
  95. package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
  96. package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
  97. package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
  98. package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
  99. package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
  100. package/Server/Types/Workflow/Components/API/Get.ts +1 -1
  101. package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
  102. package/Server/Types/Workflow/Components/API/Post.ts +1 -1
  103. package/Server/Types/Workflow/Components/API/Put.ts +1 -1
  104. package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
  105. package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
  106. package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
  107. package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
  108. package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
  109. package/Server/Types/Workflow/Components/Email.ts +25 -7
  110. package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
  111. package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
  112. package/Server/Types/Workflow/TriggerCode.ts +12 -0
  113. package/Server/Types/Workflow/Workflow.ts +5 -0
  114. package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
  115. package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
  116. package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
  117. package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
  118. package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
  119. package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
  120. package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
  121. package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
  122. package/Server/Utils/AI/Toolbox/Index.ts +48 -0
  123. package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
  124. package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
  125. package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
  126. package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
  127. package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
  128. package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
  129. package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
  130. package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
  131. package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
  132. package/Server/Utils/ClientIp.ts +221 -0
  133. package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
  134. package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
  135. package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
  136. package/Server/Utils/Express.ts +12 -17
  137. package/Server/Utils/LLM/LLMService.ts +85 -8
  138. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
  139. package/Server/Utils/SSRFProtection.ts +98 -23
  140. package/Server/Utils/StartServer.ts +12 -3
  141. package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
  142. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
  143. package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
  144. package/Server/Utils/VM/VMAPI.ts +56 -8
  145. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
  146. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
  147. package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
  148. package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
  149. package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
  150. package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
  151. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
  152. package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
  153. package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
  154. package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
  155. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
  156. package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
  157. package/Tests/Models/InventoryItemModel.test.ts +174 -0
  158. package/Tests/Models/InventoryItemNaming.test.ts +302 -0
  159. package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
  160. package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
  161. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
  162. package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
  163. package/Tests/Server/API/Helpers.ts +24 -15
  164. package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
  165. package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
  166. package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
  167. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
  168. package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
  169. package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
  170. package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
  171. package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
  172. package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
  173. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
  174. package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
  175. package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
  176. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
  177. package/Tests/Server/Services/InventoryItemManualCreate.test.ts +248 -0
  178. package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
  179. package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
  180. package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
  181. package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
  182. package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
  183. package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
  184. package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
  185. package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
  186. package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
  187. package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
  188. package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
  189. package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
  190. package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
  191. package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
  192. package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
  193. package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
  194. package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
  195. package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
  196. package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
  197. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
  198. package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
  199. package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
  200. package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
  201. package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
  202. package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
  203. package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
  204. package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
  205. package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
  206. package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
  207. package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
  208. package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
  209. package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
  210. package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
  211. package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
  212. package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
  213. package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
  214. package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
  215. package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
  216. package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
  217. package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
  218. package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
  219. package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
  220. package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
  221. package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
  222. package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
  223. package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
  224. package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
  225. package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
  226. package/Tests/Server/Utils/ClientIp.test.ts +438 -0
  227. package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
  228. package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
  229. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
  230. package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
  231. package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
  232. package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
  233. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
  234. package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
  235. package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
  236. package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
  237. package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
  238. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
  239. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
  240. package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
  241. package/Tests/Types/IP/IP.test.ts +263 -0
  242. package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
  243. package/Tests/Types/IP/IPv6.test.ts +12 -1
  244. package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
  245. package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
  246. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
  247. package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
  248. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
  249. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
  250. package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
  251. package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
  252. package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
  253. package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
  254. package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
  255. package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
  256. package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
  257. package/Tests/Types/Workflow/TemplateSyntax.test.ts +491 -0
  258. package/Tests/Types/Workflow/Templates.test.ts +1218 -0
  259. package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
  260. package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
  261. package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
  262. package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
  263. package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
  264. package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
  265. package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
  266. package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
  267. package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
  268. package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
  269. package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
  270. package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
  271. package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
  272. package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
  273. package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
  274. package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
  275. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
  276. package/Tests/UI/Components/Workflow/GraphLint.test.ts +893 -0
  277. package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
  278. package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +522 -0
  279. package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +193 -0
  280. package/Tests/UI/Components/Workflow/ModelSchema.test.ts +388 -0
  281. package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +210 -0
  282. package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
  283. package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
  284. package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
  285. package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
  286. package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
  287. package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
  288. package/Tests/UI/EsbuildConfig.test.ts +607 -0
  289. package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
  290. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
  291. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
  292. package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
  293. package/Tests/UI/Utils/Platform.test.ts +147 -0
  294. package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
  295. package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
  296. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
  297. package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +727 -0
  298. package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
  299. package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
  300. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
  301. package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
  302. package/Tests/__mocks__/bullmq.js +55 -0
  303. package/Types/AI/AIChatMessageStatus.ts +8 -1
  304. package/Types/AI/AIChatTypes.ts +12 -0
  305. package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
  306. package/Types/Exception/ExceptionCode.ts +2 -0
  307. package/Types/Exception/ServiceUnavailableException.ts +8 -0
  308. package/Types/Exception/TooManyRequestsException.ts +8 -0
  309. package/Types/IP/IP.ts +93 -47
  310. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +80 -3
  311. package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
  312. package/Types/OnCallDutyPolicy/Layer.ts +203 -149
  313. package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
  314. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
  315. package/Types/Permission.ts +193 -0
  316. package/Types/SerializableObjectDictionary.ts +133 -39
  317. package/Types/Telemetry/EntityRelationshipType.ts +1 -1
  318. package/Types/Telemetry/EntitySource.ts +40 -0
  319. package/Types/Telemetry/EntityType.ts +28 -1
  320. package/Types/Telemetry/EntityTypeGroups.ts +65 -0
  321. package/Types/Workflow/Component.ts +29 -0
  322. package/Types/Workflow/Components/API.ts +35 -0
  323. package/Types/Workflow/Components/BaseModel.ts +77 -27
  324. package/Types/Workflow/Components/Discord.ts +1 -0
  325. package/Types/Workflow/Components/Email.ts +12 -3
  326. package/Types/Workflow/Components/JavaScript.ts +7 -0
  327. package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
  328. package/Types/Workflow/Components/Slack.ts +1 -0
  329. package/Types/Workflow/Components/Telegram.ts +1 -0
  330. package/Types/Workflow/StepTrace.ts +181 -0
  331. package/Types/Workflow/TemplateSyntax.ts +543 -0
  332. package/Types/Workflow/Templates.ts +2368 -0
  333. package/UI/Components/Calendar/Calendar.css +43 -0
  334. package/UI/Components/Calendar/Calendar.tsx +8 -0
  335. package/UI/Components/Card/Card.tsx +2 -2
  336. package/UI/Components/Checkbox/Checkbox.tsx +16 -0
  337. package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
  338. package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
  339. package/UI/Components/Dictionary/Dictionary.tsx +71 -15
  340. package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
  341. package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
  342. package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
  343. package/UI/Components/Forms/Types/Field.ts +7 -0
  344. package/UI/Components/Forms/Validation.ts +63 -1
  345. package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
  346. package/UI/Components/Input/Input.tsx +32 -3
  347. package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
  348. package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
  349. package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
  350. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
  351. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
  352. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
  353. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
  354. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
  355. package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
  356. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
  357. package/UI/Components/LogsViewer/types.ts +23 -0
  358. package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
  359. package/UI/Components/Modal/Modal.tsx +37 -4
  360. package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
  361. package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
  362. package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
  363. package/UI/Components/Table/Table.tsx +49 -16
  364. package/UI/Components/Table/TableBody.tsx +53 -28
  365. package/UI/Components/Table/TableHeader.tsx +13 -0
  366. package/UI/Components/Table/TableRow.tsx +58 -26
  367. package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
  368. package/UI/Components/Workflow/ArgumentsForm.tsx +341 -8
  369. package/UI/Components/Workflow/Component.tsx +28 -26
  370. package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
  371. package/UI/Components/Workflow/ComponentSettingsModal.tsx +79 -9
  372. package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
  373. package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
  374. package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
  375. package/UI/Components/Workflow/GraphLint.ts +628 -0
  376. package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
  377. package/UI/Components/Workflow/ModelColumnEditor.tsx +528 -0
  378. package/UI/Components/Workflow/ModelSchema.ts +278 -0
  379. package/UI/Components/Workflow/RunForm.tsx +41 -7
  380. package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
  381. package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
  382. package/UI/Components/Workflow/UseRunWatch.ts +212 -0
  383. package/UI/Components/Workflow/Utils.ts +93 -1
  384. package/UI/Components/Workflow/VariableModal.tsx +6 -2
  385. package/UI/Components/Workflow/Workflow.tsx +126 -7
  386. package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
  387. package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
  388. package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
  389. package/UI/Types/LayeredDismissal.ts +31 -0
  390. package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
  391. package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
  392. package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
  393. package/UI/Utils/Platform.ts +149 -0
  394. package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
  395. package/UI/esbuild-config.js +22 -1
  396. package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
  397. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
  398. package/Utils/Monitor/NetworkTopologyUtil.ts +917 -155
  399. package/Utils/Telemetry/CrossSignalScope.ts +502 -0
  400. package/Utils/Telemetry/EntityKey.ts +71 -5
  401. package/Utils/Telemetry/EntityRelationship.ts +1 -1
  402. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
  403. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
  404. package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
  405. package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
  406. package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
  407. package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
  408. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
  409. package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
  410. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
  411. package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
  412. package/build/dist/Models/DatabaseModels/Index.js +12 -4
  413. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  414. package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +212 -29
  415. package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
  416. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
  417. package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
  418. package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +52 -25
  419. package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
  420. package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
  421. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  422. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
  423. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
  424. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
  425. package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
  426. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
  427. package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
  428. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
  429. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
  430. package/build/dist/Models/DatabaseModels/Project.js +80 -0
  431. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  432. package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
  433. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  434. package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
  435. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  436. package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
  437. package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
  438. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
  439. package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
  440. package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
  441. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  442. package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
  443. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  444. package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
  445. package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
  446. package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
  447. package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
  448. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
  449. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
  451. package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
  453. package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
  454. package/build/dist/Server/API/AIChatAPI.js +237 -1
  455. package/build/dist/Server/API/AIChatAPI.js.map +1 -1
  456. package/build/dist/Server/API/DashboardAPI.js +165 -13
  457. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  458. package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
  459. package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
  460. package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
  461. package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
  462. package/build/dist/Server/API/TelemetryAPI.js +129 -18
  463. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  464. package/build/dist/Server/API/UserAPI.js +16 -1
  465. package/build/dist/Server/API/UserAPI.js.map +1 -1
  466. package/build/dist/Server/EnvironmentConfig.js +45 -0
  467. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  468. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
  469. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  470. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
  471. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
  472. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
  473. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
  474. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
  475. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
  476. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
  477. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
  478. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
  479. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
  480. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
  481. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
  482. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
  483. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
  484. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
  495. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +24 -0
  496. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  497. package/build/dist/Server/Infrastructure/Queue.js +72 -13
  498. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  499. package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
  500. package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
  501. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
  502. package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
  503. package/build/dist/Server/Services/AIService.js +1 -0
  504. package/build/dist/Server/Services/AIService.js.map +1 -1
  505. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
  506. package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
  507. package/build/dist/Server/Services/AlertSeverityService.js +54 -0
  508. package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
  509. package/build/dist/Server/Services/DashboardService.js +10 -9
  510. package/build/dist/Server/Services/DashboardService.js.map +1 -1
  511. package/build/dist/Server/Services/DatabaseService.js +24 -2
  512. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  513. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
  514. package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
  515. package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
  516. package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
  517. package/build/dist/Server/Services/Index.js +12 -4
  518. package/build/dist/Server/Services/Index.js.map +1 -1
  519. package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
  520. package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
  521. package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +9 -6
  522. package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
  523. package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +158 -23
  524. package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
  525. package/build/dist/Server/Services/LogAggregationService.js +27 -8
  526. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  527. package/build/dist/Server/Services/MetricAggregationService.js +80 -0
  528. package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
  529. package/build/dist/Server/Services/MetricService.js +6 -6
  530. package/build/dist/Server/Services/MetricService.js.map +1 -1
  531. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
  532. package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
  533. package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
  534. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
  535. package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
  536. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  537. package/build/dist/Server/Services/NetworkSiteService.js +53 -13
  538. package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
  539. package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
  540. package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
  541. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
  542. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  543. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
  544. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
  545. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
  546. package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
  547. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
  548. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  549. package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
  550. package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
  551. package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
  552. package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
  553. package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
  554. package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
  555. package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
  556. package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
  557. package/build/dist/Server/Services/StatusPageService.js +11 -10
  558. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  559. package/build/dist/Server/Services/TeamComplianceService.js +312 -160
  560. package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
  561. package/build/dist/Server/Services/UserCallService.js +24 -1
  562. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  563. package/build/dist/Server/Services/UserEmailService.js +24 -1
  564. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  565. package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
  566. package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
  567. package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
  568. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  569. package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
  570. package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
  571. package/build/dist/Server/Services/UserPushService.js +26 -0
  572. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  573. package/build/dist/Server/Services/UserService.js +10 -0
  574. package/build/dist/Server/Services/UserService.js.map +1 -1
  575. package/build/dist/Server/Services/UserSmsService.js +24 -1
  576. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  577. package/build/dist/Server/Services/UserTelegramService.js +22 -1
  578. package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
  579. package/build/dist/Server/Services/UserWebhookService.js +25 -1
  580. package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
  581. package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
  582. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  583. package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
  584. package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
  585. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
  586. package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
  587. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
  588. package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
  589. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
  590. package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
  591. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
  592. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  593. package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
  594. package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
  595. package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
  596. package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
  597. package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
  598. package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
  599. package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
  600. package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
  601. package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
  602. package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
  603. package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
  604. package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
  605. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
  606. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
  607. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
  608. package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
  609. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
  610. package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
  611. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
  612. package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
  613. package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
  614. package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
  615. package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
  616. package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
  617. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
  618. package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
  619. package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
  620. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
  621. package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
  622. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
  623. package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
  624. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
  625. package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
  626. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
  627. package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
  628. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
  629. package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
  630. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
  631. package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
  632. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
  633. package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
  634. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
  635. package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
  636. package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
  637. package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
  638. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
  639. package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
  640. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
  641. package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
  642. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
  643. package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
  644. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
  645. package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
  646. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
  647. package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
  648. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
  649. package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
  650. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
  651. package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
  652. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
  653. package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
  654. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
  655. package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
  656. package/build/dist/Server/Utils/ClientIp.js +137 -0
  657. package/build/dist/Server/Utils/ClientIp.js.map +1 -0
  658. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
  659. package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
  660. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
  661. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
  662. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
  663. package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
  664. package/build/dist/Server/Utils/Express.js +12 -12
  665. package/build/dist/Server/Utils/Express.js.map +1 -1
  666. package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
  667. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  668. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
  669. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  670. package/build/dist/Server/Utils/SSRFProtection.js +82 -21
  671. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  672. package/build/dist/Server/Utils/StartServer.js +12 -4
  673. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  674. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
  675. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  676. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
  677. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
  678. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
  679. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  680. package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
  681. package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
  682. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
  683. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  684. package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
  685. package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
  686. package/build/dist/Types/AI/AIChatTypes.js +12 -0
  687. package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
  688. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
  689. package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
  690. package/build/dist/Types/Exception/ExceptionCode.js +2 -0
  691. package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
  692. package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
  693. package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
  694. package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
  695. package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
  696. package/build/dist/Types/IP/IP.js +87 -43
  697. package/build/dist/Types/IP/IP.js.map +1 -1
  698. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
  699. package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
  700. package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
  701. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  702. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
  703. package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
  704. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
  705. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  706. package/build/dist/Types/Permission.js +174 -0
  707. package/build/dist/Types/Permission.js.map +1 -1
  708. package/build/dist/Types/SerializableObjectDictionary.js +133 -39
  709. package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
  710. package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
  711. package/build/dist/Types/Telemetry/EntitySource.js +39 -0
  712. package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
  713. package/build/dist/Types/Telemetry/EntityType.js +28 -1
  714. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  715. package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
  716. package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
  717. package/build/dist/Types/Workflow/Component.js +21 -0
  718. package/build/dist/Types/Workflow/Component.js.map +1 -1
  719. package/build/dist/Types/Workflow/Components/API.js +35 -0
  720. package/build/dist/Types/Workflow/Components/API.js.map +1 -1
  721. package/build/dist/Types/Workflow/Components/BaseModel.js +68 -26
  722. package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
  723. package/build/dist/Types/Workflow/Components/Discord.js +1 -0
  724. package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
  725. package/build/dist/Types/Workflow/Components/Email.js +12 -3
  726. package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
  727. package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
  728. package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
  729. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
  730. package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
  731. package/build/dist/Types/Workflow/Components/Slack.js +1 -0
  732. package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
  733. package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
  734. package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
  735. package/build/dist/Types/Workflow/StepTrace.js +104 -0
  736. package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
  737. package/build/dist/Types/Workflow/TemplateSyntax.js +338 -0
  738. package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
  739. package/build/dist/Types/Workflow/Templates.js +2111 -0
  740. package/build/dist/Types/Workflow/Templates.js.map +1 -0
  741. package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
  742. package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
  743. package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
  744. package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
  745. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
  746. package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
  747. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
  748. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
  749. package/build/dist/UI/Components/Dictionary/Dictionary.js +47 -17
  750. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  751. package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
  752. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
  753. package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
  754. package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
  755. package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
  756. package/build/dist/UI/Components/Forms/Validation.js +44 -0
  757. package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
  758. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
  759. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  760. package/build/dist/UI/Components/Input/Input.js +13 -4
  761. package/build/dist/UI/Components/Input/Input.js.map +1 -1
  762. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
  763. package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
  764. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
  765. package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
  766. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
  767. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  768. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
  769. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
  770. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
  771. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
  772. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
  773. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  774. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
  775. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
  776. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
  777. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  778. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
  779. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  780. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
  781. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  782. package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
  783. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
  784. package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
  785. package/build/dist/UI/Components/Modal/Modal.js +31 -5
  786. package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
  787. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
  788. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  789. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
  790. package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
  791. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
  792. package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
  793. package/build/dist/UI/Components/Table/Table.js +27 -15
  794. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  795. package/build/dist/UI/Components/Table/TableBody.js +24 -18
  796. package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
  797. package/build/dist/UI/Components/Table/TableHeader.js +9 -1
  798. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  799. package/build/dist/UI/Components/Table/TableRow.js +29 -21
  800. package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
  801. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
  802. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
  803. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +261 -14
  804. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  805. package/build/dist/UI/Components/Workflow/Component.js +20 -19
  806. package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
  807. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
  808. package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
  809. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +35 -7
  810. package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
  811. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
  812. package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
  813. package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
  814. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  815. package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
  816. package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
  817. package/build/dist/UI/Components/Workflow/GraphLint.js +425 -0
  818. package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
  819. package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
  820. package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
  821. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +320 -0
  822. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
  823. package/build/dist/UI/Components/Workflow/ModelSchema.js +156 -0
  824. package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
  825. package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
  826. package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
  827. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
  828. package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
  829. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
  830. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
  831. package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
  832. package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
  833. package/build/dist/UI/Components/Workflow/Utils.js +73 -1
  834. package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
  835. package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
  836. package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
  837. package/build/dist/UI/Components/Workflow/Workflow.js +92 -7
  838. package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
  839. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
  840. package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
  841. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
  842. package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
  843. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
  844. package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
  845. package/build/dist/UI/Types/LayeredDismissal.js +21 -0
  846. package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
  847. package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
  848. package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
  849. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
  850. package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
  851. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
  852. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  853. package/build/dist/UI/Utils/Platform.js +118 -0
  854. package/build/dist/UI/Utils/Platform.js.map +1 -0
  855. package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
  856. package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
  857. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
  858. package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
  859. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
  860. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
  861. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +661 -126
  862. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  863. package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
  864. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
  865. package/build/dist/Utils/Telemetry/EntityKey.js +57 -5
  866. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  867. package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
  868. package/jest.config.json +1 -0
  869. package/package.json +1 -1
  870. package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
  871. package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
  872. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
  873. package/build/dist/Server/Services/TelemetryEntityService.js.map +0 -1
@@ -0,0 +1,1802 @@
1
+ import RepairEpisodeNotificationRuleSeverity from "../../../../App/FeatureSet/Workers/DataMigrations/RepairEpisodeNotificationRuleSeverity";
2
+ import AlertSeverityService from "../../../Server/Services/AlertSeverityService";
3
+ import IncidentSeverityService from "../../../Server/Services/IncidentSeverityService";
4
+ import UserNotificationRuleService, {
5
+ NotificationMethodDescriptor,
6
+ } from "../../../Server/Services/UserNotificationRuleService";
7
+ import CreateBy from "../../../Server/Types/Database/CreateBy";
8
+ import logger from "../../../Server/Utils/Logger";
9
+ import AlertSeverity from "../../../Models/DatabaseModels/AlertSeverity";
10
+ import IncidentSeverity from "../../../Models/DatabaseModels/IncidentSeverity";
11
+ import Model from "../../../Models/DatabaseModels/UserNotificationRule";
12
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
13
+ import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
14
+ import NotificationRuleType from "../../../Types/NotificationRule/NotificationRuleType";
15
+ import ObjectID from "../../../Types/ObjectID";
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import { FindOperator } from "typeorm";
19
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
20
+
21
+ /*
22
+ * GAP G: the episode notification rules that pointed at no severity.
23
+ *
24
+ * A UserNotificationRule is matched to a page by (ruleType x severity). The
25
+ * on-call path counts them with a CONCRETE severity id in the query -
26
+ * UserOnCallLogService reads `alertEpisode.alertSeverityId` /
27
+ * `incidentEpisode.incidentSeverityId` and hands it straight to countBy - and
28
+ * `NULL = '<uuid>'` is never true in SQL. So a rule with a NULL severity id is
29
+ * not "a rule that matches everything"; it is a rule that matches NOTHING.
30
+ *
31
+ * That is what the defaults used to create. `createIncidentOnCallRules` and
32
+ * `createAlertOnCallRules` seeded their two rule types once per severity, but
33
+ * the two EPISODE rule types went through `createSingleRule`, which sets only
34
+ * `ruleType` and `notifyAfterMinutes`. Every responder who took the defaults -
35
+ * which is everyone who never opened User Settings - therefore had two episode
36
+ * "rules" that could not be reached by any alert-episode or incident-episode
37
+ * page. The system said they were covered. They were not.
38
+ *
39
+ * This file covers both halves of the fix.
40
+ *
41
+ * HALF 1 - creation.
42
+ * `addDefaultNotificationRulesForVerifiedMethod` now seeds the two episode
43
+ * rule types per severity, alert episodes against AlertSeverity and incident
44
+ * episodes against IncidentSeverity. The severity MODEL each type is scoped
45
+ * by is asserted explicitly and with disjoint, differently-sized id sets,
46
+ * because crossing the two lists is the natural way to get this wrong and it
47
+ * fails silently: alert severity ids on incident-episode rules produce rows
48
+ * that are perfectly well-formed, perfectly visible, and still match no page
49
+ * ever. WHEN_USER_GOES_ON_CALL and WHEN_USER_GOES_OFF_CALL keep going
50
+ * through the single-rule path with no severity at all, because they are
51
+ * about the responder's shift rather than about anything that fired - they
52
+ * are the two rule types for which severity-less is CORRECT, and conflating
53
+ * them with the episode types is the mirror-image mistake.
54
+ *
55
+ * HALF 2 - repair. `RepairEpisodeNotificationRuleSeverity` (a data migration
56
+ * in App/FeatureSet/Workers/DataMigrations, registered in that folder's
57
+ * Index.ts) fixes the rows already written: each NULL-severity episode rule
58
+ * becomes one row per severity in its project, preserving the notification
59
+ * method FK and `notifyAfterMinutes` so the responder's stated intent -
60
+ * "reach me HERE, after THIS long" - survives the repair.
61
+ *
62
+ * WHY THE MIGRATION DELETES THE NULL ORIGINALS RATHER THAN LEAVING THEM.
63
+ * Leaving a row is normally the conservative choice, so the opposite call is
64
+ * worth stating precisely. These rows are unreachable AND invisible at the
65
+ * same time:
66
+ *
67
+ * 1. Unreachable. The count query the on-call path runs always supplies a
68
+ * concrete severity id, so a NULL row can never be counted, can never be
69
+ * executed, and can never page anybody. That is true by construction, not
70
+ * merely likely.
71
+ * 2. Invisible. Both episode rule pages in the dashboard
72
+ * (UserSettings/EpisodeOnCallRules.tsx and
73
+ * UserSettings/IncidentEpisodeOnCallRules.tsx) scope their ModelTable by
74
+ * a severity id, exactly as the non-episode pages do. A row with no
75
+ * severity renders in no table on any page.
76
+ *
77
+ * So the user can neither be paged by the row nor see it nor delete it. Left
78
+ * behind it is pure clutter that only ever surfaces as an inflated rule count
79
+ * in some future readiness or compliance number - and the replacement rows
80
+ * carry everything it expressed. Deleting loses nothing; keeping costs a lie.
81
+ *
82
+ * The migration's most dangerous possible over-reach is the opposite of its
83
+ * job: sweeping up the shift rules, which are legitimately severity-less and
84
+ * whose fan-out would create rules that fire on every severity change of a
85
+ * responder's roster. That case, and the "already has a severity" and
86
+ * "not an episode rule type" cases, are all pinned below.
87
+ *
88
+ * Nothing here touches Postgres. Half 1 stubs the two severity services and
89
+ * the rule service's own findOneBy/create. Half 2 runs the migration against
90
+ * an in-memory stand-in for the user_notification_rule table that honours the
91
+ * query's rule type and NULL-severity filter, the `_id ASC` ordering and the
92
+ * skip/limit window - so the paging cursor, the drain condition and the
93
+ * idempotence are exercised for real rather than asserted against a script of
94
+ * canned pages.
95
+ */
96
+
97
+ /*
98
+ * ------------------------------------------------------------------ *
99
+ * Fixtures shared by both halves.
100
+ * ------------------------------------------------------------------
101
+ */
102
+
103
+ const PROJECT_A: ObjectID = new ObjectID(
104
+ "11111111-1111-4111-8111-111111111111",
105
+ );
106
+ const PROJECT_B: ObjectID = new ObjectID(
107
+ "11111111-2222-4111-8111-111111111111",
108
+ );
109
+ const PROJECT_NO_SEVERITIES: ObjectID = new ObjectID(
110
+ "11111111-3333-4111-8111-111111111111",
111
+ );
112
+
113
+ const USER_ID: ObjectID = new ObjectID("22222222-2222-4222-8222-222222222222");
114
+ const OTHER_USER_ID: ObjectID = new ObjectID(
115
+ "22222222-3333-4222-8222-222222222222",
116
+ );
117
+
118
+ /*
119
+ * Deliberately disjoint id sets, and deliberately different sizes (3 incident
120
+ * vs 2 alert). A fan-out that reached for the wrong severity list would still
121
+ * produce plausible-looking rows, so the count is the second, independent
122
+ * signal that the right list was used.
123
+ */
124
+ const INCIDENT_SEV_1: ObjectID = new ObjectID(
125
+ "aaaaaaa1-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
126
+ );
127
+ const INCIDENT_SEV_2: ObjectID = new ObjectID(
128
+ "aaaaaaa2-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
129
+ );
130
+ const INCIDENT_SEV_3: ObjectID = new ObjectID(
131
+ "aaaaaaa3-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
132
+ );
133
+
134
+ const ALERT_SEV_1: ObjectID = new ObjectID(
135
+ "bbbbbbb1-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
136
+ );
137
+ const ALERT_SEV_2: ObjectID = new ObjectID(
138
+ "bbbbbbb2-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
139
+ );
140
+
141
+ /* Project B's severities, so "fans out over ITS OWN project" is observable. */
142
+ const PROJECT_B_INCIDENT_SEV: ObjectID = new ObjectID(
143
+ "aaaaaaa9-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
144
+ );
145
+ const PROJECT_B_ALERT_SEV: ObjectID = new ObjectID(
146
+ "bbbbbbb9-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
147
+ );
148
+
149
+ const INCIDENT_SEVERITY_IDS: Array<ObjectID> = [
150
+ INCIDENT_SEV_1,
151
+ INCIDENT_SEV_2,
152
+ INCIDENT_SEV_3,
153
+ ];
154
+
155
+ const ALERT_SEVERITY_IDS: Array<ObjectID> = [ALERT_SEV_1, ALERT_SEV_2];
156
+
157
+ const EMAIL_METHOD_ID: ObjectID = new ObjectID(
158
+ "ccccccc1-cccc-4ccc-8ccc-cccccccccccc",
159
+ );
160
+ const TELEGRAM_METHOD_ID: ObjectID = new ObjectID(
161
+ "ccccccc2-cccc-4ccc-8ccc-cccccccccccc",
162
+ );
163
+
164
+ /* The seven FK columns a rule can name a notification method through. */
165
+ type MethodColumn =
166
+ | "userEmailId"
167
+ | "userSmsId"
168
+ | "userCallId"
169
+ | "userWhatsAppId"
170
+ | "userTelegramId"
171
+ | "userWebhookId"
172
+ | "userPushId";
173
+
174
+ const ALL_METHOD_COLUMNS: Array<MethodColumn> = [
175
+ "userEmailId",
176
+ "userSmsId",
177
+ "userCallId",
178
+ "userWhatsAppId",
179
+ "userTelegramId",
180
+ "userWebhookId",
181
+ "userPushId",
182
+ ];
183
+
184
+ const EPISODE_RULE_TYPES: Array<NotificationRuleType> = [
185
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
186
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
187
+ ];
188
+
189
+ /* The two rule types for which "no severity" is the correct answer. */
190
+ const SHIFT_RULE_TYPES: Array<NotificationRuleType> = [
191
+ NotificationRuleType.WHEN_USER_GOES_ON_CALL,
192
+ NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
193
+ ];
194
+
195
+ function makeAlertSeverity(id: ObjectID): AlertSeverity {
196
+ const severity: AlertSeverity = new AlertSeverity(id);
197
+ severity._id = id.toString();
198
+
199
+ return severity;
200
+ }
201
+
202
+ function makeIncidentSeverity(id: ObjectID): IncidentSeverity {
203
+ const severity: IncidentSeverity = new IncidentSeverity(id);
204
+ severity._id = id.toString();
205
+
206
+ return severity;
207
+ }
208
+
209
+ function toStrings(ids: Array<ObjectID>): Array<string> {
210
+ return ids.map((id: ObjectID): string => {
211
+ return id.toString();
212
+ });
213
+ }
214
+
215
+ /*
216
+ * ------------------------------------------------------------------ *
217
+ * HALF 1 - what the defaults create.
218
+ * ------------------------------------------------------------------
219
+ */
220
+
221
+ describe("GAP G half 1 - addDefaultNotificationRulesForVerifiedMethod seeds episode rules per severity", () => {
222
+ let seedCreateSpy: jest.SpyInstance;
223
+ let seedFindOneBySpy: jest.SpyInstance;
224
+ let seedAlertSeveritiesSpy: jest.SpyInstance;
225
+ let seedIncidentSeveritiesSpy: jest.SpyInstance;
226
+
227
+ function seededRules(): Array<Model> {
228
+ return seedCreateSpy.mock.calls.map((call: Array<unknown>): Model => {
229
+ return (call[0] as CreateBy<Model>).data;
230
+ });
231
+ }
232
+
233
+ function seededRulesOfType(ruleType: NotificationRuleType): Array<Model> {
234
+ return seededRules().filter((rule: Model): boolean => {
235
+ return rule.ruleType === ruleType;
236
+ });
237
+ }
238
+
239
+ function duplicateCheckQueries(): Array<Record<string, unknown>> {
240
+ return seedFindOneBySpy.mock.calls.map(
241
+ (call: Array<unknown>): Record<string, unknown> => {
242
+ return (call[0] as { query: Record<string, unknown> }).query;
243
+ },
244
+ );
245
+ }
246
+
247
+ function seedFor(
248
+ notificationMethod: NotificationMethodDescriptor,
249
+ ): Promise<void> {
250
+ return UserNotificationRuleService.addDefaultNotificationRulesForVerifiedMethod(
251
+ {
252
+ projectId: PROJECT_A,
253
+ userId: USER_ID,
254
+ notificationMethod: notificationMethod,
255
+ },
256
+ );
257
+ }
258
+
259
+ function seedForEmail(): Promise<void> {
260
+ return seedFor({ userEmailId: EMAIL_METHOD_ID });
261
+ }
262
+
263
+ beforeEach(() => {
264
+ seedIncidentSeveritiesSpy = jest
265
+ .spyOn(IncidentSeverityService, "findBy")
266
+ .mockResolvedValue(
267
+ INCIDENT_SEVERITY_IDS.map(makeIncidentSeverity) as never,
268
+ );
269
+
270
+ seedAlertSeveritiesSpy = jest
271
+ .spyOn(AlertSeverityService, "findBy")
272
+ .mockResolvedValue(ALERT_SEVERITY_IDS.map(makeAlertSeverity) as never);
273
+
274
+ // Nothing exists yet, so every candidate rule is created.
275
+ seedFindOneBySpy = jest.spyOn(UserNotificationRuleService, "findOneBy");
276
+ seedFindOneBySpy.mockResolvedValue(null as never);
277
+
278
+ seedCreateSpy = jest.spyOn(UserNotificationRuleService, "create");
279
+ seedCreateSpy.mockImplementation(
280
+ (createBy: CreateBy<Model>): Promise<Model> => {
281
+ return Promise.resolve(createBy.data);
282
+ },
283
+ );
284
+ });
285
+
286
+ afterEach(() => {
287
+ jest.restoreAllMocks();
288
+ });
289
+
290
+ test("one ALERT episode rule per alert severity, each carrying that severity id", async () => {
291
+ await seedForEmail();
292
+
293
+ const episodeRules: Array<Model> = seededRulesOfType(
294
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
295
+ );
296
+
297
+ expect(episodeRules).toHaveLength(ALERT_SEVERITY_IDS.length);
298
+ expect(
299
+ episodeRules.map((rule: Model): string => {
300
+ return rule.alertSeverityId!.toString();
301
+ }),
302
+ ).toEqual(toStrings(ALERT_SEVERITY_IDS));
303
+ });
304
+
305
+ test("one INCIDENT episode rule per incident severity, each carrying that severity id", async () => {
306
+ await seedForEmail();
307
+
308
+ const episodeRules: Array<Model> = seededRulesOfType(
309
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
310
+ );
311
+
312
+ expect(episodeRules).toHaveLength(INCIDENT_SEVERITY_IDS.length);
313
+ expect(
314
+ episodeRules.map((rule: Model): string => {
315
+ return rule.incidentSeverityId!.toString();
316
+ }),
317
+ ).toEqual(toStrings(INCIDENT_SEVERITY_IDS));
318
+ });
319
+
320
+ test("alert episode rules are scoped by the ALERT severity model, never the incident one", async () => {
321
+ /*
322
+ * The crossing bug. Handing `createSeverityScopedRules` the incident list
323
+ * for the alert-episode rule type would write rows whose alertSeverityId
324
+ * is an IncidentSeverity's primary key - a foreign key pointing into the
325
+ * wrong table, which no alert episode's severity id will ever equal. The
326
+ * rows would look fine in every log and page nobody, so both halves are
327
+ * asserted: the ids are drawn from the alert list, and none of them comes
328
+ * from the incident list.
329
+ */
330
+ await seedForEmail();
331
+
332
+ const incidentIds: Array<string> = toStrings(INCIDENT_SEVERITY_IDS);
333
+
334
+ for (const rule of seededRulesOfType(
335
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
336
+ )) {
337
+ expect(toStrings(ALERT_SEVERITY_IDS)).toContain(
338
+ rule.alertSeverityId!.toString(),
339
+ );
340
+ expect(incidentIds).not.toContain(rule.alertSeverityId!.toString());
341
+ expect(rule.incidentSeverityId).toBeUndefined();
342
+ }
343
+ });
344
+
345
+ test("incident episode rules are scoped by the INCIDENT severity model, never the alert one", async () => {
346
+ await seedForEmail();
347
+
348
+ const alertIds: Array<string> = toStrings(ALERT_SEVERITY_IDS);
349
+
350
+ for (const rule of seededRulesOfType(
351
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
352
+ )) {
353
+ expect(toStrings(INCIDENT_SEVERITY_IDS)).toContain(
354
+ rule.incidentSeverityId!.toString(),
355
+ );
356
+ expect(alertIds).not.toContain(rule.incidentSeverityId!.toString());
357
+ expect(rule.alertSeverityId).toBeUndefined();
358
+ }
359
+ });
360
+
361
+ test("the count of each episode rule type tracks its OWN severity list, not the other one", async () => {
362
+ /*
363
+ * A second, count-based check on the same crossing bug, with the two list
364
+ * lengths pulled far apart so a swap cannot coincidentally agree: four
365
+ * alert severities and one incident severity must give four alert-episode
366
+ * rules and one incident-episode rule, not the reverse.
367
+ */
368
+ const alertSeverityIds: Array<ObjectID> = [
369
+ ALERT_SEV_1,
370
+ ALERT_SEV_2,
371
+ PROJECT_B_ALERT_SEV,
372
+ new ObjectID("bbbbbbb4-bbbb-4bbb-8bbb-bbbbbbbbbbbb"),
373
+ ];
374
+
375
+ seedAlertSeveritiesSpy.mockResolvedValue(
376
+ alertSeverityIds.map(makeAlertSeverity) as never,
377
+ );
378
+ seedIncidentSeveritiesSpy.mockResolvedValue([
379
+ makeIncidentSeverity(INCIDENT_SEV_1),
380
+ ] as never);
381
+
382
+ await seedForEmail();
383
+
384
+ expect(
385
+ seededRulesOfType(NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE),
386
+ ).toHaveLength(4);
387
+ expect(
388
+ seededRulesOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE),
389
+ ).toHaveLength(1);
390
+ });
391
+
392
+ test("no episode rule is created without a severity - the createSingleRule path is gone for these two", async () => {
393
+ /*
394
+ * The precise statement of GAP G. One severity-less episode row is enough
395
+ * to reintroduce it, so this asserts over every episode row rather than
396
+ * over a count.
397
+ */
398
+ await seedForEmail();
399
+
400
+ const episodeRules: Array<Model> = seededRules().filter(
401
+ (rule: Model): boolean => {
402
+ return EPISODE_RULE_TYPES.includes(rule.ruleType!);
403
+ },
404
+ );
405
+
406
+ expect(episodeRules).toHaveLength(
407
+ ALERT_SEVERITY_IDS.length + INCIDENT_SEVERITY_IDS.length,
408
+ );
409
+
410
+ for (const rule of episodeRules) {
411
+ expect(
412
+ Boolean(rule.alertSeverityId) || Boolean(rule.incidentSeverityId),
413
+ ).toBe(true);
414
+ }
415
+ });
416
+
417
+ test("the ONLY severity-less rules seeded are the two shift rules", async () => {
418
+ /*
419
+ * The complement of the assertion above, and the guard against
420
+ * over-correcting: WHEN_USER_GOES_ON_CALL / OFF_CALL describe the
421
+ * responder's roster rather than anything that fired, so they carry no
422
+ * severity by design and must keep going through createSingleRule.
423
+ */
424
+ await seedForEmail();
425
+
426
+ const severityLessRules: Array<Model> = seededRules().filter(
427
+ (rule: Model): boolean => {
428
+ return !rule.alertSeverityId && !rule.incidentSeverityId;
429
+ },
430
+ );
431
+
432
+ expect(
433
+ severityLessRules.map((rule: Model): NotificationRuleType => {
434
+ return rule.ruleType!;
435
+ }),
436
+ ).toEqual(SHIFT_RULE_TYPES);
437
+ });
438
+
439
+ test("each shift rule is seeded exactly once, regardless of how many severities exist", async () => {
440
+ await seedForEmail();
441
+
442
+ for (const ruleType of SHIFT_RULE_TYPES) {
443
+ expect(seededRulesOfType(ruleType)).toHaveLength(1);
444
+ }
445
+ });
446
+
447
+ test("the shift rules' duplicate check carries no severity key at all", async () => {
448
+ /*
449
+ * `createSingleRule` is private, so the observable signature of "this went
450
+ * through the single-rule path" is its lookup: project + user + method +
451
+ * ruleType and nothing else. An episode type appearing here without a
452
+ * severity key would mean GAP G had returned.
453
+ */
454
+ await seedForEmail();
455
+
456
+ const queries: Array<Record<string, unknown>> = duplicateCheckQueries();
457
+
458
+ const shiftQueries: Array<Record<string, unknown>> = queries.filter(
459
+ (query: Record<string, unknown>): boolean => {
460
+ return SHIFT_RULE_TYPES.includes(
461
+ query["ruleType"] as NotificationRuleType,
462
+ );
463
+ },
464
+ );
465
+
466
+ expect(shiftQueries).toHaveLength(SHIFT_RULE_TYPES.length);
467
+
468
+ for (const query of shiftQueries) {
469
+ expect(Object.keys(query)).not.toContain("alertSeverityId");
470
+ expect(Object.keys(query)).not.toContain("incidentSeverityId");
471
+ }
472
+ });
473
+
474
+ test("every episode rule's duplicate check IS keyed on a severity", async () => {
475
+ await seedForEmail();
476
+
477
+ const episodeQueries: Array<Record<string, unknown>> =
478
+ duplicateCheckQueries().filter(
479
+ (query: Record<string, unknown>): boolean => {
480
+ return EPISODE_RULE_TYPES.includes(
481
+ query["ruleType"] as NotificationRuleType,
482
+ );
483
+ },
484
+ );
485
+
486
+ expect(episodeQueries).toHaveLength(
487
+ ALERT_SEVERITY_IDS.length + INCIDENT_SEVERITY_IDS.length,
488
+ );
489
+
490
+ for (const query of episodeQueries) {
491
+ const isAlertEpisode: boolean =
492
+ query["ruleType"] ===
493
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE;
494
+
495
+ const severityId: unknown = isAlertEpisode
496
+ ? query["alertSeverityId"]
497
+ : query["incidentSeverityId"];
498
+
499
+ expect(severityId).toBeDefined();
500
+ expect(
501
+ toStrings(
502
+ isAlertEpisode ? ALERT_SEVERITY_IDS : INCIDENT_SEVERITY_IDS,
503
+ ).includes((severityId as ObjectID).toString()),
504
+ ).toBe(true);
505
+ }
506
+ });
507
+
508
+ test("the verified method lands on every episode rule, on its own column only", async () => {
509
+ await seedFor({ userTelegramId: TELEGRAM_METHOD_ID });
510
+
511
+ const episodeRules: Array<Model> = seededRules().filter(
512
+ (rule: Model): boolean => {
513
+ return EPISODE_RULE_TYPES.includes(rule.ruleType!);
514
+ },
515
+ );
516
+
517
+ expect(episodeRules).toHaveLength(
518
+ ALERT_SEVERITY_IDS.length + INCIDENT_SEVERITY_IDS.length,
519
+ );
520
+
521
+ for (const rule of episodeRules) {
522
+ expect(rule.userTelegramId!.toString()).toBe(
523
+ TELEGRAM_METHOD_ID.toString(),
524
+ );
525
+
526
+ for (const column of ALL_METHOD_COLUMNS) {
527
+ if (column === "userTelegramId") {
528
+ continue;
529
+ }
530
+ expect(rule[column]).toBeUndefined();
531
+ }
532
+ }
533
+ });
534
+
535
+ test("every seeded episode rule pages immediately and belongs to the project and user", async () => {
536
+ await seedForEmail();
537
+
538
+ for (const rule of seededRules().filter((rule: Model): boolean => {
539
+ return EPISODE_RULE_TYPES.includes(rule.ruleType!);
540
+ })) {
541
+ expect(rule.notifyAfterMinutes).toBe(0);
542
+ expect(rule.projectId!.toString()).toBe(PROJECT_A.toString());
543
+ expect(rule.userId!.toString()).toBe(USER_ID.toString());
544
+ }
545
+ });
546
+
547
+ test("a project with no ALERT severities gets no alert-episode rules but keeps its incident ones", async () => {
548
+ /*
549
+ * Episode rules follow the severities now, so "none of that kind exist"
550
+ * has to mean "seed none" rather than "seed one severity-less row" - which
551
+ * is exactly the shape the old createSingleRule call had.
552
+ */
553
+ seedAlertSeveritiesSpy.mockResolvedValue([] as never);
554
+
555
+ await seedForEmail();
556
+
557
+ expect(
558
+ seededRulesOfType(NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE),
559
+ ).toHaveLength(0);
560
+ expect(
561
+ seededRulesOfType(NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE),
562
+ ).toHaveLength(INCIDENT_SEVERITY_IDS.length);
563
+
564
+ for (const ruleType of SHIFT_RULE_TYPES) {
565
+ expect(seededRulesOfType(ruleType)).toHaveLength(1);
566
+ }
567
+ });
568
+
569
+ test("a project with no severities of either kind still seeds the two shift rules and nothing else", async () => {
570
+ seedAlertSeveritiesSpy.mockResolvedValue([] as never);
571
+ seedIncidentSeveritiesSpy.mockResolvedValue([] as never);
572
+
573
+ await seedForEmail();
574
+
575
+ expect(
576
+ seededRules().map((rule: Model): NotificationRuleType => {
577
+ return rule.ruleType!;
578
+ }),
579
+ ).toEqual(SHIFT_RULE_TYPES);
580
+ });
581
+ });
582
+
583
+ /*
584
+ * ------------------------------------------------------------------ *
585
+ * HALF 2 - the repair migration.
586
+ * ------------------------------------------------------------------
587
+ */
588
+
589
+ /* The shape of a findBy the migration issues against the rule table. */
590
+ interface RuleFindByArgs {
591
+ query: Record<string, unknown>;
592
+ select?: Record<string, boolean> | undefined;
593
+ sort?: Record<string, SortOrder> | undefined;
594
+ skip?: number | undefined;
595
+ limit?: number | undefined;
596
+ props?: { isRoot?: boolean | undefined } | undefined;
597
+ }
598
+
599
+ interface RuleSeed {
600
+ id: ObjectID;
601
+ ruleType: NotificationRuleType;
602
+ projectId?: ObjectID | undefined;
603
+ userId?: ObjectID | undefined;
604
+ notifyAfterMinutes?: number | undefined;
605
+ alertSeverityId?: ObjectID | undefined;
606
+ incidentSeverityId?: ObjectID | undefined;
607
+ methodColumn?: MethodColumn | undefined;
608
+ methodId?: ObjectID | undefined;
609
+ }
610
+
611
+ function makeRule(seed: RuleSeed): Model {
612
+ const rule: Model = new Model(seed.id);
613
+ rule._id = seed.id.toString();
614
+ rule.ruleType = seed.ruleType;
615
+
616
+ if (seed.projectId) {
617
+ rule.projectId = seed.projectId;
618
+ }
619
+
620
+ if (seed.userId) {
621
+ rule.userId = seed.userId;
622
+ }
623
+
624
+ if (seed.notifyAfterMinutes !== undefined) {
625
+ rule.notifyAfterMinutes = seed.notifyAfterMinutes;
626
+ }
627
+
628
+ if (seed.alertSeverityId) {
629
+ rule.alertSeverityId = seed.alertSeverityId;
630
+ }
631
+
632
+ if (seed.incidentSeverityId) {
633
+ rule.incidentSeverityId = seed.incidentSeverityId;
634
+ }
635
+
636
+ if (seed.methodColumn && seed.methodId) {
637
+ rule[seed.methodColumn] = seed.methodId;
638
+ }
639
+
640
+ return rule;
641
+ }
642
+
643
+ /* Ids of rows the fixtures reference by name. */
644
+ const NULL_ALERT_EPISODE_ID: ObjectID = new ObjectID(
645
+ "d0000001-dddd-4ddd-8ddd-dddddddddddd",
646
+ );
647
+ const NULL_INCIDENT_EPISODE_ID: ObjectID = new ObjectID(
648
+ "d0000002-dddd-4ddd-8ddd-dddddddddddd",
649
+ );
650
+ const SCOPED_ALERT_EPISODE_ID: ObjectID = new ObjectID(
651
+ "d0000003-dddd-4ddd-8ddd-dddddddddddd",
652
+ );
653
+ const NULL_ALERT_ON_CALL_ID: ObjectID = new ObjectID(
654
+ "d0000004-dddd-4ddd-8ddd-dddddddddddd",
655
+ );
656
+ const NULL_INCIDENT_ON_CALL_ID: ObjectID = new ObjectID(
657
+ "d0000005-dddd-4ddd-8ddd-dddddddddddd",
658
+ );
659
+ const GOES_ON_CALL_ID: ObjectID = new ObjectID(
660
+ "d0000006-dddd-4ddd-8ddd-dddddddddddd",
661
+ );
662
+ const GOES_OFF_CALL_ID: ObjectID = new ObjectID(
663
+ "d0000007-dddd-4ddd-8ddd-dddddddddddd",
664
+ );
665
+ const NO_METHOD_ID: ObjectID = new ObjectID(
666
+ "d0000008-dddd-4ddd-8ddd-dddddddddddd",
667
+ );
668
+ const NO_PROJECT_ID: ObjectID = new ObjectID(
669
+ "d0000009-dddd-4ddd-8ddd-dddddddddddd",
670
+ );
671
+ const PROJECT_B_NULL_ALERT_EPISODE_ID: ObjectID = new ObjectID(
672
+ "d000000a-dddd-4ddd-8ddd-dddddddddddd",
673
+ );
674
+ const ORPHAN_PROJECT_NULL_EPISODE_ID: ObjectID = new ObjectID(
675
+ "d000000b-dddd-4ddd-8ddd-dddddddddddd",
676
+ );
677
+
678
+ /* A NULL-severity alert-episode rule: exactly what the old defaults wrote. */
679
+ function nullAlertEpisodeRule(overrides?: Partial<RuleSeed>): Model {
680
+ return makeRule({
681
+ id: NULL_ALERT_EPISODE_ID,
682
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
683
+ projectId: PROJECT_A,
684
+ userId: USER_ID,
685
+ notifyAfterMinutes: 15,
686
+ methodColumn: "userTelegramId",
687
+ methodId: TELEGRAM_METHOD_ID,
688
+ ...overrides,
689
+ });
690
+ }
691
+
692
+ function nullIncidentEpisodeRule(overrides?: Partial<RuleSeed>): Model {
693
+ return makeRule({
694
+ id: NULL_INCIDENT_EPISODE_ID,
695
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
696
+ projectId: PROJECT_A,
697
+ userId: USER_ID,
698
+ notifyAfterMinutes: 0,
699
+ methodColumn: "userEmailId",
700
+ methodId: EMAIL_METHOD_ID,
701
+ ...overrides,
702
+ });
703
+ }
704
+
705
+ describe("GAP G half 2 - RepairEpisodeNotificationRuleSeverity", () => {
706
+ /* The in-memory stand-in for the user_notification_rule table. */
707
+ let table: Array<Model> = [];
708
+ /* An ordered log of the writes, so "delete only after create" is testable. */
709
+ let events: Array<string> = [];
710
+ let findByCalls: Array<RuleFindByArgs> = [];
711
+ let createdRuleCounter: number = 0;
712
+
713
+ let ruleFindBySpy: jest.SpyInstance;
714
+ let ruleFindOneBySpy: jest.SpyInstance;
715
+ let ruleCreateSpy: jest.SpyInstance;
716
+ let ruleDeleteSpy: jest.SpyInstance;
717
+ let alertSeveritiesSpy: jest.SpyInstance;
718
+ let incidentSeveritiesSpy: jest.SpyInstance;
719
+
720
+ const alertSeveritiesByProject: Map<string, Array<ObjectID>> = new Map<
721
+ string,
722
+ Array<ObjectID>
723
+ >();
724
+ const incidentSeveritiesByProject: Map<string, Array<ObjectID>> = new Map<
725
+ string,
726
+ Array<ObjectID>
727
+ >();
728
+
729
+ function runMigration(): Promise<void> {
730
+ return new RepairEpisodeNotificationRuleSeverity().migrate();
731
+ }
732
+
733
+ function createdRules(): Array<Model> {
734
+ return ruleCreateSpy.mock.calls.map((call: Array<unknown>): Model => {
735
+ return (call[0] as CreateBy<Model>).data;
736
+ });
737
+ }
738
+
739
+ function createdRulesOfType(ruleType: NotificationRuleType): Array<Model> {
740
+ return createdRules().filter((rule: Model): boolean => {
741
+ return rule.ruleType === ruleType;
742
+ });
743
+ }
744
+
745
+ function deletedRuleIds(): Array<string> {
746
+ return ruleDeleteSpy.mock.calls.map((call: Array<unknown>): string => {
747
+ return (call[0] as { id: ObjectID }).id.toString();
748
+ });
749
+ }
750
+
751
+ function tableIds(): Array<string> {
752
+ return table.map((rule: Model): string => {
753
+ return rule._id!;
754
+ });
755
+ }
756
+
757
+ function findByCallsForRuleType(
758
+ ruleType: NotificationRuleType,
759
+ ): Array<RuleFindByArgs> {
760
+ return findByCalls.filter((call: RuleFindByArgs): boolean => {
761
+ return call.query["ruleType"] === ruleType;
762
+ });
763
+ }
764
+
765
+ /* Does this stored row satisfy every key of the given query? */
766
+ function matchesQuery(rule: Model, query: Record<string, unknown>): boolean {
767
+ return Object.keys(query).every((key: string): boolean => {
768
+ const expected: unknown = query[key];
769
+ const actual: unknown = (rule as unknown as Record<string, unknown>)[key];
770
+
771
+ if (expected instanceof ObjectID) {
772
+ return (
773
+ actual instanceof ObjectID &&
774
+ actual.toString() === expected.toString()
775
+ );
776
+ }
777
+
778
+ return actual === expected;
779
+ });
780
+ }
781
+
782
+ beforeEach(() => {
783
+ table = [];
784
+ events = [];
785
+ findByCalls = [];
786
+ createdRuleCounter = 0;
787
+
788
+ alertSeveritiesByProject.clear();
789
+ incidentSeveritiesByProject.clear();
790
+ alertSeveritiesByProject.set(PROJECT_A.toString(), ALERT_SEVERITY_IDS);
791
+ alertSeveritiesByProject.set(PROJECT_B.toString(), [PROJECT_B_ALERT_SEV]);
792
+ alertSeveritiesByProject.set(PROJECT_NO_SEVERITIES.toString(), []);
793
+ incidentSeveritiesByProject.set(
794
+ PROJECT_A.toString(),
795
+ INCIDENT_SEVERITY_IDS,
796
+ );
797
+ incidentSeveritiesByProject.set(PROJECT_B.toString(), [
798
+ PROJECT_B_INCIDENT_SEV,
799
+ ]);
800
+ incidentSeveritiesByProject.set(PROJECT_NO_SEVERITIES.toString(), []);
801
+
802
+ /*
803
+ * The paging read. This honours the parts of the query the migration's
804
+ * correctness rests on - the rule type, the IS NULL severity filter, the
805
+ * `_id ASC` ordering and the skip/limit window - so a cursor that failed
806
+ * to advance would hang the loop here exactly as it would in production,
807
+ * rather than being papered over by a scripted sequence of pages.
808
+ */
809
+ ruleFindBySpy = jest.spyOn(UserNotificationRuleService, "findBy");
810
+ ruleFindBySpy.mockImplementation(
811
+ (findBy: RuleFindByArgs): Promise<Array<Model>> => {
812
+ findByCalls.push(findBy);
813
+
814
+ const ruleType: unknown = findBy.query["ruleType"];
815
+ const isAlertEpisodeQuery: boolean =
816
+ findBy.query["alertSeverityId"] !== undefined;
817
+
818
+ const matching: Array<Model> = table
819
+ .filter((rule: Model): boolean => {
820
+ if (rule.ruleType !== ruleType) {
821
+ return false;
822
+ }
823
+
824
+ return isAlertEpisodeQuery
825
+ ? !rule.alertSeverityId
826
+ : !rule.incidentSeverityId;
827
+ })
828
+ .sort((first: Model, second: Model): number => {
829
+ return (first._id || "").localeCompare(second._id || "");
830
+ });
831
+
832
+ const skip: number = findBy.skip ?? 0;
833
+ const limit: number = findBy.limit ?? matching.length;
834
+
835
+ return Promise.resolve(matching.slice(skip, skip + limit));
836
+ },
837
+ );
838
+
839
+ /* The duplicate check that makes the pass idempotent and restartable. */
840
+ ruleFindOneBySpy = jest.spyOn(UserNotificationRuleService, "findOneBy");
841
+ ruleFindOneBySpy.mockImplementation(
842
+ (findOneBy: {
843
+ query: Record<string, unknown>;
844
+ }): Promise<Model | null> => {
845
+ const found: Model | undefined = table.find((rule: Model): boolean => {
846
+ return matchesQuery(rule, findOneBy.query);
847
+ });
848
+
849
+ return Promise.resolve(found ?? null);
850
+ },
851
+ );
852
+
853
+ ruleCreateSpy = jest.spyOn(UserNotificationRuleService, "create");
854
+ ruleCreateSpy.mockImplementation(
855
+ (createBy: CreateBy<Model>): Promise<Model> => {
856
+ const created: Model = createBy.data;
857
+ createdRuleCounter++;
858
+ created._id = `cccccccc-0000-4000-8000-${String(createdRuleCounter).padStart(12, "0")}`;
859
+
860
+ const severityId: ObjectID | undefined =
861
+ created.alertSeverityId || created.incidentSeverityId;
862
+
863
+ events.push(`create:${severityId ? severityId.toString() : "none"}`);
864
+ table.push(created);
865
+
866
+ return Promise.resolve(created);
867
+ },
868
+ );
869
+
870
+ ruleDeleteSpy = jest.spyOn(UserNotificationRuleService, "deleteOneById");
871
+ ruleDeleteSpy.mockImplementation(
872
+ (deleteById: { id: ObjectID }): Promise<number> => {
873
+ events.push(`delete:${deleteById.id.toString()}`);
874
+
875
+ const index: number = table.findIndex((rule: Model): boolean => {
876
+ return rule._id === deleteById.id.toString();
877
+ });
878
+
879
+ if (index < 0) {
880
+ return Promise.resolve(0);
881
+ }
882
+
883
+ table.splice(index, 1);
884
+
885
+ return Promise.resolve(1);
886
+ },
887
+ );
888
+
889
+ alertSeveritiesSpy = jest.spyOn(AlertSeverityService, "findBy");
890
+ alertSeveritiesSpy.mockImplementation(
891
+ (findBy: {
892
+ query: { projectId: ObjectID };
893
+ }): Promise<Array<AlertSeverity>> => {
894
+ const ids: Array<ObjectID> =
895
+ alertSeveritiesByProject.get(findBy.query.projectId.toString()) ?? [];
896
+
897
+ return Promise.resolve(ids.map(makeAlertSeverity));
898
+ },
899
+ );
900
+
901
+ incidentSeveritiesSpy = jest.spyOn(IncidentSeverityService, "findBy");
902
+ incidentSeveritiesSpy.mockImplementation(
903
+ (findBy: {
904
+ query: { projectId: ObjectID };
905
+ }): Promise<Array<IncidentSeverity>> => {
906
+ const ids: Array<ObjectID> =
907
+ incidentSeveritiesByProject.get(findBy.query.projectId.toString()) ??
908
+ [];
909
+
910
+ return Promise.resolve(ids.map(makeIncidentSeverity));
911
+ },
912
+ );
913
+
914
+ jest.spyOn(logger, "debug").mockImplementation((): void => {
915
+ return undefined;
916
+ });
917
+ });
918
+
919
+ afterEach(() => {
920
+ jest.restoreAllMocks();
921
+ });
922
+
923
+ describe("fanning a NULL-severity episode rule out", () => {
924
+ test("an alert episode rule becomes one rule per ALERT severity in its project", async () => {
925
+ table = [nullAlertEpisodeRule()];
926
+
927
+ await runMigration();
928
+
929
+ const created: Array<Model> = createdRules();
930
+
931
+ expect(created).toHaveLength(ALERT_SEVERITY_IDS.length);
932
+ expect(
933
+ created.map((rule: Model): string => {
934
+ return rule.alertSeverityId!.toString();
935
+ }),
936
+ ).toEqual(toStrings(ALERT_SEVERITY_IDS));
937
+
938
+ for (const rule of created) {
939
+ expect(rule.ruleType).toBe(
940
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
941
+ );
942
+ expect(rule.incidentSeverityId).toBeUndefined();
943
+ }
944
+ });
945
+
946
+ test("an incident episode rule becomes one rule per INCIDENT severity in its project", async () => {
947
+ table = [nullIncidentEpisodeRule()];
948
+
949
+ await runMigration();
950
+
951
+ const created: Array<Model> = createdRules();
952
+
953
+ expect(created).toHaveLength(INCIDENT_SEVERITY_IDS.length);
954
+ expect(
955
+ created.map((rule: Model): string => {
956
+ return rule.incidentSeverityId!.toString();
957
+ }),
958
+ ).toEqual(toStrings(INCIDENT_SEVERITY_IDS));
959
+
960
+ for (const rule of created) {
961
+ expect(rule.ruleType).toBe(
962
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
963
+ );
964
+ expect(rule.alertSeverityId).toBeUndefined();
965
+ }
966
+ });
967
+
968
+ test("neither fan-out borrows the other kind's severity ids", async () => {
969
+ /*
970
+ * The repair's version of the crossing bug: an alert-episode row fanned
971
+ * out over incident severities would be just as unreachable as the NULL
972
+ * row it replaced, and the migration would then delete the evidence.
973
+ */
974
+ table = [nullAlertEpisodeRule(), nullIncidentEpisodeRule()];
975
+
976
+ await runMigration();
977
+
978
+ const alertIds: Array<string> = toStrings(ALERT_SEVERITY_IDS);
979
+ const incidentIds: Array<string> = toStrings(INCIDENT_SEVERITY_IDS);
980
+
981
+ for (const rule of createdRulesOfType(
982
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
983
+ )) {
984
+ expect(alertIds).toContain(rule.alertSeverityId!.toString());
985
+ expect(incidentIds).not.toContain(rule.alertSeverityId!.toString());
986
+ }
987
+
988
+ for (const rule of createdRulesOfType(
989
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
990
+ )) {
991
+ expect(incidentIds).toContain(rule.incidentSeverityId!.toString());
992
+ expect(alertIds).not.toContain(rule.incidentSeverityId!.toString());
993
+ }
994
+
995
+ expect(
996
+ createdRulesOfType(NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE),
997
+ ).toHaveLength(ALERT_SEVERITY_IDS.length);
998
+ expect(
999
+ createdRulesOfType(
1000
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
1001
+ ),
1002
+ ).toHaveLength(INCIDENT_SEVERITY_IDS.length);
1003
+ });
1004
+
1005
+ test("every replacement keeps the notification method the responder chose", async () => {
1006
+ /*
1007
+ * The whole point of a fan-out rather than a delete-and-reseed: a user
1008
+ * who said "reach me on Telegram" must not silently become an e-mail
1009
+ * user because the repair rebuilt from defaults.
1010
+ */
1011
+ table = [nullAlertEpisodeRule()];
1012
+
1013
+ await runMigration();
1014
+
1015
+ for (const rule of createdRules()) {
1016
+ expect(rule.userTelegramId!.toString()).toBe(
1017
+ TELEGRAM_METHOD_ID.toString(),
1018
+ );
1019
+
1020
+ for (const column of ALL_METHOD_COLUMNS) {
1021
+ if (column === "userTelegramId") {
1022
+ continue;
1023
+ }
1024
+ expect(rule[column]).toBeUndefined();
1025
+ }
1026
+ }
1027
+ });
1028
+
1029
+ test("every replacement keeps notifyAfterMinutes", async () => {
1030
+ /*
1031
+ * The delay is the other half of the responder's intent - it is what
1032
+ * makes them the second line rather than the first. Resetting it to 0
1033
+ * would page them immediately on every severity.
1034
+ */
1035
+ table = [nullAlertEpisodeRule({ notifyAfterMinutes: 15 })];
1036
+
1037
+ await runMigration();
1038
+
1039
+ for (const rule of createdRules()) {
1040
+ expect(rule.notifyAfterMinutes).toBe(15);
1041
+ }
1042
+ });
1043
+
1044
+ test("notifyAfterMinutes defaults to 0 when the original carried none", async () => {
1045
+ // The column is NOT NULL, so `undefined` would fail at insert time.
1046
+ table = [nullAlertEpisodeRule({ notifyAfterMinutes: undefined })];
1047
+
1048
+ await runMigration();
1049
+
1050
+ expect(createdRules()).toHaveLength(ALERT_SEVERITY_IDS.length);
1051
+ for (const rule of createdRules()) {
1052
+ expect(rule.notifyAfterMinutes).toBe(0);
1053
+ }
1054
+ });
1055
+
1056
+ test("every replacement is stamped with the original's project and user", async () => {
1057
+ table = [nullAlertEpisodeRule()];
1058
+
1059
+ await runMigration();
1060
+
1061
+ for (const rule of createdRules()) {
1062
+ expect(rule.projectId!.toString()).toBe(PROJECT_A.toString());
1063
+ expect(rule.userId!.toString()).toBe(USER_ID.toString());
1064
+ }
1065
+ });
1066
+
1067
+ test("replacements and deletions are internal, root-scoped writes", async () => {
1068
+ table = [nullAlertEpisodeRule()];
1069
+
1070
+ await runMigration();
1071
+
1072
+ for (const call of ruleCreateSpy.mock.calls) {
1073
+ expect((call[0] as CreateBy<Model>).props.isRoot).toBe(true);
1074
+ }
1075
+
1076
+ for (const call of ruleDeleteSpy.mock.calls) {
1077
+ expect((call[0] as { props: { isRoot: boolean } }).props.isRoot).toBe(
1078
+ true,
1079
+ );
1080
+ }
1081
+ });
1082
+
1083
+ test("each rule fans out over its OWN project's severities", async () => {
1084
+ table = [
1085
+ nullAlertEpisodeRule(),
1086
+ nullAlertEpisodeRule({
1087
+ id: PROJECT_B_NULL_ALERT_EPISODE_ID,
1088
+ projectId: PROJECT_B,
1089
+ }),
1090
+ ];
1091
+
1092
+ await runMigration();
1093
+
1094
+ const projectARules: Array<Model> = createdRules().filter(
1095
+ (rule: Model): boolean => {
1096
+ return rule.projectId!.toString() === PROJECT_A.toString();
1097
+ },
1098
+ );
1099
+ const projectBRules: Array<Model> = createdRules().filter(
1100
+ (rule: Model): boolean => {
1101
+ return rule.projectId!.toString() === PROJECT_B.toString();
1102
+ },
1103
+ );
1104
+
1105
+ expect(
1106
+ projectARules.map((rule: Model): string => {
1107
+ return rule.alertSeverityId!.toString();
1108
+ }),
1109
+ ).toEqual(toStrings(ALERT_SEVERITY_IDS));
1110
+
1111
+ expect(
1112
+ projectBRules.map((rule: Model): string => {
1113
+ return rule.alertSeverityId!.toString();
1114
+ }),
1115
+ ).toEqual([PROJECT_B_ALERT_SEV.toString()]);
1116
+ });
1117
+
1118
+ test("a project's severity list is read once, not once per rule", async () => {
1119
+ /*
1120
+ * The fan-out is per rule but the severity list is per project, and a
1121
+ * project with thousands of affected responders is the common shape of
1122
+ * this bug. Two rules in one project must still be one severity read.
1123
+ */
1124
+ table = [
1125
+ nullAlertEpisodeRule(),
1126
+ nullAlertEpisodeRule({
1127
+ id: PROJECT_B_NULL_ALERT_EPISODE_ID,
1128
+ userId: OTHER_USER_ID,
1129
+ }),
1130
+ ];
1131
+
1132
+ await runMigration();
1133
+
1134
+ expect(alertSeveritiesSpy).toHaveBeenCalledTimes(1);
1135
+ // No incident-episode rows at all, so that list is never needed.
1136
+ expect(incidentSeveritiesSpy).not.toHaveBeenCalled();
1137
+ });
1138
+
1139
+ test("the severity read is scoped to the project, root-scoped and bounded", async () => {
1140
+ table = [nullAlertEpisodeRule()];
1141
+
1142
+ await runMigration();
1143
+
1144
+ const arg: {
1145
+ query: { projectId: ObjectID };
1146
+ props: { isRoot: boolean };
1147
+ limit: number;
1148
+ skip: number;
1149
+ } = alertSeveritiesSpy.mock.calls[0][0] as {
1150
+ query: { projectId: ObjectID };
1151
+ props: { isRoot: boolean };
1152
+ limit: number;
1153
+ skip: number;
1154
+ };
1155
+
1156
+ expect(arg.query.projectId.toString()).toBe(PROJECT_A.toString());
1157
+ expect(arg.props.isRoot).toBe(true);
1158
+ expect(arg.limit).toBe(LIMIT_PER_PROJECT);
1159
+ expect(arg.skip).toBe(0);
1160
+ });
1161
+ });
1162
+
1163
+ describe("removing the NULL original", () => {
1164
+ test("the NULL-severity row is deleted", async () => {
1165
+ /*
1166
+ * See the file header for why this row is removed rather than left: it
1167
+ * can never be counted by the severity-filtered query AND it renders in
1168
+ * no UI table, so the responder can neither be paged by it nor see it
1169
+ * nor delete it themselves.
1170
+ */
1171
+ table = [nullAlertEpisodeRule()];
1172
+
1173
+ await runMigration();
1174
+
1175
+ expect(deletedRuleIds()).toEqual([NULL_ALERT_EPISODE_ID.toString()]);
1176
+ });
1177
+
1178
+ test("the deletion happens only after every replacement exists", async () => {
1179
+ /*
1180
+ * Ordering is the migration's crash-safety story: a throw part-way
1181
+ * through the fan-out must leave the original in place so the retry can
1182
+ * finish the job. Deleting first would turn a crash into data loss.
1183
+ */
1184
+ table = [nullAlertEpisodeRule()];
1185
+
1186
+ await runMigration();
1187
+
1188
+ expect(events).toEqual([
1189
+ `create:${ALERT_SEV_1.toString()}`,
1190
+ `create:${ALERT_SEV_2.toString()}`,
1191
+ `delete:${NULL_ALERT_EPISODE_ID.toString()}`,
1192
+ ]);
1193
+ });
1194
+
1195
+ test("the end state is severity-scoped rows only - no NULL row survives", async () => {
1196
+ table = [nullAlertEpisodeRule(), nullIncidentEpisodeRule()];
1197
+
1198
+ await runMigration();
1199
+
1200
+ expect(table).toHaveLength(
1201
+ ALERT_SEVERITY_IDS.length + INCIDENT_SEVERITY_IDS.length,
1202
+ );
1203
+
1204
+ for (const rule of table) {
1205
+ expect(
1206
+ Boolean(rule.alertSeverityId) || Boolean(rule.incidentSeverityId),
1207
+ ).toBe(true);
1208
+ }
1209
+ });
1210
+ });
1211
+
1212
+ describe("rows it must not touch", () => {
1213
+ test("an episode rule that already carries a severity is left exactly as it was", async () => {
1214
+ /*
1215
+ * Scoped to a different responder so it cannot double as one of the
1216
+ * fan-out's own replacements - this asserts "not swept up", not "not
1217
+ * duplicated".
1218
+ */
1219
+ const alreadyScoped: Model = makeRule({
1220
+ id: SCOPED_ALERT_EPISODE_ID,
1221
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1222
+ projectId: PROJECT_A,
1223
+ userId: OTHER_USER_ID,
1224
+ notifyAfterMinutes: 30,
1225
+ alertSeverityId: ALERT_SEV_1,
1226
+ methodColumn: "userEmailId",
1227
+ methodId: EMAIL_METHOD_ID,
1228
+ });
1229
+
1230
+ table = [nullAlertEpisodeRule(), alreadyScoped];
1231
+
1232
+ await runMigration();
1233
+
1234
+ expect(deletedRuleIds()).toEqual([NULL_ALERT_EPISODE_ID.toString()]);
1235
+ expect(tableIds()).toContain(SCOPED_ALERT_EPISODE_ID.toString());
1236
+ expect(alreadyScoped.notifyAfterMinutes).toBe(30);
1237
+ expect(alreadyScoped.alertSeverityId!.toString()).toBe(
1238
+ ALERT_SEV_1.toString(),
1239
+ );
1240
+
1241
+ for (const rule of createdRules()) {
1242
+ expect(rule.userId!.toString()).toBe(USER_ID.toString());
1243
+ }
1244
+ });
1245
+
1246
+ test("NULL-severity rules of a NON-episode rule type are left alone", async () => {
1247
+ const nonEpisodeRules: Array<Model> = [
1248
+ makeRule({
1249
+ id: NULL_ALERT_ON_CALL_ID,
1250
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1251
+ projectId: PROJECT_A,
1252
+ userId: USER_ID,
1253
+ notifyAfterMinutes: 5,
1254
+ methodColumn: "userEmailId",
1255
+ methodId: EMAIL_METHOD_ID,
1256
+ }),
1257
+ makeRule({
1258
+ id: NULL_INCIDENT_ON_CALL_ID,
1259
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
1260
+ projectId: PROJECT_A,
1261
+ userId: USER_ID,
1262
+ notifyAfterMinutes: 5,
1263
+ methodColumn: "userEmailId",
1264
+ methodId: EMAIL_METHOD_ID,
1265
+ }),
1266
+ ];
1267
+
1268
+ table = [...nonEpisodeRules];
1269
+
1270
+ await runMigration();
1271
+
1272
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1273
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1274
+ expect(tableIds()).toEqual([
1275
+ NULL_ALERT_ON_CALL_ID.toString(),
1276
+ NULL_INCIDENT_ON_CALL_ID.toString(),
1277
+ ]);
1278
+ });
1279
+
1280
+ test("WHEN_USER_GOES_ON_CALL / OFF_CALL rules are never fanned out", async () => {
1281
+ /*
1282
+ * The migration's most dangerous possible over-reach. These two rule
1283
+ * types are severity-less BY DESIGN - they describe the responder's
1284
+ * roster, not anything that fired - so a repair keyed on "episode-ish
1285
+ * rule with no severity" that reached them would multiply every user's
1286
+ * shift rules by the severity count and then delete the originals.
1287
+ * Nothing about the result would look broken until people started
1288
+ * getting one "you are on call" message per severity.
1289
+ */
1290
+ const shiftRules: Array<Model> = [
1291
+ makeRule({
1292
+ id: GOES_ON_CALL_ID,
1293
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1294
+ projectId: PROJECT_A,
1295
+ userId: USER_ID,
1296
+ notifyAfterMinutes: 0,
1297
+ methodColumn: "userEmailId",
1298
+ methodId: EMAIL_METHOD_ID,
1299
+ }),
1300
+ makeRule({
1301
+ id: GOES_OFF_CALL_ID,
1302
+ ruleType: NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
1303
+ projectId: PROJECT_A,
1304
+ userId: USER_ID,
1305
+ notifyAfterMinutes: 0,
1306
+ methodColumn: "userEmailId",
1307
+ methodId: EMAIL_METHOD_ID,
1308
+ }),
1309
+ ];
1310
+
1311
+ table = [...shiftRules];
1312
+
1313
+ await runMigration();
1314
+
1315
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1316
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1317
+ expect(tableIds()).toEqual([
1318
+ GOES_ON_CALL_ID.toString(),
1319
+ GOES_OFF_CALL_ID.toString(),
1320
+ ]);
1321
+ });
1322
+
1323
+ test("the migration only ever asks for the two episode rule types", async () => {
1324
+ /*
1325
+ * Stronger than "the shift rules survived": the shift rows are never
1326
+ * even read, so no future edit to the row-level guards can reach them.
1327
+ */
1328
+ table = [
1329
+ nullAlertEpisodeRule(),
1330
+ makeRule({
1331
+ id: GOES_ON_CALL_ID,
1332
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1333
+ projectId: PROJECT_A,
1334
+ userId: USER_ID,
1335
+ notifyAfterMinutes: 0,
1336
+ methodColumn: "userEmailId",
1337
+ methodId: EMAIL_METHOD_ID,
1338
+ }),
1339
+ ];
1340
+
1341
+ await runMigration();
1342
+
1343
+ const queriedRuleTypes: Set<unknown> = new Set<unknown>(
1344
+ findByCalls.map((call: RuleFindByArgs): unknown => {
1345
+ return call.query["ruleType"];
1346
+ }),
1347
+ );
1348
+
1349
+ expect(queriedRuleTypes).toEqual(
1350
+ new Set<NotificationRuleType>(EPISODE_RULE_TYPES),
1351
+ );
1352
+ });
1353
+
1354
+ test("a whole mixed table is repaired in exactly the two places it should be", async () => {
1355
+ const shiftRule: Model = makeRule({
1356
+ id: GOES_ON_CALL_ID,
1357
+ ruleType: NotificationRuleType.WHEN_USER_GOES_ON_CALL,
1358
+ projectId: PROJECT_A,
1359
+ userId: USER_ID,
1360
+ notifyAfterMinutes: 0,
1361
+ methodColumn: "userEmailId",
1362
+ methodId: EMAIL_METHOD_ID,
1363
+ });
1364
+
1365
+ const scopedEpisodeRule: Model = makeRule({
1366
+ id: SCOPED_ALERT_EPISODE_ID,
1367
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1368
+ projectId: PROJECT_A,
1369
+ userId: OTHER_USER_ID,
1370
+ notifyAfterMinutes: 0,
1371
+ alertSeverityId: ALERT_SEV_1,
1372
+ methodColumn: "userEmailId",
1373
+ methodId: EMAIL_METHOD_ID,
1374
+ });
1375
+
1376
+ const onCallRule: Model = makeRule({
1377
+ id: NULL_ALERT_ON_CALL_ID,
1378
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
1379
+ projectId: PROJECT_A,
1380
+ userId: USER_ID,
1381
+ notifyAfterMinutes: 0,
1382
+ methodColumn: "userEmailId",
1383
+ methodId: EMAIL_METHOD_ID,
1384
+ });
1385
+
1386
+ table = [
1387
+ nullAlertEpisodeRule(),
1388
+ nullIncidentEpisodeRule(),
1389
+ scopedEpisodeRule,
1390
+ onCallRule,
1391
+ shiftRule,
1392
+ ];
1393
+
1394
+ await runMigration();
1395
+
1396
+ expect(deletedRuleIds().sort()).toEqual(
1397
+ [
1398
+ NULL_ALERT_EPISODE_ID.toString(),
1399
+ NULL_INCIDENT_EPISODE_ID.toString(),
1400
+ ].sort(),
1401
+ );
1402
+
1403
+ expect(createdRules()).toHaveLength(
1404
+ ALERT_SEVERITY_IDS.length + INCIDENT_SEVERITY_IDS.length,
1405
+ );
1406
+
1407
+ for (const survivor of [scopedEpisodeRule, onCallRule, shiftRule]) {
1408
+ expect(tableIds()).toContain(survivor._id!);
1409
+ }
1410
+ });
1411
+ });
1412
+
1413
+ describe("rows it deliberately leaves in place", () => {
1414
+ test("a NULL row with no notification method is left alone, not deleted", async () => {
1415
+ /*
1416
+ * Every row createSingleRule wrote carries exactly one method, so a
1417
+ * method-less row means something this migration does not understand.
1418
+ * Fanning it out would write rules that deliver nothing (and that
1419
+ * onBeforeCreate would reject); deleting it would destroy a row whose
1420
+ * meaning is unknown. Leaving it is the only defensible option.
1421
+ */
1422
+ table = [
1423
+ nullAlertEpisodeRule({
1424
+ id: NO_METHOD_ID,
1425
+ methodColumn: undefined,
1426
+ methodId: undefined,
1427
+ }),
1428
+ ];
1429
+
1430
+ await runMigration();
1431
+
1432
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1433
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1434
+ expect(tableIds()).toEqual([NO_METHOD_ID.toString()]);
1435
+ });
1436
+
1437
+ test("a NULL row with no projectId is left alone, not deleted", async () => {
1438
+ table = [
1439
+ nullAlertEpisodeRule({ id: NO_PROJECT_ID, projectId: undefined }),
1440
+ ];
1441
+
1442
+ await runMigration();
1443
+
1444
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1445
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1446
+ expect(tableIds()).toEqual([NO_PROJECT_ID.toString()]);
1447
+ });
1448
+
1449
+ test("a project with no severities of that kind keeps its row", async () => {
1450
+ /*
1451
+ * There is nothing to fan out to yet. Deleting would throw away the
1452
+ * responder's chosen method and delay for a severity that simply has
1453
+ * not been created; the row is equally unreachable either way, and the
1454
+ * severity-creation backfill mirrors it forward when the project's
1455
+ * first severity appears.
1456
+ */
1457
+ table = [
1458
+ nullAlertEpisodeRule({
1459
+ id: ORPHAN_PROJECT_NULL_EPISODE_ID,
1460
+ projectId: PROJECT_NO_SEVERITIES,
1461
+ }),
1462
+ ];
1463
+
1464
+ await runMigration();
1465
+
1466
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1467
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1468
+ expect(tableIds()).toEqual([ORPHAN_PROJECT_NULL_EPISODE_ID.toString()]);
1469
+ });
1470
+
1471
+ test("left-in-place rows advance the paging cursor so the sweep still drains", async () => {
1472
+ /*
1473
+ * The subtle failure mode of "skip the row and move on": a row left in
1474
+ * the table is returned again by the next page-zero query, so a cursor
1475
+ * that did not count it would re-read the same page forever and the
1476
+ * migration would never finish booting the pod. The skip offset is the
1477
+ * running count of rows left behind, which is sound because `_id ASC`
1478
+ * puts every already-seen row before every unseen one.
1479
+ */
1480
+ table = [
1481
+ nullAlertEpisodeRule({
1482
+ id: NO_METHOD_ID,
1483
+ methodColumn: undefined,
1484
+ methodId: undefined,
1485
+ }),
1486
+ nullAlertEpisodeRule({
1487
+ id: NO_PROJECT_ID,
1488
+ projectId: undefined,
1489
+ }),
1490
+ nullAlertEpisodeRule({
1491
+ id: ORPHAN_PROJECT_NULL_EPISODE_ID,
1492
+ projectId: PROJECT_NO_SEVERITIES,
1493
+ }),
1494
+ nullAlertEpisodeRule(),
1495
+ ];
1496
+
1497
+ await runMigration();
1498
+
1499
+ const alertPassSkips: Array<number | undefined> = findByCallsForRuleType(
1500
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1501
+ ).map((call: RuleFindByArgs): number | undefined => {
1502
+ return call.skip;
1503
+ });
1504
+
1505
+ /*
1506
+ * One page returning all four rows, then a second read skipping the
1507
+ * three that stayed - which finds nothing and ends the loop.
1508
+ */
1509
+ expect(alertPassSkips).toEqual([0, 3]);
1510
+ expect(deletedRuleIds()).toEqual([NULL_ALERT_EPISODE_ID.toString()]);
1511
+ expect(table).toHaveLength(3 + ALERT_SEVERITY_IDS.length);
1512
+ });
1513
+ });
1514
+
1515
+ describe("the query it runs", () => {
1516
+ test("the alert pass filters on a NULL alertSeverityId", async () => {
1517
+ table = [nullAlertEpisodeRule()];
1518
+
1519
+ await runMigration();
1520
+
1521
+ const call: RuleFindByArgs = findByCallsForRuleType(
1522
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1523
+ )[0]!;
1524
+
1525
+ const operator: FindOperator<unknown> = call.query[
1526
+ "alertSeverityId"
1527
+ ] as FindOperator<unknown>;
1528
+
1529
+ const getSql: ((aliasPath: string) => string) | undefined =
1530
+ operator.getSql;
1531
+
1532
+ expect(getSql).toBeDefined();
1533
+ expect(getSql!("UserNotificationRule.alertSeverityId")).toBe(
1534
+ "(UserNotificationRule.alertSeverityId IS NULL)",
1535
+ );
1536
+
1537
+ /*
1538
+ * The other severity column is not constrained at all: an alert-episode
1539
+ * rule has no incident severity to speak of.
1540
+ */
1541
+ expect(Object.keys(call.query)).not.toContain("incidentSeverityId");
1542
+ });
1543
+
1544
+ test("the incident pass filters on a NULL incidentSeverityId", async () => {
1545
+ table = [nullIncidentEpisodeRule()];
1546
+
1547
+ await runMigration();
1548
+
1549
+ const call: RuleFindByArgs = findByCallsForRuleType(
1550
+ NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
1551
+ )[0]!;
1552
+
1553
+ const getSql: ((aliasPath: string) => string) | undefined = (
1554
+ call.query["incidentSeverityId"] as FindOperator<unknown>
1555
+ ).getSql;
1556
+
1557
+ expect(getSql).toBeDefined();
1558
+ expect(getSql!("UserNotificationRule.incidentSeverityId")).toBe(
1559
+ "(UserNotificationRule.incidentSeverityId IS NULL)",
1560
+ );
1561
+ expect(Object.keys(call.query)).not.toContain("alertSeverityId");
1562
+ });
1563
+
1564
+ test("the read selects everything the fan-out has to copy forward", async () => {
1565
+ /*
1566
+ * The select is load-bearing in a way that fails silently: an
1567
+ * unselected `notifyAfterMinutes` arrives as undefined and is defaulted
1568
+ * to 0, which would quietly reset every responder's escalation delay
1569
+ * while the migration reported success. Same for the method FKs - a
1570
+ * dropped column turns into "carries no notification method" and the
1571
+ * row is skipped instead of repaired.
1572
+ */
1573
+ table = [nullAlertEpisodeRule()];
1574
+
1575
+ await runMigration();
1576
+
1577
+ const select: Record<string, boolean> = findByCallsForRuleType(
1578
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1579
+ )[0]!.select!;
1580
+
1581
+ for (const key of [
1582
+ "_id",
1583
+ "projectId",
1584
+ "userId",
1585
+ "notifyAfterMinutes",
1586
+ ...ALL_METHOD_COLUMNS,
1587
+ ]) {
1588
+ expect(select[key]).toBe(true);
1589
+ }
1590
+ });
1591
+
1592
+ test("the read is root-scoped", async () => {
1593
+ table = [nullAlertEpisodeRule()];
1594
+
1595
+ await runMigration();
1596
+
1597
+ for (const call of findByCalls) {
1598
+ expect(call.props!.isRoot).toBe(true);
1599
+ }
1600
+ });
1601
+
1602
+ test("the duplicate check is keyed on project, user, rule type, severity and method", async () => {
1603
+ table = [nullAlertEpisodeRule()];
1604
+
1605
+ await runMigration();
1606
+
1607
+ const query: Record<string, unknown> = (
1608
+ ruleFindOneBySpy.mock.calls[0][0] as {
1609
+ query: Record<string, unknown>;
1610
+ }
1611
+ ).query;
1612
+
1613
+ expect((query["projectId"] as ObjectID).toString()).toBe(
1614
+ PROJECT_A.toString(),
1615
+ );
1616
+ expect((query["userId"] as ObjectID).toString()).toBe(USER_ID.toString());
1617
+ expect(query["ruleType"]).toBe(
1618
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1619
+ );
1620
+ expect((query["alertSeverityId"] as ObjectID).toString()).toBe(
1621
+ ALERT_SEV_1.toString(),
1622
+ );
1623
+ expect((query["userTelegramId"] as ObjectID).toString()).toBe(
1624
+ TELEGRAM_METHOD_ID.toString(),
1625
+ );
1626
+ });
1627
+ });
1628
+
1629
+ describe("paging", () => {
1630
+ test("it pages instead of loading every affected row at once", async () => {
1631
+ /*
1632
+ * Each row fans out into one INSERT per severity, so an unbounded read
1633
+ * would turn the repair into one enormous write burst against a table
1634
+ * the on-call path reads on every single page. 250 rows must arrive as
1635
+ * bounded pages, not as one array.
1636
+ */
1637
+ alertSeveritiesByProject.set(PROJECT_A.toString(), [ALERT_SEV_1]);
1638
+
1639
+ /*
1640
+ * Each row must belong to a DIFFERENT responder. The repair is idempotent
1641
+ * by design: before writing, it asks findOneBy whether a rule already
1642
+ * exists for (project, user, ruleType, severity, method). 250 rows that
1643
+ * differ only by id describe the same rule 250 times, so the first one
1644
+ * would be repaired and the other 249 correctly skipped as duplicates -
1645
+ * which is the migration behaving properly, but it measures deduplication
1646
+ * rather than paging and leaves this test asserting the wrong thing.
1647
+ * Varying the user makes all 250 genuinely distinct repairs.
1648
+ */
1649
+ table = Array.from({ length: 250 }, (_unused: unknown, index: number) => {
1650
+ return nullAlertEpisodeRule({
1651
+ id: new ObjectID(
1652
+ `eeeeeeee-0000-4000-8000-${String(index).padStart(12, "0")}`,
1653
+ ),
1654
+ userId: new ObjectID(
1655
+ `dddddddd-0000-4000-8000-${String(index).padStart(12, "0")}`,
1656
+ ),
1657
+ });
1658
+ });
1659
+
1660
+ await runMigration();
1661
+
1662
+ const alertPassCalls: Array<RuleFindByArgs> = findByCallsForRuleType(
1663
+ NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1664
+ );
1665
+
1666
+ // Three full-ish pages plus the empty read that ends the loop.
1667
+ expect(alertPassCalls).toHaveLength(4);
1668
+
1669
+ for (const call of alertPassCalls) {
1670
+ expect(call.limit).toBe(100);
1671
+ expect(call.limit!).toBeLessThan(LIMIT_MAX);
1672
+ }
1673
+
1674
+ expect(ruleCreateSpy).toHaveBeenCalledTimes(250);
1675
+ expect(ruleDeleteSpy).toHaveBeenCalledTimes(250);
1676
+ expect(table).toHaveLength(250);
1677
+ });
1678
+
1679
+ test("every page is ordered by _id ascending, which is what makes the cursor sound", async () => {
1680
+ table = [nullAlertEpisodeRule(), nullIncidentEpisodeRule()];
1681
+
1682
+ await runMigration();
1683
+
1684
+ expect(findByCalls.length).toBeGreaterThan(0);
1685
+
1686
+ for (const call of findByCalls) {
1687
+ expect(call.sort).toEqual({ _id: SortOrder.Ascending });
1688
+ }
1689
+ });
1690
+ });
1691
+
1692
+ describe("idempotence", () => {
1693
+ test("a second run over a repaired table writes nothing", async () => {
1694
+ table = [nullAlertEpisodeRule(), nullIncidentEpisodeRule()];
1695
+
1696
+ await runMigration();
1697
+
1698
+ const repairedIds: Array<string> = tableIds();
1699
+
1700
+ ruleCreateSpy.mockClear();
1701
+ ruleDeleteSpy.mockClear();
1702
+
1703
+ await runMigration();
1704
+
1705
+ expect(ruleCreateSpy).not.toHaveBeenCalled();
1706
+ expect(ruleDeleteSpy).not.toHaveBeenCalled();
1707
+ expect(tableIds()).toEqual(repairedIds);
1708
+ });
1709
+
1710
+ test("a half-finished pass is resumed without duplicating what it already wrote", async () => {
1711
+ /*
1712
+ * Restartability: the pod is killed after one replacement is inserted
1713
+ * but before the original is deleted. The re-run must fill in only the
1714
+ * missing severity - a duplicate here is a duplicate page.
1715
+ */
1716
+ const alreadyWritten: Model = makeRule({
1717
+ id: SCOPED_ALERT_EPISODE_ID,
1718
+ ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
1719
+ projectId: PROJECT_A,
1720
+ userId: USER_ID,
1721
+ notifyAfterMinutes: 15,
1722
+ alertSeverityId: ALERT_SEV_1,
1723
+ methodColumn: "userTelegramId",
1724
+ methodId: TELEGRAM_METHOD_ID,
1725
+ });
1726
+
1727
+ table = [nullAlertEpisodeRule(), alreadyWritten];
1728
+
1729
+ await runMigration();
1730
+
1731
+ expect(createdRules()).toHaveLength(1);
1732
+ expect(createdRules()[0]!.alertSeverityId!.toString()).toBe(
1733
+ ALERT_SEV_2.toString(),
1734
+ );
1735
+ expect(deletedRuleIds()).toEqual([NULL_ALERT_EPISODE_ID.toString()]);
1736
+ expect(table).toHaveLength(2);
1737
+ });
1738
+
1739
+ test("running it twice does not double a responder's rules", async () => {
1740
+ table = [nullAlertEpisodeRule()];
1741
+
1742
+ await runMigration();
1743
+ await runMigration();
1744
+
1745
+ expect(table).toHaveLength(ALERT_SEVERITY_IDS.length);
1746
+ expect(
1747
+ table
1748
+ .map((rule: Model): string => {
1749
+ return rule.alertSeverityId!.toString();
1750
+ })
1751
+ .sort(),
1752
+ ).toEqual(toStrings(ALERT_SEVERITY_IDS).sort());
1753
+ });
1754
+ });
1755
+
1756
+ describe("how it is wired in", () => {
1757
+ test("rollback is a deliberate no-op rather than the base class's throw", async () => {
1758
+ /*
1759
+ * DataMigrationBase.rollback throws NotImplementedException. This one
1760
+ * overrides it to do nothing on purpose: recreating the NULL originals
1761
+ * would restore rows that page nobody and that the responder can
1762
+ * neither see nor delete - the exact defect being removed.
1763
+ */
1764
+ await expect(
1765
+ new RepairEpisodeNotificationRuleSeverity().rollback(),
1766
+ ).resolves.toBeUndefined();
1767
+ });
1768
+
1769
+ test("its recorded name matches the class name", () => {
1770
+ /*
1771
+ * The name is what the runner writes to the migrations table, so it is
1772
+ * the identity that decides whether this migration ever runs again.
1773
+ */
1774
+ expect(new RepairEpisodeNotificationRuleSeverity().name).toBe(
1775
+ "RepairEpisodeNotificationRuleSeverity",
1776
+ );
1777
+ });
1778
+
1779
+ test("it is registered in the DataMigrations list", () => {
1780
+ /*
1781
+ * Source-level pin. Importing the list itself would drag in the whole
1782
+ * ClickHouse migration graph, so the registration - which is two edits,
1783
+ * an import and an array entry, and easy to half-do - is checked as
1784
+ * text instead. An unregistered migration is a repair that never runs.
1785
+ */
1786
+ const indexSource: string = fs.readFileSync(
1787
+ path.join(
1788
+ __dirname,
1789
+ "../../../../App/FeatureSet/Workers/DataMigrations/Index.ts",
1790
+ ),
1791
+ "utf8",
1792
+ );
1793
+
1794
+ expect(indexSource).toContain(
1795
+ 'import RepairEpisodeNotificationRuleSeverity from "./RepairEpisodeNotificationRuleSeverity";',
1796
+ );
1797
+ expect(indexSource).toContain(
1798
+ "new RepairEpisodeNotificationRuleSeverity()",
1799
+ );
1800
+ });
1801
+ });
1802
+ });