@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
|
@@ -8,6 +8,8 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
8
8
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
9
9
|
};
|
|
10
10
|
import DatabaseConfig from "../DatabaseConfig";
|
|
11
|
+
import DatabaseRequestType from "../Types/BaseDatabase/DatabaseRequestType";
|
|
12
|
+
import TenantPermission from "../Types/Database/Permissions/TenantPermission";
|
|
11
13
|
import Markdown, { MarkdownContentType } from "../Types/Markdown";
|
|
12
14
|
import CallService from "./CallService";
|
|
13
15
|
import DatabaseService from "./DatabaseService";
|
|
@@ -21,12 +23,23 @@ import TelegramService from "./TelegramService";
|
|
|
21
23
|
import WebhookService from "./WebhookService";
|
|
22
24
|
import WhatsAppService from "./WhatsAppService";
|
|
23
25
|
import UserEmailService from "./UserEmailService";
|
|
26
|
+
import UserCallService from "./UserCallService";
|
|
27
|
+
import UserPushService from "./UserPushService";
|
|
28
|
+
import UserSmsService from "./UserSmsService";
|
|
29
|
+
import UserTelegramService from "./UserTelegramService";
|
|
30
|
+
import UserWebhookService from "./UserWebhookService";
|
|
31
|
+
import UserWhatsAppService from "./UserWhatsAppService";
|
|
32
|
+
import ProjectService from "./ProjectService";
|
|
33
|
+
import UserNotificationRuleAdminService from "./UserNotificationRuleAdminService";
|
|
34
|
+
import OnCallReadinessService, { ReadinessMethodType, ReadinessStatus, ResponderSource, } from "./OnCallReadinessService";
|
|
24
35
|
import UserOnCallLogService from "./UserOnCallLogService";
|
|
25
36
|
import UserOnCallLogTimelineService from "./UserOnCallLogTimelineService";
|
|
26
37
|
import { AppApiRoute } from "../../ServiceRoute";
|
|
27
38
|
import Route from "../../Types/API/Route";
|
|
28
39
|
import URL from "../../Types/API/URL";
|
|
29
|
-
import
|
|
40
|
+
import AuditLogAction from "../../Types/AuditLog/AuditLogAction";
|
|
41
|
+
import SortOrder from "../../Types/BaseDatabase/SortOrder";
|
|
42
|
+
import LIMIT_MAX, { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
30
43
|
import QueryHelper from "../Types/Database/QueryHelper";
|
|
31
44
|
import Email from "../../Types/Email";
|
|
32
45
|
import EmailTemplateType from "../../Types/Email/EmailTemplateType";
|
|
@@ -49,19 +62,268 @@ import AlertSeverityService from "./AlertSeverityService";
|
|
|
49
62
|
import AlertEpisode from "../../Models/DatabaseModels/AlertEpisode";
|
|
50
63
|
import AlertEpisodeService from "./AlertEpisodeService";
|
|
51
64
|
import AlertEpisodeMemberService from "./AlertEpisodeMemberService";
|
|
65
|
+
import IncidentEpisode from "../../Models/DatabaseModels/IncidentEpisode";
|
|
52
66
|
import IncidentEpisodeService from "./IncidentEpisodeService";
|
|
67
|
+
import IncidentEpisodeMemberService from "./IncidentEpisodeMemberService";
|
|
53
68
|
import WorkspaceNotificationRuleService from "./WorkspaceNotificationRuleService";
|
|
54
69
|
import PushNotificationService from "./PushNotificationService";
|
|
55
70
|
import NotificationRuleEventType from "../../Types/Workspace/NotificationRules/EventType";
|
|
56
71
|
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
57
72
|
import logger from "../Utils/Logger";
|
|
58
73
|
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
74
|
+
/*
|
|
75
|
+
* Why the fallback returns an outcome and not just a boolean.
|
|
76
|
+
*
|
|
77
|
+
* Its caller (UserOnCallLogService.onCreateSuccess) has to pick a
|
|
78
|
+
* UserNotificationExecutionStatus out of the answer, and
|
|
79
|
+
* UserNotificationExecutionStatus.Error is TERMINAL — ExecutePendingExecutions
|
|
80
|
+
* selects Executing and TimeoutStuckExecutions selects Started, so nothing
|
|
81
|
+
* anywhere re-selects an Error log. That makes the two ways of not notifying
|
|
82
|
+
* somebody opposites rather than synonyms: "this responder has nothing we can
|
|
83
|
+
* page them on" is a real, permanent misconfiguration worth burning the log
|
|
84
|
+
* for, while "the send raised" is a bad minute that a terminal status would
|
|
85
|
+
* turn into a permanently dropped page. Both are `notified: false`, so the
|
|
86
|
+
* difference has to survive the return or the caller cannot act on it.
|
|
87
|
+
*/
|
|
88
|
+
export var FallbackNotificationOutcome;
|
|
89
|
+
(function (FallbackNotificationOutcome) {
|
|
90
|
+
/*
|
|
91
|
+
* A page was handed to at least one sender. Nothing below observes what the
|
|
92
|
+
* sender then did with it — every send in deliverNotificationForRule is
|
|
93
|
+
* fire-and-forget — so this means dispatched, not received.
|
|
94
|
+
*/
|
|
95
|
+
FallbackNotificationOutcome["Delivered"] = "Delivered";
|
|
96
|
+
/*
|
|
97
|
+
* There was nothing to try. The responder has no verified method the
|
|
98
|
+
* fallback may use and no webhook, or the only paid channels they have are
|
|
99
|
+
* switched off at the project level. Permanent: a retry finds the same
|
|
100
|
+
* nothing, and only a human adding a notification method changes it.
|
|
101
|
+
*/
|
|
102
|
+
FallbackNotificationOutcome["NoUsableNotificationMethod"] = "NoUsableNotificationMethod";
|
|
103
|
+
/*
|
|
104
|
+
* There was something to try and none of it went out: a send raised, or a
|
|
105
|
+
* chosen channel had no template for this event type, or another run already
|
|
106
|
+
* holds the fallback claim on this log and owns the outcome. All three are
|
|
107
|
+
* transient from the caller's point of view — none of them is evidence that
|
|
108
|
+
* the responder is unreachable, so none of them justifies a terminal status.
|
|
109
|
+
*/
|
|
110
|
+
FallbackNotificationOutcome["DeliveryFailed"] = "DeliveryFailed";
|
|
111
|
+
})(FallbackNotificationOutcome || (FallbackNotificationOutcome = {}));
|
|
112
|
+
/*
|
|
113
|
+
* The fallback is not tied to any UserNotificationRule row — there is no rule,
|
|
114
|
+
* which is the whole reason it runs — so it claims the on-call log under this
|
|
115
|
+
* reserved literal instead of a rule id. `executedNotificationRules` is a jsonb
|
|
116
|
+
* map keyed by arbitrary text, so the literal sits beside real rule uuids and
|
|
117
|
+
* can never collide with one.
|
|
118
|
+
*/
|
|
119
|
+
export const FALLBACK_NOTIFICATION_CLAIM_KEY = "__fallback__";
|
|
120
|
+
/*
|
|
121
|
+
* ---------------------------------------------------------------------------
|
|
122
|
+
* DELETION IMPACT — "what would I lose by deleting this?", asked BEFORE the
|
|
123
|
+
* delete.
|
|
124
|
+
*
|
|
125
|
+
* Two writes a responder makes about their own configuration can take away the
|
|
126
|
+
* only thing standing between a page and nobody hearing it, and neither one
|
|
127
|
+
* looks like that from the screen it is made on:
|
|
128
|
+
*
|
|
129
|
+
* - Deleting a RULE can remove the LAST rule covering one
|
|
130
|
+
* (ruleType x severity) cell. The rule table is a list of rows, not a
|
|
131
|
+
* coverage grid, so "this is the only thing left for Sev1 incidents" is
|
|
132
|
+
* visible nowhere at the moment somebody clicks delete.
|
|
133
|
+
*
|
|
134
|
+
* - Deleting a METHOD CASCADES. Every method foreign key on
|
|
135
|
+
* UserNotificationRule is onDelete: "CASCADE" — and each method service
|
|
136
|
+
* deletes the rows in its own onBeforeDelete as well, so the cascade
|
|
137
|
+
* happens whether or not the database does it — which means removing one
|
|
138
|
+
* phone number destroys every rule that pointed at it. The delete dialog
|
|
139
|
+
* for a phone number mentions notification rules nowhere at all. This is
|
|
140
|
+
* the more dangerous of the two by a distance, because the loss is not even
|
|
141
|
+
* the thing being deleted.
|
|
142
|
+
*
|
|
143
|
+
* Everything here is ADVISORY and is deliberately shaped as a QUESTION the
|
|
144
|
+
* caller asks first, not as a hook that throws. The deletion still goes through
|
|
145
|
+
* the ordinary CRUD path afterwards and nothing below can stop it, for two
|
|
146
|
+
* reasons. The first is that this is the user's own configuration and they are
|
|
147
|
+
* entitled to it — turning "I do not want to be woken by Sev4 alerts" into
|
|
148
|
+
* something a human needs permission for is a worse product than the accident
|
|
149
|
+
* it prevents. The second is that a throwing hook would break the LEGITIMATE
|
|
150
|
+
* deletes too: a user leaving a project, an admin retiring a decommissioned
|
|
151
|
+
* number, a team cleaning up after a migration. The goal is not that nobody
|
|
152
|
+
* does this. It is that nobody does it by accident.
|
|
153
|
+
*
|
|
154
|
+
* "Is this person on call anywhere" is answered by OnCallReadinessService and
|
|
155
|
+
* is never re-derived here. A second answer to that question that disagreed
|
|
156
|
+
* with the readiness page would be worse than no answer: an admin who is told
|
|
157
|
+
* "you are not on call" by a delete dialog and "NotReachable on 3 policies" by
|
|
158
|
+
* the readiness table has no way to know which one to believe, and will end up
|
|
159
|
+
* believing the reassuring one.
|
|
160
|
+
* ---------------------------------------------------------------------------
|
|
161
|
+
*/
|
|
162
|
+
/*
|
|
163
|
+
* The channel vocabulary is ReadinessMethodType, re-exported under the name the
|
|
164
|
+
* deletion API uses. Sharing one enum with readiness (which in turn shares its
|
|
165
|
+
* literals with the fallback's `channelsUsed`) means an operator reading
|
|
166
|
+
* "Telegram" in a delete warning, "Telegram" in the readiness table and
|
|
167
|
+
* "notified via fallback (Telegram)" in an execution log is reading the same
|
|
168
|
+
* word about the same thing. It is also what lets a notification-method service
|
|
169
|
+
* name its own channel without importing the readiness module.
|
|
170
|
+
*/
|
|
171
|
+
export { ReadinessMethodType as NotificationMethodChannel };
|
|
172
|
+
/**
|
|
173
|
+
* Whether anything will still be able to page this user once the deletion has
|
|
174
|
+
* happened.
|
|
175
|
+
*
|
|
176
|
+
* Four values rather than a boolean because two of the four are things this
|
|
177
|
+
* preview knows FOR CERTAIN and two are not, and collapsing them would mean
|
|
178
|
+
* either inventing a false green or crying wolf at everybody.
|
|
179
|
+
*
|
|
180
|
+
* The certainty comes from one structural fact: a method must be verified to be
|
|
181
|
+
* used at all, and of the seven channels, Push, Email and Webhook have no
|
|
182
|
+
* project switch that can turn them off (see OnCallReadinessService.
|
|
183
|
+
* isChannelEnabled — the first two are zero-cost and the third is somebody
|
|
184
|
+
* else's endpoint). So "no verified method survives" is definitely unreachable,
|
|
185
|
+
* and "a verified Push/Email/Webhook survives" is definitely reachable. What is
|
|
186
|
+
* left over — a user whose surviving methods are all on the four paid channels
|
|
187
|
+
* — depends on project settings this preview does not read, and says so.
|
|
188
|
+
*/
|
|
189
|
+
export var PostDeletionReachability;
|
|
190
|
+
(function (PostDeletionReachability) {
|
|
191
|
+
/** A verified method on a channel no project setting can disable survives. */
|
|
192
|
+
PostDeletionReachability["Reachable"] = "Reachable";
|
|
193
|
+
/**
|
|
194
|
+
* Verified methods survive, but every one of them is on a paid channel
|
|
195
|
+
* (SMS, Call, WhatsApp, Telegram) that the project can switch off. Whether
|
|
196
|
+
* this user can still be paged is a project setting, and the readiness page
|
|
197
|
+
* is the surface that knows.
|
|
198
|
+
*/
|
|
199
|
+
PostDeletionReachability["DependsOnProjectSettings"] = "DependsOnProjectSettings";
|
|
200
|
+
/**
|
|
201
|
+
* Nothing verified survives. This deletion is the one that takes away the
|
|
202
|
+
* last way of reaching this person — no rule, and no fallback either, since
|
|
203
|
+
* the fallback needs a verified method too.
|
|
204
|
+
*/
|
|
205
|
+
PostDeletionReachability["NotReachable"] = "NotReachable";
|
|
206
|
+
/**
|
|
207
|
+
* They could not be paged before this deletion either. Worth its own value
|
|
208
|
+
* rather than being folded into NotReachable: the sentence an admin needs is
|
|
209
|
+
* "this was already broken", not "you are about to break it".
|
|
210
|
+
*/
|
|
211
|
+
PostDeletionReachability["AlreadyNotReachable"] = "AlreadyNotReachable";
|
|
212
|
+
/**
|
|
213
|
+
* Readiness had no answer — the user is not a member of this project, or has
|
|
214
|
+
* no User row. Never guessed at, because a guess here is exactly the false
|
|
215
|
+
* green this whole feature exists to prevent.
|
|
216
|
+
*/
|
|
217
|
+
PostDeletionReachability["Unknown"] = "Unknown";
|
|
218
|
+
})(PostDeletionReachability || (PostDeletionReachability = {}));
|
|
219
|
+
var SeverityKind;
|
|
220
|
+
(function (SeverityKind) {
|
|
221
|
+
SeverityKind["Incident"] = "Incident";
|
|
222
|
+
SeverityKind["Alert"] = "Alert";
|
|
223
|
+
})(SeverityKind || (SeverityKind = {}));
|
|
224
|
+
/*
|
|
225
|
+
* Which severity column scopes which rule type. This is the same table
|
|
226
|
+
* OnCallReadinessService keeps as RULE_TYPE_SCOPES and it has to stay in
|
|
227
|
+
* agreement with it: an alert rule matched against an incident severity id
|
|
228
|
+
* matches nothing at runtime, so a preview that paired them would report a cell
|
|
229
|
+
* as covered by a rule that can never fire — the exact shape of Gap G, where
|
|
230
|
+
* episode rules were written with a NULL severity and were unreachable and
|
|
231
|
+
* invisible at the same time. The severity is always taken from the column the
|
|
232
|
+
* RULE TYPE dictates, never from whichever one happens to be populated.
|
|
233
|
+
*/
|
|
234
|
+
const PAGING_RULE_TYPE_SCOPES = [
|
|
235
|
+
{
|
|
236
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
237
|
+
severityKind: SeverityKind.Incident,
|
|
238
|
+
severityColumn: "incidentSeverityId",
|
|
239
|
+
subjectNoun: "incidents",
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
243
|
+
severityKind: SeverityKind.Incident,
|
|
244
|
+
severityColumn: "incidentSeverityId",
|
|
245
|
+
subjectNoun: "incident episodes",
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
249
|
+
severityKind: SeverityKind.Alert,
|
|
250
|
+
severityColumn: "alertSeverityId",
|
|
251
|
+
subjectNoun: "alerts",
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
255
|
+
severityKind: SeverityKind.Alert,
|
|
256
|
+
severityColumn: "alertSeverityId",
|
|
257
|
+
subjectNoun: "alert episodes",
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
/*
|
|
261
|
+
* The two rule types that are about the user's shift rather than about anything
|
|
262
|
+
* that fired. They carry no severity, so they are one cell each.
|
|
263
|
+
*/
|
|
264
|
+
const HANDOFF_RULE_TYPES = [
|
|
265
|
+
NotificationRuleType.WHEN_USER_GOES_ON_CALL,
|
|
266
|
+
NotificationRuleType.WHEN_USER_GOES_OFF_CALL,
|
|
267
|
+
];
|
|
268
|
+
/**
|
|
269
|
+
* The three channels no project setting can switch off. Push and Email are
|
|
270
|
+
* zero-cost and Webhook is somebody else's endpoint, so nothing gates them —
|
|
271
|
+
* which is what makes "a verified one of these survives" a CERTAIN answer to
|
|
272
|
+
* "can this person still be paged" rather than a hopeful one. Kept in step with
|
|
273
|
+
* OnCallReadinessService.isChannelEnabled, which returns true for exactly these
|
|
274
|
+
* three unconditionally.
|
|
275
|
+
*
|
|
276
|
+
* A FUNCTION rather than a module-level constant, and that is load-bearing
|
|
277
|
+
* rather than stylistic: this module and OnCallReadinessService import each
|
|
278
|
+
* other, so whichever one is loaded second sees the other's exports still
|
|
279
|
+
* empty. Reading ReadinessMethodType while this module is being evaluated
|
|
280
|
+
* therefore throws on ONE of the two load orders and not the other — a crash
|
|
281
|
+
* that depends on which file some unrelated caller happened to import first,
|
|
282
|
+
* which is about the worst possible failure to debug. Read on call, both orders
|
|
283
|
+
* are long since settled. The same rule applies to every enum below that comes
|
|
284
|
+
* from OnCallReadinessService.
|
|
285
|
+
*/
|
|
286
|
+
const channelsWithNoProjectSwitch = () => {
|
|
287
|
+
return [
|
|
288
|
+
ReadinessMethodType.Push,
|
|
289
|
+
ReadinessMethodType.Email,
|
|
290
|
+
ReadinessMethodType.Webhook,
|
|
291
|
+
];
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* How each responder source reads in a sentence. The enum values are single
|
|
295
|
+
* words chosen for a chip; a warning has room to say what they mean, and
|
|
296
|
+
* "Override" on its own tells a user nothing about why they are on call.
|
|
297
|
+
*
|
|
298
|
+
* The map is built inside the call for the module-evaluation reason above —
|
|
299
|
+
* a computed key is read at definition time, so a module-level Record would
|
|
300
|
+
* carry exactly the same load-order crash. Typed as a full Record so that a new
|
|
301
|
+
* ResponderSource fails to compile here rather than rendering as a blank.
|
|
302
|
+
*/
|
|
303
|
+
const responderSourceProse = (source) => {
|
|
304
|
+
const prose = {
|
|
305
|
+
[ResponderSource.Direct]: "directly on an escalation rule",
|
|
306
|
+
[ResponderSource.Team]: "through a team",
|
|
307
|
+
[ResponderSource.Schedule]: "through a schedule",
|
|
308
|
+
[ResponderSource.Override]: "through an override",
|
|
309
|
+
};
|
|
310
|
+
return prose[source];
|
|
311
|
+
};
|
|
312
|
+
/*
|
|
313
|
+
* Rows per page for the rule read, and a ceiling on how many pages one preview
|
|
314
|
+
* may take. LIMIT_PER_PROJECT is the largest read the database layer will
|
|
315
|
+
* serve, so it is the biggest page that survives a round trip. One user's rules
|
|
316
|
+
* are bounded by (rule types x severities x methods) and land far below one
|
|
317
|
+
* page in any real project; the loop exists so that the one project where that
|
|
318
|
+
* is not true gets a truthful answer instead of a silently truncated one.
|
|
319
|
+
*/
|
|
320
|
+
const DELETION_IMPACT_PAGE_SIZE = LIMIT_PER_PROJECT;
|
|
321
|
+
const MAX_DELETION_IMPACT_PAGES = 50;
|
|
59
322
|
export class Service extends DatabaseService {
|
|
60
323
|
constructor() {
|
|
61
324
|
super(Model);
|
|
62
325
|
}
|
|
63
326
|
async executeNotificationRuleItem(userNotificationRuleId, options) {
|
|
64
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49;
|
|
65
327
|
/*
|
|
66
328
|
* Atomically claim this rule for this on-call log BEFORE sending, so two
|
|
67
329
|
* overlapping cron runs cannot both mark the rule un-executed and both
|
|
@@ -83,36 +345,51 @@ export class Service extends DatabaseService {
|
|
|
83
345
|
select: {
|
|
84
346
|
_id: true,
|
|
85
347
|
userId: true,
|
|
348
|
+
/*
|
|
349
|
+
* Every method relation also selects its OWN userId, which none of the
|
|
350
|
+
* channel blocks below read. It is here for the ownership check that
|
|
351
|
+
* runs before delivery: the address a page is sent to comes from these
|
|
352
|
+
* relations, while whose page it is comes from the rule's userId, and
|
|
353
|
+
* nothing in the ORM ever compares the two. See
|
|
354
|
+
* getNotificationMethodsNotOwnedByRuleOwner.
|
|
355
|
+
*/
|
|
86
356
|
userCall: {
|
|
87
357
|
phone: true,
|
|
88
358
|
isVerified: true,
|
|
359
|
+
userId: true,
|
|
89
360
|
},
|
|
90
361
|
userSms: {
|
|
91
362
|
phone: true,
|
|
92
363
|
isVerified: true,
|
|
364
|
+
userId: true,
|
|
93
365
|
},
|
|
94
366
|
userWhatsApp: {
|
|
95
367
|
phone: true,
|
|
96
368
|
isVerified: true,
|
|
369
|
+
userId: true,
|
|
97
370
|
},
|
|
98
371
|
userTelegram: {
|
|
99
372
|
telegramChatId: true,
|
|
100
373
|
telegramUserHandle: true,
|
|
101
374
|
isVerified: true,
|
|
375
|
+
userId: true,
|
|
102
376
|
},
|
|
103
377
|
userWebhook: {
|
|
104
378
|
webhookUrl: true,
|
|
105
379
|
name: true,
|
|
106
380
|
secret: true,
|
|
381
|
+
userId: true,
|
|
107
382
|
},
|
|
108
383
|
userEmail: {
|
|
109
384
|
email: true,
|
|
110
385
|
isVerified: true,
|
|
386
|
+
userId: true,
|
|
111
387
|
},
|
|
112
388
|
userPush: {
|
|
113
389
|
deviceToken: true,
|
|
114
390
|
deviceType: true,
|
|
115
391
|
isVerified: true,
|
|
392
|
+
userId: true,
|
|
116
393
|
},
|
|
117
394
|
},
|
|
118
395
|
props: {
|
|
@@ -123,19 +400,121 @@ export class Service extends DatabaseService {
|
|
|
123
400
|
throw new BadDataException("Notification rule item not found.");
|
|
124
401
|
}
|
|
125
402
|
/*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
403
|
+
* The last line of defence, and the only one that survives every write path
|
|
404
|
+
* — including ones that do not exist yet.
|
|
405
|
+
*
|
|
406
|
+
* The write-side guards in UserNotificationRuleAdminService stop a rule
|
|
407
|
+
* whose ownership column and method relation name different people from
|
|
408
|
+
* being SAVED. This stops one that somehow exists from being ACTED ON: a
|
|
409
|
+
* row written before those guards landed, one written by internal code
|
|
410
|
+
* running as root, or one written through a path a future change forgets to
|
|
411
|
+
* route through them. Without it, a single bad row silently redirects a
|
|
412
|
+
* responder's pages for as long as nobody thinks to compare two columns
|
|
413
|
+
* that no screen shows side by side.
|
|
129
414
|
*/
|
|
130
|
-
const
|
|
415
|
+
const mismatchedChannels = this.getNotificationMethodsNotOwnedByRuleOwner(notificationRuleItem);
|
|
416
|
+
if (mismatchedChannels.length > 0) {
|
|
417
|
+
await this.recordMismatchedNotificationMethod(notificationRuleItem, options, mismatchedChannels);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await this.deliverNotificationForRule(notificationRuleItem, options);
|
|
421
|
+
}
|
|
422
|
+
/*
|
|
423
|
+
* Which of a rule's method relations are owned by somebody other than the
|
|
424
|
+
* rule itself.
|
|
425
|
+
*
|
|
426
|
+
* Only a method whose userId was actually LOADED and actually DISAGREES is
|
|
427
|
+
* reported. An unselected column arrives as `undefined`, and reading absence
|
|
428
|
+
* as disagreement would turn this guard into a page-dropping machine on every
|
|
429
|
+
* caller that does not select userId — precisely the failure this whole epic
|
|
430
|
+
* exists to eliminate. Silence here means "no evidence of a mismatch", which
|
|
431
|
+
* is the only safe default for a check that can suppress a page.
|
|
432
|
+
*/
|
|
433
|
+
getNotificationMethodsNotOwnedByRuleOwner(notificationRuleItem) {
|
|
434
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
435
|
+
const ruleOwnerUserId = notificationRuleItem.userId;
|
|
436
|
+
if (!ruleOwnerUserId) {
|
|
437
|
+
/*
|
|
438
|
+
* An unowned rule cannot be paged for anybody in the first place — the
|
|
439
|
+
* caller found it by id, not by owner — so there is no owner to compare
|
|
440
|
+
* against and nothing to report.
|
|
441
|
+
*/
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
const methodOwners = [
|
|
445
|
+
{ label: "Email", ownerUserId: (_a = notificationRuleItem.userEmail) === null || _a === void 0 ? void 0 : _a.userId },
|
|
446
|
+
{ label: "SMS", ownerUserId: (_b = notificationRuleItem.userSms) === null || _b === void 0 ? void 0 : _b.userId },
|
|
447
|
+
{ label: "Call", ownerUserId: (_c = notificationRuleItem.userCall) === null || _c === void 0 ? void 0 : _c.userId },
|
|
448
|
+
{
|
|
449
|
+
label: "WhatsApp",
|
|
450
|
+
ownerUserId: (_d = notificationRuleItem.userWhatsApp) === null || _d === void 0 ? void 0 : _d.userId,
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
label: "Telegram",
|
|
454
|
+
ownerUserId: (_e = notificationRuleItem.userTelegram) === null || _e === void 0 ? void 0 : _e.userId,
|
|
455
|
+
},
|
|
456
|
+
{ label: "Push", ownerUserId: (_f = notificationRuleItem.userPush) === null || _f === void 0 ? void 0 : _f.userId },
|
|
457
|
+
{
|
|
458
|
+
label: "Webhook",
|
|
459
|
+
ownerUserId: (_g = notificationRuleItem.userWebhook) === null || _g === void 0 ? void 0 : _g.userId,
|
|
460
|
+
},
|
|
461
|
+
];
|
|
462
|
+
const mismatched = [];
|
|
463
|
+
for (const methodOwner of methodOwners) {
|
|
464
|
+
if (methodOwner.ownerUserId &&
|
|
465
|
+
methodOwner.ownerUserId.toString() !== ruleOwnerUserId.toString()) {
|
|
466
|
+
mismatched.push(methodOwner.label);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return mismatched;
|
|
470
|
+
}
|
|
471
|
+
/*
|
|
472
|
+
* Refuse the whole rule, not merely the offending channel.
|
|
473
|
+
*
|
|
474
|
+
* A rule with a foreign method on it is not a rule with one bad field; it is
|
|
475
|
+
* a row somebody wrote to redirect a page, and delivering its other channels
|
|
476
|
+
* would let the row keep working well enough to escape notice. The timeline
|
|
477
|
+
* row is the point: it is the surface a responder and an operator both read,
|
|
478
|
+
* and it names the channel so the mismatch can be found and repaired rather
|
|
479
|
+
* than merely felt as a page that never arrived.
|
|
480
|
+
*/
|
|
481
|
+
async recordMismatchedNotificationMethod(notificationRuleItem, options, mismatchedChannels) {
|
|
482
|
+
var _a, _b;
|
|
483
|
+
logger.error(`Notification rule ${(_a = notificationRuleItem.id) === null || _a === void 0 ? void 0 : _a.toString()} was not executed: its ${mismatchedChannels.join(", ")} notification method does not belong to the user the rule belongs to (${(_b = notificationRuleItem.userId) === null || _b === void 0 ? void 0 : _b.toString()}).`);
|
|
484
|
+
const logTimelineItem = this.buildLogTimelineItem(notificationRuleItem, options);
|
|
485
|
+
logTimelineItem.status = UserNotificationStatus.Error;
|
|
486
|
+
logTimelineItem.statusMessage = `Notification not sent because the ${mismatchedChannels.join(", ")} notification method on this rule belongs to a different user. Please review this notification rule.`;
|
|
487
|
+
await UserOnCallLogTimelineService.create({
|
|
488
|
+
data: logTimelineItem,
|
|
489
|
+
props: {
|
|
490
|
+
isRoot: true,
|
|
491
|
+
},
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/*
|
|
495
|
+
* Build the timeline row every channel block stamps its status onto.
|
|
496
|
+
*
|
|
497
|
+
* Callers keep ONE instance and mutate it, because after the first create()
|
|
498
|
+
* the instance carries an _id and a second create() with it UPDATEs the row
|
|
499
|
+
* it already wrote instead of inserting a new one. Anything that needs a row
|
|
500
|
+
* genuinely independent of the delivery attempts (the fell-through guard
|
|
501
|
+
* below) must therefore call this again for a fresh instance rather than
|
|
502
|
+
* reuse the one the channel blocks have been writing to.
|
|
503
|
+
*/
|
|
504
|
+
buildLogTimelineItem(notificationRuleItem, options) {
|
|
131
505
|
const logTimelineItem = new UserOnCallLogTimeline();
|
|
132
506
|
logTimelineItem.projectId = options.projectId;
|
|
133
507
|
logTimelineItem.userNotificationLogId = options.userNotificationLogId;
|
|
134
|
-
logTimelineItem.userNotificationRuleId = userNotificationRuleId;
|
|
135
|
-
logTimelineItem.userNotificationLogId = options.userNotificationLogId;
|
|
136
508
|
logTimelineItem.userId = notificationRuleItem.userId;
|
|
137
509
|
logTimelineItem.userNotificationEventType =
|
|
138
510
|
options.userNotificationEventType;
|
|
511
|
+
/*
|
|
512
|
+
* The fallback delivers through rules it builds in memory and never saves,
|
|
513
|
+
* so there is not always a rule id to point the row at.
|
|
514
|
+
*/
|
|
515
|
+
if (notificationRuleItem.id) {
|
|
516
|
+
logTimelineItem.userNotificationRuleId = notificationRuleItem.id;
|
|
517
|
+
}
|
|
139
518
|
if (options.userBelongsToTeamId) {
|
|
140
519
|
logTimelineItem.userBelongsToTeamId = options.userBelongsToTeamId;
|
|
141
520
|
}
|
|
@@ -168,6 +547,43 @@ export class Service extends DatabaseService {
|
|
|
168
547
|
logTimelineItem.onCallDutyPolicyExecutionLogTimelineId =
|
|
169
548
|
options.onCallDutyPolicyExecutionLogTimelineId;
|
|
170
549
|
}
|
|
550
|
+
return logTimelineItem;
|
|
551
|
+
}
|
|
552
|
+
/*
|
|
553
|
+
* The delivery half of executeNotificationRuleItem: given a rule that is
|
|
554
|
+
* already loaded with its method relations, decide what to send on which
|
|
555
|
+
* channel and hand it to the senders.
|
|
556
|
+
*
|
|
557
|
+
* It is split out from the public method so executeFallbackNotification can
|
|
558
|
+
* reuse it with a rule it assembled in memory and never persisted. The claim
|
|
559
|
+
* and the rule lookup that the public method does first are meaningless for a
|
|
560
|
+
* rule that does not exist in the database; everything from here down is
|
|
561
|
+
* exactly what the fallback needs.
|
|
562
|
+
*
|
|
563
|
+
* Returns whether a page was actually handed to a sender, which the fallback
|
|
564
|
+
* needs and the normal path ignores. Resolving without throwing is NOT the
|
|
565
|
+
* same as having sent something: a rule whose channel has no block for this
|
|
566
|
+
* event type falls all the way through to the guard at the bottom, writes an
|
|
567
|
+
* Error row and sends nothing. A caller that read "did not throw" as "paged"
|
|
568
|
+
* would name a channel the responder never heard from.
|
|
569
|
+
*/
|
|
570
|
+
async deliverNotificationForRule(notificationRuleItem, options) {
|
|
571
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55;
|
|
572
|
+
/*
|
|
573
|
+
* If the project has a default Twilio config set, use it for all
|
|
574
|
+
* team-member SMS and Calls in this rule. Otherwise the global config
|
|
575
|
+
* is used by the notification service.
|
|
576
|
+
*/
|
|
577
|
+
const projectTwilioConfig = await ProjectCallSMSConfigService.getProjectDefaultTwilioConfig(options.projectId);
|
|
578
|
+
const logTimelineItem = this.buildLogTimelineItem(notificationRuleItem, options);
|
|
579
|
+
/*
|
|
580
|
+
* Which channels this rule could actually deliver on, and whether any block
|
|
581
|
+
* below matched the event type. If a channel is contactable but no branch
|
|
582
|
+
* claimed the event, the page vanishes without a trace — the guard at the
|
|
583
|
+
* end of this method turns that into a visible Error row.
|
|
584
|
+
*/
|
|
585
|
+
const contactableChannels = this.getContactableChannelNames(notificationRuleItem);
|
|
586
|
+
let deliveryAttempted = false;
|
|
171
587
|
// add status and status message and save.
|
|
172
588
|
let incident = null;
|
|
173
589
|
let alert = null;
|
|
@@ -294,6 +710,7 @@ export class Service extends DatabaseService {
|
|
|
294
710
|
UserNotificationEventType.AlertCreated &&
|
|
295
711
|
alert) {
|
|
296
712
|
// create an error log.
|
|
713
|
+
deliveryAttempted = true;
|
|
297
714
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
298
715
|
logTimelineItem.statusMessage = `Sending email to ${(_c = notificationRuleItem.userEmail) === null || _c === void 0 ? void 0 : _c.email.toString()}`;
|
|
299
716
|
logTimelineItem.userEmailId = notificationRuleItem.userEmail.id;
|
|
@@ -333,6 +750,7 @@ export class Service extends DatabaseService {
|
|
|
333
750
|
UserNotificationEventType.IncidentCreated &&
|
|
334
751
|
incident) {
|
|
335
752
|
// create an error log.
|
|
753
|
+
deliveryAttempted = true;
|
|
336
754
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
337
755
|
logTimelineItem.statusMessage = `Sending email to ${(_e = notificationRuleItem.userEmail) === null || _e === void 0 ? void 0 : _e.email.toString()}`;
|
|
338
756
|
logTimelineItem.userEmailId = notificationRuleItem.userEmail.id;
|
|
@@ -371,6 +789,7 @@ export class Service extends DatabaseService {
|
|
|
371
789
|
if (options.userNotificationEventType ===
|
|
372
790
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
373
791
|
alertEpisode) {
|
|
792
|
+
deliveryAttempted = true;
|
|
374
793
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
375
794
|
logTimelineItem.statusMessage = `Sending email to ${(_g = notificationRuleItem.userEmail) === null || _g === void 0 ? void 0 : _g.email.toString()}`;
|
|
376
795
|
logTimelineItem.userEmailId = notificationRuleItem.userEmail.id;
|
|
@@ -404,13 +823,55 @@ export class Service extends DatabaseService {
|
|
|
404
823
|
});
|
|
405
824
|
});
|
|
406
825
|
}
|
|
826
|
+
// send email for incident episode
|
|
827
|
+
if (options.userNotificationEventType ===
|
|
828
|
+
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
829
|
+
incidentEpisode) {
|
|
830
|
+
deliveryAttempted = true;
|
|
831
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
832
|
+
logTimelineItem.statusMessage = `Sending email to ${(_j = notificationRuleItem.userEmail) === null || _j === void 0 ? void 0 : _j.email.toString()}`;
|
|
833
|
+
logTimelineItem.userEmailId = notificationRuleItem.userEmail.id;
|
|
834
|
+
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
835
|
+
data: logTimelineItem,
|
|
836
|
+
props: {
|
|
837
|
+
isRoot: true,
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
const emailMessage = await this.generateEmailTemplateForIncidentEpisodeCreated((_k = notificationRuleItem.userEmail) === null || _k === void 0 ? void 0 : _k.email, incidentEpisode, updatedLog.id);
|
|
841
|
+
/*
|
|
842
|
+
* No incidentEpisodeId is passed: MailService.sendMail accepts the key
|
|
843
|
+
* in its options type but never serialises it onto the request body, so
|
|
844
|
+
* passing it would look like a link that does not exist.
|
|
845
|
+
*/
|
|
846
|
+
MailService.sendMail(emailMessage, {
|
|
847
|
+
userOnCallLogTimelineId: updatedLog.id,
|
|
848
|
+
projectId: options.projectId,
|
|
849
|
+
userId: notificationRuleItem.userId,
|
|
850
|
+
onCallPolicyId: options.onCallPolicyId,
|
|
851
|
+
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
|
852
|
+
teamId: options.userBelongsToTeamId,
|
|
853
|
+
onCallDutyPolicyExecutionLogTimelineId: options.onCallDutyPolicyExecutionLogTimelineId,
|
|
854
|
+
onCallScheduleId: options.onCallScheduleId,
|
|
855
|
+
}).catch(async (err) => {
|
|
856
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
857
|
+
id: updatedLog.id,
|
|
858
|
+
data: {
|
|
859
|
+
status: UserNotificationStatus.Error,
|
|
860
|
+
statusMessage: err.message || "Error sending email.",
|
|
861
|
+
},
|
|
862
|
+
props: {
|
|
863
|
+
isRoot: true,
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
}
|
|
407
868
|
}
|
|
408
869
|
// if you have an email but is not verified, then create a log.
|
|
409
|
-
if (((
|
|
410
|
-
!((
|
|
870
|
+
if (((_l = notificationRuleItem.userEmail) === null || _l === void 0 ? void 0 : _l.email) &&
|
|
871
|
+
!((_m = notificationRuleItem.userEmail) === null || _m === void 0 ? void 0 : _m.isVerified)) {
|
|
411
872
|
// create an error log.
|
|
412
873
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
413
|
-
logTimelineItem.statusMessage = `Email notification not sent because email ${(
|
|
874
|
+
logTimelineItem.statusMessage = `Email notification not sent because email ${(_o = notificationRuleItem.userEmail) === null || _o === void 0 ? void 0 : _o.email.toString()} is not verified.`;
|
|
414
875
|
await UserOnCallLogTimelineService.create({
|
|
415
876
|
data: logTimelineItem,
|
|
416
877
|
props: {
|
|
@@ -419,15 +880,16 @@ export class Service extends DatabaseService {
|
|
|
419
880
|
});
|
|
420
881
|
}
|
|
421
882
|
// send sms.
|
|
422
|
-
if (((
|
|
423
|
-
((
|
|
883
|
+
if (((_p = notificationRuleItem.userSms) === null || _p === void 0 ? void 0 : _p.phone) &&
|
|
884
|
+
((_q = notificationRuleItem.userSms) === null || _q === void 0 ? void 0 : _q.isVerified)) {
|
|
424
885
|
//send sms for alert
|
|
425
886
|
if (options.userNotificationEventType ===
|
|
426
887
|
UserNotificationEventType.AlertCreated &&
|
|
427
888
|
alert) {
|
|
428
889
|
// create an error log.
|
|
890
|
+
deliveryAttempted = true;
|
|
429
891
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
430
|
-
logTimelineItem.statusMessage = `Sending SMS to ${(
|
|
892
|
+
logTimelineItem.statusMessage = `Sending SMS to ${(_r = notificationRuleItem.userSms) === null || _r === void 0 ? void 0 : _r.phone.toString()}.`;
|
|
431
893
|
logTimelineItem.userSmsId = notificationRuleItem.userSms.id;
|
|
432
894
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
433
895
|
data: logTimelineItem,
|
|
@@ -466,8 +928,9 @@ export class Service extends DatabaseService {
|
|
|
466
928
|
UserNotificationEventType.IncidentCreated &&
|
|
467
929
|
incident) {
|
|
468
930
|
// create an error log.
|
|
931
|
+
deliveryAttempted = true;
|
|
469
932
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
470
|
-
logTimelineItem.statusMessage = `Sending SMS to ${(
|
|
933
|
+
logTimelineItem.statusMessage = `Sending SMS to ${(_s = notificationRuleItem.userSms) === null || _s === void 0 ? void 0 : _s.phone.toString()}.`;
|
|
471
934
|
logTimelineItem.userSmsId = notificationRuleItem.userSms.id;
|
|
472
935
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
473
936
|
data: logTimelineItem,
|
|
@@ -505,8 +968,9 @@ export class Service extends DatabaseService {
|
|
|
505
968
|
if (options.userNotificationEventType ===
|
|
506
969
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
507
970
|
alertEpisode) {
|
|
971
|
+
deliveryAttempted = true;
|
|
508
972
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
509
|
-
logTimelineItem.statusMessage = `Sending SMS to ${(
|
|
973
|
+
logTimelineItem.statusMessage = `Sending SMS to ${(_t = notificationRuleItem.userSms) === null || _t === void 0 ? void 0 : _t.phone.toString()}.`;
|
|
510
974
|
logTimelineItem.userSmsId = notificationRuleItem.userSms.id;
|
|
511
975
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
512
976
|
data: logTimelineItem,
|
|
@@ -539,12 +1003,54 @@ export class Service extends DatabaseService {
|
|
|
539
1003
|
});
|
|
540
1004
|
});
|
|
541
1005
|
}
|
|
1006
|
+
// send sms for incident episode
|
|
1007
|
+
if (options.userNotificationEventType ===
|
|
1008
|
+
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
1009
|
+
incidentEpisode) {
|
|
1010
|
+
deliveryAttempted = true;
|
|
1011
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1012
|
+
logTimelineItem.statusMessage = `Sending SMS to ${(_u = notificationRuleItem.userSms) === null || _u === void 0 ? void 0 : _u.phone.toString()}.`;
|
|
1013
|
+
logTimelineItem.userSmsId = notificationRuleItem.userSms.id;
|
|
1014
|
+
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1015
|
+
data: logTimelineItem,
|
|
1016
|
+
props: {
|
|
1017
|
+
isRoot: true,
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
const smsMessage = await this.generateSmsTemplateForIncidentEpisodeCreated(notificationRuleItem.userSms.phone, incidentEpisode, updatedLog.id);
|
|
1021
|
+
/*
|
|
1022
|
+
* SmsService accepts incidentEpisodeId but drops it on the floor when
|
|
1023
|
+
* building the request body, so it is deliberately not passed here.
|
|
1024
|
+
*/
|
|
1025
|
+
SmsService.sendSms(smsMessage, {
|
|
1026
|
+
projectId: incidentEpisode.projectId,
|
|
1027
|
+
customTwilioConfig: projectTwilioConfig,
|
|
1028
|
+
userOnCallLogTimelineId: updatedLog.id,
|
|
1029
|
+
userId: notificationRuleItem.userId,
|
|
1030
|
+
onCallPolicyId: options.onCallPolicyId,
|
|
1031
|
+
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
|
1032
|
+
teamId: options.userBelongsToTeamId,
|
|
1033
|
+
onCallDutyPolicyExecutionLogTimelineId: options.onCallDutyPolicyExecutionLogTimelineId,
|
|
1034
|
+
onCallScheduleId: options.onCallScheduleId,
|
|
1035
|
+
}).catch(async (err) => {
|
|
1036
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
1037
|
+
id: updatedLog.id,
|
|
1038
|
+
data: {
|
|
1039
|
+
status: UserNotificationStatus.Error,
|
|
1040
|
+
statusMessage: err.message || "Error sending SMS.",
|
|
1041
|
+
},
|
|
1042
|
+
props: {
|
|
1043
|
+
isRoot: true,
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
542
1048
|
}
|
|
543
|
-
if (((
|
|
544
|
-
!((
|
|
1049
|
+
if (((_v = notificationRuleItem.userSms) === null || _v === void 0 ? void 0 : _v.phone) &&
|
|
1050
|
+
!((_w = notificationRuleItem.userSms) === null || _w === void 0 ? void 0 : _w.isVerified)) {
|
|
545
1051
|
// create a log.
|
|
546
1052
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
547
|
-
logTimelineItem.statusMessage = `SMS not sent because phone ${(
|
|
1053
|
+
logTimelineItem.statusMessage = `SMS not sent because phone ${(_x = notificationRuleItem.userSms) === null || _x === void 0 ? void 0 : _x.phone.toString()} is not verified.`;
|
|
548
1054
|
await UserOnCallLogTimelineService.create({
|
|
549
1055
|
data: logTimelineItem,
|
|
550
1056
|
props: {
|
|
@@ -552,13 +1058,14 @@ export class Service extends DatabaseService {
|
|
|
552
1058
|
},
|
|
553
1059
|
});
|
|
554
1060
|
}
|
|
555
|
-
if (((
|
|
556
|
-
((
|
|
1061
|
+
if (((_y = notificationRuleItem.userWhatsApp) === null || _y === void 0 ? void 0 : _y.phone) &&
|
|
1062
|
+
((_z = notificationRuleItem.userWhatsApp) === null || _z === void 0 ? void 0 : _z.isVerified)) {
|
|
557
1063
|
if (options.userNotificationEventType ===
|
|
558
1064
|
UserNotificationEventType.AlertCreated &&
|
|
559
1065
|
alert) {
|
|
1066
|
+
deliveryAttempted = true;
|
|
560
1067
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
561
|
-
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(
|
|
1068
|
+
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(_0 = notificationRuleItem.userWhatsApp) === null || _0 === void 0 ? void 0 : _0.phone.toString()}.`;
|
|
562
1069
|
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id;
|
|
563
1070
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
564
1071
|
data: logTimelineItem,
|
|
@@ -593,8 +1100,9 @@ export class Service extends DatabaseService {
|
|
|
593
1100
|
if (options.userNotificationEventType ===
|
|
594
1101
|
UserNotificationEventType.IncidentCreated &&
|
|
595
1102
|
incident) {
|
|
1103
|
+
deliveryAttempted = true;
|
|
596
1104
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
597
|
-
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(
|
|
1105
|
+
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(_1 = notificationRuleItem.userWhatsApp) === null || _1 === void 0 ? void 0 : _1.phone.toString()}.`;
|
|
598
1106
|
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id;
|
|
599
1107
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
600
1108
|
data: logTimelineItem,
|
|
@@ -630,8 +1138,9 @@ export class Service extends DatabaseService {
|
|
|
630
1138
|
if (options.userNotificationEventType ===
|
|
631
1139
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
632
1140
|
alertEpisode) {
|
|
1141
|
+
deliveryAttempted = true;
|
|
633
1142
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
634
|
-
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(
|
|
1143
|
+
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(_2 = notificationRuleItem.userWhatsApp) === null || _2 === void 0 ? void 0 : _2.phone.toString()}.`;
|
|
635
1144
|
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id;
|
|
636
1145
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
637
1146
|
data: logTimelineItem,
|
|
@@ -663,11 +1172,52 @@ export class Service extends DatabaseService {
|
|
|
663
1172
|
});
|
|
664
1173
|
});
|
|
665
1174
|
}
|
|
1175
|
+
// send WhatsApp for incident episode
|
|
1176
|
+
if (options.userNotificationEventType ===
|
|
1177
|
+
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
1178
|
+
incidentEpisode) {
|
|
1179
|
+
deliveryAttempted = true;
|
|
1180
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1181
|
+
logTimelineItem.statusMessage = `Sending WhatsApp message to ${(_3 = notificationRuleItem.userWhatsApp) === null || _3 === void 0 ? void 0 : _3.phone.toString()}.`;
|
|
1182
|
+
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id;
|
|
1183
|
+
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1184
|
+
data: logTimelineItem,
|
|
1185
|
+
props: {
|
|
1186
|
+
isRoot: true,
|
|
1187
|
+
},
|
|
1188
|
+
});
|
|
1189
|
+
const whatsAppMessage = await this.generateWhatsAppTemplateForIncidentEpisodeCreated(notificationRuleItem.userWhatsApp.phone, incidentEpisode, updatedLog.id);
|
|
1190
|
+
/*
|
|
1191
|
+
* WhatsAppService accepts incidentEpisodeId but never writes it onto
|
|
1192
|
+
* the request body, so it is deliberately not passed here.
|
|
1193
|
+
*/
|
|
1194
|
+
WhatsAppService.sendWhatsAppMessage(whatsAppMessage, {
|
|
1195
|
+
projectId: incidentEpisode.projectId,
|
|
1196
|
+
userOnCallLogTimelineId: updatedLog.id,
|
|
1197
|
+
userId: notificationRuleItem.userId,
|
|
1198
|
+
onCallPolicyId: options.onCallPolicyId,
|
|
1199
|
+
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
|
1200
|
+
teamId: options.userBelongsToTeamId,
|
|
1201
|
+
onCallDutyPolicyExecutionLogTimelineId: options.onCallDutyPolicyExecutionLogTimelineId,
|
|
1202
|
+
onCallScheduleId: options.onCallScheduleId,
|
|
1203
|
+
}).catch(async (err) => {
|
|
1204
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
1205
|
+
id: updatedLog.id,
|
|
1206
|
+
data: {
|
|
1207
|
+
status: UserNotificationStatus.Error,
|
|
1208
|
+
statusMessage: err.message || "Error sending WhatsApp message.",
|
|
1209
|
+
},
|
|
1210
|
+
props: {
|
|
1211
|
+
isRoot: true,
|
|
1212
|
+
},
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
666
1216
|
}
|
|
667
|
-
if (((
|
|
668
|
-
!((
|
|
1217
|
+
if (((_4 = notificationRuleItem.userWhatsApp) === null || _4 === void 0 ? void 0 : _4.phone) &&
|
|
1218
|
+
!((_5 = notificationRuleItem.userWhatsApp) === null || _5 === void 0 ? void 0 : _5.isVerified)) {
|
|
669
1219
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
670
|
-
logTimelineItem.statusMessage = `WhatsApp message not sent because phone ${(
|
|
1220
|
+
logTimelineItem.statusMessage = `WhatsApp message not sent because phone ${(_6 = notificationRuleItem.userWhatsApp) === null || _6 === void 0 ? void 0 : _6.phone.toString()} is not verified.`;
|
|
671
1221
|
logTimelineItem.userWhatsAppId = notificationRuleItem.userWhatsApp.id;
|
|
672
1222
|
await UserOnCallLogTimelineService.create({
|
|
673
1223
|
data: logTimelineItem,
|
|
@@ -677,11 +1227,12 @@ export class Service extends DatabaseService {
|
|
|
677
1227
|
});
|
|
678
1228
|
}
|
|
679
1229
|
// send Telegram.
|
|
680
|
-
if (((
|
|
681
|
-
((
|
|
1230
|
+
if (((_7 = notificationRuleItem.userTelegram) === null || _7 === void 0 ? void 0 : _7.telegramChatId) &&
|
|
1231
|
+
((_8 = notificationRuleItem.userTelegram) === null || _8 === void 0 ? void 0 : _8.isVerified)) {
|
|
682
1232
|
if (options.userNotificationEventType ===
|
|
683
1233
|
UserNotificationEventType.AlertCreated &&
|
|
684
1234
|
alert) {
|
|
1235
|
+
deliveryAttempted = true;
|
|
685
1236
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
686
1237
|
logTimelineItem.statusMessage = `Sending Telegram message.`;
|
|
687
1238
|
logTimelineItem.userTelegramId = notificationRuleItem.userTelegram.id;
|
|
@@ -723,6 +1274,7 @@ export class Service extends DatabaseService {
|
|
|
723
1274
|
if (options.userNotificationEventType ===
|
|
724
1275
|
UserNotificationEventType.IncidentCreated &&
|
|
725
1276
|
incident) {
|
|
1277
|
+
deliveryAttempted = true;
|
|
726
1278
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
727
1279
|
logTimelineItem.statusMessage = `Sending Telegram message.`;
|
|
728
1280
|
logTimelineItem.userTelegramId = notificationRuleItem.userTelegram.id;
|
|
@@ -764,6 +1316,7 @@ export class Service extends DatabaseService {
|
|
|
764
1316
|
if (options.userNotificationEventType ===
|
|
765
1317
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
766
1318
|
alertEpisode) {
|
|
1319
|
+
deliveryAttempted = true;
|
|
767
1320
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
768
1321
|
logTimelineItem.statusMessage = `Sending Telegram message.`;
|
|
769
1322
|
logTimelineItem.userTelegramId = notificationRuleItem.userTelegram.id;
|
|
@@ -802,9 +1355,54 @@ export class Service extends DatabaseService {
|
|
|
802
1355
|
});
|
|
803
1356
|
});
|
|
804
1357
|
}
|
|
1358
|
+
if (options.userNotificationEventType ===
|
|
1359
|
+
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
1360
|
+
incidentEpisode) {
|
|
1361
|
+
deliveryAttempted = true;
|
|
1362
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1363
|
+
logTimelineItem.statusMessage = `Sending Telegram message.`;
|
|
1364
|
+
logTimelineItem.userTelegramId = notificationRuleItem.userTelegram.id;
|
|
1365
|
+
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1366
|
+
data: logTimelineItem,
|
|
1367
|
+
props: {
|
|
1368
|
+
isRoot: true,
|
|
1369
|
+
},
|
|
1370
|
+
});
|
|
1371
|
+
const telegramMessage = {
|
|
1372
|
+
to: notificationRuleItem.userTelegram.telegramChatId,
|
|
1373
|
+
body: await this.generateTelegramBodyForIncidentEpisodeCreated(incidentEpisode, updatedLog.id),
|
|
1374
|
+
parseMode: "HTML",
|
|
1375
|
+
disableWebPagePreview: true,
|
|
1376
|
+
};
|
|
1377
|
+
/*
|
|
1378
|
+
* TelegramService accepts incidentEpisodeId but never writes it onto
|
|
1379
|
+
* the request body, so it is deliberately not passed here.
|
|
1380
|
+
*/
|
|
1381
|
+
TelegramService.sendTelegramMessage(telegramMessage, {
|
|
1382
|
+
projectId: incidentEpisode.projectId,
|
|
1383
|
+
userOnCallLogTimelineId: updatedLog.id,
|
|
1384
|
+
userId: notificationRuleItem.userId,
|
|
1385
|
+
onCallPolicyId: options.onCallPolicyId,
|
|
1386
|
+
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
|
1387
|
+
teamId: options.userBelongsToTeamId,
|
|
1388
|
+
onCallDutyPolicyExecutionLogTimelineId: options.onCallDutyPolicyExecutionLogTimelineId,
|
|
1389
|
+
onCallScheduleId: options.onCallScheduleId,
|
|
1390
|
+
}).catch(async (err) => {
|
|
1391
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
1392
|
+
id: updatedLog.id,
|
|
1393
|
+
data: {
|
|
1394
|
+
status: UserNotificationStatus.Error,
|
|
1395
|
+
statusMessage: err.message || "Error sending Telegram message.",
|
|
1396
|
+
},
|
|
1397
|
+
props: {
|
|
1398
|
+
isRoot: true,
|
|
1399
|
+
},
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
805
1403
|
}
|
|
806
1404
|
if (notificationRuleItem.userTelegram &&
|
|
807
|
-
!((
|
|
1405
|
+
!((_9 = notificationRuleItem.userTelegram) === null || _9 === void 0 ? void 0 : _9.isVerified)) {
|
|
808
1406
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
809
1407
|
logTimelineItem.statusMessage = `Telegram message not sent because the Telegram account is not verified.`;
|
|
810
1408
|
logTimelineItem.userTelegramId = notificationRuleItem.userTelegram.id;
|
|
@@ -816,11 +1414,12 @@ export class Service extends DatabaseService {
|
|
|
816
1414
|
});
|
|
817
1415
|
}
|
|
818
1416
|
// send webhook.
|
|
819
|
-
if ((
|
|
1417
|
+
if ((_10 = notificationRuleItem.userWebhook) === null || _10 === void 0 ? void 0 : _10.webhookUrl) {
|
|
820
1418
|
const webhookUrl = notificationRuleItem.userWebhook.webhookUrl;
|
|
821
1419
|
const webhookSecret = notificationRuleItem.userWebhook.secret;
|
|
822
1420
|
const userWebhookId = notificationRuleItem.userWebhook.id;
|
|
823
1421
|
const dispatchWebhook = async (params) => {
|
|
1422
|
+
deliveryAttempted = true;
|
|
824
1423
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
825
1424
|
logTimelineItem.statusMessage = `Sending webhook to ${webhookUrl}.`;
|
|
826
1425
|
logTimelineItem.userWebhookId = userWebhookId;
|
|
@@ -865,19 +1464,19 @@ export class Service extends DatabaseService {
|
|
|
865
1464
|
payload: {
|
|
866
1465
|
eventType: "on-call.alert.created",
|
|
867
1466
|
timestamp: new Date().toISOString(),
|
|
868
|
-
projectId: ((
|
|
1467
|
+
projectId: ((_11 = alert.projectId) === null || _11 === void 0 ? void 0 : _11.toString()) || "",
|
|
869
1468
|
userId: notificationRuleItem.userId.toString(),
|
|
870
1469
|
alert: {
|
|
871
|
-
id: ((
|
|
1470
|
+
id: ((_12 = alert.id) === null || _12 === void 0 ? void 0 : _12.toString()) || "",
|
|
872
1471
|
title: alert.title || "",
|
|
873
1472
|
description: alert.description || "",
|
|
874
1473
|
alertNumber: alert.alertNumber || null,
|
|
875
1474
|
alertNumberWithPrefix: alert.alertNumberWithPrefix || null,
|
|
876
|
-
severity: ((
|
|
877
|
-
state: ((
|
|
1475
|
+
severity: ((_13 = alert.alertSeverity) === null || _13 === void 0 ? void 0 : _13.name) || null,
|
|
1476
|
+
state: ((_14 = alert.currentAlertState) === null || _14 === void 0 ? void 0 : _14.name) || null,
|
|
878
1477
|
},
|
|
879
|
-
onCallPolicyId: ((
|
|
880
|
-
onCallPolicyEscalationRuleId: ((
|
|
1478
|
+
onCallPolicyId: ((_15 = options.onCallPolicyId) === null || _15 === void 0 ? void 0 : _15.toString()) || null,
|
|
1479
|
+
onCallPolicyEscalationRuleId: ((_16 = options.onCallPolicyEscalationRuleId) === null || _16 === void 0 ? void 0 : _16.toString()) || null,
|
|
881
1480
|
},
|
|
882
1481
|
});
|
|
883
1482
|
}
|
|
@@ -891,19 +1490,19 @@ export class Service extends DatabaseService {
|
|
|
891
1490
|
payload: {
|
|
892
1491
|
eventType: "on-call.incident.created",
|
|
893
1492
|
timestamp: new Date().toISOString(),
|
|
894
|
-
projectId: ((
|
|
1493
|
+
projectId: ((_17 = incident.projectId) === null || _17 === void 0 ? void 0 : _17.toString()) || "",
|
|
895
1494
|
userId: notificationRuleItem.userId.toString(),
|
|
896
1495
|
incident: {
|
|
897
|
-
id: ((
|
|
1496
|
+
id: ((_18 = incident.id) === null || _18 === void 0 ? void 0 : _18.toString()) || "",
|
|
898
1497
|
title: incident.title || "",
|
|
899
1498
|
description: incident.description || "",
|
|
900
1499
|
incidentNumber: incident.incidentNumber || null,
|
|
901
1500
|
incidentNumberWithPrefix: incident.incidentNumberWithPrefix || null,
|
|
902
|
-
severity: ((
|
|
903
|
-
state: ((
|
|
1501
|
+
severity: ((_19 = incident.incidentSeverity) === null || _19 === void 0 ? void 0 : _19.name) || null,
|
|
1502
|
+
state: ((_20 = incident.currentIncidentState) === null || _20 === void 0 ? void 0 : _20.name) || null,
|
|
904
1503
|
},
|
|
905
|
-
onCallPolicyId: ((
|
|
906
|
-
onCallPolicyEscalationRuleId: ((
|
|
1504
|
+
onCallPolicyId: ((_21 = options.onCallPolicyId) === null || _21 === void 0 ? void 0 : _21.toString()) || null,
|
|
1505
|
+
onCallPolicyEscalationRuleId: ((_22 = options.onCallPolicyEscalationRuleId) === null || _22 === void 0 ? void 0 : _22.toString()) || null,
|
|
907
1506
|
},
|
|
908
1507
|
});
|
|
909
1508
|
}
|
|
@@ -916,19 +1515,19 @@ export class Service extends DatabaseService {
|
|
|
916
1515
|
payload: {
|
|
917
1516
|
eventType: "on-call.alertEpisode.created",
|
|
918
1517
|
timestamp: new Date().toISOString(),
|
|
919
|
-
projectId: ((
|
|
1518
|
+
projectId: ((_23 = alertEpisode.projectId) === null || _23 === void 0 ? void 0 : _23.toString()) || "",
|
|
920
1519
|
userId: notificationRuleItem.userId.toString(),
|
|
921
1520
|
alertEpisode: {
|
|
922
|
-
id: ((
|
|
1521
|
+
id: ((_24 = alertEpisode.id) === null || _24 === void 0 ? void 0 : _24.toString()) || "",
|
|
923
1522
|
title: alertEpisode.title || "",
|
|
924
1523
|
description: alertEpisode.description || "",
|
|
925
1524
|
episodeNumber: alertEpisode.episodeNumber || null,
|
|
926
1525
|
episodeNumberWithPrefix: alertEpisode.episodeNumberWithPrefix || null,
|
|
927
|
-
severity: ((
|
|
928
|
-
state: ((
|
|
1526
|
+
severity: ((_25 = alertEpisode.alertSeverity) === null || _25 === void 0 ? void 0 : _25.name) || null,
|
|
1527
|
+
state: ((_26 = alertEpisode.currentAlertState) === null || _26 === void 0 ? void 0 : _26.name) || null,
|
|
929
1528
|
},
|
|
930
|
-
onCallPolicyId: ((
|
|
931
|
-
onCallPolicyEscalationRuleId: ((
|
|
1529
|
+
onCallPolicyId: ((_27 = options.onCallPolicyId) === null || _27 === void 0 ? void 0 : _27.toString()) || null,
|
|
1530
|
+
onCallPolicyEscalationRuleId: ((_28 = options.onCallPolicyEscalationRuleId) === null || _28 === void 0 ? void 0 : _28.toString()) || null,
|
|
932
1531
|
},
|
|
933
1532
|
});
|
|
934
1533
|
}
|
|
@@ -941,33 +1540,34 @@ export class Service extends DatabaseService {
|
|
|
941
1540
|
payload: {
|
|
942
1541
|
eventType: "on-call.incidentEpisode.created",
|
|
943
1542
|
timestamp: new Date().toISOString(),
|
|
944
|
-
projectId: ((
|
|
1543
|
+
projectId: ((_29 = incidentEpisode.projectId) === null || _29 === void 0 ? void 0 : _29.toString()) || "",
|
|
945
1544
|
userId: notificationRuleItem.userId.toString(),
|
|
946
1545
|
incidentEpisode: {
|
|
947
|
-
id: ((
|
|
1546
|
+
id: ((_30 = incidentEpisode.id) === null || _30 === void 0 ? void 0 : _30.toString()) || "",
|
|
948
1547
|
title: incidentEpisode.title || "",
|
|
949
1548
|
description: incidentEpisode.description || "",
|
|
950
1549
|
episodeNumber: incidentEpisode.episodeNumber || null,
|
|
951
1550
|
episodeNumberWithPrefix: incidentEpisode.episodeNumberWithPrefix || null,
|
|
952
|
-
severity: ((
|
|
953
|
-
state: ((
|
|
1551
|
+
severity: ((_31 = incidentEpisode.incidentSeverity) === null || _31 === void 0 ? void 0 : _31.name) || null,
|
|
1552
|
+
state: ((_32 = incidentEpisode.currentIncidentState) === null || _32 === void 0 ? void 0 : _32.name) || null,
|
|
954
1553
|
},
|
|
955
|
-
onCallPolicyId: ((
|
|
956
|
-
onCallPolicyEscalationRuleId: ((
|
|
1554
|
+
onCallPolicyId: ((_33 = options.onCallPolicyId) === null || _33 === void 0 ? void 0 : _33.toString()) || null,
|
|
1555
|
+
onCallPolicyEscalationRuleId: ((_34 = options.onCallPolicyEscalationRuleId) === null || _34 === void 0 ? void 0 : _34.toString()) || null,
|
|
957
1556
|
},
|
|
958
1557
|
});
|
|
959
1558
|
}
|
|
960
1559
|
}
|
|
961
1560
|
// send call.
|
|
962
|
-
if (((
|
|
963
|
-
((
|
|
1561
|
+
if (((_35 = notificationRuleItem.userCall) === null || _35 === void 0 ? void 0 : _35.phone) &&
|
|
1562
|
+
((_36 = notificationRuleItem.userCall) === null || _36 === void 0 ? void 0 : _36.isVerified)) {
|
|
964
1563
|
// send call for alert
|
|
965
1564
|
if (options.userNotificationEventType ===
|
|
966
1565
|
UserNotificationEventType.AlertCreated &&
|
|
967
1566
|
alert) {
|
|
968
1567
|
// create an error log.
|
|
1568
|
+
deliveryAttempted = true;
|
|
969
1569
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
970
|
-
logTimelineItem.statusMessage = `Making a call to ${(
|
|
1570
|
+
logTimelineItem.statusMessage = `Making a call to ${(_37 = notificationRuleItem.userCall) === null || _37 === void 0 ? void 0 : _37.phone.toString()}.`;
|
|
971
1571
|
logTimelineItem.userCallId = notificationRuleItem.userCall.id;
|
|
972
1572
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
973
1573
|
data: logTimelineItem,
|
|
@@ -975,7 +1575,7 @@ export class Service extends DatabaseService {
|
|
|
975
1575
|
isRoot: true,
|
|
976
1576
|
},
|
|
977
1577
|
});
|
|
978
|
-
const callRequest = await this.generateCallTemplateForAlertCreated((
|
|
1578
|
+
const callRequest = await this.generateCallTemplateForAlertCreated((_38 = notificationRuleItem.userCall) === null || _38 === void 0 ? void 0 : _38.phone, alert, updatedLog.id);
|
|
979
1579
|
// send call.
|
|
980
1580
|
CallService.makeCall(callRequest, {
|
|
981
1581
|
projectId: alert.projectId,
|
|
@@ -1005,8 +1605,9 @@ export class Service extends DatabaseService {
|
|
|
1005
1605
|
UserNotificationEventType.IncidentCreated &&
|
|
1006
1606
|
incident) {
|
|
1007
1607
|
// send call for incident
|
|
1608
|
+
deliveryAttempted = true;
|
|
1008
1609
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1009
|
-
logTimelineItem.statusMessage = `Making a call to ${(
|
|
1610
|
+
logTimelineItem.statusMessage = `Making a call to ${(_39 = notificationRuleItem.userCall) === null || _39 === void 0 ? void 0 : _39.phone.toString()}.`;
|
|
1010
1611
|
logTimelineItem.userCallId = notificationRuleItem.userCall.id;
|
|
1011
1612
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1012
1613
|
data: logTimelineItem,
|
|
@@ -1014,7 +1615,7 @@ export class Service extends DatabaseService {
|
|
|
1014
1615
|
isRoot: true,
|
|
1015
1616
|
},
|
|
1016
1617
|
});
|
|
1017
|
-
const callRequest = await this.generateCallTemplateForIncidentCreated((
|
|
1618
|
+
const callRequest = await this.generateCallTemplateForIncidentCreated((_40 = notificationRuleItem.userCall) === null || _40 === void 0 ? void 0 : _40.phone, incident, updatedLog.id);
|
|
1018
1619
|
// send call.
|
|
1019
1620
|
CallService.makeCall(callRequest, {
|
|
1020
1621
|
projectId: incident.projectId,
|
|
@@ -1044,8 +1645,9 @@ export class Service extends DatabaseService {
|
|
|
1044
1645
|
if (options.userNotificationEventType ===
|
|
1045
1646
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
1046
1647
|
alertEpisode) {
|
|
1648
|
+
deliveryAttempted = true;
|
|
1047
1649
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1048
|
-
logTimelineItem.statusMessage = `Making a call to ${(
|
|
1650
|
+
logTimelineItem.statusMessage = `Making a call to ${(_41 = notificationRuleItem.userCall) === null || _41 === void 0 ? void 0 : _41.phone.toString()}.`;
|
|
1049
1651
|
logTimelineItem.userCallId = notificationRuleItem.userCall.id;
|
|
1050
1652
|
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1051
1653
|
data: logTimelineItem,
|
|
@@ -1053,7 +1655,7 @@ export class Service extends DatabaseService {
|
|
|
1053
1655
|
isRoot: true,
|
|
1054
1656
|
},
|
|
1055
1657
|
});
|
|
1056
|
-
const callRequest = await this.generateCallTemplateForAlertEpisodeCreated((
|
|
1658
|
+
const callRequest = await this.generateCallTemplateForAlertEpisodeCreated((_42 = notificationRuleItem.userCall) === null || _42 === void 0 ? void 0 : _42.phone, alertEpisode, updatedLog.id);
|
|
1057
1659
|
CallService.makeCall(callRequest, {
|
|
1058
1660
|
projectId: alertEpisode.projectId,
|
|
1059
1661
|
customTwilioConfig: projectTwilioConfig,
|
|
@@ -1078,12 +1680,54 @@ export class Service extends DatabaseService {
|
|
|
1078
1680
|
});
|
|
1079
1681
|
});
|
|
1080
1682
|
}
|
|
1683
|
+
// send call for incident episode
|
|
1684
|
+
if (options.userNotificationEventType ===
|
|
1685
|
+
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
1686
|
+
incidentEpisode) {
|
|
1687
|
+
deliveryAttempted = true;
|
|
1688
|
+
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1689
|
+
logTimelineItem.statusMessage = `Making a call to ${(_43 = notificationRuleItem.userCall) === null || _43 === void 0 ? void 0 : _43.phone.toString()}.`;
|
|
1690
|
+
logTimelineItem.userCallId = notificationRuleItem.userCall.id;
|
|
1691
|
+
const updatedLog = await UserOnCallLogTimelineService.create({
|
|
1692
|
+
data: logTimelineItem,
|
|
1693
|
+
props: {
|
|
1694
|
+
isRoot: true,
|
|
1695
|
+
},
|
|
1696
|
+
});
|
|
1697
|
+
const callRequest = await this.generateCallTemplateForIncidentEpisodeCreated((_44 = notificationRuleItem.userCall) === null || _44 === void 0 ? void 0 : _44.phone, incidentEpisode, updatedLog.id);
|
|
1698
|
+
/*
|
|
1699
|
+
* CallService accepts incidentEpisodeId but never writes it onto the
|
|
1700
|
+
* request body, so it is deliberately not passed here.
|
|
1701
|
+
*/
|
|
1702
|
+
CallService.makeCall(callRequest, {
|
|
1703
|
+
projectId: incidentEpisode.projectId,
|
|
1704
|
+
customTwilioConfig: projectTwilioConfig,
|
|
1705
|
+
userOnCallLogTimelineId: updatedLog.id,
|
|
1706
|
+
userId: notificationRuleItem.userId,
|
|
1707
|
+
onCallPolicyId: options.onCallPolicyId,
|
|
1708
|
+
onCallPolicyEscalationRuleId: options.onCallPolicyEscalationRuleId,
|
|
1709
|
+
teamId: options.userBelongsToTeamId,
|
|
1710
|
+
onCallDutyPolicyExecutionLogTimelineId: options.onCallDutyPolicyExecutionLogTimelineId,
|
|
1711
|
+
onCallScheduleId: options.onCallScheduleId,
|
|
1712
|
+
}).catch(async (err) => {
|
|
1713
|
+
await UserOnCallLogTimelineService.updateOneById({
|
|
1714
|
+
id: updatedLog.id,
|
|
1715
|
+
data: {
|
|
1716
|
+
status: UserNotificationStatus.Error,
|
|
1717
|
+
statusMessage: err.message || "Error making call.",
|
|
1718
|
+
},
|
|
1719
|
+
props: {
|
|
1720
|
+
isRoot: true,
|
|
1721
|
+
},
|
|
1722
|
+
});
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1081
1725
|
}
|
|
1082
|
-
if (((
|
|
1083
|
-
!((
|
|
1726
|
+
if (((_45 = notificationRuleItem.userCall) === null || _45 === void 0 ? void 0 : _45.phone) &&
|
|
1727
|
+
!((_46 = notificationRuleItem.userCall) === null || _46 === void 0 ? void 0 : _46.isVerified)) {
|
|
1084
1728
|
// create a log.
|
|
1085
1729
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
1086
|
-
logTimelineItem.statusMessage = `Call not sent because phone ${(
|
|
1730
|
+
logTimelineItem.statusMessage = `Call not sent because phone ${(_47 = notificationRuleItem.userCall) === null || _47 === void 0 ? void 0 : _47.phone.toString()} is not verified.`;
|
|
1087
1731
|
await UserOnCallLogTimelineService.create({
|
|
1088
1732
|
data: logTimelineItem,
|
|
1089
1733
|
props: {
|
|
@@ -1092,13 +1736,14 @@ export class Service extends DatabaseService {
|
|
|
1092
1736
|
});
|
|
1093
1737
|
}
|
|
1094
1738
|
// send push notification.
|
|
1095
|
-
if (((
|
|
1096
|
-
((
|
|
1739
|
+
if (((_48 = notificationRuleItem.userPush) === null || _48 === void 0 ? void 0 : _48.deviceToken) &&
|
|
1740
|
+
((_49 = notificationRuleItem.userPush) === null || _49 === void 0 ? void 0 : _49.isVerified)) {
|
|
1097
1741
|
// send push notification for alert
|
|
1098
1742
|
if (options.userNotificationEventType ===
|
|
1099
1743
|
UserNotificationEventType.AlertCreated &&
|
|
1100
1744
|
alert) {
|
|
1101
1745
|
// create a log.
|
|
1746
|
+
deliveryAttempted = true;
|
|
1102
1747
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1103
1748
|
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
1104
1749
|
logTimelineItem.userPushId = notificationRuleItem.userPush.id;
|
|
@@ -1108,7 +1753,7 @@ export class Service extends DatabaseService {
|
|
|
1108
1753
|
isRoot: true,
|
|
1109
1754
|
},
|
|
1110
1755
|
});
|
|
1111
|
-
const pushMessage = PushNotificationUtil.createAlertCreatedNotification(Object.assign(Object.assign(Object.assign({ alertTitle: alert.title, projectName: ((
|
|
1756
|
+
const pushMessage = PushNotificationUtil.createAlertCreatedNotification(Object.assign(Object.assign(Object.assign({ alertTitle: alert.title, projectName: ((_50 = alert.project) === null || _50 === void 0 ? void 0 : _50.name) || "OneUptime", alertViewLink: (await AlertService.getAlertLinkInDashboard(alert.projectId, alert.id)).toString() }, (alert.alertNumber !== undefined && {
|
|
1112
1757
|
alertNumber: alert.alertNumber,
|
|
1113
1758
|
})), (alert.alertNumberWithPrefix && {
|
|
1114
1759
|
alertNumberWithPrefix: alert.alertNumberWithPrefix,
|
|
@@ -1151,6 +1796,7 @@ export class Service extends DatabaseService {
|
|
|
1151
1796
|
UserNotificationEventType.IncidentCreated &&
|
|
1152
1797
|
incident) {
|
|
1153
1798
|
// create a log.
|
|
1799
|
+
deliveryAttempted = true;
|
|
1154
1800
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1155
1801
|
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
1156
1802
|
logTimelineItem.userPushId = notificationRuleItem.userPush.id;
|
|
@@ -1160,7 +1806,7 @@ export class Service extends DatabaseService {
|
|
|
1160
1806
|
isRoot: true,
|
|
1161
1807
|
},
|
|
1162
1808
|
});
|
|
1163
|
-
const pushMessage = PushNotificationUtil.createIncidentCreatedNotification(Object.assign(Object.assign(Object.assign({ incidentTitle: incident.title, projectName: ((
|
|
1809
|
+
const pushMessage = PushNotificationUtil.createIncidentCreatedNotification(Object.assign(Object.assign(Object.assign({ incidentTitle: incident.title, projectName: ((_51 = incident.project) === null || _51 === void 0 ? void 0 : _51.name) || "OneUptime", incidentViewLink: (await IncidentService.getIncidentLinkInDashboard(incident.projectId, incident.id)).toString() }, (incident.incidentNumber !== undefined && {
|
|
1164
1810
|
incidentNumber: incident.incidentNumber,
|
|
1165
1811
|
})), (incident.incidentNumberWithPrefix && {
|
|
1166
1812
|
incidentNumberWithPrefix: incident.incidentNumberWithPrefix,
|
|
@@ -1202,6 +1848,7 @@ export class Service extends DatabaseService {
|
|
|
1202
1848
|
if (options.userNotificationEventType ===
|
|
1203
1849
|
UserNotificationEventType.AlertEpisodeCreated &&
|
|
1204
1850
|
alertEpisode) {
|
|
1851
|
+
deliveryAttempted = true;
|
|
1205
1852
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1206
1853
|
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
1207
1854
|
logTimelineItem.userPushId = notificationRuleItem.userPush.id;
|
|
@@ -1211,7 +1858,7 @@ export class Service extends DatabaseService {
|
|
|
1211
1858
|
isRoot: true,
|
|
1212
1859
|
},
|
|
1213
1860
|
});
|
|
1214
|
-
const pushMessage = PushNotificationUtil.createAlertEpisodeCreatedNotification(Object.assign(Object.assign(Object.assign({ alertEpisodeTitle: alertEpisode.title, projectName: ((
|
|
1861
|
+
const pushMessage = PushNotificationUtil.createAlertEpisodeCreatedNotification(Object.assign(Object.assign(Object.assign({ alertEpisodeTitle: alertEpisode.title, projectName: ((_52 = alertEpisode.project) === null || _52 === void 0 ? void 0 : _52.name) || "OneUptime", alertEpisodeViewLink: (await AlertEpisodeService.getEpisodeLinkInDashboard(alertEpisode.projectId, alertEpisode.id)).toString() }, (alertEpisode.episodeNumber !== undefined && {
|
|
1215
1862
|
episodeNumber: alertEpisode.episodeNumber,
|
|
1216
1863
|
})), (alertEpisode.episodeNumberWithPrefix && {
|
|
1217
1864
|
episodeNumberWithPrefix: alertEpisode.episodeNumberWithPrefix,
|
|
@@ -1252,6 +1899,7 @@ export class Service extends DatabaseService {
|
|
|
1252
1899
|
if (options.userNotificationEventType ===
|
|
1253
1900
|
UserNotificationEventType.IncidentEpisodeCreated &&
|
|
1254
1901
|
incidentEpisode) {
|
|
1902
|
+
deliveryAttempted = true;
|
|
1255
1903
|
logTimelineItem.status = UserNotificationStatus.Sending;
|
|
1256
1904
|
logTimelineItem.statusMessage = `Sending push notification to device.`;
|
|
1257
1905
|
logTimelineItem.userPushId = notificationRuleItem.userPush.id;
|
|
@@ -1261,7 +1909,7 @@ export class Service extends DatabaseService {
|
|
|
1261
1909
|
isRoot: true,
|
|
1262
1910
|
},
|
|
1263
1911
|
});
|
|
1264
|
-
const pushMessage = PushNotificationUtil.createIncidentEpisodeCreatedNotification(Object.assign(Object.assign(Object.assign({ incidentEpisodeTitle: incidentEpisode.title, projectName: ((
|
|
1912
|
+
const pushMessage = PushNotificationUtil.createIncidentEpisodeCreatedNotification(Object.assign(Object.assign(Object.assign({ incidentEpisodeTitle: incidentEpisode.title, projectName: ((_53 = incidentEpisode.project) === null || _53 === void 0 ? void 0 : _53.name) || "OneUptime", incidentEpisodeViewLink: (await IncidentEpisodeService.getEpisodeLinkInDashboard(incidentEpisode.projectId, incidentEpisode.id)).toString() }, (incidentEpisode.episodeNumber !== undefined && {
|
|
1265
1913
|
episodeNumber: incidentEpisode.episodeNumber,
|
|
1266
1914
|
})), (incidentEpisode.episodeNumberWithPrefix && {
|
|
1267
1915
|
episodeNumberWithPrefix: incidentEpisode.episodeNumberWithPrefix,
|
|
@@ -1298,8 +1946,8 @@ export class Service extends DatabaseService {
|
|
|
1298
1946
|
});
|
|
1299
1947
|
}
|
|
1300
1948
|
}
|
|
1301
|
-
if (((
|
|
1302
|
-
!((
|
|
1949
|
+
if (((_54 = notificationRuleItem.userPush) === null || _54 === void 0 ? void 0 : _54.deviceToken) &&
|
|
1950
|
+
!((_55 = notificationRuleItem.userPush) === null || _55 === void 0 ? void 0 : _55.isVerified)) {
|
|
1303
1951
|
// create a log.
|
|
1304
1952
|
logTimelineItem.status = UserNotificationStatus.Error;
|
|
1305
1953
|
logTimelineItem.statusMessage = `Push notification not sent because device is not verified.`;
|
|
@@ -1310,29 +1958,420 @@ export class Service extends DatabaseService {
|
|
|
1310
1958
|
},
|
|
1311
1959
|
});
|
|
1312
1960
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1961
|
+
/*
|
|
1962
|
+
* The fell-through guard.
|
|
1963
|
+
*
|
|
1964
|
+
* Gap F was a whole class of lost pages: a contactable channel, an event
|
|
1965
|
+
* type that no block in that channel branched on, and therefore neither a
|
|
1966
|
+
* send nor an error row — the responder was simply never told, and nothing
|
|
1967
|
+
* anywhere recorded that. Rather than trust that every future event type
|
|
1968
|
+
* gets wired into all seven blocks, make the omission loud.
|
|
1969
|
+
*
|
|
1970
|
+
* The row is built fresh instead of reusing logTimelineItem: that instance
|
|
1971
|
+
* picks up an _id as soon as any block has created a row with it, and a
|
|
1972
|
+
* second create() with it would UPDATE that row rather than insert this one.
|
|
1973
|
+
*/
|
|
1974
|
+
if (contactableChannels.length > 0 && !deliveryAttempted) {
|
|
1975
|
+
const statusMessage = `No notification template for ${options.userNotificationEventType} on ${contactableChannels.join(", ")}.`;
|
|
1976
|
+
const fellThroughRow = this.buildLogTimelineItem(notificationRuleItem, options);
|
|
1977
|
+
fellThroughRow.status = UserNotificationStatus.Error;
|
|
1978
|
+
fellThroughRow.statusMessage = statusMessage;
|
|
1979
|
+
await UserOnCallLogTimelineService.create({
|
|
1980
|
+
data: fellThroughRow,
|
|
1981
|
+
props: {
|
|
1982
|
+
isRoot: true,
|
|
1331
1983
|
},
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1984
|
+
});
|
|
1985
|
+
logger.error(`${statusMessage} User on-call log: ${options.userNotificationLogId.toString()}`);
|
|
1986
|
+
}
|
|
1987
|
+
return deliveryAttempted;
|
|
1988
|
+
}
|
|
1989
|
+
/*
|
|
1990
|
+
* The channels this rule could actually reach the user on, by display name.
|
|
1991
|
+
*
|
|
1992
|
+
* These are the same gates each channel block opens with, so an empty list
|
|
1993
|
+
* means "this rule can contact nobody" — a rule whose method was
|
|
1994
|
+
* cascade-deleted, say — and a non-empty one means a page was expected to go
|
|
1995
|
+
* out. Webhooks have no verification concept at all (UserWebhook has no
|
|
1996
|
+
* isVerified column), so presence of a URL is the whole gate there.
|
|
1997
|
+
*/
|
|
1998
|
+
getContactableChannelNames(notificationRuleItem) {
|
|
1999
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
2000
|
+
const channels = [];
|
|
2001
|
+
if (((_a = notificationRuleItem.userEmail) === null || _a === void 0 ? void 0 : _a.email) &&
|
|
2002
|
+
((_b = notificationRuleItem.userEmail) === null || _b === void 0 ? void 0 : _b.isVerified)) {
|
|
2003
|
+
channels.push("Email");
|
|
2004
|
+
}
|
|
2005
|
+
if (((_c = notificationRuleItem.userSms) === null || _c === void 0 ? void 0 : _c.phone) &&
|
|
2006
|
+
((_d = notificationRuleItem.userSms) === null || _d === void 0 ? void 0 : _d.isVerified)) {
|
|
2007
|
+
channels.push("SMS");
|
|
2008
|
+
}
|
|
2009
|
+
if (((_e = notificationRuleItem.userWhatsApp) === null || _e === void 0 ? void 0 : _e.phone) &&
|
|
2010
|
+
((_f = notificationRuleItem.userWhatsApp) === null || _f === void 0 ? void 0 : _f.isVerified)) {
|
|
2011
|
+
channels.push("WhatsApp");
|
|
2012
|
+
}
|
|
2013
|
+
if (((_g = notificationRuleItem.userTelegram) === null || _g === void 0 ? void 0 : _g.telegramChatId) &&
|
|
2014
|
+
((_h = notificationRuleItem.userTelegram) === null || _h === void 0 ? void 0 : _h.isVerified)) {
|
|
2015
|
+
channels.push("Telegram");
|
|
2016
|
+
}
|
|
2017
|
+
if ((_j = notificationRuleItem.userWebhook) === null || _j === void 0 ? void 0 : _j.webhookUrl) {
|
|
2018
|
+
channels.push("Webhook");
|
|
2019
|
+
}
|
|
2020
|
+
if (((_k = notificationRuleItem.userCall) === null || _k === void 0 ? void 0 : _k.phone) &&
|
|
2021
|
+
((_l = notificationRuleItem.userCall) === null || _l === void 0 ? void 0 : _l.isVerified)) {
|
|
2022
|
+
channels.push("Call");
|
|
2023
|
+
}
|
|
2024
|
+
if (((_m = notificationRuleItem.userPush) === null || _m === void 0 ? void 0 : _m.deviceToken) &&
|
|
2025
|
+
((_o = notificationRuleItem.userPush) === null || _o === void 0 ? void 0 : _o.isVerified)) {
|
|
2026
|
+
channels.push("Push");
|
|
2027
|
+
}
|
|
2028
|
+
return channels;
|
|
2029
|
+
}
|
|
2030
|
+
/*
|
|
2031
|
+
* Page a responder who has NO notification rule matching what just fired.
|
|
2032
|
+
*
|
|
2033
|
+
* Zero matching rules is indistinguishable from "never configured" unless the
|
|
2034
|
+
* user said otherwise, so the caller (UserOnCallLogService.onCreateSuccess)
|
|
2035
|
+
* checks for an explicit opt-out row first and only reaches here when the
|
|
2036
|
+
* silence looks accidental. Reaching a human on whatever they have verified
|
|
2037
|
+
* beats honouring a configuration they never made.
|
|
2038
|
+
*
|
|
2039
|
+
* Nothing here observes delivery success: every send below is fire-and-forget
|
|
2040
|
+
* (see deliverNotificationForRule), so `notified` means "a page was handed to
|
|
2041
|
+
* the sender", not "a phone rang".
|
|
2042
|
+
*
|
|
2043
|
+
* The three ways this can end are spelled out in FallbackNotificationOutcome,
|
|
2044
|
+
* and the caller must branch on them rather than on `notified` alone: only
|
|
2045
|
+
* NoUsableNotificationMethod describes a responder who cannot be reached, and
|
|
2046
|
+
* only that one is safe to record as a terminal status.
|
|
2047
|
+
*/
|
|
2048
|
+
async executeFallbackNotification(options) {
|
|
2049
|
+
/*
|
|
2050
|
+
* Claim the log under the reserved fallback key before doing anything, so
|
|
2051
|
+
* two overlapping cron ticks cannot both fall back and double-page the same
|
|
2052
|
+
* responder for one escalation.
|
|
2053
|
+
*/
|
|
2054
|
+
const claimed = await UserOnCallLogService.claimNotificationExecution({
|
|
2055
|
+
userOnCallLogId: options.userOnCallLogId,
|
|
2056
|
+
claimKey: FALLBACK_NOTIFICATION_CLAIM_KEY,
|
|
2057
|
+
});
|
|
2058
|
+
if (!claimed) {
|
|
2059
|
+
/*
|
|
2060
|
+
* A concurrent run already fell back for this log; it owns everything
|
|
2061
|
+
* that happens next, including the log's final status. Reported as the
|
|
2062
|
+
* transient outcome rather than as "no usable method", because the
|
|
2063
|
+
* caller's response to the latter is a terminal Error — which would
|
|
2064
|
+
* stamp "this responder is unreachable" over a page that is in flight.
|
|
2065
|
+
*/
|
|
2066
|
+
return {
|
|
2067
|
+
outcome: FallbackNotificationOutcome.DeliveryFailed,
|
|
2068
|
+
notified: false,
|
|
2069
|
+
channelsUsed: [],
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
const fallbackRules = await this.chooseFallbackChannels(options);
|
|
2073
|
+
if (fallbackRules.length === 0) {
|
|
2074
|
+
logger.warn(`On-call fallback found no usable notification method for user ${options.userId.toString()} in project ${options.projectId.toString()} (${options.severityName} ${options.ruleType}). The page cannot be delivered.`);
|
|
2075
|
+
return {
|
|
2076
|
+
outcome: FallbackNotificationOutcome.NoUsableNotificationMethod,
|
|
2077
|
+
notified: false,
|
|
2078
|
+
channelsUsed: [],
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
const channelsUsed = [];
|
|
2082
|
+
let anAttemptFailed = false;
|
|
2083
|
+
/*
|
|
2084
|
+
* One delivery call per channel, never a loop inside one call: the timeline
|
|
2085
|
+
* row is a single mutable object inside deliverNotificationForRule, and a
|
|
2086
|
+
* second create() with it would UPDATE the row the first channel wrote
|
|
2087
|
+
* instead of inserting a second one — the second page would vanish from the
|
|
2088
|
+
* timeline and, worse, overwrite the first one's status.
|
|
2089
|
+
*/
|
|
2090
|
+
for (const fallbackRule of fallbackRules) {
|
|
2091
|
+
try {
|
|
2092
|
+
const dispatched = await this.deliverNotificationForRule(fallbackRule.rule, options);
|
|
2093
|
+
/*
|
|
2094
|
+
* Only a genuine dispatch earns a place in channelsUsed. The channel
|
|
2095
|
+
* names in here are read back to the operator as "notified via fallback
|
|
2096
|
+
* (Push, Email)", so a name added merely because the call resolved is a
|
|
2097
|
+
* lie in the one place somebody looks to find out whether the responder
|
|
2098
|
+
* was reached — and deliverNotificationForRule resolves perfectly
|
|
2099
|
+
* happily when no block claimed the event type.
|
|
2100
|
+
*/
|
|
2101
|
+
if (dispatched) {
|
|
2102
|
+
channelsUsed.push(fallbackRule.channelName);
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
anAttemptFailed = true;
|
|
2106
|
+
logger.error(`On-call fallback dispatched nothing on ${fallbackRule.channelName} for user ${options.userId.toString()}: no notification template matched ${options.userNotificationEventType}.`);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
catch (err) {
|
|
2110
|
+
anAttemptFailed = true;
|
|
2111
|
+
logger.error(`On-call fallback failed to deliver on ${fallbackRule.channelName} for user ${options.userId.toString()}.`);
|
|
2112
|
+
logger.error(err);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
if (channelsUsed.length > 0) {
|
|
2116
|
+
return {
|
|
2117
|
+
outcome: FallbackNotificationOutcome.Delivered,
|
|
2118
|
+
notified: true,
|
|
2119
|
+
channelsUsed: channelsUsed,
|
|
2120
|
+
};
|
|
2121
|
+
}
|
|
2122
|
+
/*
|
|
2123
|
+
* There were channels to try and not one of them carried a page. That is
|
|
2124
|
+
* emphatically not the "responder has no notification method" case —
|
|
2125
|
+
* chooseFallbackChannels returns only verified, project-enabled methods, so
|
|
2126
|
+
* the responder is reachable and today simply failed to be reached.
|
|
2127
|
+
*
|
|
2128
|
+
* anAttemptFailed is necessarily true on this line, since every path
|
|
2129
|
+
* through the loop that does not push a channel sets it. It is read rather
|
|
2130
|
+
* than assumed so that a future channel that can finish without either
|
|
2131
|
+
* dispatching or failing degrades into the transient outcome instead of
|
|
2132
|
+
* silently telling the operator the responder has nothing configured.
|
|
2133
|
+
*/
|
|
2134
|
+
return {
|
|
2135
|
+
outcome: anAttemptFailed
|
|
2136
|
+
? FallbackNotificationOutcome.DeliveryFailed
|
|
2137
|
+
: FallbackNotificationOutcome.NoUsableNotificationMethod,
|
|
2138
|
+
notified: false,
|
|
2139
|
+
channelsUsed: [],
|
|
2140
|
+
};
|
|
2141
|
+
}
|
|
2142
|
+
/*
|
|
2143
|
+
* Pick what to page the user on, and build an unsaved rule for each choice.
|
|
2144
|
+
*
|
|
2145
|
+
* Zero-cost channels win: push and email reach the most people for no money
|
|
2146
|
+
* and no billing surprise, and there is no reason to pick between them, so a
|
|
2147
|
+
* user who has both gets both. Only a user with neither is worth spending on,
|
|
2148
|
+
* and then just once, in escalating-intrusiveness order.
|
|
2149
|
+
*
|
|
2150
|
+
* Paid channels are additionally gated on the project's own enable flags.
|
|
2151
|
+
* SmsService and CallService enforce those at send time, but WhatsApp and
|
|
2152
|
+
* Telegram only check them when a method is created — so a project that
|
|
2153
|
+
* switched WhatsApp off would still be billed by a fallback that did not look.
|
|
2154
|
+
*/
|
|
2155
|
+
async chooseFallbackChannels(options) {
|
|
2156
|
+
const chosen = [];
|
|
2157
|
+
const userPush = await UserPushService.findOneBy({
|
|
2158
|
+
query: {
|
|
2159
|
+
projectId: options.projectId,
|
|
2160
|
+
userId: options.userId,
|
|
2161
|
+
isVerified: true,
|
|
2162
|
+
},
|
|
2163
|
+
select: {
|
|
2164
|
+
_id: true,
|
|
2165
|
+
deviceToken: true,
|
|
2166
|
+
deviceType: true,
|
|
2167
|
+
isVerified: true,
|
|
2168
|
+
},
|
|
2169
|
+
props: {
|
|
2170
|
+
isRoot: true,
|
|
2171
|
+
},
|
|
2172
|
+
});
|
|
2173
|
+
if (userPush) {
|
|
2174
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2175
|
+
rule.userPush = userPush;
|
|
2176
|
+
rule.userPushId = userPush.id;
|
|
2177
|
+
chosen.push({ channelName: "Push", rule: rule });
|
|
2178
|
+
}
|
|
2179
|
+
const userEmail = await UserEmailService.findOneBy({
|
|
2180
|
+
query: {
|
|
2181
|
+
projectId: options.projectId,
|
|
2182
|
+
userId: options.userId,
|
|
2183
|
+
isVerified: true,
|
|
2184
|
+
},
|
|
2185
|
+
select: {
|
|
2186
|
+
_id: true,
|
|
2187
|
+
email: true,
|
|
2188
|
+
isVerified: true,
|
|
2189
|
+
},
|
|
2190
|
+
props: {
|
|
2191
|
+
isRoot: true,
|
|
2192
|
+
},
|
|
2193
|
+
});
|
|
2194
|
+
if (userEmail) {
|
|
2195
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2196
|
+
rule.userEmail = userEmail;
|
|
2197
|
+
rule.userEmailId = userEmail.id;
|
|
2198
|
+
chosen.push({ channelName: "Email", rule: rule });
|
|
2199
|
+
}
|
|
2200
|
+
if (chosen.length > 0) {
|
|
2201
|
+
return chosen;
|
|
2202
|
+
}
|
|
2203
|
+
const project = await ProjectService.findOneById({
|
|
2204
|
+
id: options.projectId,
|
|
2205
|
+
select: {
|
|
2206
|
+
enableSmsNotifications: true,
|
|
2207
|
+
enableCallNotifications: true,
|
|
2208
|
+
enableWhatsAppNotifications: true,
|
|
2209
|
+
enableTelegramNotifications: true,
|
|
2210
|
+
},
|
|
2211
|
+
props: {
|
|
2212
|
+
isRoot: true,
|
|
2213
|
+
},
|
|
2214
|
+
});
|
|
2215
|
+
if (project === null || project === void 0 ? void 0 : project.enableSmsNotifications) {
|
|
2216
|
+
const userSms = await UserSmsService.findOneBy({
|
|
2217
|
+
query: {
|
|
2218
|
+
projectId: options.projectId,
|
|
2219
|
+
userId: options.userId,
|
|
2220
|
+
isVerified: true,
|
|
2221
|
+
},
|
|
2222
|
+
select: {
|
|
2223
|
+
_id: true,
|
|
2224
|
+
phone: true,
|
|
2225
|
+
isVerified: true,
|
|
2226
|
+
},
|
|
2227
|
+
props: {
|
|
2228
|
+
isRoot: true,
|
|
2229
|
+
},
|
|
2230
|
+
});
|
|
2231
|
+
if (userSms) {
|
|
2232
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2233
|
+
rule.userSms = userSms;
|
|
2234
|
+
rule.userSmsId = userSms.id;
|
|
2235
|
+
return [{ channelName: "SMS", rule: rule }];
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
if (project === null || project === void 0 ? void 0 : project.enableCallNotifications) {
|
|
2239
|
+
const userCall = await UserCallService.findOneBy({
|
|
2240
|
+
query: {
|
|
2241
|
+
projectId: options.projectId,
|
|
2242
|
+
userId: options.userId,
|
|
2243
|
+
isVerified: true,
|
|
2244
|
+
},
|
|
2245
|
+
select: {
|
|
2246
|
+
_id: true,
|
|
2247
|
+
phone: true,
|
|
2248
|
+
isVerified: true,
|
|
2249
|
+
},
|
|
2250
|
+
props: {
|
|
2251
|
+
isRoot: true,
|
|
2252
|
+
},
|
|
2253
|
+
});
|
|
2254
|
+
if (userCall) {
|
|
2255
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2256
|
+
rule.userCall = userCall;
|
|
2257
|
+
rule.userCallId = userCall.id;
|
|
2258
|
+
return [{ channelName: "Call", rule: rule }];
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (project === null || project === void 0 ? void 0 : project.enableWhatsAppNotifications) {
|
|
2262
|
+
const userWhatsApp = await UserWhatsAppService.findOneBy({
|
|
2263
|
+
query: {
|
|
2264
|
+
projectId: options.projectId,
|
|
2265
|
+
userId: options.userId,
|
|
2266
|
+
isVerified: true,
|
|
2267
|
+
},
|
|
2268
|
+
select: {
|
|
2269
|
+
_id: true,
|
|
2270
|
+
phone: true,
|
|
2271
|
+
isVerified: true,
|
|
2272
|
+
},
|
|
2273
|
+
props: {
|
|
2274
|
+
isRoot: true,
|
|
2275
|
+
},
|
|
2276
|
+
});
|
|
2277
|
+
if (userWhatsApp) {
|
|
2278
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2279
|
+
rule.userWhatsApp = userWhatsApp;
|
|
2280
|
+
rule.userWhatsAppId = userWhatsApp.id;
|
|
2281
|
+
return [{ channelName: "WhatsApp", rule: rule }];
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (project === null || project === void 0 ? void 0 : project.enableTelegramNotifications) {
|
|
2285
|
+
const userTelegram = await UserTelegramService.findOneBy({
|
|
2286
|
+
query: {
|
|
2287
|
+
projectId: options.projectId,
|
|
2288
|
+
userId: options.userId,
|
|
2289
|
+
isVerified: true,
|
|
2290
|
+
},
|
|
2291
|
+
select: {
|
|
2292
|
+
_id: true,
|
|
2293
|
+
telegramChatId: true,
|
|
2294
|
+
telegramUserHandle: true,
|
|
2295
|
+
isVerified: true,
|
|
2296
|
+
},
|
|
2297
|
+
props: {
|
|
2298
|
+
isRoot: true,
|
|
2299
|
+
},
|
|
2300
|
+
});
|
|
2301
|
+
if (userTelegram) {
|
|
2302
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2303
|
+
rule.userTelegram = userTelegram;
|
|
2304
|
+
rule.userTelegramId = userTelegram.id;
|
|
2305
|
+
return [{ channelName: "Telegram", rule: rule }];
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
/*
|
|
2309
|
+
* A webhook costs the project nothing and has no verification concept at
|
|
2310
|
+
* all (UserWebhook has no isVerified column), so its presence is the whole
|
|
2311
|
+
* test, and there is no project flag to consult.
|
|
2312
|
+
*/
|
|
2313
|
+
const userWebhook = await UserWebhookService.findOneBy({
|
|
2314
|
+
query: {
|
|
2315
|
+
projectId: options.projectId,
|
|
2316
|
+
userId: options.userId,
|
|
2317
|
+
},
|
|
2318
|
+
select: {
|
|
2319
|
+
_id: true,
|
|
2320
|
+
webhookUrl: true,
|
|
2321
|
+
name: true,
|
|
2322
|
+
secret: true,
|
|
2323
|
+
},
|
|
2324
|
+
props: {
|
|
2325
|
+
isRoot: true,
|
|
2326
|
+
},
|
|
2327
|
+
});
|
|
2328
|
+
if (userWebhook) {
|
|
2329
|
+
const rule = this.buildUnsavedFallbackRule(options);
|
|
2330
|
+
rule.userWebhook = userWebhook;
|
|
2331
|
+
rule.userWebhookId = userWebhook.id;
|
|
2332
|
+
return [{ channelName: "Webhook", rule: rule }];
|
|
2333
|
+
}
|
|
2334
|
+
return chosen;
|
|
2335
|
+
}
|
|
2336
|
+
/*
|
|
2337
|
+
* A UserNotificationRule that exists only for the length of one delivery.
|
|
2338
|
+
*
|
|
2339
|
+
* It is never saved: the user did not ask for this rule, and persisting it
|
|
2340
|
+
* would silently rewrite their configuration behind their back. The method
|
|
2341
|
+
* relation is populated as a loaded entity rather than just its FK because
|
|
2342
|
+
* deliverNotificationForRule reads the relation (userEmail.email,
|
|
2343
|
+
* userEmail.isVerified) and never dereferences the id.
|
|
2344
|
+
*/
|
|
2345
|
+
buildUnsavedFallbackRule(options) {
|
|
2346
|
+
const rule = new Model();
|
|
2347
|
+
rule.projectId = options.projectId;
|
|
2348
|
+
rule.userId = options.userId;
|
|
2349
|
+
rule.ruleType = options.ruleType;
|
|
2350
|
+
rule.notifyAfterMinutes = 0;
|
|
2351
|
+
return rule;
|
|
2352
|
+
}
|
|
2353
|
+
async generateCallTemplateForAlertCreated(to, alert, userOnCallLogTimelineId) {
|
|
2354
|
+
const host = await DatabaseConfig.getHost();
|
|
2355
|
+
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
2356
|
+
const alertIdentifier = alert.alertNumber !== undefined
|
|
2357
|
+
? `Alert number ${alert.alertNumber}, ${alert.title || "Alert"}`
|
|
2358
|
+
: alert.title || "Alert";
|
|
2359
|
+
const callRequest = {
|
|
2360
|
+
to: to,
|
|
2361
|
+
data: [
|
|
2362
|
+
{
|
|
2363
|
+
sayMessage: "This is a call from One Uptime",
|
|
2364
|
+
},
|
|
2365
|
+
{
|
|
2366
|
+
sayMessage: "A new alert has been created",
|
|
2367
|
+
},
|
|
2368
|
+
{
|
|
2369
|
+
sayMessage: alertIdentifier,
|
|
2370
|
+
},
|
|
2371
|
+
{
|
|
2372
|
+
introMessage: "To acknowledge this alert press 1",
|
|
2373
|
+
numDigits: 1,
|
|
2374
|
+
timeoutInSeconds: 10,
|
|
1336
2375
|
noInputMessage: "You have not entered any input. Good bye",
|
|
1337
2376
|
onInputCallRequest: {
|
|
1338
2377
|
"1": {
|
|
@@ -1430,6 +2469,47 @@ export class Service extends DatabaseService {
|
|
|
1430
2469
|
};
|
|
1431
2470
|
return callRequest;
|
|
1432
2471
|
}
|
|
2472
|
+
async generateCallTemplateForIncidentEpisodeCreated(to, incidentEpisode, userOnCallLogTimelineId) {
|
|
2473
|
+
const host = await DatabaseConfig.getHost();
|
|
2474
|
+
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
2475
|
+
const episodeIdentifier = incidentEpisode.episodeNumberWithPrefix
|
|
2476
|
+
? `Incident episode ${incidentEpisode.episodeNumberWithPrefix}, ${incidentEpisode.title || "Incident Episode"}`
|
|
2477
|
+
: incidentEpisode.episodeNumber !== undefined
|
|
2478
|
+
? `Incident episode number ${incidentEpisode.episodeNumber}, ${incidentEpisode.title || "Incident Episode"}`
|
|
2479
|
+
: incidentEpisode.title || "Incident Episode";
|
|
2480
|
+
const callRequest = {
|
|
2481
|
+
to: to,
|
|
2482
|
+
data: [
|
|
2483
|
+
{
|
|
2484
|
+
sayMessage: "This is a call from One Uptime",
|
|
2485
|
+
},
|
|
2486
|
+
{
|
|
2487
|
+
sayMessage: "A new incident episode has been created",
|
|
2488
|
+
},
|
|
2489
|
+
{
|
|
2490
|
+
sayMessage: episodeIdentifier,
|
|
2491
|
+
},
|
|
2492
|
+
{
|
|
2493
|
+
introMessage: "To acknowledge this incident episode press 1",
|
|
2494
|
+
numDigits: 1,
|
|
2495
|
+
timeoutInSeconds: 10,
|
|
2496
|
+
noInputMessage: "You have not entered any input. Good bye",
|
|
2497
|
+
onInputCallRequest: {
|
|
2498
|
+
"1": {
|
|
2499
|
+
sayMessage: "You have acknowledged this incident episode. Good bye",
|
|
2500
|
+
},
|
|
2501
|
+
default: {
|
|
2502
|
+
sayMessage: "Invalid input. Good bye",
|
|
2503
|
+
},
|
|
2504
|
+
},
|
|
2505
|
+
responseUrl: new URL(httpProtocol, host, new Route(AppApiRoute.toString())
|
|
2506
|
+
.addRoute(new UserOnCallLogTimeline().crudApiPath)
|
|
2507
|
+
.addRoute("/call/gather-input/" + userOnCallLogTimelineId.toString())),
|
|
2508
|
+
},
|
|
2509
|
+
],
|
|
2510
|
+
};
|
|
2511
|
+
return callRequest;
|
|
2512
|
+
}
|
|
1433
2513
|
async generateSmsTemplateForAlertCreated(to, alert, userOnCallLogTimelineId) {
|
|
1434
2514
|
const host = await DatabaseConfig.getHost();
|
|
1435
2515
|
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
@@ -1480,6 +2560,19 @@ export class Service extends DatabaseService {
|
|
|
1480
2560
|
};
|
|
1481
2561
|
return sms;
|
|
1482
2562
|
}
|
|
2563
|
+
async generateSmsTemplateForIncidentEpisodeCreated(to, incidentEpisode, userOnCallLogTimelineId) {
|
|
2564
|
+
const url = await this.buildOnCallAcknowledgeShortUrl(userOnCallLogTimelineId);
|
|
2565
|
+
const episodeIdentifier = incidentEpisode.episodeNumberWithPrefix
|
|
2566
|
+
? `${incidentEpisode.episodeNumberWithPrefix} (${incidentEpisode.title || "Incident Episode"})`
|
|
2567
|
+
: incidentEpisode.episodeNumber !== undefined
|
|
2568
|
+
? `#${incidentEpisode.episodeNumber} (${incidentEpisode.title || "Incident Episode"})`
|
|
2569
|
+
: incidentEpisode.title || "Incident Episode";
|
|
2570
|
+
const sms = {
|
|
2571
|
+
to,
|
|
2572
|
+
message: `This is a message from OneUptime. A new incident episode has been created: ${episodeIdentifier}. To acknowledge this incident episode, please click on the following link ${url.toString()}`,
|
|
2573
|
+
};
|
|
2574
|
+
return sms;
|
|
2575
|
+
}
|
|
1483
2576
|
async buildOnCallAcknowledgeShortUrl(userOnCallLogTimelineId) {
|
|
1484
2577
|
const host = await DatabaseConfig.getHost();
|
|
1485
2578
|
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
@@ -1557,6 +2650,27 @@ export class Service extends DatabaseService {
|
|
|
1557
2650
|
lines.push("", `✅ <a href="${this.escapeTelegramHtml(ackUrl.toString())}">Tap to acknowledge</a>`);
|
|
1558
2651
|
return lines.join("\n");
|
|
1559
2652
|
}
|
|
2653
|
+
async generateTelegramBodyForIncidentEpisodeCreated(incidentEpisode, userOnCallLogTimelineId) {
|
|
2654
|
+
const ackUrl = await this.buildOnCallAcknowledgeShortUrl(userOnCallLogTimelineId);
|
|
2655
|
+
const episodeIdentifier = incidentEpisode.episodeNumberWithPrefix
|
|
2656
|
+
? `${incidentEpisode.episodeNumberWithPrefix} — ${incidentEpisode.title || "Incident Episode"}`
|
|
2657
|
+
: incidentEpisode.episodeNumber !== undefined
|
|
2658
|
+
? `#${incidentEpisode.episodeNumber} — ${incidentEpisode.title || "Incident Episode"}`
|
|
2659
|
+
: incidentEpisode.title || "Incident Episode";
|
|
2660
|
+
const lines = [
|
|
2661
|
+
"🔥 <b>New incident episode assigned to you</b>",
|
|
2662
|
+
"",
|
|
2663
|
+
`📋 <b>${this.escapeTelegramHtml(episodeIdentifier)}</b>`,
|
|
2664
|
+
"",
|
|
2665
|
+
"👤 You're getting this because you're on call.",
|
|
2666
|
+
];
|
|
2667
|
+
if (incidentEpisode.projectId && incidentEpisode.id) {
|
|
2668
|
+
const dashboardUrl = await IncidentEpisodeService.getEpisodeLinkInDashboard(incidentEpisode.projectId, incidentEpisode.id);
|
|
2669
|
+
lines.push("", `🔎 <a href="${this.escapeTelegramHtml(dashboardUrl.toString())}">View incident episode in OneUptime</a>`);
|
|
2670
|
+
}
|
|
2671
|
+
lines.push("", `✅ <a href="${this.escapeTelegramHtml(ackUrl.toString())}">Tap to acknowledge</a>`);
|
|
2672
|
+
return lines.join("\n");
|
|
2673
|
+
}
|
|
1560
2674
|
async generateWhatsAppTemplateForAlertCreated(to, alert, userOnCallLogTimelineId) {
|
|
1561
2675
|
var _a;
|
|
1562
2676
|
const host = await DatabaseConfig.getHost();
|
|
@@ -1646,6 +2760,32 @@ export class Service extends DatabaseService {
|
|
|
1646
2760
|
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
|
|
1647
2761
|
};
|
|
1648
2762
|
}
|
|
2763
|
+
async generateWhatsAppTemplateForIncidentEpisodeCreated(to, incidentEpisode, userOnCallLogTimelineId) {
|
|
2764
|
+
var _a;
|
|
2765
|
+
const acknowledgeUrl = await this.buildOnCallAcknowledgeShortUrl(userOnCallLogTimelineId);
|
|
2766
|
+
const episodeLinkOnDashboard = incidentEpisode.projectId && incidentEpisode.id
|
|
2767
|
+
? (await IncidentEpisodeService.getEpisodeLinkInDashboard(incidentEpisode.projectId, incidentEpisode.id)).toString()
|
|
2768
|
+
: acknowledgeUrl.toString();
|
|
2769
|
+
const templateKey = WhatsAppTemplateIds.IncidentEpisodeCreated;
|
|
2770
|
+
const templateVariables = {
|
|
2771
|
+
project_name: ((_a = incidentEpisode.project) === null || _a === void 0 ? void 0 : _a.name) || "OneUptime",
|
|
2772
|
+
episode_title: incidentEpisode.title || "",
|
|
2773
|
+
acknowledge_url: acknowledgeUrl.toString(),
|
|
2774
|
+
episode_number: incidentEpisode.episodeNumberWithPrefix ||
|
|
2775
|
+
(incidentEpisode.episodeNumber !== undefined
|
|
2776
|
+
? incidentEpisode.episodeNumber.toString()
|
|
2777
|
+
: ""),
|
|
2778
|
+
episode_link: episodeLinkOnDashboard,
|
|
2779
|
+
};
|
|
2780
|
+
const body = renderWhatsAppTemplate(templateKey, templateVariables);
|
|
2781
|
+
return {
|
|
2782
|
+
to,
|
|
2783
|
+
body,
|
|
2784
|
+
templateKey,
|
|
2785
|
+
templateVariables,
|
|
2786
|
+
templateLanguageCode: WhatsAppTemplateLanguage[templateKey],
|
|
2787
|
+
};
|
|
2788
|
+
}
|
|
1649
2789
|
async generateEmailTemplateForAlertCreated(to, alert, userOnCallLogTimelineId) {
|
|
1650
2790
|
const host = await DatabaseConfig.getHost();
|
|
1651
2791
|
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
@@ -1822,30 +2962,169 @@ export class Service extends DatabaseService {
|
|
|
1822
2962
|
};
|
|
1823
2963
|
return emailMessage;
|
|
1824
2964
|
}
|
|
1825
|
-
async
|
|
1826
|
-
|
|
1827
|
-
const
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
2965
|
+
async generateEmailTemplateForIncidentEpisodeCreated(to, incidentEpisode, userOnCallLogTimelineId) {
|
|
2966
|
+
const host = await DatabaseConfig.getHost();
|
|
2967
|
+
const httpProtocol = await DatabaseConfig.getHttpProtocol();
|
|
2968
|
+
// Fetch incidents that are members of this episode
|
|
2969
|
+
const episodeMembers = await IncidentEpisodeMemberService.findBy({
|
|
2970
|
+
query: {
|
|
2971
|
+
incidentEpisodeId: incidentEpisode.id,
|
|
2972
|
+
},
|
|
2973
|
+
select: {
|
|
2974
|
+
incidentId: true,
|
|
2975
|
+
},
|
|
2976
|
+
props: {
|
|
2977
|
+
isRoot: true,
|
|
2978
|
+
},
|
|
2979
|
+
limit: LIMIT_PER_PROJECT,
|
|
2980
|
+
skip: 0,
|
|
2981
|
+
});
|
|
2982
|
+
// Get the incident IDs
|
|
2983
|
+
const incidentIds = episodeMembers
|
|
2984
|
+
.map((member) => {
|
|
2985
|
+
return member.incidentId;
|
|
2986
|
+
})
|
|
2987
|
+
.filter((id) => {
|
|
2988
|
+
return id !== undefined;
|
|
2989
|
+
});
|
|
2990
|
+
// Fetch full incident data with monitors
|
|
2991
|
+
const incidents = incidentIds.length > 0
|
|
2992
|
+
? await IncidentService.findBy({
|
|
2993
|
+
query: {
|
|
2994
|
+
_id: QueryHelper.any(incidentIds),
|
|
2995
|
+
},
|
|
2996
|
+
select: {
|
|
2997
|
+
_id: true,
|
|
2998
|
+
title: true,
|
|
2999
|
+
incidentNumber: true,
|
|
3000
|
+
incidentNumberWithPrefix: true,
|
|
3001
|
+
monitors: {
|
|
3002
|
+
_id: true,
|
|
3003
|
+
name: true,
|
|
3004
|
+
},
|
|
3005
|
+
},
|
|
3006
|
+
props: {
|
|
3007
|
+
isRoot: true,
|
|
3008
|
+
},
|
|
3009
|
+
limit: LIMIT_PER_PROJECT,
|
|
3010
|
+
skip: 0,
|
|
3011
|
+
})
|
|
3012
|
+
: [];
|
|
3013
|
+
/*
|
|
3014
|
+
* Unique monitors across every incident in the episode. An incident carries
|
|
3015
|
+
* a list of monitors (unlike an alert, which has exactly one), so this
|
|
3016
|
+
* flattens rather than reading a single relation.
|
|
3017
|
+
*/
|
|
3018
|
+
const monitorNames = new Set();
|
|
3019
|
+
for (const incident of incidents) {
|
|
3020
|
+
for (const monitor of incident.monitors || []) {
|
|
3021
|
+
if (monitor.name) {
|
|
3022
|
+
monitorNames.add(monitor.name);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
const resourcesAffected = monitorNames.size > 0
|
|
3027
|
+
? Array.from(monitorNames).join(", ")
|
|
3028
|
+
: "No resources identified";
|
|
3029
|
+
// Build incidents list HTML with proper email styling
|
|
3030
|
+
let incidentsListHtml = "";
|
|
3031
|
+
if (incidents.length > 0) {
|
|
3032
|
+
const incidentRows = [];
|
|
3033
|
+
for (const incident of incidents) {
|
|
3034
|
+
const incidentTitle = incident.title || "Untitled Incident";
|
|
3035
|
+
const incidentNumber = incident.incidentNumberWithPrefix ||
|
|
3036
|
+
(incident.incidentNumber ? `#${incident.incidentNumber}` : "");
|
|
3037
|
+
const incidentLink = (await IncidentService.getIncidentLinkInDashboard(incidentEpisode.projectId, incident.id)).toString();
|
|
3038
|
+
const monitorName = (incident.monitors || [])
|
|
3039
|
+
.map((monitor) => {
|
|
3040
|
+
return monitor.name || "";
|
|
3041
|
+
})
|
|
3042
|
+
.filter((name) => {
|
|
3043
|
+
return name.length > 0;
|
|
3044
|
+
})
|
|
3045
|
+
.join(", ") || "";
|
|
3046
|
+
incidentRows.push(`
|
|
3047
|
+
<tr>
|
|
3048
|
+
<td style="padding: 12px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
3049
|
+
<table cellpadding="0" cellspacing="0" width="100%">
|
|
3050
|
+
<tr>
|
|
3051
|
+
<td style="vertical-align: middle;">
|
|
3052
|
+
<span style="display: inline-block; background-color: #fee2e2; color: #991b1b; font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 4px; margin-right: 8px;">${incidentNumber}</span>
|
|
3053
|
+
<a href="${incidentLink}" style="color: #2563eb; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 14px; font-weight: 500; text-decoration: none;">${incidentTitle}</a>
|
|
3054
|
+
${monitorName ? `<span style="display: block; color: #64748b; font-size: 12px; margin-top: 4px;">Monitor: ${monitorName}</span>` : ""}
|
|
3055
|
+
</td>
|
|
3056
|
+
<td style="text-align: right; vertical-align: middle;">
|
|
3057
|
+
<a href="${incidentLink}" style="color: #2563eb; font-size: 12px; text-decoration: none;">View →</a>
|
|
3058
|
+
</td>
|
|
3059
|
+
</tr>
|
|
3060
|
+
</table>
|
|
3061
|
+
</td>
|
|
3062
|
+
</tr>
|
|
3063
|
+
`);
|
|
3064
|
+
}
|
|
3065
|
+
if (incidentRows.length > 0) {
|
|
3066
|
+
incidentsListHtml = `
|
|
3067
|
+
<table cellpadding="0" cellspacing="0" width="100%" style="background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-radius: 8px; border: 1px solid #e2e8f0; margin: 8px 0 16px 0;">
|
|
3068
|
+
<tbody>
|
|
3069
|
+
${incidentRows.join("")}
|
|
3070
|
+
</tbody>
|
|
3071
|
+
</table>
|
|
3072
|
+
`;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
const episodeNumber = incidentEpisode.episodeNumberWithPrefix ||
|
|
3076
|
+
(incidentEpisode.episodeNumber
|
|
3077
|
+
? `#${incidentEpisode.episodeNumber}`
|
|
3078
|
+
: "");
|
|
3079
|
+
const vars = {
|
|
3080
|
+
incidentEpisodeTitle: incidentEpisode.title,
|
|
3081
|
+
episodeNumber: episodeNumber,
|
|
3082
|
+
projectName: incidentEpisode.project.name,
|
|
3083
|
+
currentState: incidentEpisode.currentIncidentState.name,
|
|
3084
|
+
incidentEpisodeDescription: await Markdown.convertToHTML(incidentEpisode.description || "", MarkdownContentType.Email),
|
|
3085
|
+
incidentEpisodeSeverity: incidentEpisode.incidentSeverity.name,
|
|
3086
|
+
resourcesAffected: resourcesAffected,
|
|
3087
|
+
rootCause: incidentEpisode.rootCause ||
|
|
3088
|
+
"No root cause identified for this incident episode",
|
|
3089
|
+
incidentsList: incidentsListHtml,
|
|
3090
|
+
incidentsCount: incidents.length.toString(),
|
|
3091
|
+
incidentEpisodeViewLink: (await IncidentEpisodeService.getEpisodeLinkInDashboard(incidentEpisode.projectId, incidentEpisode.id)).toString(),
|
|
3092
|
+
acknowledgeIncidentEpisodeLink: new URL(httpProtocol, host, new Route(AppApiRoute.toString())
|
|
3093
|
+
.addRoute(new UserOnCallLogTimeline().crudApiPath)
|
|
3094
|
+
.addRoute("/acknowledge-page/" + userOnCallLogTimelineId.toString())).toString(),
|
|
3095
|
+
};
|
|
3096
|
+
const emailMessage = {
|
|
3097
|
+
toEmail: to,
|
|
3098
|
+
templateType: EmailTemplateType.AcknowledgeIncidentEpisode,
|
|
3099
|
+
vars: vars,
|
|
3100
|
+
subject: `ACTION REQUIRED: Incident Episode ${episodeNumber} created - ${incidentEpisode.title}`,
|
|
3101
|
+
};
|
|
3102
|
+
return emailMessage;
|
|
3103
|
+
}
|
|
3104
|
+
async startUserNotificationRulesExecution(userId, options) {
|
|
3105
|
+
// add user notification log.
|
|
3106
|
+
const userOnCallLog = new UserOnCallLog();
|
|
3107
|
+
userOnCallLog.userId = userId;
|
|
3108
|
+
userOnCallLog.projectId = options.projectId;
|
|
3109
|
+
if (options.triggeredByIncidentId) {
|
|
3110
|
+
userOnCallLog.triggeredByIncidentId = options.triggeredByIncidentId;
|
|
3111
|
+
}
|
|
3112
|
+
if (options.triggeredByAlertId) {
|
|
3113
|
+
userOnCallLog.triggeredByAlertId = options.triggeredByAlertId;
|
|
3114
|
+
}
|
|
3115
|
+
if (options.triggeredByAlertEpisodeId) {
|
|
3116
|
+
userOnCallLog.triggeredByAlertEpisodeId =
|
|
3117
|
+
options.triggeredByAlertEpisodeId;
|
|
3118
|
+
}
|
|
3119
|
+
if (options.triggeredByIncidentEpisodeId) {
|
|
3120
|
+
userOnCallLog.triggeredByIncidentEpisodeId =
|
|
3121
|
+
options.triggeredByIncidentEpisodeId;
|
|
3122
|
+
}
|
|
3123
|
+
userOnCallLog.userNotificationEventType = options.userNotificationEventType;
|
|
3124
|
+
if (options.onCallPolicyExecutionLogId) {
|
|
3125
|
+
userOnCallLog.onCallDutyPolicyExecutionLogId =
|
|
3126
|
+
options.onCallPolicyExecutionLogId;
|
|
3127
|
+
}
|
|
1849
3128
|
if (options.onCallPolicyId) {
|
|
1850
3129
|
userOnCallLog.onCallDutyPolicyId = options.onCallPolicyId;
|
|
1851
3130
|
}
|
|
@@ -1933,33 +3212,612 @@ export class Service extends DatabaseService {
|
|
|
1933
3212
|
});
|
|
1934
3213
|
}
|
|
1935
3214
|
async onBeforeCreate(createBy) {
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
3215
|
+
const carrier = createBy.data;
|
|
3216
|
+
/*
|
|
3217
|
+
* THE OWNERSHIP COLUMN IS REDUCED TO ONE SPELLING BEFORE ANYTHING READS IT,
|
|
3218
|
+
* and it has to happen here, first, rather than inside the guard below.
|
|
3219
|
+
*
|
|
3220
|
+
* `userId` and `user` are two decorated members over one join column. Every
|
|
3221
|
+
* check from this line down — the roster check, the method-ownership check,
|
|
3222
|
+
* the create invariants, CreatePermission's own ownership gate, and the
|
|
3223
|
+
* audit line after the write — asks "who does this row belong to", and each
|
|
3224
|
+
* of them would otherwise have to answer it from two disagreeing sources.
|
|
3225
|
+
* A payload carrying `user: { _id: <somebody else> }` and no `userId` is
|
|
3226
|
+
* the concrete failure: the guard reads the scalar, finds nothing, falls
|
|
3227
|
+
* back to the actor and validates a self-write, while TypeORM writes the
|
|
3228
|
+
* relation's id and the row belongs to somebody else entirely.
|
|
3229
|
+
*
|
|
3230
|
+
* So: refuse a payload whose two spellings disagree, then fold the survivor
|
|
3231
|
+
* into the scalar. After these two lines `createBy.data.userId` is the
|
|
3232
|
+
* single, authoritative owner, and it is the value that will be persisted.
|
|
3233
|
+
*
|
|
3234
|
+
* Deliberately NOT behind the root short-circuit that guards the checks
|
|
3235
|
+
* below. This is a reduction of the payload rather than a permission
|
|
3236
|
+
* decision, and an internal caller writing an ambiguous row would be just
|
|
3237
|
+
* as ambiguous a row.
|
|
3238
|
+
*/
|
|
3239
|
+
UserNotificationRuleAdminService.assertOneRuleOwner(carrier);
|
|
3240
|
+
UserNotificationRuleAdminService.collapseRuleOwnerRelationOnCreate(carrier);
|
|
3241
|
+
await this.assertWriteIsPermittedForRuleOwner(createBy);
|
|
3242
|
+
/*
|
|
3243
|
+
* Ambiguity is refused, then removed — in that order, and only after the
|
|
3244
|
+
* ownership guard above has had its say. A payload that names two different
|
|
3245
|
+
* methods for one channel is a payload nobody legitimately sends, and one
|
|
3246
|
+
* that names the same method twice is folded down to a single spelling so
|
|
3247
|
+
* that the invariants below, and the ORM after them, are reading the one
|
|
3248
|
+
* value that will actually be written.
|
|
3249
|
+
*/
|
|
3250
|
+
UserNotificationRuleAdminService.assertOneMethodPerNotificationChannel(carrier);
|
|
3251
|
+
UserNotificationRuleAdminService.collapseNotificationMethodRelationsOnCreate(carrier);
|
|
3252
|
+
const hasNotificationMethod = UserNotificationRuleAdminService.carriesAnyNotificationMethod(carrier);
|
|
3253
|
+
this.assertRuleIsCoherent({
|
|
3254
|
+
isOptOut: Boolean(createBy.data.isOptOut),
|
|
3255
|
+
hasNotificationMethod: hasNotificationMethod,
|
|
3256
|
+
});
|
|
1952
3257
|
return {
|
|
1953
3258
|
createBy,
|
|
1954
3259
|
carryForward: null,
|
|
1955
3260
|
};
|
|
1956
3261
|
}
|
|
3262
|
+
/**
|
|
3263
|
+
* The two invariants that decide whether a rule row means anything, enforced
|
|
3264
|
+
* from one place because create and update can each break both of them.
|
|
3265
|
+
*
|
|
3266
|
+
* An opt-out row is how a user says "deliberately do not page me for this rule
|
|
3267
|
+
* type at this severity". It carries the rule type and the severity and
|
|
3268
|
+
* nothing else — a method on it would be self-contradictory (reach me here;
|
|
3269
|
+
* also never reach me), and its whole purpose is to make silence explicit so
|
|
3270
|
+
* that every OTHER zero-rule case can be treated as misconfiguration and
|
|
3271
|
+
* rescued by the fallback. A rule that is NOT opt-out and names no method is
|
|
3272
|
+
* the mirror failure: it looks like coverage on every screen and delivers
|
|
3273
|
+
* nothing.
|
|
3274
|
+
*
|
|
3275
|
+
* The wording of both messages is load-bearing — the dashboard and the API
|
|
3276
|
+
* docs quote them — so they are written once here rather than once per path.
|
|
3277
|
+
*/
|
|
3278
|
+
assertRuleIsCoherent(data) {
|
|
3279
|
+
if (data.isOptOut && data.hasNotificationMethod) {
|
|
3280
|
+
throw new BadDataException("An opt-out notification rule cannot have a notification method. Remove the notification method, or turn off opt-out.");
|
|
3281
|
+
}
|
|
3282
|
+
if (!data.isOptOut && !data.hasNotificationMethod) {
|
|
3283
|
+
throw new BadDataException("Call, SMS, WhatsApp, Telegram, Webhook, Email, or Push notification is required");
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* The create-path half of the on-behalf-of guards (audit R1 and R3).
|
|
3288
|
+
*
|
|
3289
|
+
* CreatePermission.checkCreateOwnership decides WHETHER a caller may name
|
|
3290
|
+
* somebody else in the ownership column: CurrentUser-only callers may not, a
|
|
3291
|
+
* caller holding a real role permission in the model's create list may. That
|
|
3292
|
+
* check is deliberately permission-shaped and value-blind past that point —
|
|
3293
|
+
* it has no notion of a project roster and no notion of what the rest of the
|
|
3294
|
+
* row says. Both of those are checked here, because both of them are how the
|
|
3295
|
+
* widened permission turns into somebody else's pages.
|
|
3296
|
+
*
|
|
3297
|
+
* Root and master-admin writes are exempt, matching the short-circuit at the
|
|
3298
|
+
* top of CreatePermission. Every internal seeder (default rules on method
|
|
3299
|
+
* verification, invitation acceptance, migrations) builds the ownership
|
|
3300
|
+
* column and the method reference from one and the same userId, so the guard
|
|
3301
|
+
* could only ever cost them a query per row; and the delivery-time check in
|
|
3302
|
+
* executeNotificationRuleItem is the backstop that keeps even an internally
|
|
3303
|
+
* written bad row from being acted on.
|
|
3304
|
+
*/
|
|
3305
|
+
async assertWriteIsPermittedForRuleOwner(createBy) {
|
|
3306
|
+
if (createBy.props.isRoot || createBy.props.isMasterAdmin) {
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
/*
|
|
3310
|
+
* `createBy.data.userId` alone is enough HERE, and only because
|
|
3311
|
+
* onBeforeCreate has already folded the `user` relation into it. Read on
|
|
3312
|
+
* its own — before that reduction existed — this line was a bypass: a
|
|
3313
|
+
* payload spelling the owner as `user: { _id: <somebody else> }` left the
|
|
3314
|
+
* scalar empty, fell through to props.userId, and every check below was
|
|
3315
|
+
* answered about the actor while the row was written for the victim. If
|
|
3316
|
+
* that fold is ever moved or removed, this line becomes wrong again.
|
|
3317
|
+
*
|
|
3318
|
+
* The fallback to props.userId is a different thing and stays: an omitted
|
|
3319
|
+
* ownership column means "for myself". CreatePermission stamps props.userId
|
|
3320
|
+
* onto it, but it does so AFTER this hook has run, so the value is not on
|
|
3321
|
+
* the model yet and reading data.userId alone would treat every ordinary
|
|
3322
|
+
* self-service create as an unowned row.
|
|
3323
|
+
*/
|
|
3324
|
+
const ruleOwnerUserId = createBy.data.userId || createBy.props.userId;
|
|
3325
|
+
if (!ruleOwnerUserId) {
|
|
3326
|
+
throw new BadDataException("A notification rule must belong to a user. Sign in as the user this rule is for, or name the user the rule belongs to.");
|
|
3327
|
+
}
|
|
3328
|
+
const actorUserId = createBy.props.userId;
|
|
3329
|
+
const isWritingForSomebodyElse = !actorUserId || actorUserId.toString() !== ruleOwnerUserId.toString();
|
|
3330
|
+
if (isWritingForSomebodyElse) {
|
|
3331
|
+
/*
|
|
3332
|
+
* R1. Holding an administrative permission is a claim about a PROJECT, so
|
|
3333
|
+
* it can only ever authorise writing for users of that project. Without
|
|
3334
|
+
* this, one throwaway project where the caller is an admin would license
|
|
3335
|
+
* writing notification rules for any user id in the installation.
|
|
3336
|
+
*/
|
|
3337
|
+
await UserNotificationRuleAdminService.assertTargetUserIsProjectMember({
|
|
3338
|
+
targetUserId: ruleOwnerUserId,
|
|
3339
|
+
props: createBy.props,
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
/*
|
|
3343
|
+
* R3, and note that it runs for a self-write too. "userId is me, but the
|
|
3344
|
+
* email row I am pointing at is yours" is the mirror image of the hijack —
|
|
3345
|
+
* it does not steal my pages, it copies them to your inbox — and it was
|
|
3346
|
+
* writable long before this phase widened anything.
|
|
3347
|
+
*/
|
|
3348
|
+
const references = UserNotificationRuleAdminService.collectNotificationMethodReferences(createBy.data);
|
|
3349
|
+
await UserNotificationRuleAdminService.assertNotificationMethodsBelongToUser({
|
|
3350
|
+
ownerUserId: ruleOwnerUserId,
|
|
3351
|
+
references: references,
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
/*
|
|
3355
|
+
* R6 for the create path.
|
|
3356
|
+
*
|
|
3357
|
+
* Keyed on the actor the SERVER resolved (props.userId) against the userId
|
|
3358
|
+
* the row was actually PERSISTED with — read off createdItem, after
|
|
3359
|
+
* CreatePermission has had its say and after the insert. Nothing in the
|
|
3360
|
+
* request body reaches this comparison, because the body is the thing being
|
|
3361
|
+
* audited.
|
|
3362
|
+
*/
|
|
3363
|
+
async onCreateSuccess(onCreate, createdItem) {
|
|
3364
|
+
const actorUserId = onCreate.createBy.props.userId;
|
|
3365
|
+
const ruleOwnerUserId = createdItem.userId;
|
|
3366
|
+
if (actorUserId &&
|
|
3367
|
+
ruleOwnerUserId &&
|
|
3368
|
+
actorUserId.toString() !== ruleOwnerUserId.toString()) {
|
|
3369
|
+
await UserNotificationRuleAdminService.recordAdminRuleChange({
|
|
3370
|
+
action: AuditLogAction.Create,
|
|
3371
|
+
actorUserId: actorUserId,
|
|
3372
|
+
ownerUserId: ruleOwnerUserId,
|
|
3373
|
+
projectId: createdItem.projectId || onCreate.createBy.props.tenantId,
|
|
3374
|
+
ruleId: createdItem.id,
|
|
3375
|
+
after: createdItem,
|
|
3376
|
+
notifyOwner: true,
|
|
3377
|
+
props: onCreate.createBy.props,
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
return createdItem;
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Narrow a caller-supplied query to the rows that caller is actually entitled
|
|
3384
|
+
* to write, for use by the write hooks.
|
|
3385
|
+
*
|
|
3386
|
+
* WHY THIS EXISTS AT ALL. DatabaseService runs the hooks BEFORE the permission
|
|
3387
|
+
* layer: _updateBy calls onBeforeUpdate and only then
|
|
3388
|
+
* ModelPermission.checkUpdateQueryPermissions; _deleteBy calls onBeforeDelete
|
|
3389
|
+
* and only then checkDeleteQueryPermission. So a hook that reads
|
|
3390
|
+
* `updateBy.query` is reading the RAW request — no tenant predicate, no
|
|
3391
|
+
* ownership predicate — and the hooks below read it with `isRoot` props on
|
|
3392
|
+
* top, because the question they ask is a question about the database's state
|
|
3393
|
+
* rather than about the caller's visibility. Left there, a caller could point
|
|
3394
|
+
* the guard at rows in another project entirely: the guard would validate
|
|
3395
|
+
* against them, the audit trail would name their owners, and the write itself
|
|
3396
|
+
* would touch a completely different set.
|
|
3397
|
+
*
|
|
3398
|
+
* WHY NOT JUST CALL ModelPermission. That is the obvious fix and it is the
|
|
3399
|
+
* wrong one. checkUpdateQueryPermissions does two jobs — it narrows the query
|
|
3400
|
+
* AND it authorises the request — and running it here would run the second
|
|
3401
|
+
* job twice, moving every table- and column-level rejection into the hook and
|
|
3402
|
+
* duplicating the team lookups behind the tenant scope on every write. The
|
|
3403
|
+
* hook does not need to authorise anything; _updateBy authorises it a few
|
|
3404
|
+
* lines later and is the authority. What the hook needs is only that the row
|
|
3405
|
+
* set it reasons about is no wider than the row set the write can reach.
|
|
3406
|
+
*
|
|
3407
|
+
* WHAT IS REPRODUCED, AND WHY THAT IS THE WHOLE OF IT. For this model the
|
|
3408
|
+
* narrowing is exactly two predicates: the tenant column
|
|
3409
|
+
* (TenantPermission.addTenantScopeToQuery for a member,
|
|
3410
|
+
* PermissionUtil.addTenantScopeToQueryAsRoot on the delete path for root) and,
|
|
3411
|
+
* when Permission.CurrentUser is the ONLY thing letting the caller through,
|
|
3412
|
+
* the ownership column. Nothing else applies: UserNotificationRule declares no
|
|
3413
|
+
* access-control column, is not an operational resource and has no
|
|
3414
|
+
* @OwnedThrough, so addAccessControlIdsToQuery and addOwnedScopeToQuery are
|
|
3415
|
+
* both no-ops on it. IF ANY OF THAT CHANGES ON THE MODEL, THIS MUST CHANGE
|
|
3416
|
+
* WITH IT — a narrowing the permission layer applies and this does not is a
|
|
3417
|
+
* guard validating rows the write never touches.
|
|
3418
|
+
*
|
|
3419
|
+
* Root and master-admin queries are returned untouched. They are entitled to
|
|
3420
|
+
* every row, and narrowing them would make the guard read FEWER rows than the
|
|
3421
|
+
* write reaches, which is the one direction it must never be wrong in.
|
|
3422
|
+
*/
|
|
3423
|
+
narrowQueryToCallerEntitlement(query, props, requestType) {
|
|
3424
|
+
if (props.isRoot || props.isMasterAdmin) {
|
|
3425
|
+
return query;
|
|
3426
|
+
}
|
|
3427
|
+
const scopedQuery = Object.assign({}, query);
|
|
3428
|
+
const tenantColumn = this.getModel().getTenantColumn();
|
|
3429
|
+
if (tenantColumn && props.tenantId && !props.isMultiTenantRequest) {
|
|
3430
|
+
scopedQuery[tenantColumn] = props.tenantId;
|
|
3431
|
+
}
|
|
3432
|
+
const userColumn = this.getModel().getUserColumn();
|
|
3433
|
+
if (userColumn &&
|
|
3434
|
+
props.userId &&
|
|
3435
|
+
TenantPermission.isAccessGrantedOnlyByCurrentUser(this.modelType, props, requestType)) {
|
|
3436
|
+
/*
|
|
3437
|
+
* Set rather than merged. A CurrentUser-only caller whose query names
|
|
3438
|
+
* somebody else is rejected outright by addCurrentUserScopeToQuery a
|
|
3439
|
+
* moment from now, so the only thing that matters here is that the guard
|
|
3440
|
+
* never reads rows that rejection would have protected.
|
|
3441
|
+
*/
|
|
3442
|
+
scopedQuery[userColumn] = props.userId;
|
|
3443
|
+
}
|
|
3444
|
+
return scopedQuery;
|
|
3445
|
+
}
|
|
3446
|
+
/**
|
|
3447
|
+
* The update-path half of R3, plus the read that R6 needs.
|
|
3448
|
+
*
|
|
3449
|
+
* The rule's owner is re-read FROM THE DATABASE here and never taken from
|
|
3450
|
+
* updateBy.data. That is the whole point of the hook: on update the caller
|
|
3451
|
+
* controls the body, so a userId in it is a claim ("this row is mine") made
|
|
3452
|
+
* by exactly the party the guard exists to doubt. The persisted value is the
|
|
3453
|
+
* only one that decides whose pages the row selects, so it is the only one
|
|
3454
|
+
* worth comparing a method's owner against.
|
|
3455
|
+
*
|
|
3456
|
+
* The lookup runs with isRoot rather than the caller's own props on purpose.
|
|
3457
|
+
* Scoping it to what the CALLER can READ would let a caller who cannot see a
|
|
3458
|
+
* row edit it unchecked — the query would simply return nothing and the loop
|
|
3459
|
+
* below would have nothing to reject. Read permission and write permission are
|
|
3460
|
+
* different lists, and it is the write one that decides what this hook has to
|
|
3461
|
+
* answer for.
|
|
3462
|
+
*
|
|
3463
|
+
* The QUERY, on the other hand, is narrowed first. Root props remove the
|
|
3464
|
+
* caller's visibility from the answer; they must not also remove the caller's
|
|
3465
|
+
* ENTITLEMENT from it, and this hook runs before ModelPermission has applied
|
|
3466
|
+
* either. See narrowQueryToCallerEntitlement for why the narrowing is
|
|
3467
|
+
* reproduced here rather than delegated.
|
|
3468
|
+
*
|
|
3469
|
+
* The rows are carried forward so onUpdateSuccess can audit against the
|
|
3470
|
+
* owner as it stood BEFORE the write, without a second read and without
|
|
3471
|
+
* trusting anything the request said.
|
|
3472
|
+
*/
|
|
3473
|
+
async onBeforeUpdate(updateBy) {
|
|
3474
|
+
var _a;
|
|
3475
|
+
const patch = updateBy.data;
|
|
3476
|
+
const references = UserNotificationRuleAdminService.collectNotificationMethodReferences(patch);
|
|
3477
|
+
const isInternalWrite = Boolean(updateBy.props.isRoot || updateBy.props.isMasterAdmin);
|
|
3478
|
+
/*
|
|
3479
|
+
* Three reasons to read the affected rows, and only one of them is R3. An
|
|
3480
|
+
* actor id means this write might be somebody editing somebody else's
|
|
3481
|
+
* configuration, which R6 has to be able to report on even when no method FK
|
|
3482
|
+
* is being touched; and a patch that touches `isOptOut` or any method column
|
|
3483
|
+
* can break a row-level invariant that is only visible once the patch is
|
|
3484
|
+
* laid over the row it is being applied to.
|
|
3485
|
+
*/
|
|
3486
|
+
const touchesRuleCoherence = patch["isOptOut"] !== undefined ||
|
|
3487
|
+
UserNotificationRuleAdminService.mentionsAnyNotificationMethodColumn(patch);
|
|
3488
|
+
const needsAffectedRules = (references.length > 0 && !isInternalWrite) ||
|
|
3489
|
+
touchesRuleCoherence ||
|
|
3490
|
+
Boolean(updateBy.props.userId);
|
|
3491
|
+
if (!needsAffectedRules) {
|
|
3492
|
+
return {
|
|
3493
|
+
updateBy,
|
|
3494
|
+
carryForward: null,
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
const affectedRules = await this.findBy({
|
|
3498
|
+
query: this.narrowQueryToCallerEntitlement(updateBy.query, updateBy.props, DatabaseRequestType.Update),
|
|
3499
|
+
select: {
|
|
3500
|
+
_id: true,
|
|
3501
|
+
userId: true,
|
|
3502
|
+
projectId: true,
|
|
3503
|
+
ruleType: true,
|
|
3504
|
+
notifyAfterMinutes: true,
|
|
3505
|
+
isOptOut: true,
|
|
3506
|
+
incidentSeverityId: true,
|
|
3507
|
+
alertSeverityId: true,
|
|
3508
|
+
userEmailId: true,
|
|
3509
|
+
userSmsId: true,
|
|
3510
|
+
userCallId: true,
|
|
3511
|
+
userWhatsAppId: true,
|
|
3512
|
+
userTelegramId: true,
|
|
3513
|
+
userPushId: true,
|
|
3514
|
+
userWebhookId: true,
|
|
3515
|
+
},
|
|
3516
|
+
limit: LIMIT_MAX,
|
|
3517
|
+
skip: 0,
|
|
3518
|
+
props: {
|
|
3519
|
+
isRoot: true,
|
|
3520
|
+
ignoreHooks: true,
|
|
3521
|
+
},
|
|
3522
|
+
});
|
|
3523
|
+
if (references.length > 0 && !isInternalWrite) {
|
|
3524
|
+
/*
|
|
3525
|
+
* One validation per DISTINCT owner rather than per row. A bulk update
|
|
3526
|
+
* across twenty of one user's rules asks the same question twenty times,
|
|
3527
|
+
* and each question costs a lookup per referenced method.
|
|
3528
|
+
*/
|
|
3529
|
+
const validatedOwnerIds = new Set();
|
|
3530
|
+
for (const affectedRule of affectedRules) {
|
|
3531
|
+
const ownerKey = ((_a = affectedRule.userId) === null || _a === void 0 ? void 0 : _a.toString()) || "";
|
|
3532
|
+
if (validatedOwnerIds.has(ownerKey)) {
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
validatedOwnerIds.add(ownerKey);
|
|
3536
|
+
await UserNotificationRuleAdminService.assertNotificationMethodsBelongToUser({
|
|
3537
|
+
ownerUserId: affectedRule.userId,
|
|
3538
|
+
references: references,
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
/*
|
|
3543
|
+
* Ambiguity is refused here as it is on create, but NOT folded away. The
|
|
3544
|
+
* relation members are `update: []` on this model while the `*Id` members
|
|
3545
|
+
* are open to an administrator, so rewriting one spelling into the other
|
|
3546
|
+
* would smuggle a column write past the very ColumnPermission check that
|
|
3547
|
+
* runs immediately after this hook. Refusal leaves nothing for the ORM to
|
|
3548
|
+
* choose between without moving a value across a permission boundary.
|
|
3549
|
+
*/
|
|
3550
|
+
UserNotificationRuleAdminService.assertOneMethodPerNotificationChannel(patch);
|
|
3551
|
+
if (touchesRuleCoherence) {
|
|
3552
|
+
/*
|
|
3553
|
+
* The create-time invariants, re-checked per affected row.
|
|
3554
|
+
*
|
|
3555
|
+
* They were enforced only on create, which left update as a way to reach
|
|
3556
|
+
* the states create refuses: flip `isOptOut` on a rule that carries an
|
|
3557
|
+
* email and you have a row that says both "reach me here" and "never
|
|
3558
|
+
* reach me"; null the last method on a rule that is not opt-out and you
|
|
3559
|
+
* have a row that looks like coverage on every screen and delivers
|
|
3560
|
+
* nothing — indistinguishable, to the fallback, from a deliberate choice
|
|
3561
|
+
* to stay silent. Neither is visible from the patch alone, which is why
|
|
3562
|
+
* this waits until the affected rows have been read.
|
|
3563
|
+
*/
|
|
3564
|
+
for (const affectedRule of affectedRules) {
|
|
3565
|
+
const methodIdsAfterPatch = UserNotificationRuleAdminService.getNotificationMethodIdsAfterPatch({
|
|
3566
|
+
patch: patch,
|
|
3567
|
+
currentRow: affectedRule,
|
|
3568
|
+
});
|
|
3569
|
+
const isOptOutAfterPatch = patch["isOptOut"] !== undefined
|
|
3570
|
+
? Boolean(patch["isOptOut"])
|
|
3571
|
+
: Boolean(affectedRule.isOptOut);
|
|
3572
|
+
this.assertRuleIsCoherent({
|
|
3573
|
+
isOptOut: isOptOutAfterPatch,
|
|
3574
|
+
hasNotificationMethod: methodIdsAfterPatch.length > 0,
|
|
3575
|
+
});
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return {
|
|
3579
|
+
updateBy,
|
|
3580
|
+
carryForward: {
|
|
3581
|
+
affectedRules: affectedRules,
|
|
3582
|
+
},
|
|
3583
|
+
};
|
|
3584
|
+
}
|
|
3585
|
+
/*
|
|
3586
|
+
* R6 for the update path.
|
|
3587
|
+
*
|
|
3588
|
+
* Every row whose PERSISTED owner is somebody other than the actor gets an
|
|
3589
|
+
* audit entry; the owner gets at most one mail no matter how many of their
|
|
3590
|
+
* rules one request touched, because twenty copies of "an admin changed your
|
|
3591
|
+
* rules" is a message people learn to delete rather than read.
|
|
3592
|
+
*
|
|
3593
|
+
* `updatedItemIds` is the set of rows the write ACTUALLY touched, and the
|
|
3594
|
+
* carried-forward rows are filtered down to it. The two can differ: the hook
|
|
3595
|
+
* read every row the (narrowed) query matched, while _updateBy applies the
|
|
3596
|
+
* caller's own skip/limit and drops rows that were hard-deleted between the
|
|
3597
|
+
* two. Reporting an unchanged row would put a change in the audit trail that
|
|
3598
|
+
* never happened and mail somebody about it.
|
|
3599
|
+
*/
|
|
3600
|
+
async onUpdateSuccess(onUpdate, updatedItemIds) {
|
|
3601
|
+
var _a;
|
|
3602
|
+
const actorUserId = onUpdate.updateBy.props.userId;
|
|
3603
|
+
if (!actorUserId) {
|
|
3604
|
+
return onUpdate;
|
|
3605
|
+
}
|
|
3606
|
+
const affectedRules = ((_a = onUpdate.carryForward) === null || _a === void 0 ? void 0 : _a.affectedRules) || [];
|
|
3607
|
+
const updatedIds = new Set(updatedItemIds.map((id) => {
|
|
3608
|
+
return id.toString();
|
|
3609
|
+
}));
|
|
3610
|
+
await this.reportAdministrativeChange({
|
|
3611
|
+
action: AuditLogAction.Update,
|
|
3612
|
+
actorUserId: actorUserId,
|
|
3613
|
+
rules: affectedRules.filter((rule) => {
|
|
3614
|
+
return Boolean(rule.id && updatedIds.has(rule.id.toString()));
|
|
3615
|
+
}),
|
|
3616
|
+
updatedFields: onUpdate.updateBy.data,
|
|
3617
|
+
props: onUpdate.updateBy.props,
|
|
3618
|
+
});
|
|
3619
|
+
return onUpdate;
|
|
3620
|
+
}
|
|
3621
|
+
/**
|
|
3622
|
+
* R6, factored out because create, update and delete all owe the same debt.
|
|
3623
|
+
*
|
|
3624
|
+
* One audit entry per ROW, because that is what an investigator reconstructs
|
|
3625
|
+
* a timeline from, and at most one mail per PERSON, because one request that
|
|
3626
|
+
* touches twenty of somebody's rules is still one thing that happened to
|
|
3627
|
+
* them.
|
|
3628
|
+
*
|
|
3629
|
+
* Rules the actor owns are skipped: configuring your own paging is not an
|
|
3630
|
+
* administrative act and does not need announcing to yourself.
|
|
3631
|
+
*/
|
|
3632
|
+
async reportAdministrativeChange(data) {
|
|
3633
|
+
const notifiedOwnerIds = new Set();
|
|
3634
|
+
for (const rule of data.rules) {
|
|
3635
|
+
const ruleOwnerUserId = rule.userId;
|
|
3636
|
+
if (!ruleOwnerUserId ||
|
|
3637
|
+
ruleOwnerUserId.toString() === data.actorUserId.toString()) {
|
|
3638
|
+
continue;
|
|
3639
|
+
}
|
|
3640
|
+
const ownerKey = ruleOwnerUserId.toString();
|
|
3641
|
+
const isFirstRuleForThisOwner = !notifiedOwnerIds.has(ownerKey);
|
|
3642
|
+
notifiedOwnerIds.add(ownerKey);
|
|
3643
|
+
await UserNotificationRuleAdminService.recordAdminRuleChange({
|
|
3644
|
+
action: data.action,
|
|
3645
|
+
actorUserId: data.actorUserId,
|
|
3646
|
+
ownerUserId: ruleOwnerUserId,
|
|
3647
|
+
projectId: rule.projectId || data.props.tenantId,
|
|
3648
|
+
ruleId: rule.id,
|
|
3649
|
+
before: rule,
|
|
3650
|
+
updatedFields: data.updatedFields,
|
|
3651
|
+
notifyOwner: isFirstRuleForThisOwner,
|
|
3652
|
+
props: data.props,
|
|
3653
|
+
});
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
/**
|
|
3657
|
+
* The delete-path guard, and the reason it is a guard at all.
|
|
3658
|
+
*
|
|
3659
|
+
* Deleting somebody's notification rules is the most destructive of the three
|
|
3660
|
+
* write verbs and, until this hook existed, the only unguarded one: the model
|
|
3661
|
+
* opened `delete` to the administrative permissions, and nothing here noticed.
|
|
3662
|
+
* An admin — or anyone who had got hold of an admin session — could remove a
|
|
3663
|
+
* responder's entire paging configuration and leave no record and no warning.
|
|
3664
|
+
* The person it happened to would find out during an incident.
|
|
3665
|
+
*
|
|
3666
|
+
* What a delete guard can and cannot be. There is no R3 analogue: a deleted
|
|
3667
|
+
* row routes nothing anywhere, so there is no method-versus-owner pair left to
|
|
3668
|
+
* disagree. Nor is there an R1 analogue: refusing to delete the rules of
|
|
3669
|
+
* somebody who is no longer on the roster would block exactly the cleanup an
|
|
3670
|
+
* admin performs after a member leaves. What is left, and what actually
|
|
3671
|
+
* matters, is EVIDENTIARY — establish from the database who owned these rows
|
|
3672
|
+
* before they cease to exist, because after the write nothing can answer that
|
|
3673
|
+
* question and the request body was never allowed to.
|
|
3674
|
+
*
|
|
3675
|
+
* The rows must be read here rather than in onDeleteSuccess for the same
|
|
3676
|
+
* reason: _deleteBy hands the success hook only the ids it deleted, and by
|
|
3677
|
+
* then the rows are gone.
|
|
3678
|
+
*
|
|
3679
|
+
* The query is narrowed first. onBeforeDelete runs BEFORE
|
|
3680
|
+
* ModelPermission.checkDeleteQueryPermission, exactly as onBeforeUpdate runs
|
|
3681
|
+
* before its update counterpart, so the raw query carries neither the tenant
|
|
3682
|
+
* predicate nor the ownership predicate — see narrowQueryToCallerEntitlement.
|
|
3683
|
+
* Without it, a member could point this read at another project's rules and
|
|
3684
|
+
* have their owners written into the audit trail and mailed a warning about a
|
|
3685
|
+
* deletion that never touched them.
|
|
3686
|
+
*/
|
|
3687
|
+
async onBeforeDelete(deleteBy) {
|
|
3688
|
+
if (!deleteBy.props.userId) {
|
|
3689
|
+
/*
|
|
3690
|
+
* No actor, nothing to attribute. Workers and migrations delete rules
|
|
3691
|
+
* (project teardown, method removal cascades) and an "an administrator
|
|
3692
|
+
* deleted your rules" mail for every one of those is how the message
|
|
3693
|
+
* that matters gets filtered away.
|
|
3694
|
+
*/
|
|
3695
|
+
return {
|
|
3696
|
+
deleteBy,
|
|
3697
|
+
carryForward: null,
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
const deletedRules = await this.findBy({
|
|
3701
|
+
query: this.narrowQueryToCallerEntitlement(deleteBy.query, deleteBy.props, DatabaseRequestType.Delete),
|
|
3702
|
+
select: {
|
|
3703
|
+
_id: true,
|
|
3704
|
+
userId: true,
|
|
3705
|
+
projectId: true,
|
|
3706
|
+
ruleType: true,
|
|
3707
|
+
notifyAfterMinutes: true,
|
|
3708
|
+
isOptOut: true,
|
|
3709
|
+
incidentSeverityId: true,
|
|
3710
|
+
alertSeverityId: true,
|
|
3711
|
+
userEmailId: true,
|
|
3712
|
+
userSmsId: true,
|
|
3713
|
+
userCallId: true,
|
|
3714
|
+
userWhatsAppId: true,
|
|
3715
|
+
userTelegramId: true,
|
|
3716
|
+
userPushId: true,
|
|
3717
|
+
userWebhookId: true,
|
|
3718
|
+
},
|
|
3719
|
+
limit: LIMIT_MAX,
|
|
3720
|
+
skip: 0,
|
|
3721
|
+
props: {
|
|
3722
|
+
isRoot: true,
|
|
3723
|
+
ignoreHooks: true,
|
|
3724
|
+
},
|
|
3725
|
+
});
|
|
3726
|
+
return {
|
|
3727
|
+
deleteBy,
|
|
3728
|
+
carryForward: {
|
|
3729
|
+
deletedRules: deletedRules,
|
|
3730
|
+
},
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
/*
|
|
3734
|
+
* R6 for the delete path.
|
|
3735
|
+
*
|
|
3736
|
+
* Keyed the same way as the other two: the actor the SERVER resolved against
|
|
3737
|
+
* the owner the DATABASE recorded, read before the rows were removed. The
|
|
3738
|
+
* snapshot carried forward is now the only description of what those rules
|
|
3739
|
+
* were, so it is what the audit entry is built from.
|
|
3740
|
+
*/
|
|
3741
|
+
async onDeleteSuccess(onDelete, deletedItemIds) {
|
|
3742
|
+
var _a;
|
|
3743
|
+
const actorUserId = onDelete.deleteBy.props.userId;
|
|
3744
|
+
if (!actorUserId) {
|
|
3745
|
+
return onDelete;
|
|
3746
|
+
}
|
|
3747
|
+
const deletedRules = ((_a = onDelete.carryForward) === null || _a === void 0 ? void 0 : _a.deletedRules) || [];
|
|
3748
|
+
const deletedIds = new Set(deletedItemIds.map((id) => {
|
|
3749
|
+
return id.toString();
|
|
3750
|
+
}));
|
|
3751
|
+
await this.reportAdministrativeChange({
|
|
3752
|
+
action: AuditLogAction.Delete,
|
|
3753
|
+
actorUserId: actorUserId,
|
|
3754
|
+
/*
|
|
3755
|
+
* Only rows the delete actually removed. The hook read every row the
|
|
3756
|
+
* narrowed query matched; _deleteBy then applied the caller's own
|
|
3757
|
+
* skip/limit on top, so the two sets are not always the same and a
|
|
3758
|
+
* warning about a rule that still exists is a false alarm.
|
|
3759
|
+
*/
|
|
3760
|
+
rules: deletedRules.filter((rule) => {
|
|
3761
|
+
return Boolean(rule.id && deletedIds.has(rule.id.toString()));
|
|
3762
|
+
}),
|
|
3763
|
+
props: onDelete.deleteBy.props,
|
|
3764
|
+
});
|
|
3765
|
+
return onDelete;
|
|
3766
|
+
}
|
|
1957
3767
|
async addDefaultNotificationRulesForVerifiedMethod(data) {
|
|
1958
3768
|
const { projectId, userId, notificationMethod } = data;
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
3769
|
+
/*
|
|
3770
|
+
* Read each severity list once and reuse it for both rule types it drives.
|
|
3771
|
+
* Incident severities scope both ON_CALL_EXECUTED_INCIDENT and
|
|
3772
|
+
* ON_CALL_EXECUTED_INCIDENT_EPISODE; alert severities do the same for their
|
|
3773
|
+
* two.
|
|
3774
|
+
*/
|
|
3775
|
+
const incidentSeverityIds = await this.getIncidentSeverityIds(projectId);
|
|
3776
|
+
const alertSeverityIds = await this.getAlertSeverityIds(projectId);
|
|
3777
|
+
await this.createSeverityScopedRules({
|
|
3778
|
+
projectId,
|
|
3779
|
+
userId,
|
|
3780
|
+
notificationMethod,
|
|
3781
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT,
|
|
3782
|
+
severityIds: incidentSeverityIds,
|
|
3783
|
+
severityColumn: "incidentSeverityId",
|
|
3784
|
+
});
|
|
3785
|
+
await this.createSeverityScopedRules({
|
|
3786
|
+
projectId,
|
|
3787
|
+
userId,
|
|
3788
|
+
notificationMethod,
|
|
3789
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT,
|
|
3790
|
+
severityIds: alertSeverityIds,
|
|
3791
|
+
severityColumn: "alertSeverityId",
|
|
3792
|
+
});
|
|
3793
|
+
/*
|
|
3794
|
+
* The two episode rule types are severity-scoped as well, and used not to
|
|
3795
|
+
* be. UserOnCallLogService counts episode rules filtered by a concrete
|
|
3796
|
+
* severity id, and the episode rule pages in User Settings scope their
|
|
3797
|
+
* tables the same way — so a NULL-severity episode rule matched no page and
|
|
3798
|
+
* appeared in no table. Users got "defaults" that were unreachable and
|
|
3799
|
+
* invisible at the same time.
|
|
3800
|
+
*/
|
|
3801
|
+
await this.createSeverityScopedRules({
|
|
3802
|
+
projectId,
|
|
3803
|
+
userId,
|
|
3804
|
+
notificationMethod,
|
|
3805
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT_EPISODE,
|
|
3806
|
+
severityIds: alertSeverityIds,
|
|
3807
|
+
severityColumn: "alertSeverityId",
|
|
3808
|
+
});
|
|
3809
|
+
await this.createSeverityScopedRules({
|
|
3810
|
+
projectId,
|
|
3811
|
+
userId,
|
|
3812
|
+
notificationMethod,
|
|
3813
|
+
ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT_EPISODE,
|
|
3814
|
+
severityIds: incidentSeverityIds,
|
|
3815
|
+
severityColumn: "incidentSeverityId",
|
|
3816
|
+
});
|
|
3817
|
+
/*
|
|
3818
|
+
* These two are about the user's shift, not about anything that fired, so
|
|
3819
|
+
* they legitimately have no severity and stay single rules.
|
|
3820
|
+
*/
|
|
1963
3821
|
await this.createSingleRule(projectId, userId, notificationMethod, NotificationRuleType.WHEN_USER_GOES_ON_CALL);
|
|
1964
3822
|
await this.createSingleRule(projectId, userId, notificationMethod, NotificationRuleType.WHEN_USER_GOES_OFF_CALL);
|
|
1965
3823
|
}
|
|
@@ -2011,7 +3869,7 @@ export class Service extends DatabaseService {
|
|
|
2011
3869
|
}
|
|
2012
3870
|
return query;
|
|
2013
3871
|
}
|
|
2014
|
-
async
|
|
3872
|
+
async getIncidentSeverityIds(projectId) {
|
|
2015
3873
|
const incidentSeverities = await IncidentSeverityService.findBy({
|
|
2016
3874
|
query: {
|
|
2017
3875
|
projectId,
|
|
@@ -2025,33 +3883,11 @@ export class Service extends DatabaseService {
|
|
|
2025
3883
|
_id: true,
|
|
2026
3884
|
},
|
|
2027
3885
|
});
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
userId }, this.getNotificationMethodQuery(notificationMethod)), { incidentSeverityId: incidentSeverity.id, ruleType: NotificationRuleType.ON_CALL_EXECUTED_INCIDENT }),
|
|
2032
|
-
props: {
|
|
2033
|
-
isRoot: true,
|
|
2034
|
-
},
|
|
2035
|
-
});
|
|
2036
|
-
if (existingRule) {
|
|
2037
|
-
continue;
|
|
2038
|
-
}
|
|
2039
|
-
const rule = new Model();
|
|
2040
|
-
rule.projectId = projectId;
|
|
2041
|
-
rule.userId = userId;
|
|
2042
|
-
this.applyNotificationMethod(rule, notificationMethod);
|
|
2043
|
-
rule.incidentSeverityId = incidentSeverity.id;
|
|
2044
|
-
rule.notifyAfterMinutes = 0;
|
|
2045
|
-
rule.ruleType = NotificationRuleType.ON_CALL_EXECUTED_INCIDENT;
|
|
2046
|
-
await this.create({
|
|
2047
|
-
data: rule,
|
|
2048
|
-
props: {
|
|
2049
|
-
isRoot: true,
|
|
2050
|
-
},
|
|
2051
|
-
});
|
|
2052
|
-
}
|
|
3886
|
+
return incidentSeverities.map((severity) => {
|
|
3887
|
+
return severity.id;
|
|
3888
|
+
});
|
|
2053
3889
|
}
|
|
2054
|
-
async
|
|
3890
|
+
async getAlertSeverityIds(projectId) {
|
|
2055
3891
|
const alertSeverities = await AlertSeverityService.findBy({
|
|
2056
3892
|
query: {
|
|
2057
3893
|
projectId,
|
|
@@ -2065,10 +3901,20 @@ export class Service extends DatabaseService {
|
|
|
2065
3901
|
_id: true,
|
|
2066
3902
|
},
|
|
2067
3903
|
});
|
|
2068
|
-
|
|
3904
|
+
return alertSeverities.map((severity) => {
|
|
3905
|
+
return severity.id;
|
|
3906
|
+
});
|
|
3907
|
+
}
|
|
3908
|
+
/*
|
|
3909
|
+
* Seed one rule per severity for a severity-scoped rule type, skipping any
|
|
3910
|
+
* (method, severity, ruleType) triple the user already has. The duplicate
|
|
3911
|
+
* check is keyed on the same columns the write sets, so re-verifying a method
|
|
3912
|
+
* never doubles a user's rules — and therefore never doubles their pages.
|
|
3913
|
+
*/
|
|
3914
|
+
async createSeverityScopedRules(data) {
|
|
3915
|
+
for (const severityId of data.severityIds) {
|
|
2069
3916
|
const existingRule = await this.findOneBy({
|
|
2070
|
-
query: Object.assign(Object.assign({ projectId,
|
|
2071
|
-
userId }, this.getNotificationMethodQuery(notificationMethod)), { alertSeverityId: alertSeverity.id, ruleType: NotificationRuleType.ON_CALL_EXECUTED_ALERT }),
|
|
3917
|
+
query: Object.assign(Object.assign({ projectId: data.projectId, userId: data.userId }, this.getNotificationMethodQuery(data.notificationMethod)), { [data.severityColumn]: severityId, ruleType: data.ruleType }),
|
|
2072
3918
|
props: {
|
|
2073
3919
|
isRoot: true,
|
|
2074
3920
|
},
|
|
@@ -2077,12 +3923,12 @@ export class Service extends DatabaseService {
|
|
|
2077
3923
|
continue;
|
|
2078
3924
|
}
|
|
2079
3925
|
const rule = new Model();
|
|
2080
|
-
rule.projectId = projectId;
|
|
2081
|
-
rule.userId = userId;
|
|
2082
|
-
this.applyNotificationMethod(rule, notificationMethod);
|
|
2083
|
-
rule.
|
|
3926
|
+
rule.projectId = data.projectId;
|
|
3927
|
+
rule.userId = data.userId;
|
|
3928
|
+
this.applyNotificationMethod(rule, data.notificationMethod);
|
|
3929
|
+
rule[data.severityColumn] = severityId;
|
|
2084
3930
|
rule.notifyAfterMinutes = 0;
|
|
2085
|
-
rule.ruleType =
|
|
3931
|
+
rule.ruleType = data.ruleType;
|
|
2086
3932
|
await this.create({
|
|
2087
3933
|
data: rule,
|
|
2088
3934
|
props: {
|
|
@@ -2147,6 +3993,728 @@ export class Service extends DatabaseService {
|
|
|
2147
3993
|
},
|
|
2148
3994
|
});
|
|
2149
3995
|
}
|
|
3996
|
+
/**
|
|
3997
|
+
* What this user would lose by deleting these notification rules.
|
|
3998
|
+
*
|
|
3999
|
+
* Ask this BEFORE deleting. It reads and returns; it writes nothing and
|
|
4000
|
+
* refuses nothing, and the caller is expected to go ahead and delete anyway
|
|
4001
|
+
* if that is what the human wants after reading it.
|
|
4002
|
+
*
|
|
4003
|
+
* The rule ids are INTERSECTED with the rules this user actually has in this
|
|
4004
|
+
* project rather than trusted — the read is scoped by (projectId, userId) in
|
|
4005
|
+
* the query itself, so an id belonging to somebody else, or to another
|
|
4006
|
+
* project, simply matches nothing and contributes nothing to the answer.
|
|
4007
|
+
* That is the only place row scoping can come from: a column access list
|
|
4008
|
+
* cannot restrict WHICH ROWS a caller sees, because Permission.CurrentUser is
|
|
4009
|
+
* auto-granted to every authenticated caller and so never means "only my own
|
|
4010
|
+
* row".
|
|
4011
|
+
*
|
|
4012
|
+
* An empty id list is legal and returns a zero-deletion impact, which is
|
|
4013
|
+
* still worth something: it carries this user's responder status, their
|
|
4014
|
+
* reachability and whether the project's fallback is on.
|
|
4015
|
+
*
|
|
4016
|
+
* ---------------------------------------------------------------------------
|
|
4017
|
+
* NOT WIRED TODAY. READ THIS BEFORE ASSUMING IT GUARDS ANYTHING.
|
|
4018
|
+
*
|
|
4019
|
+
* This method and getNotificationMethodDeletionImpact below have NO production
|
|
4020
|
+
* caller. The delete guard that actually ships is client-side, in
|
|
4021
|
+
* App/FeatureSet/Dashboard/src/Components/NotificationMethods/NotificationMethod.tsx
|
|
4022
|
+
* (useNotificationMethodDeleteGuard + DeletionImpactModal), and it computes the
|
|
4023
|
+
* same answer in the browser from the deleting user's OWN rules.
|
|
4024
|
+
*
|
|
4025
|
+
* That is adequate for the case that ships: a person deleting their own
|
|
4026
|
+
* notification method or their own rule can read all of their own rules, and
|
|
4027
|
+
* one user's rule set is small and bounded. It is NOT adequate for an
|
|
4028
|
+
* administrator computing the impact of deleting somebody ELSE's
|
|
4029
|
+
* configuration, which is what this exists for - and that is Phase 3, which is
|
|
4030
|
+
* not merged.
|
|
4031
|
+
*
|
|
4032
|
+
* So this is deliberately-retained, currently-unreachable code, kept because
|
|
4033
|
+
* the admin path needs exactly it and because it holds one thing the browser
|
|
4034
|
+
* copy cannot: it reads EVERY rule for the user rather than a page, and it
|
|
4035
|
+
* derives a rule's severity from the column its RULE TYPE dictates rather than
|
|
4036
|
+
* whichever column happens to be populated.
|
|
4037
|
+
*
|
|
4038
|
+
* Retaining unreachable code is a real cost and this comment is the price of
|
|
4039
|
+
* it: nothing here enforces anything server-side today. A deletion is not
|
|
4040
|
+
* validated, refused or even observed by this service. If you are reading this
|
|
4041
|
+
* because you assumed the server checked, it does not.
|
|
4042
|
+
* ---------------------------------------------------------------------------
|
|
4043
|
+
*/
|
|
4044
|
+
async getRuleDeletionImpact(data) {
|
|
4045
|
+
const targetRuleIds = new Set(data.notificationRuleIds.map((ruleId) => {
|
|
4046
|
+
return ruleId.toString();
|
|
4047
|
+
}));
|
|
4048
|
+
return this.computeDeletionImpact({
|
|
4049
|
+
projectId: data.projectId,
|
|
4050
|
+
userId: data.userId,
|
|
4051
|
+
isBeingDeleted: (rule) => {
|
|
4052
|
+
var _a;
|
|
4053
|
+
return targetRuleIds.has(((_a = rule.id) === null || _a === void 0 ? void 0 : _a.toString()) || "");
|
|
4054
|
+
},
|
|
4055
|
+
deletedMethod: undefined,
|
|
4056
|
+
});
|
|
4057
|
+
}
|
|
4058
|
+
/**
|
|
4059
|
+
* What this user would lose by deleting one notification method.
|
|
4060
|
+
*
|
|
4061
|
+
* This is the dangerous one. Deleting a method is not a small write: every
|
|
4062
|
+
* method foreign key on UserNotificationRule is onDelete: "CASCADE", and each
|
|
4063
|
+
* method service deletes the same rows itself in onBeforeDelete, so removing
|
|
4064
|
+
* one phone number takes every rule that pointed at it with it. Somebody
|
|
4065
|
+
* tidying up an old number has no reason to expect that, and nothing on the
|
|
4066
|
+
* screen tells them.
|
|
4067
|
+
*
|
|
4068
|
+
* The method row is looked up scoped by projectId, and the userId comes from
|
|
4069
|
+
* the ROW rather than from the caller. Both matter: the projectId scope is
|
|
4070
|
+
* what stops a caller probing method ids from other projects, and taking the
|
|
4071
|
+
* userId from the row is what stops a caller asking for one user's method
|
|
4072
|
+
* under another user's name and getting an answer that belongs to neither.
|
|
4073
|
+
*/
|
|
4074
|
+
async getNotificationMethodDeletionImpact(data) {
|
|
4075
|
+
const method = await this.resolveNotificationMethod(data);
|
|
4076
|
+
return this.computeDeletionImpact({
|
|
4077
|
+
projectId: data.projectId,
|
|
4078
|
+
userId: method.userId,
|
|
4079
|
+
isBeingDeleted: (rule) => {
|
|
4080
|
+
var _a;
|
|
4081
|
+
return (((_a = this.getRuleMethodId(rule, data.methodType)) === null || _a === void 0 ? void 0 : _a.toString()) ===
|
|
4082
|
+
data.methodId.toString());
|
|
4083
|
+
},
|
|
4084
|
+
deletedMethod: method,
|
|
4085
|
+
});
|
|
4086
|
+
}
|
|
4087
|
+
/**
|
|
4088
|
+
* Read the method row being deleted, scoped to the project, and reduce it to
|
|
4089
|
+
* the three things the preview needs: whose it is, which channel it is on,
|
|
4090
|
+
* and whether it was ever verified.
|
|
4091
|
+
*
|
|
4092
|
+
* Webhooks report isVerified: true with no column behind it. UserWebhook has
|
|
4093
|
+
* no verification concept at all — its presence IS the whole test, which is
|
|
4094
|
+
* how the fallback treats it and how readiness reports it — so calling it
|
|
4095
|
+
* unverified here would tell somebody that deleting the one channel
|
|
4096
|
+
* guaranteed to work costs them nothing.
|
|
4097
|
+
*/
|
|
4098
|
+
async resolveNotificationMethod(data) {
|
|
4099
|
+
const query = {
|
|
4100
|
+
_id: data.methodId,
|
|
4101
|
+
projectId: data.projectId,
|
|
4102
|
+
};
|
|
4103
|
+
const props = {
|
|
4104
|
+
isRoot: true,
|
|
4105
|
+
};
|
|
4106
|
+
let row = null;
|
|
4107
|
+
let isVerified = false;
|
|
4108
|
+
if (data.methodType === ReadinessMethodType.Email) {
|
|
4109
|
+
row = await UserEmailService.findOneBy({
|
|
4110
|
+
query: query,
|
|
4111
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4112
|
+
props: props,
|
|
4113
|
+
});
|
|
4114
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4115
|
+
}
|
|
4116
|
+
else if (data.methodType === ReadinessMethodType.SMS) {
|
|
4117
|
+
row = await UserSmsService.findOneBy({
|
|
4118
|
+
query: query,
|
|
4119
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4120
|
+
props: props,
|
|
4121
|
+
});
|
|
4122
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4123
|
+
}
|
|
4124
|
+
else if (data.methodType === ReadinessMethodType.Call) {
|
|
4125
|
+
row = await UserCallService.findOneBy({
|
|
4126
|
+
query: query,
|
|
4127
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4128
|
+
props: props,
|
|
4129
|
+
});
|
|
4130
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4131
|
+
}
|
|
4132
|
+
else if (data.methodType === ReadinessMethodType.Push) {
|
|
4133
|
+
row = await UserPushService.findOneBy({
|
|
4134
|
+
query: query,
|
|
4135
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4136
|
+
props: props,
|
|
4137
|
+
});
|
|
4138
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4139
|
+
}
|
|
4140
|
+
else if (data.methodType === ReadinessMethodType.WhatsApp) {
|
|
4141
|
+
row = await UserWhatsAppService.findOneBy({
|
|
4142
|
+
query: query,
|
|
4143
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4144
|
+
props: props,
|
|
4145
|
+
});
|
|
4146
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4147
|
+
}
|
|
4148
|
+
else if (data.methodType === ReadinessMethodType.Telegram) {
|
|
4149
|
+
row = await UserTelegramService.findOneBy({
|
|
4150
|
+
query: query,
|
|
4151
|
+
select: { _id: true, userId: true, isVerified: true },
|
|
4152
|
+
props: props,
|
|
4153
|
+
});
|
|
4154
|
+
isVerified = Boolean(row === null || row === void 0 ? void 0 : row.isVerified);
|
|
4155
|
+
}
|
|
4156
|
+
else if (data.methodType === ReadinessMethodType.Webhook) {
|
|
4157
|
+
row = await UserWebhookService.findOneBy({
|
|
4158
|
+
query: query,
|
|
4159
|
+
select: { _id: true, userId: true },
|
|
4160
|
+
props: props,
|
|
4161
|
+
});
|
|
4162
|
+
isVerified = Boolean(row);
|
|
4163
|
+
}
|
|
4164
|
+
else {
|
|
4165
|
+
throw new BadDataException(`${data.methodType} is not a notification method`);
|
|
4166
|
+
}
|
|
4167
|
+
if (!row || !row.userId) {
|
|
4168
|
+
throw new BadDataException("Notification method not found");
|
|
4169
|
+
}
|
|
4170
|
+
return {
|
|
4171
|
+
methodType: data.methodType,
|
|
4172
|
+
userId: row.userId,
|
|
4173
|
+
isVerified: isVerified,
|
|
4174
|
+
};
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* The foreign key a rule uses to point at a method of this channel. One
|
|
4178
|
+
* lookup table rather than seven inline comparisons, so a rule can never be
|
|
4179
|
+
* tested against the wrong column — which would report a WhatsApp rule as
|
|
4180
|
+
* surviving the deletion of the SMS number it does not use, or worse, the
|
|
4181
|
+
* reverse.
|
|
4182
|
+
*/
|
|
4183
|
+
getRuleMethodId(rule, methodType) {
|
|
4184
|
+
if (methodType === ReadinessMethodType.Email) {
|
|
4185
|
+
return rule.userEmailId;
|
|
4186
|
+
}
|
|
4187
|
+
if (methodType === ReadinessMethodType.SMS) {
|
|
4188
|
+
return rule.userSmsId;
|
|
4189
|
+
}
|
|
4190
|
+
if (methodType === ReadinessMethodType.Call) {
|
|
4191
|
+
return rule.userCallId;
|
|
4192
|
+
}
|
|
4193
|
+
if (methodType === ReadinessMethodType.Push) {
|
|
4194
|
+
return rule.userPushId;
|
|
4195
|
+
}
|
|
4196
|
+
if (methodType === ReadinessMethodType.WhatsApp) {
|
|
4197
|
+
return rule.userWhatsAppId;
|
|
4198
|
+
}
|
|
4199
|
+
if (methodType === ReadinessMethodType.Telegram) {
|
|
4200
|
+
return rule.userTelegramId;
|
|
4201
|
+
}
|
|
4202
|
+
if (methodType === ReadinessMethodType.Webhook) {
|
|
4203
|
+
return rule.userWebhookId;
|
|
4204
|
+
}
|
|
4205
|
+
return undefined;
|
|
4206
|
+
}
|
|
4207
|
+
/**
|
|
4208
|
+
* The before/after picture both entry points share.
|
|
4209
|
+
*
|
|
4210
|
+
* Deliberately shaped as "read every rule this user has, then ask a predicate
|
|
4211
|
+
* which of them go" rather than "count the rules that go". Coverage is a
|
|
4212
|
+
* property of what is LEFT, so the rules that survive are as load-bearing as
|
|
4213
|
+
* the ones that do not: a cell with two rules on two methods loses nothing
|
|
4214
|
+
* when one of them goes, and the only way to know that is to have read both.
|
|
4215
|
+
*/
|
|
4216
|
+
async computeDeletionImpact(data) {
|
|
4217
|
+
const cellStates = new Map();
|
|
4218
|
+
const handoffStates = new Map();
|
|
4219
|
+
let rulesDeletedCount = 0;
|
|
4220
|
+
/*
|
|
4221
|
+
* Folded as each page arrives rather than accumulated and folded after.
|
|
4222
|
+
* Every rule collapses into one of a few dozen cells, so holding the rows
|
|
4223
|
+
* would mean carrying the whole table in memory to produce a map orders of
|
|
4224
|
+
* magnitude smaller — and the one project where that matters is exactly the
|
|
4225
|
+
* project where this read takes more than one page.
|
|
4226
|
+
*/
|
|
4227
|
+
const foldRule = (rule) => {
|
|
4228
|
+
const isDeleted = data.isBeingDeleted(rule);
|
|
4229
|
+
if (isDeleted) {
|
|
4230
|
+
rulesDeletedCount++;
|
|
4231
|
+
}
|
|
4232
|
+
/*
|
|
4233
|
+
* `isOptOut === true`, never `=== false`. The column is nullable and was
|
|
4234
|
+
* added long after these rows started existing, so it is NULL on every
|
|
4235
|
+
* rule in every existing install; testing for false would classify all of
|
|
4236
|
+
* them as neither rules nor opt-outs and report a fully configured user
|
|
4237
|
+
* as having nothing to lose. This is the exact dual of the predicate
|
|
4238
|
+
* readiness folds with, and it has to stay that way.
|
|
4239
|
+
*/
|
|
4240
|
+
const isOptOut = rule.isOptOut === true;
|
|
4241
|
+
const scope = PAGING_RULE_TYPE_SCOPES.find((candidate) => {
|
|
4242
|
+
return candidate.ruleType === rule.ruleType;
|
|
4243
|
+
});
|
|
4244
|
+
if (scope) {
|
|
4245
|
+
const severityId = rule[scope.severityColumn];
|
|
4246
|
+
/*
|
|
4247
|
+
* A severity-scoped rule with a NULL severity matches no page at
|
|
4248
|
+
* runtime, so it covers no cell and losing it costs nothing. Counting
|
|
4249
|
+
* it would promise coverage that never existed.
|
|
4250
|
+
*/
|
|
4251
|
+
if (!severityId) {
|
|
4252
|
+
return;
|
|
4253
|
+
}
|
|
4254
|
+
const key = `${scope.ruleType}|${severityId.toString()}`;
|
|
4255
|
+
const state = cellStates.get(key) || {
|
|
4256
|
+
ruleType: scope.ruleType,
|
|
4257
|
+
severityKind: scope.severityKind,
|
|
4258
|
+
severityId: severityId.toString(),
|
|
4259
|
+
rulesBefore: 0,
|
|
4260
|
+
rulesRemoved: 0,
|
|
4261
|
+
hasOptOut: false,
|
|
4262
|
+
};
|
|
4263
|
+
if (isOptOut) {
|
|
4264
|
+
/*
|
|
4265
|
+
* Only a SURVIVING opt-out makes the silence deliberate. Deleting the
|
|
4266
|
+
* opt-out along with the last rule leaves neither, and that cell is a
|
|
4267
|
+
* real gap however it was created.
|
|
4268
|
+
*/
|
|
4269
|
+
if (!isDeleted) {
|
|
4270
|
+
state.hasOptOut = true;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
else {
|
|
4274
|
+
state.rulesBefore++;
|
|
4275
|
+
if (isDeleted) {
|
|
4276
|
+
state.rulesRemoved++;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
cellStates.set(key, state);
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
const handoffType = HANDOFF_RULE_TYPES.find((candidate) => {
|
|
4283
|
+
return candidate === rule.ruleType;
|
|
4284
|
+
});
|
|
4285
|
+
if (!handoffType) {
|
|
4286
|
+
// Not a rule type anything pages on. It covers nothing, so it loses nothing.
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
const handoffState = handoffStates.get(handoffType) || {
|
|
4290
|
+
rulesBefore: 0,
|
|
4291
|
+
rulesRemoved: 0,
|
|
4292
|
+
hasOptOut: false,
|
|
4293
|
+
};
|
|
4294
|
+
if (isOptOut) {
|
|
4295
|
+
if (!isDeleted) {
|
|
4296
|
+
handoffState.hasOptOut = true;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
else {
|
|
4300
|
+
handoffState.rulesBefore++;
|
|
4301
|
+
if (isDeleted) {
|
|
4302
|
+
handoffState.rulesRemoved++;
|
|
4303
|
+
}
|
|
4304
|
+
}
|
|
4305
|
+
handoffStates.set(handoffType, handoffState);
|
|
4306
|
+
};
|
|
4307
|
+
const isTruncated = await this.readEveryNotificationRuleForUser({
|
|
4308
|
+
projectId: data.projectId,
|
|
4309
|
+
userId: data.userId,
|
|
4310
|
+
consume: (rows) => {
|
|
4311
|
+
for (const rule of rows) {
|
|
4312
|
+
foldRule(rule);
|
|
4313
|
+
}
|
|
4314
|
+
},
|
|
4315
|
+
});
|
|
4316
|
+
const lostCells = Array.from(cellStates.values()).filter((state) => {
|
|
4317
|
+
return (state.rulesBefore > 0 &&
|
|
4318
|
+
state.rulesRemoved === state.rulesBefore &&
|
|
4319
|
+
!state.hasOptOut);
|
|
4320
|
+
});
|
|
4321
|
+
const handoffNotificationsLost = HANDOFF_RULE_TYPES.filter((ruleType) => {
|
|
4322
|
+
const state = handoffStates.get(ruleType);
|
|
4323
|
+
return Boolean(state &&
|
|
4324
|
+
state.rulesBefore > 0 &&
|
|
4325
|
+
state.rulesRemoved === state.rulesBefore &&
|
|
4326
|
+
!state.hasOptOut);
|
|
4327
|
+
});
|
|
4328
|
+
/*
|
|
4329
|
+
* Severity names are read only when something was actually lost. The common
|
|
4330
|
+
* case for this call is "nothing you care about goes", and that case should
|
|
4331
|
+
* not cost two extra round trips to name cells nobody will be shown.
|
|
4332
|
+
*/
|
|
4333
|
+
const severityNames = await this.loadSeverityNamesForCells(data.projectId, lostCells);
|
|
4334
|
+
const coverageLost = this.buildCoverageLossCells(lostCells, severityNames);
|
|
4335
|
+
/*
|
|
4336
|
+
* "Is this person on call anywhere" comes from OnCallReadinessService and
|
|
4337
|
+
* from nowhere else. getReadinessForUsers rather than getReadinessForUser
|
|
4338
|
+
* because the plural form OMITS a user who is not a member of the project
|
|
4339
|
+
* instead of throwing for them: somebody being removed from a project is
|
|
4340
|
+
* one of the perfectly legitimate reasons their methods are being deleted,
|
|
4341
|
+
* and that must produce an honest "unknown" rather than an exception in
|
|
4342
|
+
* front of an admin doing housekeeping.
|
|
4343
|
+
*/
|
|
4344
|
+
const readinessList = await OnCallReadinessService.getReadinessForUsers([data.userId], data.projectId);
|
|
4345
|
+
const readiness = readinessList[0];
|
|
4346
|
+
const verifiedMethods = ((readiness === null || readiness === void 0 ? void 0 : readiness.methods) || []).filter((method) => {
|
|
4347
|
+
return method.isVerified;
|
|
4348
|
+
});
|
|
4349
|
+
const remainingVerifiedMethods = [
|
|
4350
|
+
...verifiedMethods,
|
|
4351
|
+
];
|
|
4352
|
+
if (data.deletedMethod && data.deletedMethod.isVerified) {
|
|
4353
|
+
/*
|
|
4354
|
+
* Readiness returns one entry per method ROW, and the row being deleted is
|
|
4355
|
+
* identified by its CHANNEL rather than by its id. Removing exactly ONE
|
|
4356
|
+
* entry of that channel is what makes the count right for a user with two
|
|
4357
|
+
* verified SMS numbers who is deleting one of them: the other survives,
|
|
4358
|
+
* and so does the entry.
|
|
4359
|
+
*
|
|
4360
|
+
* ReadinessMethod does now carry methodId, so an exact match is available;
|
|
4361
|
+
* matching on the channel is kept because it is equivalent HERE and not
|
|
4362
|
+
* because the id is missing. The only consumer of this list is
|
|
4363
|
+
* resolveReachability, which reads its LENGTH and its methodTypes and
|
|
4364
|
+
* nothing else, so which of two same-channel entries is dropped cannot
|
|
4365
|
+
* change the answer. Anything added below that cares about a specific row
|
|
4366
|
+
* must match on methodId instead — this equivalence is a property of the
|
|
4367
|
+
* current consumer, not a licence.
|
|
4368
|
+
*/
|
|
4369
|
+
const index = remainingVerifiedMethods.findIndex((method) => {
|
|
4370
|
+
var _a;
|
|
4371
|
+
return method.methodType === ((_a = data.deletedMethod) === null || _a === void 0 ? void 0 : _a.methodType);
|
|
4372
|
+
});
|
|
4373
|
+
if (index >= 0) {
|
|
4374
|
+
remainingVerifiedMethods.splice(index, 1);
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
const reachability = this.resolveReachability(readiness, remainingVerifiedMethods);
|
|
4378
|
+
const project = await ProjectService.findOneById({
|
|
4379
|
+
id: data.projectId,
|
|
4380
|
+
select: {
|
|
4381
|
+
_id: true,
|
|
4382
|
+
disableOnCallNotificationFallback: true,
|
|
4383
|
+
},
|
|
4384
|
+
props: {
|
|
4385
|
+
isRoot: true,
|
|
4386
|
+
},
|
|
4387
|
+
});
|
|
4388
|
+
/*
|
|
4389
|
+
* Read exactly as readiness reads it, including what a missing project row
|
|
4390
|
+
* means. Agreeing with the readiness page matters more here than picking
|
|
4391
|
+
* the louder default independently would: two surfaces that disagree about
|
|
4392
|
+
* whether pages are dropped teach people to trust neither.
|
|
4393
|
+
*/
|
|
4394
|
+
const isFallbackEnabled = !(project === null || project === void 0 ? void 0 : project.disableOnCallNotificationFallback);
|
|
4395
|
+
return {
|
|
4396
|
+
projectId: data.projectId,
|
|
4397
|
+
userId: data.userId,
|
|
4398
|
+
isOnCallResponder: Boolean(readiness && readiness.reachedVia.length > 0),
|
|
4399
|
+
reachedVia: (readiness === null || readiness === void 0 ? void 0 : readiness.reachedVia) || [],
|
|
4400
|
+
rulesDeletedCount: rulesDeletedCount,
|
|
4401
|
+
coverageLost: coverageLost,
|
|
4402
|
+
handoffNotificationsLost: handoffNotificationsLost,
|
|
4403
|
+
reachability: reachability,
|
|
4404
|
+
verifiedMethodCountAfterDeletion: remainingVerifiedMethods.length,
|
|
4405
|
+
isFallbackEnabled: isFallbackEnabled,
|
|
4406
|
+
isTruncated: isTruncated,
|
|
4407
|
+
warnings: this.buildDeletionWarnings({
|
|
4408
|
+
deletedMethod: data.deletedMethod,
|
|
4409
|
+
rulesDeletedCount: rulesDeletedCount,
|
|
4410
|
+
readiness: readiness,
|
|
4411
|
+
reachability: reachability,
|
|
4412
|
+
remainingVerifiedMethods: remainingVerifiedMethods,
|
|
4413
|
+
coverageLost: coverageLost,
|
|
4414
|
+
handoffNotificationsLost: handoffNotificationsLost,
|
|
4415
|
+
isFallbackEnabled: isFallbackEnabled,
|
|
4416
|
+
isTruncated: isTruncated,
|
|
4417
|
+
}),
|
|
4418
|
+
};
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Every notification rule this user has in this project, one page at a time.
|
|
4422
|
+
*
|
|
4423
|
+
* Returns whether the read was TRUNCATED rather than throwing or silently
|
|
4424
|
+
* stopping. A truncated read here is not symmetric in its consequences:
|
|
4425
|
+
* unread rules that would have survived make this over-warn, which costs a
|
|
4426
|
+
* moment of an admin's attention, while unread rules that would have been
|
|
4427
|
+
* DELETED make it under-warn, which is the whole failure this feature exists
|
|
4428
|
+
* to prevent. Either way the caller is told.
|
|
4429
|
+
*
|
|
4430
|
+
* The sort is `_id` ascending because OFFSET paging over a query with no
|
|
4431
|
+
* total order can return one row twice and skip another — and the default
|
|
4432
|
+
* sort would be `createdAt DESC`, which is emphatically not unique for a
|
|
4433
|
+
* user whose default rules were all written in one transaction.
|
|
4434
|
+
*/
|
|
4435
|
+
async readEveryNotificationRuleForUser(data) {
|
|
4436
|
+
let skip = 0;
|
|
4437
|
+
for (let page = 0; page < MAX_DELETION_IMPACT_PAGES; page++) {
|
|
4438
|
+
const rows = await this.findBy({
|
|
4439
|
+
query: {
|
|
4440
|
+
projectId: data.projectId,
|
|
4441
|
+
userId: data.userId,
|
|
4442
|
+
},
|
|
4443
|
+
select: {
|
|
4444
|
+
_id: true,
|
|
4445
|
+
ruleType: true,
|
|
4446
|
+
incidentSeverityId: true,
|
|
4447
|
+
alertSeverityId: true,
|
|
4448
|
+
isOptOut: true,
|
|
4449
|
+
userEmailId: true,
|
|
4450
|
+
userSmsId: true,
|
|
4451
|
+
userCallId: true,
|
|
4452
|
+
userPushId: true,
|
|
4453
|
+
userWhatsAppId: true,
|
|
4454
|
+
userTelegramId: true,
|
|
4455
|
+
userWebhookId: true,
|
|
4456
|
+
},
|
|
4457
|
+
sort: {
|
|
4458
|
+
_id: SortOrder.Ascending,
|
|
4459
|
+
},
|
|
4460
|
+
limit: DELETION_IMPACT_PAGE_SIZE,
|
|
4461
|
+
skip: skip,
|
|
4462
|
+
props: {
|
|
4463
|
+
isRoot: true,
|
|
4464
|
+
},
|
|
4465
|
+
});
|
|
4466
|
+
data.consume(rows);
|
|
4467
|
+
if (rows.length < DELETION_IMPACT_PAGE_SIZE) {
|
|
4468
|
+
return false;
|
|
4469
|
+
}
|
|
4470
|
+
skip += rows.length;
|
|
4471
|
+
}
|
|
4472
|
+
logger.error(`UserNotificationRuleService stopped reading notification rules for user ${data.userId.toString()} in project ${data.projectId.toString()} after ${MAX_DELETION_IMPACT_PAGES} pages of ${DELETION_IMPACT_PAGE_SIZE} rows. The deletion impact for this user is INCOMPLETE and may understate what the deletion removes.`);
|
|
4473
|
+
return true;
|
|
4474
|
+
}
|
|
4475
|
+
/**
|
|
4476
|
+
* Display names for the severities of the cells that are actually lost, in
|
|
4477
|
+
* the project's own severity order.
|
|
4478
|
+
*
|
|
4479
|
+
* Not paged, unlike the rule read: severity lists are a handful of rows per
|
|
4480
|
+
* project by construction (they are a UI-managed enumeration, not user data),
|
|
4481
|
+
* and LIMIT_PER_PROJECT is three orders of magnitude past any of them.
|
|
4482
|
+
*/
|
|
4483
|
+
async loadSeverityNamesForCells(projectId, lostCells) {
|
|
4484
|
+
const severityNames = new Map();
|
|
4485
|
+
const needsIncident = lostCells.some((cell) => {
|
|
4486
|
+
return cell.severityKind === SeverityKind.Incident;
|
|
4487
|
+
});
|
|
4488
|
+
const needsAlert = lostCells.some((cell) => {
|
|
4489
|
+
return cell.severityKind === SeverityKind.Alert;
|
|
4490
|
+
});
|
|
4491
|
+
if (needsIncident) {
|
|
4492
|
+
const incidentSeverities = await IncidentSeverityService.findBy({
|
|
4493
|
+
query: {
|
|
4494
|
+
projectId: projectId,
|
|
4495
|
+
},
|
|
4496
|
+
select: {
|
|
4497
|
+
_id: true,
|
|
4498
|
+
name: true,
|
|
4499
|
+
},
|
|
4500
|
+
sort: {
|
|
4501
|
+
order: SortOrder.Ascending,
|
|
4502
|
+
},
|
|
4503
|
+
limit: LIMIT_PER_PROJECT,
|
|
4504
|
+
skip: 0,
|
|
4505
|
+
props: {
|
|
4506
|
+
isRoot: true,
|
|
4507
|
+
},
|
|
4508
|
+
});
|
|
4509
|
+
incidentSeverities.forEach((severity, index) => {
|
|
4510
|
+
if (!severity.id) {
|
|
4511
|
+
return;
|
|
4512
|
+
}
|
|
4513
|
+
severityNames.set(this.severityNameKey(SeverityKind.Incident, severity.id.toString()), {
|
|
4514
|
+
name: severity.name || "Unnamed Severity",
|
|
4515
|
+
rank: index,
|
|
4516
|
+
});
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
4519
|
+
if (needsAlert) {
|
|
4520
|
+
const alertSeverities = await AlertSeverityService.findBy({
|
|
4521
|
+
query: {
|
|
4522
|
+
projectId: projectId,
|
|
4523
|
+
},
|
|
4524
|
+
select: {
|
|
4525
|
+
_id: true,
|
|
4526
|
+
name: true,
|
|
4527
|
+
},
|
|
4528
|
+
sort: {
|
|
4529
|
+
order: SortOrder.Ascending,
|
|
4530
|
+
},
|
|
4531
|
+
limit: LIMIT_PER_PROJECT,
|
|
4532
|
+
skip: 0,
|
|
4533
|
+
props: {
|
|
4534
|
+
isRoot: true,
|
|
4535
|
+
},
|
|
4536
|
+
});
|
|
4537
|
+
alertSeverities.forEach((severity, index) => {
|
|
4538
|
+
if (!severity.id) {
|
|
4539
|
+
return;
|
|
4540
|
+
}
|
|
4541
|
+
severityNames.set(this.severityNameKey(SeverityKind.Alert, severity.id.toString()), {
|
|
4542
|
+
name: severity.name || "Unnamed Severity",
|
|
4543
|
+
rank: index,
|
|
4544
|
+
});
|
|
4545
|
+
});
|
|
4546
|
+
}
|
|
4547
|
+
return severityNames;
|
|
4548
|
+
}
|
|
4549
|
+
severityNameKey(kind, severityId) {
|
|
4550
|
+
/*
|
|
4551
|
+
* Keyed by KIND as well as by id. Incident and alert severities are
|
|
4552
|
+
* different tables with independently generated ids, and a map keyed on the
|
|
4553
|
+
* id alone would let one name a cell of the other kind if the two ever
|
|
4554
|
+
* collided — a one-in-a-uuid event that would be indistinguishable from a
|
|
4555
|
+
* mislabelled warning if it happened.
|
|
4556
|
+
*/
|
|
4557
|
+
return `${kind}|${severityId}`;
|
|
4558
|
+
}
|
|
4559
|
+
buildCoverageLossCells(lostCells, severityNames) {
|
|
4560
|
+
const ordered = [...lostCells].sort((a, b) => {
|
|
4561
|
+
const ruleTypeDifference = this.pagingRuleTypeRank(a.ruleType) -
|
|
4562
|
+
this.pagingRuleTypeRank(b.ruleType);
|
|
4563
|
+
if (ruleTypeDifference !== 0) {
|
|
4564
|
+
return ruleTypeDifference;
|
|
4565
|
+
}
|
|
4566
|
+
/*
|
|
4567
|
+
* Severity order, not alphabetical. "Sev1, Sev2, Sev3" happens to sort
|
|
4568
|
+
* both ways; "Critical, High, Low" does not, and a warning that lists
|
|
4569
|
+
* severities in an order the user has never seen them in reads as a
|
|
4570
|
+
* different set of severities.
|
|
4571
|
+
*/
|
|
4572
|
+
return (this.severityRank(a, severityNames) -
|
|
4573
|
+
this.severityRank(b, severityNames));
|
|
4574
|
+
});
|
|
4575
|
+
return ordered.map((cell) => {
|
|
4576
|
+
const ref = severityNames.get(this.severityNameKey(cell.severityKind, cell.severityId));
|
|
4577
|
+
return {
|
|
4578
|
+
ruleType: cell.ruleType,
|
|
4579
|
+
severityId: new ObjectID(cell.severityId),
|
|
4580
|
+
severityName: ref === null || ref === void 0 ? void 0 : ref.name,
|
|
4581
|
+
rulesRemoved: cell.rulesRemoved,
|
|
4582
|
+
};
|
|
4583
|
+
});
|
|
4584
|
+
}
|
|
4585
|
+
pagingRuleTypeRank(ruleType) {
|
|
4586
|
+
const index = PAGING_RULE_TYPE_SCOPES.findIndex((scope) => {
|
|
4587
|
+
return scope.ruleType === ruleType;
|
|
4588
|
+
});
|
|
4589
|
+
return index < 0 ? PAGING_RULE_TYPE_SCOPES.length : index;
|
|
4590
|
+
}
|
|
4591
|
+
severityRank(cell, severityNames) {
|
|
4592
|
+
const ref = severityNames.get(this.severityNameKey(cell.severityKind, cell.severityId));
|
|
4593
|
+
/*
|
|
4594
|
+
* A severity we could not name sorts last rather than first. It is the one
|
|
4595
|
+
* entry whose sentence will read "this severity", and burying it under the
|
|
4596
|
+
* ones that read properly costs nothing; leading with it looks like a bug.
|
|
4597
|
+
*/
|
|
4598
|
+
return ref ? ref.rank : Number.MAX_SAFE_INTEGER;
|
|
4599
|
+
}
|
|
4600
|
+
resolveReachability(readiness, remainingVerifiedMethods) {
|
|
4601
|
+
if (!readiness) {
|
|
4602
|
+
return PostDeletionReachability.Unknown;
|
|
4603
|
+
}
|
|
4604
|
+
/*
|
|
4605
|
+
* Checked first, and it is not merely a nicety of wording. Readiness says
|
|
4606
|
+
* NotReachable when the user has no USABLE method — which includes the case
|
|
4607
|
+
* where every verified method they own is on a channel the project has
|
|
4608
|
+
* switched off — so this branch is also what keeps the branches below from
|
|
4609
|
+
* promising "still reachable" on the strength of a verified method that
|
|
4610
|
+
* nothing can send on.
|
|
4611
|
+
*/
|
|
4612
|
+
if (readiness.status === ReadinessStatus.NotReachable) {
|
|
4613
|
+
return PostDeletionReachability.AlreadyNotReachable;
|
|
4614
|
+
}
|
|
4615
|
+
if (remainingVerifiedMethods.length === 0) {
|
|
4616
|
+
return PostDeletionReachability.NotReachable;
|
|
4617
|
+
}
|
|
4618
|
+
const unswitchableChannels = channelsWithNoProjectSwitch();
|
|
4619
|
+
const hasUnswitchableChannel = remainingVerifiedMethods.some((method) => {
|
|
4620
|
+
return unswitchableChannels.includes(method.methodType);
|
|
4621
|
+
});
|
|
4622
|
+
if (hasUnswitchableChannel) {
|
|
4623
|
+
return PostDeletionReachability.Reachable;
|
|
4624
|
+
}
|
|
4625
|
+
return PostDeletionReachability.DependsOnProjectSettings;
|
|
4626
|
+
}
|
|
4627
|
+
/**
|
|
4628
|
+
* The sentences a human reads in the confirmation dialog, most consequential
|
|
4629
|
+
* first.
|
|
4630
|
+
*
|
|
4631
|
+
* Every line names a specific thing that is lost AND what happens because of
|
|
4632
|
+
* it, which is the same contract OnCallReadinessService.buildReasons keeps
|
|
4633
|
+
* and for the same reason: "no rule for Sev4" is a shrug, "no rule for Sev4,
|
|
4634
|
+
* and those pages are dropped" is a decision. Nothing here says "are you
|
|
4635
|
+
* sure?" — the caller owns the question, and this owns the facts it is asked
|
|
4636
|
+
* about.
|
|
4637
|
+
*/
|
|
4638
|
+
buildDeletionWarnings(data) {
|
|
4639
|
+
const warnings = [];
|
|
4640
|
+
/*
|
|
4641
|
+
* The cascade goes first because it is the part nobody clicked. Everything
|
|
4642
|
+
* below is a consequence of it, and a list that started with the
|
|
4643
|
+
* consequences would read as though the notification rules were being
|
|
4644
|
+
* deleted for no reason at all.
|
|
4645
|
+
*/
|
|
4646
|
+
if (data.deletedMethod && data.rulesDeletedCount > 0) {
|
|
4647
|
+
warnings.push(`Deleting this ${data.deletedMethod.methodType} notification method also deletes ${data.rulesDeletedCount} notification ${data.rulesDeletedCount === 1 ? "rule" : "rules"} that use it - a notification rule cannot outlive the method it sends on.`);
|
|
4648
|
+
}
|
|
4649
|
+
if (data.reachability === PostDeletionReachability.NotReachable) {
|
|
4650
|
+
warnings.push("This is the last verified notification method on this account - after it there is nothing left to page this user on, and the on-call fallback has nothing to fall back to either.");
|
|
4651
|
+
}
|
|
4652
|
+
if (!data.readiness) {
|
|
4653
|
+
warnings.push("Whether this user is on call could not be determined - they may no longer be a member of this project.");
|
|
4654
|
+
}
|
|
4655
|
+
else if (data.readiness.reachedVia.length > 0) {
|
|
4656
|
+
const sources = data.readiness.reachedVia.map((source) => {
|
|
4657
|
+
return responderSourceProse(source);
|
|
4658
|
+
});
|
|
4659
|
+
warnings.push(`This user is on call in this project (${sources.join(", ")}), so anything lost here is a page that does not arrive.`);
|
|
4660
|
+
}
|
|
4661
|
+
else {
|
|
4662
|
+
/*
|
|
4663
|
+
* Said out loud rather than left as silence. "Not on call" is the one
|
|
4664
|
+
* answer that makes this whole dialog safe to click through, and an admin
|
|
4665
|
+
* who has to infer it from the absence of a warning will infer it wrongly
|
|
4666
|
+
* at least once.
|
|
4667
|
+
*/
|
|
4668
|
+
warnings.push("This user is not on any on-call policy right now, so nothing here can cost a page today - it will if they are ever added to one.");
|
|
4669
|
+
}
|
|
4670
|
+
for (const scope of PAGING_RULE_TYPE_SCOPES) {
|
|
4671
|
+
const severityNames = data.coverageLost
|
|
4672
|
+
.filter((cell) => {
|
|
4673
|
+
return cell.ruleType === scope.ruleType;
|
|
4674
|
+
})
|
|
4675
|
+
.map((cell) => {
|
|
4676
|
+
return cell.severityName || "this severity";
|
|
4677
|
+
});
|
|
4678
|
+
if (severityNames.length === 0) {
|
|
4679
|
+
continue;
|
|
4680
|
+
}
|
|
4681
|
+
/*
|
|
4682
|
+
* One sentence per rule type listing its severities, not one per cell. A
|
|
4683
|
+
* user deleting a method that carried all their default rules would
|
|
4684
|
+
* otherwise be handed one line per (rule type x severity) — sixteen
|
|
4685
|
+
* near-identical sentences that nobody reads to the end of.
|
|
4686
|
+
*/
|
|
4687
|
+
const subject = `After this, no rule covers ${severityNames.join(", ")} ${scope.subjectNoun}`;
|
|
4688
|
+
if (data.isFallbackEnabled) {
|
|
4689
|
+
warnings.push(`${subject} - those pages fall back to whatever this user has verified, which is not what they configured`);
|
|
4690
|
+
}
|
|
4691
|
+
else {
|
|
4692
|
+
warnings.push(`${subject} - those pages are dropped, because on-call fallback is disabled for this project`);
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
if (data.handoffNotificationsLost.includes(NotificationRuleType.WHEN_USER_GOES_ON_CALL)) {
|
|
4696
|
+
warnings.push("This user will no longer be told when they go on call.");
|
|
4697
|
+
}
|
|
4698
|
+
if (data.handoffNotificationsLost.includes(NotificationRuleType.WHEN_USER_GOES_OFF_CALL)) {
|
|
4699
|
+
warnings.push("This user will no longer be told when they go off call.");
|
|
4700
|
+
}
|
|
4701
|
+
if (data.reachability === PostDeletionReachability.AlreadyNotReachable) {
|
|
4702
|
+
warnings.push("This user already has no usable notification method, so nothing can page them today either - this deletion is not what breaks it.");
|
|
4703
|
+
}
|
|
4704
|
+
if (data.reachability === PostDeletionReachability.DependsOnProjectSettings) {
|
|
4705
|
+
const channels = [];
|
|
4706
|
+
for (const method of data.remainingVerifiedMethods) {
|
|
4707
|
+
if (!channels.includes(method.methodType)) {
|
|
4708
|
+
channels.push(method.methodType);
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
warnings.push(`Every verified method left on this account is on a channel the project can switch off (${channels.join(", ")}) - check On-Call > Readiness to confirm this user can still be paged.`);
|
|
4712
|
+
}
|
|
4713
|
+
if (data.isTruncated) {
|
|
4714
|
+
warnings.push("This preview is incomplete - there are more notification rules on this account than it could read, so the real loss may be larger than what is listed here.");
|
|
4715
|
+
}
|
|
4716
|
+
return warnings;
|
|
4717
|
+
}
|
|
2150
4718
|
}
|
|
2151
4719
|
__decorate([
|
|
2152
4720
|
CaptureSpan(),
|
|
@@ -2154,6 +4722,12 @@ __decorate([
|
|
|
2154
4722
|
__metadata("design:paramtypes", [ObjectID, Object]),
|
|
2155
4723
|
__metadata("design:returntype", Promise)
|
|
2156
4724
|
], Service.prototype, "executeNotificationRuleItem", null);
|
|
4725
|
+
__decorate([
|
|
4726
|
+
CaptureSpan(),
|
|
4727
|
+
__metadata("design:type", Function),
|
|
4728
|
+
__metadata("design:paramtypes", [Object]),
|
|
4729
|
+
__metadata("design:returntype", Promise)
|
|
4730
|
+
], Service.prototype, "executeFallbackNotification", null);
|
|
2157
4731
|
__decorate([
|
|
2158
4732
|
CaptureSpan(),
|
|
2159
4733
|
__metadata("design:type", Function),
|
|
@@ -2178,6 +4752,14 @@ __decorate([
|
|
|
2178
4752
|
ObjectID]),
|
|
2179
4753
|
__metadata("design:returntype", Promise)
|
|
2180
4754
|
], Service.prototype, "generateCallTemplateForAlertEpisodeCreated", null);
|
|
4755
|
+
__decorate([
|
|
4756
|
+
CaptureSpan(),
|
|
4757
|
+
__metadata("design:type", Function),
|
|
4758
|
+
__metadata("design:paramtypes", [Phone,
|
|
4759
|
+
IncidentEpisode,
|
|
4760
|
+
ObjectID]),
|
|
4761
|
+
__metadata("design:returntype", Promise)
|
|
4762
|
+
], Service.prototype, "generateCallTemplateForIncidentEpisodeCreated", null);
|
|
2181
4763
|
__decorate([
|
|
2182
4764
|
CaptureSpan(),
|
|
2183
4765
|
__metadata("design:type", Function),
|
|
@@ -2202,6 +4784,14 @@ __decorate([
|
|
|
2202
4784
|
ObjectID]),
|
|
2203
4785
|
__metadata("design:returntype", Promise)
|
|
2204
4786
|
], Service.prototype, "generateSmsTemplateForAlertEpisodeCreated", null);
|
|
4787
|
+
__decorate([
|
|
4788
|
+
CaptureSpan(),
|
|
4789
|
+
__metadata("design:type", Function),
|
|
4790
|
+
__metadata("design:paramtypes", [Phone,
|
|
4791
|
+
IncidentEpisode,
|
|
4792
|
+
ObjectID]),
|
|
4793
|
+
__metadata("design:returntype", Promise)
|
|
4794
|
+
], Service.prototype, "generateSmsTemplateForIncidentEpisodeCreated", null);
|
|
2205
4795
|
__decorate([
|
|
2206
4796
|
CaptureSpan(),
|
|
2207
4797
|
__metadata("design:type", Function),
|
|
@@ -2223,6 +4813,13 @@ __decorate([
|
|
|
2223
4813
|
ObjectID]),
|
|
2224
4814
|
__metadata("design:returntype", Promise)
|
|
2225
4815
|
], Service.prototype, "generateTelegramBodyForAlertEpisodeCreated", null);
|
|
4816
|
+
__decorate([
|
|
4817
|
+
CaptureSpan(),
|
|
4818
|
+
__metadata("design:type", Function),
|
|
4819
|
+
__metadata("design:paramtypes", [IncidentEpisode,
|
|
4820
|
+
ObjectID]),
|
|
4821
|
+
__metadata("design:returntype", Promise)
|
|
4822
|
+
], Service.prototype, "generateTelegramBodyForIncidentEpisodeCreated", null);
|
|
2226
4823
|
__decorate([
|
|
2227
4824
|
CaptureSpan(),
|
|
2228
4825
|
__metadata("design:type", Function),
|
|
@@ -2247,6 +4844,14 @@ __decorate([
|
|
|
2247
4844
|
ObjectID]),
|
|
2248
4845
|
__metadata("design:returntype", Promise)
|
|
2249
4846
|
], Service.prototype, "generateWhatsAppTemplateForAlertEpisodeCreated", null);
|
|
4847
|
+
__decorate([
|
|
4848
|
+
CaptureSpan(),
|
|
4849
|
+
__metadata("design:type", Function),
|
|
4850
|
+
__metadata("design:paramtypes", [Phone,
|
|
4851
|
+
IncidentEpisode,
|
|
4852
|
+
ObjectID]),
|
|
4853
|
+
__metadata("design:returntype", Promise)
|
|
4854
|
+
], Service.prototype, "generateWhatsAppTemplateForIncidentEpisodeCreated", null);
|
|
2250
4855
|
__decorate([
|
|
2251
4856
|
CaptureSpan(),
|
|
2252
4857
|
__metadata("design:type", Function),
|
|
@@ -2271,6 +4876,14 @@ __decorate([
|
|
|
2271
4876
|
ObjectID]),
|
|
2272
4877
|
__metadata("design:returntype", Promise)
|
|
2273
4878
|
], Service.prototype, "generateEmailTemplateForAlertEpisodeCreated", null);
|
|
4879
|
+
__decorate([
|
|
4880
|
+
CaptureSpan(),
|
|
4881
|
+
__metadata("design:type", Function),
|
|
4882
|
+
__metadata("design:paramtypes", [Email,
|
|
4883
|
+
IncidentEpisode,
|
|
4884
|
+
ObjectID]),
|
|
4885
|
+
__metadata("design:returntype", Promise)
|
|
4886
|
+
], Service.prototype, "generateEmailTemplateForIncidentEpisodeCreated", null);
|
|
2274
4887
|
__decorate([
|
|
2275
4888
|
CaptureSpan(),
|
|
2276
4889
|
__metadata("design:type", Function),
|
|
@@ -2289,6 +4902,36 @@ __decorate([
|
|
|
2289
4902
|
__metadata("design:paramtypes", [Object]),
|
|
2290
4903
|
__metadata("design:returntype", Promise)
|
|
2291
4904
|
], Service.prototype, "onBeforeCreate", null);
|
|
4905
|
+
__decorate([
|
|
4906
|
+
CaptureSpan(),
|
|
4907
|
+
__metadata("design:type", Function),
|
|
4908
|
+
__metadata("design:paramtypes", [Object, Model]),
|
|
4909
|
+
__metadata("design:returntype", Promise)
|
|
4910
|
+
], Service.prototype, "onCreateSuccess", null);
|
|
4911
|
+
__decorate([
|
|
4912
|
+
CaptureSpan(),
|
|
4913
|
+
__metadata("design:type", Function),
|
|
4914
|
+
__metadata("design:paramtypes", [Object]),
|
|
4915
|
+
__metadata("design:returntype", Promise)
|
|
4916
|
+
], Service.prototype, "onBeforeUpdate", null);
|
|
4917
|
+
__decorate([
|
|
4918
|
+
CaptureSpan(),
|
|
4919
|
+
__metadata("design:type", Function),
|
|
4920
|
+
__metadata("design:paramtypes", [Object, Array]),
|
|
4921
|
+
__metadata("design:returntype", Promise)
|
|
4922
|
+
], Service.prototype, "onUpdateSuccess", null);
|
|
4923
|
+
__decorate([
|
|
4924
|
+
CaptureSpan(),
|
|
4925
|
+
__metadata("design:type", Function),
|
|
4926
|
+
__metadata("design:paramtypes", [Object]),
|
|
4927
|
+
__metadata("design:returntype", Promise)
|
|
4928
|
+
], Service.prototype, "onBeforeDelete", null);
|
|
4929
|
+
__decorate([
|
|
4930
|
+
CaptureSpan(),
|
|
4931
|
+
__metadata("design:type", Function),
|
|
4932
|
+
__metadata("design:paramtypes", [Object, Array]),
|
|
4933
|
+
__metadata("design:returntype", Promise)
|
|
4934
|
+
], Service.prototype, "onDeleteSuccess", null);
|
|
2292
4935
|
__decorate([
|
|
2293
4936
|
CaptureSpan(),
|
|
2294
4937
|
__metadata("design:type", Function),
|
|
@@ -2303,5 +4946,17 @@ __decorate([
|
|
|
2303
4946
|
Email]),
|
|
2304
4947
|
__metadata("design:returntype", Promise)
|
|
2305
4948
|
], Service.prototype, "addDefaultNotificationRuleForUser", null);
|
|
4949
|
+
__decorate([
|
|
4950
|
+
CaptureSpan(),
|
|
4951
|
+
__metadata("design:type", Function),
|
|
4952
|
+
__metadata("design:paramtypes", [Object]),
|
|
4953
|
+
__metadata("design:returntype", Promise)
|
|
4954
|
+
], Service.prototype, "getRuleDeletionImpact", null);
|
|
4955
|
+
__decorate([
|
|
4956
|
+
CaptureSpan(),
|
|
4957
|
+
__metadata("design:type", Function),
|
|
4958
|
+
__metadata("design:paramtypes", [Object]),
|
|
4959
|
+
__metadata("design:returntype", Promise)
|
|
4960
|
+
], Service.prototype, "getNotificationMethodDeletionImpact", null);
|
|
2306
4961
|
export default new Service();
|
|
2307
4962
|
//# sourceMappingURL=UserNotificationRuleService.js.map
|