@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.
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +1 -1
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +1 -1
- package/Models/DatabaseModels/AIConversation.ts +32 -0
- package/Models/DatabaseModels/AIConversationMessage.ts +41 -0
- package/Models/DatabaseModels/AlertEpisodeMember.ts +27 -0
- package/Models/DatabaseModels/IncidentEpisodeMember.ts +28 -0
- package/Models/DatabaseModels/Index.ts +12 -4
- package/Models/DatabaseModels/{TelemetryEntity.ts → InventoryItem.ts} +184 -9
- package/Models/DatabaseModels/InventoryItemCustomField.ts +434 -0
- package/Models/DatabaseModels/{TelemetryEntityRelationship.ts → InventoryItemRelationship.ts} +33 -7
- package/Models/DatabaseModels/NetworkDevice.ts +141 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +699 -0
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +467 -0
- package/Models/DatabaseModels/NetworkTopologySuppression.ts +429 -0
- package/Models/DatabaseModels/OnCallDutyPolicyFeed.ts +9 -0
- package/Models/DatabaseModels/Project.ts +78 -0
- package/Models/DatabaseModels/UserCall.ts +42 -0
- package/Models/DatabaseModels/UserEmail.ts +44 -0
- package/Models/DatabaseModels/UserNotificationRule.ts +425 -55
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +24 -2
- package/Models/DatabaseModels/UserPush.ts +49 -0
- package/Models/DatabaseModels/UserSMS.ts +43 -0
- package/Models/DatabaseModels/UserTelegram.ts +59 -0
- package/Models/DatabaseModels/UserWebhook.ts +52 -0
- package/Models/DatabaseModels/UserWhatsApp.ts +41 -0
- package/Models/DatabaseModels/WorkflowLog.ts +38 -0
- package/Models/DatabaseModels/WorkflowVariable.ts +12 -0
- package/Server/API/AIChatAPI.ts +315 -1
- package/Server/API/DashboardAPI.ts +217 -1
- package/Server/API/OnCallReadinessAPI.ts +841 -0
- package/Server/API/TeamComplianceAPI.ts +69 -17
- package/Server/API/TelemetryAPI.ts +220 -12
- package/Server/API/UserAPI.ts +16 -1
- package/Server/EnvironmentConfig.ts +52 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.ts +4 -4
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786200000000-RestoreDroppedUniqueIndexes.ts +5 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.ts +35 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.ts +82 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.ts +91 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.ts +255 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.ts +107 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.ts +90 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.ts +33 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +24 -0
- package/Server/Infrastructure/Queue.ts +78 -13
- package/Server/Middleware/MasterAdminAuthorization.ts +11 -6
- package/Server/Middleware/PublicDashboardRateLimit.ts +593 -0
- package/Server/Services/AIService.ts +7 -0
- package/Server/Services/AlertEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/AlertSeverityService.ts +63 -0
- package/Server/Services/DashboardService.ts +9 -10
- package/Server/Services/DatabaseService.ts +32 -2
- package/Server/Services/IncidentEpisodeStateTimelineService.ts +29 -0
- package/Server/Services/IncidentSeverityService.ts +76 -0
- package/Server/Services/Index.ts +12 -4
- package/Server/Services/InventoryItemCustomFieldService.ts +9 -0
- package/Server/Services/{TelemetryEntityRelationshipService.ts → InventoryItemRelationshipService.ts} +7 -4
- package/Server/Services/{TelemetryEntityService.ts → InventoryItemService.ts} +203 -21
- package/Server/Services/LogAggregationService.ts +45 -8
- package/Server/Services/MetricAggregationService.ts +121 -0
- package/Server/Services/MetricService.ts +7 -7
- package/Server/Services/NetworkDeviceLinkRuleService.ts +10 -0
- package/Server/Services/NetworkDeviceLinkService.ts +84 -0
- package/Server/Services/NetworkDeviceService.ts +140 -0
- package/Server/Services/NetworkSiteService.ts +77 -25
- package/Server/Services/NetworkTopologySuppressionService.ts +84 -0
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +41 -29
- package/Server/Services/OnCallDutyPolicyExecutionLogService.ts +8 -0
- package/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.ts +62 -13
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +61 -1
- package/Server/Services/OnCallNotificationAlertingService.ts +742 -0
- package/Server/Services/OnCallReadinessService.ts +2803 -0
- package/Server/Services/OnCallSetupReminderService.ts +955 -0
- package/Server/Services/ProfileAggregationService.ts +123 -0
- package/Server/Services/StatusPageService.ts +9 -10
- package/Server/Services/TeamComplianceService.ts +429 -252
- package/Server/Services/UserCallService.ts +26 -1
- package/Server/Services/UserEmailService.ts +26 -1
- package/Server/Services/UserNotificationRuleAdminService.ts +1183 -0
- package/Server/Services/UserNotificationRuleService.ts +3812 -333
- package/Server/Services/UserOnCallLogService.ts +561 -48
- package/Server/Services/UserPushService.ts +29 -0
- package/Server/Services/UserService.ts +11 -0
- package/Server/Services/UserSmsService.ts +26 -1
- package/Server/Services/UserTelegramService.ts +24 -1
- package/Server/Services/UserWebhookService.ts +28 -1
- package/Server/Services/UserWhatsAppService.ts +24 -1
- package/Server/Types/Database/Permissions/BasePermission.ts +19 -0
- package/Server/Types/Database/Permissions/CreatePermission.ts +164 -0
- package/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.ts +340 -0
- package/Server/Types/Database/Permissions/QueryPermission.ts +48 -0
- package/Server/Types/Database/Permissions/TenantPermission.ts +8 -1
- package/Server/Types/Workflow/Components/API/Delete.ts +1 -1
- package/Server/Types/Workflow/Components/API/Get.ts +1 -1
- package/Server/Types/Workflow/Components/API/Patch.ts +1 -1
- package/Server/Types/Workflow/Components/API/Post.ts +1 -1
- package/Server/Types/Workflow/Components/API/Put.ts +1 -1
- package/Server/Types/Workflow/Components/API/Utils.ts +44 -1
- package/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.ts +29 -5
- package/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.ts +18 -10
- package/Server/Types/Workflow/Components/BaseModel/ModelArguments.ts +55 -0
- package/Server/Types/Workflow/Components/Conditions/IfElse.ts +3 -17
- package/Server/Types/Workflow/Components/Email.ts +25 -7
- package/Server/Types/Workflow/Components/JavaScript.ts +10 -3
- package/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.ts +1 -1
- package/Server/Types/Workflow/TriggerCode.ts +12 -0
- package/Server/Types/Workflow/Workflow.ts +5 -0
- package/Server/Utils/AI/Chat/ChatAgentRunner.ts +643 -48
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +32 -3
- package/Server/Utils/AI/Chat/ObservabilityChatPrompt.ts +20 -6
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +7 -0
- package/Server/Utils/AI/Toolbox/AIActionTools.ts +2 -2
- package/Server/Utils/AI/Toolbox/AIMetaTools.ts +863 -0
- package/Server/Utils/AI/Toolbox/AlertTools.ts +177 -15
- package/Server/Utils/AI/Toolbox/IncidentTools.ts +191 -10
- package/Server/Utils/AI/Toolbox/Index.ts +48 -0
- package/Server/Utils/AI/Toolbox/MonitorTools.ts +298 -11
- package/Server/Utils/AI/Toolbox/NoteWriteTools.ts +295 -0
- package/Server/Utils/AI/Toolbox/OnCallTools.ts +1246 -0
- package/Server/Utils/AI/Toolbox/RunbookTools.ts +424 -0
- package/Server/Utils/AI/Toolbox/SloTools.ts +456 -0
- package/Server/Utils/AI/Toolbox/StatusPageTools.ts +559 -0
- package/Server/Utils/AI/Toolbox/TeamTools.ts +327 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +615 -0
- package/Server/Utils/AI/Toolbox/WorkflowProbeTools.ts +664 -0
- package/Server/Utils/ClientIp.ts +221 -0
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +47 -0
- package/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.ts +163 -0
- package/Server/Utils/Dashboard/PublicDashboardSloWidget.ts +147 -0
- package/Server/Utils/Express.ts +12 -17
- package/Server/Utils/LLM/LLMService.ts +85 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +204 -10
- package/Server/Utils/SSRFProtection.ts +98 -23
- package/Server/Utils/StartServer.ts +12 -3
- package/Server/Utils/Telemetry/EntityRegistry.ts +205 -18
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +689 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +160 -52
- package/Server/Utils/VM/VMAPI.ts +56 -8
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +7 -3
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +2146 -0
- package/Tests/App/Dashboard/CreateWorkflowModal.test.tsx +561 -0
- package/Tests/App/Dashboard/EscalationRuleReadiness.test.tsx +2470 -0
- package/Tests/App/Dashboard/MonitorCriteriaAttributeFilter.test.tsx +574 -0
- package/Tests/App/Dashboard/OnCallPreventionGuards.test.tsx +1897 -0
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +3606 -0
- package/Tests/App/Dashboard/OnCallRulesDeleteGuard.test.tsx +784 -0
- package/Tests/App/Dashboard/OnCallRulesTable.test.tsx +1119 -0
- package/Tests/App/Dashboard/SloWidgetFetching.test.tsx +531 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +1312 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistPage.test.tsx +1390 -0
- package/Tests/Models/InventoryItemModel.test.ts +174 -0
- package/Tests/Models/InventoryItemNaming.test.ts +302 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +437 -0
- package/Tests/Server/API/DashboardPublicRateLimit.test.ts +659 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +18 -0
- package/Tests/Server/API/DashboardPublicSloAPI.test.ts +880 -0
- package/Tests/Server/API/Helpers.ts +24 -15
- package/Tests/Server/API/OnCallReadinessAPI.test.ts +2680 -0
- package/Tests/Server/API/OnCallSetupReminderAPI.test.ts +915 -0
- package/Tests/Server/API/UserProjectsAPI.test.ts +478 -4
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +533 -0
- package/Tests/Server/Infrastructure/Postgres/InventoryItemArchiveMigration.test.ts +213 -0
- package/Tests/Server/Infrastructure/Postgres/RenameInventoryItemMigration.test.ts +432 -0
- package/Tests/Server/Infrastructure/Queue.test.ts +293 -0
- package/Tests/Server/Middleware/PublicDashboardRateLimit.test.ts +1645 -0
- package/Tests/Server/Services/AdminRuleEditGuards.test.ts +2848 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1393 -0
- package/Tests/Server/Services/EpisodeRuleSeverityRepair.test.ts +1802 -0
- package/Tests/Server/Services/EpisodeStateTimelineNote.test.ts +304 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +339 -0
- package/Tests/Server/Services/InventoryItemManualCreate.test.ts +248 -0
- package/Tests/Server/Services/IpAllowlistSpoofing.test.ts +450 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +235 -1
- package/Tests/Server/Services/MetricAggregationService.test.ts +231 -0
- package/Tests/Server/Services/MetricEntityMVKeyParity.test.ts +80 -29
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +30 -30
- package/Tests/Server/Services/NetworkSiteService.test.ts +18 -3
- package/Tests/Server/Services/NotificationChannelEventCoverage.test.ts +1728 -0
- package/Tests/Server/Services/NotificationDeletionImpact.test.ts +2402 -0
- package/Tests/Server/Services/OnCallDutyPolicyExecutionLogTimelineGapFeed.test.ts +394 -0
- package/Tests/Server/Services/OnCallNotificationFallback.test.ts +1744 -0
- package/Tests/Server/Services/OnCallReadinessService.test.ts +4295 -0
- package/Tests/Server/Services/OnCallSetupReminder.test.ts +1272 -0
- package/Tests/Server/Services/OnCallWeeklyReadinessDigest.test.ts +1021 -0
- package/Tests/Server/Services/ProfileAggregationService.test.ts +296 -0
- package/Tests/Server/Services/SeverityCreationRuleBackfill.test.ts +1536 -0
- package/Tests/Server/Services/SeverityRuleBackfill.test.ts +1818 -0
- package/Tests/Server/Services/TeamComplianceServiceBehaviour.test.ts +1845 -0
- package/Tests/Server/Services/UserNotificationRuleAdminGuards.test.ts +1394 -0
- package/Tests/Server/Services/UserNotificationRuleDefaultCreation.test.ts +1166 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1468 -0
- package/Tests/Server/Services/UserOnCallLogNoNotificationRules.test.ts +1457 -0
- package/Tests/Server/Types/Database/Permissions/AdminNotificationRuleAccess.test.ts +1546 -0
- package/Tests/Server/Types/Database/Permissions/CreateOwnershipScoping.test.ts +529 -0
- package/Tests/Server/Types/Database/Permissions/OwnerOnlyColumns.test.ts +1219 -0
- package/Tests/Server/Types/Database/Permissions/UserNotificationRuleScoping.test.ts +1089 -0
- package/Tests/Server/Types/Workflow/Components/ApiComponentErrorPort.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentHeaders.test.ts +192 -0
- package/Tests/Server/Types/Workflow/Components/BaseModelDatabaseComponents.test.ts +190 -0
- package/Tests/Server/Types/Workflow/Components/ChatWebhookComponents.test.ts +44 -14
- package/Tests/Server/Types/Workflow/Components/Email.test.ts +151 -0
- package/Tests/Server/Types/Workflow/Components/IfElse.test.ts +98 -0
- package/Tests/Server/Types/Workflow/Components/JavaScript.test.ts +51 -0
- package/Tests/Server/Utils/AI/AIMetaTools.test.ts +586 -0
- package/Tests/Server/Utils/AI/AlertMonitorFilters.test.ts +582 -0
- package/Tests/Server/Utils/AI/ChatAgentRunner.test.ts +726 -0
- package/Tests/Server/Utils/AI/IncidentToolsFilters.test.ts +315 -0
- package/Tests/Server/Utils/AI/LLMServiceStopReason.test.ts +314 -0
- package/Tests/Server/Utils/AI/LLMServiceToolCalling.test.ts +26 -3
- package/Tests/Server/Utils/AI/NoteWriteTools.test.ts +268 -0
- package/Tests/Server/Utils/AI/ObservabilityChatPrompt.test.ts +169 -0
- package/Tests/Server/Utils/AI/OnCallTools.test.ts +664 -0
- package/Tests/Server/Utils/AI/RunbookTools.test.ts +325 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +306 -0
- package/Tests/Server/Utils/AI/StatusPageTools.test.ts +391 -0
- package/Tests/Server/Utils/AI/TeamTools.test.ts +257 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +472 -0
- package/Tests/Server/Utils/AI/WorkflowProbeTools.test.ts +428 -0
- package/Tests/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.test.ts +152 -0
- package/Tests/Server/Utils/ClientIp.test.ts +438 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +171 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.test.ts +383 -0
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +23 -25
- package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +6 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnssecMonitorCriteria.test.ts +307 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +468 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorTelemetryDeepLinks.test.ts +460 -0
- package/Tests/Server/Utils/ResponseRateLimitStatusCodes.test.ts +137 -0
- package/Tests/Server/Utils/SSRFProtectionBypasses.test.ts +40 -8
- package/Tests/Server/Utils/SSRFProtectionUserInfo.test.ts +351 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistryRetirement.test.ts +531 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +322 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +356 -39
- package/Tests/Server/Utils/VM/VMAPISubstitution.test.ts +243 -0
- package/Tests/Types/IP/IP.test.ts +263 -0
- package/Tests/Types/IP/IPWhitelist.test.ts +197 -0
- package/Tests/Types/IP/IPv6.test.ts +12 -1
- package/Tests/Types/Monitor/SnmpOid.test.ts +64 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceMonitoringMethod.test.ts +110 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeAudit.test.ts +106 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilMergeDifferential.test.ts +511 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageEndToEnd.test.ts +489 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageGapTolerance.test.ts +479 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleCoverageState.test.ts +822 -0
- package/Tests/Types/SerializableObjectDictionaryImportCycle.test.ts +197 -0
- package/Tests/Types/Telemetry/EntityTypeGroups.test.ts +140 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +429 -0
- package/Tests/Types/Workflow/Components/BaseModel.test.ts +225 -0
- package/Tests/Types/Workflow/IntegrationCredentialMetadata.test.ts +100 -0
- package/Tests/Types/Workflow/StepTrace.test.ts +235 -0
- package/Tests/Types/Workflow/TemplateSyntax.test.ts +491 -0
- package/Tests/Types/Workflow/Templates.test.ts +1218 -0
- package/Tests/UI/Components/ActiveFilterChipsOpenRoute.test.tsx +82 -0
- package/Tests/UI/Components/ComponentsModal.test.tsx +564 -7
- package/Tests/UI/Components/CustomTimeRangeModal.test.tsx +459 -0
- package/Tests/UI/Components/DictionaryAttributeFilterRow.test.tsx +477 -0
- package/Tests/UI/Components/Forms/AnchoredFieldPopupKeyboard.test.tsx +382 -0
- package/Tests/UI/Components/Forms/ColorPickerPicking.test.tsx +569 -0
- package/Tests/UI/Components/Forms/ValidationJSON.test.ts +241 -0
- package/Tests/UI/Components/KeyboardShortcut.test.tsx +95 -0
- package/Tests/UI/Components/LogDetailsPanelCrossSignal.test.tsx +448 -0
- package/Tests/UI/Components/LogTimeRangePicker.test.tsx +42 -0
- package/Tests/UI/Components/LogsTableCrossLinks.test.tsx +263 -0
- package/Tests/UI/Components/ModalPortalDismissal.test.tsx +90 -0
- package/Tests/UI/Components/PendingProjectInvitations.test.tsx +913 -0
- package/Tests/UI/Components/SimpleLogViewer.test.tsx +228 -0
- package/Tests/UI/Components/TableRowSelectability.test.tsx +312 -0
- package/Tests/UI/Components/TelemetryTimeRangePicker.test.tsx +49 -7
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +325 -0
- package/Tests/UI/Components/Workflow/GraphLint.test.ts +893 -0
- package/Tests/UI/Components/Workflow/GraphLintSummary.test.ts +755 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.test.ts +522 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditorServerContract.test.ts +193 -0
- package/Tests/UI/Components/Workflow/ModelSchema.test.ts +388 -0
- package/Tests/UI/Components/Workflow/RunStatusWatcher.test.ts +210 -0
- package/Tests/UI/Components/Workflow/StepTraceViewer.test.tsx +256 -0
- package/Tests/UI/Components/Workflow/UseRunWatch.test.tsx +665 -0
- package/Tests/UI/Components/Workflow/Utils.test.ts +192 -0
- package/Tests/UI/Components/Workflow/WorkflowIssuesModal.test.tsx +485 -0
- package/Tests/UI/Components/Workflow/WorkflowLogModal.test.tsx +478 -0
- package/Tests/UI/Components/Workflow/WorkflowStatusBar.test.tsx +379 -0
- package/Tests/UI/EsbuildConfig.test.ts +607 -0
- package/Tests/UI/Monitor/MonitorStepCriteriaView.test.tsx +432 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +5 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +11 -4
- package/Tests/UI/Utils/ModelAPICreateMiscData.test.ts +94 -0
- package/Tests/UI/Utils/Platform.test.ts +147 -0
- package/Tests/UI/Utils/ProjectInvitationDisplay.test.ts +357 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +198 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +514 -0
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +727 -0
- package/Tests/Utils/Schema/AnalyticsModelSchema.test.ts +469 -0
- package/Tests/Utils/Schema/ModelSchema.test.ts +268 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +698 -0
- package/Tests/Utils/Telemetry/EntityKeyNonTelemetry.test.ts +193 -0
- package/Tests/__mocks__/bullmq.js +55 -0
- package/Types/AI/AIChatMessageStatus.ts +8 -1
- package/Types/AI/AIChatTypes.ts +12 -0
- package/Types/Database/AccessControl/OwnerOnlyColumn.ts +88 -0
- package/Types/Exception/ExceptionCode.ts +2 -0
- package/Types/Exception/ServiceUnavailableException.ts +8 -0
- package/Types/Exception/TooManyRequestsException.ts +8 -0
- package/Types/IP/IP.ts +93 -47
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +80 -3
- package/Types/NetworkDevice/NetworkDeviceMonitoringMethod.ts +55 -0
- package/Types/OnCallDutyPolicy/Layer.ts +203 -149
- package/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.ts +13 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +155 -10
- package/Types/Permission.ts +193 -0
- package/Types/SerializableObjectDictionary.ts +133 -39
- package/Types/Telemetry/EntityRelationshipType.ts +1 -1
- package/Types/Telemetry/EntitySource.ts +40 -0
- package/Types/Telemetry/EntityType.ts +28 -1
- package/Types/Telemetry/EntityTypeGroups.ts +65 -0
- package/Types/Workflow/Component.ts +29 -0
- package/Types/Workflow/Components/API.ts +35 -0
- package/Types/Workflow/Components/BaseModel.ts +77 -27
- package/Types/Workflow/Components/Discord.ts +1 -0
- package/Types/Workflow/Components/Email.ts +12 -3
- package/Types/Workflow/Components/JavaScript.ts +7 -0
- package/Types/Workflow/Components/MicrosoftTeams.ts +3 -2
- package/Types/Workflow/Components/Slack.ts +1 -0
- package/Types/Workflow/Components/Telegram.ts +1 -0
- package/Types/Workflow/StepTrace.ts +181 -0
- package/Types/Workflow/TemplateSyntax.ts +543 -0
- package/Types/Workflow/Templates.ts +2368 -0
- package/UI/Components/Calendar/Calendar.css +43 -0
- package/UI/Components/Calendar/Calendar.tsx +8 -0
- package/UI/Components/Card/Card.tsx +2 -2
- package/UI/Components/Checkbox/Checkbox.tsx +16 -0
- package/UI/Components/Date/CustomTimeRangeModal.tsx +278 -0
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +250 -0
- package/UI/Components/Dictionary/Dictionary.tsx +71 -15
- package/UI/Components/FormModal/BasicFormModal.tsx +2 -1
- package/UI/Components/Forms/Fields/ColorPicker.tsx +66 -10
- package/UI/Components/Forms/Fields/IconPicker.tsx +48 -10
- package/UI/Components/Forms/Types/Field.ts +7 -0
- package/UI/Components/Forms/Validation.ts +63 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +53 -3
- package/UI/Components/Input/Input.tsx +32 -3
- package/UI/Components/KeyboardShortcut/KeyboardKey.ts +185 -0
- package/UI/Components/KeyboardShortcut/KeyboardShortcut.tsx +87 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +28 -0
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +31 -0
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +18 -18
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +363 -14
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +11 -216
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +11 -0
- package/UI/Components/LogsViewer/components/LogsTable.tsx +155 -12
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +29 -0
- package/UI/Components/LogsViewer/types.ts +23 -0
- package/UI/Components/Markdown.tsx/MarkdownEditor.tsx +9 -2
- package/UI/Components/Modal/Modal.tsx +37 -4
- package/UI/Components/Navbar/NavBarMenuModal.tsx +15 -30
- package/UI/Components/ProjectInvitations/PendingProjectInvitations.tsx +442 -0
- package/UI/Components/SimpleLogViewer/SimpleLogViewer.tsx +23 -1
- package/UI/Components/Table/Table.tsx +49 -16
- package/UI/Components/Table/TableBody.tsx +53 -28
- package/UI/Components/Table/TableHeader.tsx +13 -0
- package/UI/Components/Table/TableRow.tsx +58 -26
- package/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.tsx +11 -210
- package/UI/Components/Workflow/ArgumentsForm.tsx +341 -8
- package/UI/Components/Workflow/Component.tsx +28 -26
- package/UI/Components/Workflow/ComponentReturnValueViewer.tsx +26 -0
- package/UI/Components/Workflow/ComponentSettingsModal.tsx +79 -9
- package/UI/Components/Workflow/ComponentValuePickerModal.tsx +135 -7
- package/UI/Components/Workflow/ComponentsModal.tsx +116 -22
- package/UI/Components/Workflow/DocumentationViewer.tsx +59 -9
- package/UI/Components/Workflow/GraphLint.ts +628 -0
- package/UI/Components/Workflow/GraphLintSummary.ts +390 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +528 -0
- package/UI/Components/Workflow/ModelSchema.ts +278 -0
- package/UI/Components/Workflow/RunForm.tsx +41 -7
- package/UI/Components/Workflow/RunStatusWatcher.ts +122 -0
- package/UI/Components/Workflow/StepTraceViewer.tsx +186 -0
- package/UI/Components/Workflow/UseRunWatch.ts +212 -0
- package/UI/Components/Workflow/Utils.ts +93 -1
- package/UI/Components/Workflow/VariableModal.tsx +6 -2
- package/UI/Components/Workflow/Workflow.tsx +126 -7
- package/UI/Components/Workflow/WorkflowIssuesModal.tsx +255 -0
- package/UI/Components/Workflow/WorkflowLogModal.tsx +128 -0
- package/UI/Components/Workflow/WorkflowStatusBar.tsx +224 -0
- package/UI/Types/LayeredDismissal.ts +31 -0
- package/UI/Types/UseAnchoredFieldPopup.ts +99 -0
- package/UI/Utils/AIChatExport/ConversationMarkdown.ts +10 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +7 -1
- package/UI/Utils/Platform.ts +149 -0
- package/UI/Utils/ProjectInvitationDisplay.ts +118 -0
- package/UI/esbuild-config.js +22 -1
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +187 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +533 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +917 -155
- package/Utils/Telemetry/CrossSignalScope.ts +502 -0
- package/Utils/Telemetry/EntityKey.ts +71 -5
- package/Utils/Telemetry/EntityRelationship.ts +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +1 -1
- package/build/dist/Models/DatabaseModels/AIConversation.js +32 -0
- package/build/dist/Models/DatabaseModels/AIConversation.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js +42 -0
- package/build/dist/Models/DatabaseModels/AIConversationMessage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js +28 -0
- package/build/dist/Models/DatabaseModels/AlertEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js +29 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeMember.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +12 -4
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/{TelemetryEntity.js → InventoryItem.js} +212 -29
- package/build/dist/Models/DatabaseModels/InventoryItem.js.map +1 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js +454 -0
- package/build/dist/Models/DatabaseModels/InventoryItemCustomField.js.map +1 -0
- package/build/dist/Models/DatabaseModels/{TelemetryEntityRelationship.js → InventoryItemRelationship.js} +52 -25
- package/build/dist/Models/DatabaseModels/InventoryItemRelationship.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +141 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +719 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +475 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js +446 -0
- package/build/dist/Models/DatabaseModels/NetworkTopologySuppression.js.map +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js +9 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Project.js +80 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +46 -2
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +48 -2
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js +424 -55
- package/build/dist/Models/DatabaseModels/UserNotificationRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +24 -2
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +51 -1
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +47 -2
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTelegram.js +65 -3
- package/build/dist/Models/DatabaseModels/UserTelegram.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWebhook.js +56 -2
- package/build/dist/Models/DatabaseModels/UserWebhook.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +45 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowLog.js +39 -0
- package/build/dist/Models/DatabaseModels/WorkflowLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js +12 -0
- package/build/dist/Models/DatabaseModels/WorkflowVariable.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +237 -1
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +165 -13
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallReadinessAPI.js +599 -0
- package/build/dist/Server/API/OnCallReadinessAPI.js.map +1 -0
- package/build/dist/Server/API/TeamComplianceAPI.js +68 -9
- package/build/dist/Server/API/TeamComplianceAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +129 -18
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +16 -1
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +45 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786100000000-RestoreServiceLowerNameIndex.js +4 -4
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786551733814-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786559879134-AddWorkflowLogStepTrace.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786625176831-AddMonitoringMethodToNetworkDevice.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786634985763-AddNetworkDeviceLink.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js +38 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639512056-AddNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786639972982-AddNetworkTopologySuppression.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js +150 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786800000000-RenameTelemetryEntityToInventoryItem.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js +57 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786900000000-AddInventoryItemArchiveAndCustomFields.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js +69 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787000000000-AddOnCallNotificationFallbackColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js +32 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787100000000-AddAIConversationPageContext.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787200000000-AddAIChatMessageFeedback.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787300000000-AddEpisodeMemberNotifyIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +24 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +72 -13
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js +11 -6
- package/build/dist/Server/Middleware/MasterAdminAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js +399 -0
- package/build/dist/Server/Middleware/PublicDashboardRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AIService.js +1 -0
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/AlertEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/AlertSeverityService.js +54 -0
- package/build/dist/Server/Services/AlertSeverityService.js.map +1 -1
- package/build/dist/Server/Services/DashboardService.js +10 -9
- package/build/dist/Server/Services/DashboardService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +24 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js +24 -3
- package/build/dist/Server/Services/IncidentEpisodeStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/IncidentSeverityService.js +67 -0
- package/build/dist/Server/Services/IncidentSeverityService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -4
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js +9 -0
- package/build/dist/Server/Services/InventoryItemCustomFieldService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityRelationshipService.js → InventoryItemRelationshipService.js} +9 -6
- package/build/dist/Server/Services/InventoryItemRelationshipService.js.map +1 -0
- package/build/dist/Server/Services/{TelemetryEntityService.js → InventoryItemService.js} +158 -23
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -0
- package/build/dist/Server/Services/LogAggregationService.js +27 -8
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +80 -0
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +6 -6
- package/build/dist/Server/Services/MetricService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js +9 -0
- package/build/dist/Server/Services/NetworkDeviceLinkRuleService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +71 -0
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceService.js +113 -0
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +53 -13
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js +85 -0
- package/build/dist/Server/Services/NetworkTopologySuppressionService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +48 -32
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js +8 -0
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js +51 -12
- package/build/dist/Server/Services/OnCallDutyPolicyExecutionLogTimelineService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +57 -13
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js +548 -0
- package/build/dist/Server/Services/OnCallNotificationAlertingService.js.map +1 -0
- package/build/dist/Server/Services/OnCallReadinessService.js +1961 -0
- package/build/dist/Server/Services/OnCallReadinessService.js.map +1 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js +738 -0
- package/build/dist/Server/Services/OnCallSetupReminderService.js.map +1 -0
- package/build/dist/Server/Services/ProfileAggregationService.js +68 -4
- package/build/dist/Server/Services/ProfileAggregationService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +11 -10
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/TeamComplianceService.js +312 -160
- package/build/dist/Server/Services/TeamComplianceService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +24 -1
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +24 -1
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js +858 -0
- package/build/dist/Server/Services/UserNotificationRuleAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +2830 -175
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallLogService.js +488 -43
- package/build/dist/Server/Services/UserOnCallLogService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +26 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +10 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +24 -1
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserTelegramService.js +22 -1
- package/build/dist/Server/Services/UserTelegramService.js.map +1 -1
- package/build/dist/Server/Services/UserWebhookService.js +25 -1
- package/build/dist/Server/Services/UserWebhookService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +22 -1
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +12 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js +126 -0
- package/build/dist/Server/Types/Database/Permissions/CreatePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js +254 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerOnlyColumnPermission.js.map +1 -0
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js +47 -2
- package/build/dist/Server/Types/Database/Permissions/QueryPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +7 -0
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Delete.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Get.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Patch.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Post.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Put.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js +28 -0
- package/build/dist/Server/Types/Workflow/Components/API/Utils.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js +21 -5
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateManyBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js +14 -10
- package/build/dist/Server/Types/Workflow/Components/BaseModel/CreateOneBaseModel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js +31 -0
- package/build/dist/Server/Types/Workflow/Components/BaseModel/ModelArguments.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js +3 -9
- package/build/dist/Server/Types/Workflow/Components/Conditions/IfElse.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Email.js +15 -4
- package/build/dist/Server/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js +7 -2
- package/build/dist/Server/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js +1 -1
- package/build/dist/Server/Types/Workflow/Components/MicrosoftTeams/SendMessageToChannel.js.map +1 -1
- package/build/dist/Server/Types/Workflow/TriggerCode.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js +514 -56
- package/build/dist/Server/Utils/AI/Chat/ChatAgentRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +20 -3
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js +19 -6
- package/build/dist/Server/Utils/AI/Chat/ObservabilityChatPrompt.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +7 -0
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js +2 -2
- package/build/dist/Server/Utils/AI/Toolbox/AIActionTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js +692 -0
- package/build/dist/Server/Utils/AI/Toolbox/AIMetaTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js +148 -12
- package/build/dist/Server/Utils/AI/Toolbox/AlertTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js +157 -10
- package/build/dist/Server/Utils/AI/Toolbox/IncidentTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +37 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js +259 -14
- package/build/dist/Server/Utils/AI/Toolbox/MonitorTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js +235 -0
- package/build/dist/Server/Utils/AI/Toolbox/NoteWriteTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js +1000 -0
- package/build/dist/Server/Utils/AI/Toolbox/OnCallTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js +356 -0
- package/build/dist/Server/Utils/AI/Toolbox/RunbookTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +394 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js +465 -0
- package/build/dist/Server/Utils/AI/Toolbox/StatusPageTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js +280 -0
- package/build/dist/Server/Utils/AI/Toolbox/TeamTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +527 -0
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js +548 -0
- package/build/dist/Server/Utils/AI/Toolbox/WorkflowProbeTools.js.map +1 -0
- package/build/dist/Server/Utils/ClientIp.js +137 -0
- package/build/dist/Server/Utils/ClientIp.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +38 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js +89 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloHistoryPolicy.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js +77 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardSloWidget.js.map +1 -0
- package/build/dist/Server/Utils/Express.js +12 -12
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +70 -7
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +121 -11
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +82 -21
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +12 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +165 -18
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +507 -0
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +122 -47
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMAPI.js +51 -4
- package/build/dist/Server/Utils/VM/VMAPI.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +7 -3
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js +8 -1
- package/build/dist/Types/AI/AIChatMessageStatus.js.map +1 -1
- package/build/dist/Types/AI/AIChatTypes.js +12 -0
- package/build/dist/Types/AI/AIChatTypes.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js +60 -0
- package/build/dist/Types/Database/AccessControl/OwnerOnlyColumn.js.map +1 -0
- package/build/dist/Types/Exception/ExceptionCode.js +2 -0
- package/build/dist/Types/Exception/ExceptionCode.js.map +1 -1
- package/build/dist/Types/Exception/ServiceUnavailableException.js +8 -0
- package/build/dist/Types/Exception/ServiceUnavailableException.js.map +1 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js +8 -0
- package/build/dist/Types/Exception/TooManyRequestsException.js.map +1 -0
- package/build/dist/Types/IP/IP.js +87 -43
- package/build/dist/Types/IP/IP.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js +50 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceMonitoringMethod.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +186 -123
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js +13 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallDutyPolicyStatus.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +105 -11
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/Permission.js +174 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/SerializableObjectDictionary.js +133 -39
- package/build/dist/Types/SerializableObjectDictionary.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityRelationshipType.js +1 -1
- package/build/dist/Types/Telemetry/EntitySource.js +39 -0
- package/build/dist/Types/Telemetry/EntitySource.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +28 -1
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityTypeGroups.js +57 -0
- package/build/dist/Types/Telemetry/EntityTypeGroups.js.map +1 -0
- package/build/dist/Types/Workflow/Component.js +21 -0
- package/build/dist/Types/Workflow/Component.js.map +1 -1
- package/build/dist/Types/Workflow/Components/API.js +35 -0
- package/build/dist/Types/Workflow/Components/API.js.map +1 -1
- package/build/dist/Types/Workflow/Components/BaseModel.js +68 -26
- package/build/dist/Types/Workflow/Components/BaseModel.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Discord.js +1 -0
- package/build/dist/Types/Workflow/Components/Discord.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Email.js +12 -3
- package/build/dist/Types/Workflow/Components/Email.js.map +1 -1
- package/build/dist/Types/Workflow/Components/JavaScript.js +7 -0
- package/build/dist/Types/Workflow/Components/JavaScript.js.map +1 -1
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js +3 -2
- package/build/dist/Types/Workflow/Components/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Slack.js +1 -0
- package/build/dist/Types/Workflow/Components/Slack.js.map +1 -1
- package/build/dist/Types/Workflow/Components/Telegram.js +1 -0
- package/build/dist/Types/Workflow/Components/Telegram.js.map +1 -1
- package/build/dist/Types/Workflow/StepTrace.js +104 -0
- package/build/dist/Types/Workflow/StepTrace.js.map +1 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js +338 -0
- package/build/dist/Types/Workflow/TemplateSyntax.js.map +1 -0
- package/build/dist/Types/Workflow/Templates.js +2111 -0
- package/build/dist/Types/Workflow/Templates.js.map +1 -0
- package/build/dist/UI/Components/Calendar/Calendar.js +1 -1
- package/build/dist/UI/Components/Calendar/Calendar.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js +126 -0
- package/build/dist/UI/Components/Date/CustomTimeRangeModal.js.map +1 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +123 -0
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -0
- package/build/dist/UI/Components/Dictionary/Dictionary.js +47 -17
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js +52 -13
- package/build/dist/UI/Components/Forms/Fields/ColorPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js +28 -12
- package/build/dist/UI/Components/Forms/Fields/IconPicker.js.map +1 -1
- package/build/dist/UI/Components/Forms/Validation.js +44 -0
- package/build/dist/UI/Components/Forms/Validation.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +27 -4
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +13 -4
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js +163 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardKey.js.map +1 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js +47 -0
- package/build/dist/UI/Components/KeyboardShortcut/KeyboardShortcut.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +4 -4
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +11 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +10 -8
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +227 -14
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +5 -108
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +5 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +69 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +8 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js +9 -2
- package/build/dist/UI/Components/Markdown.tsx/MarkdownEditor.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +31 -5
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +8 -21
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js +251 -0
- package/build/dist/UI/Components/ProjectInvitations/PendingProjectInvitations.js.map +1 -0
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js +9 -2
- package/build/dist/UI/Components/SimpleLogViewer/SimpleLogViewer.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +27 -15
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableBody.js +24 -18
- package/build/dist/UI/Components/Table/TableBody.js.map +1 -1
- package/build/dist/UI/Components/Table/TableHeader.js +9 -1
- package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +29 -21
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js +5 -105
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryTimeRangePicker.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +261 -14
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Component.js +20 -19
- package/build/dist/UI/Components/Workflow/Component.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js +10 -1
- package/build/dist/UI/Components/Workflow/ComponentReturnValueViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js +35 -7
- package/build/dist/UI/Components/Workflow/ComponentSettingsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js +57 -7
- package/build/dist/UI/Components/Workflow/ComponentValuePickerModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +53 -18
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js +19 -6
- package/build/dist/UI/Components/Workflow/DocumentationViewer.js.map +1 -1
- package/build/dist/UI/Components/Workflow/GraphLint.js +425 -0
- package/build/dist/UI/Components/Workflow/GraphLint.js.map +1 -0
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js +231 -0
- package/build/dist/UI/Components/Workflow/GraphLintSummary.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +320 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js +156 -0
- package/build/dist/UI/Components/Workflow/ModelSchema.js.map +1 -0
- package/build/dist/UI/Components/Workflow/RunForm.js +22 -6
- package/build/dist/UI/Components/Workflow/RunForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js +76 -0
- package/build/dist/UI/Components/Workflow/RunStatusWatcher.js.map +1 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +76 -0
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -0
- package/build/dist/UI/Components/Workflow/UseRunWatch.js +123 -0
- package/build/dist/UI/Components/Workflow/UseRunWatch.js.map +1 -0
- package/build/dist/UI/Components/Workflow/Utils.js +73 -1
- package/build/dist/UI/Components/Workflow/Utils.js.map +1 -1
- package/build/dist/UI/Components/Workflow/VariableModal.js +3 -2
- package/build/dist/UI/Components/Workflow/VariableModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/Workflow.js +92 -7
- package/build/dist/UI/Components/Workflow/Workflow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js +99 -0
- package/build/dist/UI/Components/Workflow/WorkflowIssuesModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js +56 -0
- package/build/dist/UI/Components/Workflow/WorkflowLogModal.js.map +1 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js +92 -0
- package/build/dist/UI/Components/Workflow/WorkflowStatusBar.js.map +1 -0
- package/build/dist/UI/Types/LayeredDismissal.js +21 -0
- package/build/dist/UI/Types/LayeredDismissal.js.map +1 -0
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js +74 -1
- package/build/dist/UI/Types/UseAnchoredFieldPopup.js.map +1 -1
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js +9 -0
- package/build/dist/UI/Utils/AIChatExport/ConversationMarkdown.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/Platform.js +118 -0
- package/build/dist/UI/Utils/Platform.js.map +1 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js +106 -0
- package/build/dist/UI/Utils/ProjectInvitationDisplay.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +108 -0
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +386 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +661 -126
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +328 -0
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +57 -5
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +1 -1
- package/jest.config.json +1 -0
- package/package.json +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntity.js.map +0 -1
- package/build/dist/Models/DatabaseModels/TelemetryEntityRelationship.js.map +0 -1
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +0 -1
- 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
|
+
});
|