@oneuptime/common 12.0.28 → 12.0.30
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/DatabaseModels/DashboardDomain.ts +43 -0
- package/Models/DatabaseModels/Index.ts +11 -0
- package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
- package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
- package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
- package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
- package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
- package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
- package/Server/API/DashboardDomainAPI.ts +74 -0
- package/Server/API/MicrosoftTeamsAPI.ts +81 -180
- package/Server/API/OnCallCalendarAPI.ts +2194 -0
- package/Server/API/StatusPageDomainAPI.ts +74 -0
- package/Server/EnvironmentConfig.ts +138 -0
- package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
- package/Server/Middleware/ProjectAuthorization.ts +6 -1
- package/Server/Services/DashboardDomainService.ts +131 -0
- package/Server/Services/Index.ts +12 -0
- package/Server/Services/LogPipelineProcessorService.ts +88 -0
- package/Server/Services/MonitorTemplateService.ts +70 -11
- package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
- package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
- package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
- package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
- package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
- package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
- package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
- package/Server/Services/StatusPageDomainService.ts +163 -3
- package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
- package/Server/Services/TeamMemberService.ts +489 -1
- package/Server/Services/TeamService.ts +50 -0
- package/Server/Services/UserNotificationSettingService.ts +44 -1
- package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
- package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
- package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
- package/Server/Types/Domain.ts +17 -0
- package/Server/Utils/APIKey/AccessPermission.ts +24 -0
- package/Server/Utils/Greenlock/Greenlock.ts +156 -58
- package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
- package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
- package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
- package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
- package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
- package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
- package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
- package/Server/Utils/Response.ts +134 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
- package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
- package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
- package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
- package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
- package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
- package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
- package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
- package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
- package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
- package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
- package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
- package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
- package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
- package/Tests/Server/API/Helpers.ts +8 -0
- package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
- package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
- package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
- package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
- package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
- package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
- package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
- package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
- package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
- package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
- package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
- package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
- package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
- package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
- package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
- package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
- package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
- package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
- package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
- package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
- package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
- package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
- package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
- package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
- package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
- package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
- package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
- package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
- package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
- package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
- package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
- package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
- package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
- package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
- package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
- package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
- package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
- package/Tests/Types/API/HTTPResponse.test.ts +175 -0
- package/Tests/Types/API/Route.test.ts +36 -4
- package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
- package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
- package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
- package/Tests/Types/DateLocalTimeString.test.ts +259 -0
- package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
- package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
- package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
- package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
- package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
- package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
- package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
- package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
- package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
- package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
- package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
- package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
- package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/UI/Utils/Navigation.test.ts +82 -0
- package/Tests/Utils/Array.test.ts +100 -0
- package/Tests/Utils/CertificateReissue.test.ts +225 -0
- package/Tests/Utils/Grok/Grok.test.ts +492 -0
- package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
- package/Types/API/Route.ts +20 -5
- package/Types/Calendar/ICalendar.ts +442 -0
- package/Types/Date.ts +143 -13
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
- package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
- package/Types/OnCallDutyPolicy/Layer.ts +190 -35
- package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
- package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
- package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
- package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
- package/Types/Rum/SessionReplay.ts +62 -1
- package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
- package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
- package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
- package/UI/Utils/Navigation.ts +42 -0
- package/Utils/Array.ts +37 -0
- package/Utils/CertificateReissue.ts +128 -0
- package/Utils/Grok/Grok.ts +461 -0
- package/Utils/Grok/GrokPatterns.ts +118 -0
- package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +11 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
- package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
- package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
- package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
- package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
- package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
- package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
- package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
- package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
- package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +102 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
- package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
- package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
- package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
- package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
- package/build/dist/Server/Services/DashboardDomainService.js +104 -0
- package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +12 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
- package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
- package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
- package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
- package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
- package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
- package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +396 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/TeamService.js +43 -0
- package/build/dist/Server/Services/TeamService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
- package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
- package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
- package/build/dist/Server/Types/Domain.js +16 -0
- package/build/dist/Server/Types/Domain.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
- package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
- package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
- package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
- package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
- package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
- package/build/dist/Server/Utils/Response.js +97 -0
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +9 -1
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
- package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Route.js +16 -5
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/Calendar/ICalendar.js +290 -0
- package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
- package/build/dist/Types/Date.js +115 -13
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
- package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
- package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
- package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
- package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
- package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
- package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
- package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
- package/build/dist/Types/Rum/SessionReplay.js +38 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
- package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +30 -0
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/Utils/Array.js +25 -0
- package/build/dist/Utils/Array.js.map +1 -1
- package/build/dist/Utils/CertificateReissue.js +97 -0
- package/build/dist/Utils/CertificateReissue.js.map +1 -0
- package/build/dist/Utils/Grok/Grok.js +324 -0
- package/build/dist/Utils/Grok/Grok.js.map +1 -0
- package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
- package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
- package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
- package/package.json +1 -1
- package/UI/Utils/JsonWebToken.ts +0 -14
- package/build/dist/UI/Utils/JsonWebToken.js +0 -10
- package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
|
@@ -7,7 +7,10 @@ import CalendarEvent from "../../Types/Calendar/CalendarEvent";
|
|
|
7
7
|
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
|
8
8
|
import OneUptimeDate from "../../Types/Date";
|
|
9
9
|
import ObjectID from "../../Types/ObjectID";
|
|
10
|
-
import LayerUtil, {
|
|
10
|
+
import LayerUtil, {
|
|
11
|
+
LayerEventsResult,
|
|
12
|
+
LayerProps,
|
|
13
|
+
} from "../../Types/OnCallDutyPolicy/Layer";
|
|
11
14
|
import { RestrictionType } from "../../Types/OnCallDutyPolicy/RestrictionTimes";
|
|
12
15
|
import Recurring from "../../Types/Events/Recurring";
|
|
13
16
|
import UserOverrideUtil, {
|
|
@@ -44,11 +47,115 @@ import OnCallDutyPolicyTimeLogService from "./OnCallDutyPolicyTimeLogService";
|
|
|
44
47
|
import OnCallDutyPolicyScheduleLabelRuleEngineService from "./OnCallDutyPolicyScheduleLabelRuleEngineService";
|
|
45
48
|
import OnCallDutyPolicyScheduleOwnerRuleEngineService from "./OnCallDutyPolicyScheduleOwnerRuleEngineService";
|
|
46
49
|
import DeleteBy from "../Types/Database/DeleteBy";
|
|
47
|
-
import { OnCreate, OnDelete } from "../Types/Database/Hooks";
|
|
50
|
+
import { OnCreate, OnDelete, OnUpdate } from "../Types/Database/Hooks";
|
|
48
51
|
import PushNotificationMessage from "../../Types/PushNotification/PushNotificationMessage";
|
|
49
52
|
import PushNotificationUtil from "../Utils/PushNotificationUtil";
|
|
50
53
|
import { createWhatsAppMessageFromTemplate } from "../Utils/WhatsAppTemplateUtil";
|
|
51
54
|
import { WhatsAppMessagePayload } from "../../Types/WhatsApp/WhatsAppMessage";
|
|
55
|
+
import { MaterializedShiftPolicy } from "../../Types/OnCallDutyPolicy/MaterializedShift";
|
|
56
|
+
import OnCallCalendarFeedCache from "../Infrastructure/OnCallCalendarFeedCache";
|
|
57
|
+
import OnCallShiftChangeListeners, {
|
|
58
|
+
OnCallShiftChangeEvent,
|
|
59
|
+
OnCallShiftChangeReason,
|
|
60
|
+
} from "../Utils/OnCall/OnCallShiftChangeListeners";
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* ---------------------------------------------------------------------------
|
|
64
|
+
* Resolver contracts (on-call calendar feeds / reminders / my-shifts)
|
|
65
|
+
* ---------------------------------------------------------------------------
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
// The schedule columns a resolved window carries along for its consumers.
|
|
69
|
+
export interface ResolvedScheduleInfo {
|
|
70
|
+
id: string;
|
|
71
|
+
name: string;
|
|
72
|
+
// IANA zone; absent for legacy schedules (engine ran in the server's zone).
|
|
73
|
+
timezone?: string;
|
|
74
|
+
projectId: string;
|
|
75
|
+
shiftConfigVersion: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* Resolution of the same base events in ONE escalation policy's context.
|
|
80
|
+
* Only produced when the schedule is attached to two or more distinct policies
|
|
81
|
+
* and a policy-scoped override for a schedule member overlaps the window — the
|
|
82
|
+
* situation in which paging (which always resolves with the escalating policy)
|
|
83
|
+
* and the policy-agnostic roster can name different people.
|
|
84
|
+
*/
|
|
85
|
+
export interface ResolvedPolicyVariant {
|
|
86
|
+
policyId: string;
|
|
87
|
+
policyName: string;
|
|
88
|
+
segments: Array<CalendarEvent>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ResolvedShiftSegments {
|
|
92
|
+
schedule: ResolvedScheduleInfo;
|
|
93
|
+
/*
|
|
94
|
+
* Resolved coverage segments (CalendarEvent.title = user id) that OVERLAP
|
|
95
|
+
* [windowStart, windowEnd). Deliberately unclipped: a segment that started
|
|
96
|
+
* before the window keeps its true start, because that start is the
|
|
97
|
+
* calendar event's identity.
|
|
98
|
+
*/
|
|
99
|
+
segments: Array<CalendarEvent>;
|
|
100
|
+
policyVariants: Array<ResolvedPolicyVariant>;
|
|
101
|
+
// Distinct (policy, rule) attachments, ordered by policy then rule order.
|
|
102
|
+
attachedPolicies: Array<MaterializedShiftPolicy>;
|
|
103
|
+
// The layers as fed to the engine (for the coverage envelope).
|
|
104
|
+
layerProps: Array<LayerProps>;
|
|
105
|
+
// Distinct user ids across all layers, in layer order.
|
|
106
|
+
scheduleUserIds: Array<string>;
|
|
107
|
+
/*
|
|
108
|
+
* Every override that took part in the resolution (global ones plus, in a
|
|
109
|
+
* policy context, that policy's), so a consumer can map a segment's
|
|
110
|
+
* override meta back to its policy scope.
|
|
111
|
+
*/
|
|
112
|
+
overrides: Array<UserOverrideRecord>;
|
|
113
|
+
/*
|
|
114
|
+
* max(updatedAt) over layers, layer users, participating overrides and
|
|
115
|
+
* policy attachments. The schedule row is EXCLUDED on purpose: its
|
|
116
|
+
* updatedAt moves on every roster refresh, which would make every feed
|
|
117
|
+
* fetch look like a change.
|
|
118
|
+
*/
|
|
119
|
+
lastModifiedAt: Date;
|
|
120
|
+
// True when the engine hit its iteration cap; segments may be incomplete.
|
|
121
|
+
truncated: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ResolveShiftSegmentsOptions {
|
|
125
|
+
scheduleId: ObjectID;
|
|
126
|
+
windowStart: Date;
|
|
127
|
+
windowEnd: Date;
|
|
128
|
+
maxSimulationIterations?: number | undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface ResolveShiftSegmentsForSchedulesOptions {
|
|
132
|
+
scheduleIds: Array<ObjectID>;
|
|
133
|
+
windowStart: Date;
|
|
134
|
+
windowEnd: Date;
|
|
135
|
+
maxSimulationIterations?: number | undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/*
|
|
139
|
+
* What a CRUD hook reports when an edit may have changed who is on call and
|
|
140
|
+
* when. See propagateShiftConfigChange.
|
|
141
|
+
*/
|
|
142
|
+
export interface ShiftConfigChange {
|
|
143
|
+
scheduleIds: Array<ObjectID>;
|
|
144
|
+
projectId?: ObjectID | null | undefined;
|
|
145
|
+
// Users named directly by the change (added/removed/overridden/substitute).
|
|
146
|
+
userIds?: Array<ObjectID> | undefined;
|
|
147
|
+
reason: OnCallShiftChangeReason;
|
|
148
|
+
// The schedule rows are gone (delete) — nothing to bump.
|
|
149
|
+
skipVersionBump?: boolean | undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Rows loaded once for a batch of schedules, grouped per schedule.
|
|
153
|
+
interface ScheduleResolutionInputs {
|
|
154
|
+
schedule: OnCallDutyPolicySchedule;
|
|
155
|
+
layers: Array<OnCallDutyPolicyScheduleLayer>;
|
|
156
|
+
layerUsers: Array<OnCallDutyPolicyScheduleLayerUser>;
|
|
157
|
+
attachments: Array<OnCallDutyPolicyEscalationRuleSchedule>;
|
|
158
|
+
}
|
|
52
159
|
|
|
53
160
|
export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
54
161
|
private layerUtil = new LayerUtil();
|
|
@@ -114,12 +221,185 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
114
221
|
});
|
|
115
222
|
}
|
|
116
223
|
|
|
224
|
+
/*
|
|
225
|
+
* Capture the members of each schedule being deleted. Their layer-user
|
|
226
|
+
* rows cascade away at the database level (no hooks run), so this is the
|
|
227
|
+
* last moment the ids are available; onDeleteSuccess uses them to purge
|
|
228
|
+
* those users' calendar-feed bodies and to tell the shift-change
|
|
229
|
+
* listeners whose shift lists just changed. Best-effort.
|
|
230
|
+
*/
|
|
231
|
+
const deletedSchedules: Array<{
|
|
232
|
+
scheduleId: ObjectID;
|
|
233
|
+
projectId: ObjectID | null;
|
|
234
|
+
userIds: Array<ObjectID>;
|
|
235
|
+
}> = [];
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const scheduleIds: Array<ObjectID> = callSchedules
|
|
239
|
+
.map((schedule: OnCallDutyPolicySchedule) => {
|
|
240
|
+
return schedule.id;
|
|
241
|
+
})
|
|
242
|
+
.filter((id: ObjectID | null | undefined): id is ObjectID => {
|
|
243
|
+
return Boolean(id);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const layerUsers: Array<OnCallDutyPolicyScheduleLayerUser> =
|
|
247
|
+
scheduleIds.length > 0
|
|
248
|
+
? await OnCallDutyPolicyScheduleLayerUserService.findBy({
|
|
249
|
+
query: {
|
|
250
|
+
onCallDutyPolicyScheduleId: QueryHelper.any(scheduleIds),
|
|
251
|
+
},
|
|
252
|
+
select: {
|
|
253
|
+
userId: true,
|
|
254
|
+
onCallDutyPolicyScheduleId: true,
|
|
255
|
+
},
|
|
256
|
+
limit: LIMIT_PER_PROJECT,
|
|
257
|
+
skip: 0,
|
|
258
|
+
props: {
|
|
259
|
+
isRoot: true,
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
: [];
|
|
263
|
+
|
|
264
|
+
for (const schedule of callSchedules) {
|
|
265
|
+
if (!schedule.id) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const scheduleIdString: string = schedule.id.toString();
|
|
270
|
+
|
|
271
|
+
deletedSchedules.push({
|
|
272
|
+
scheduleId: schedule.id,
|
|
273
|
+
projectId: schedule.projectId || null,
|
|
274
|
+
userIds: OnCallShiftChangeListeners.dedupe(
|
|
275
|
+
layerUsers
|
|
276
|
+
.filter((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
277
|
+
return (
|
|
278
|
+
layerUser.onCallDutyPolicyScheduleId?.toString() ===
|
|
279
|
+
scheduleIdString
|
|
280
|
+
);
|
|
281
|
+
})
|
|
282
|
+
.map((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
283
|
+
return layerUser.userId;
|
|
284
|
+
})
|
|
285
|
+
.filter((id: ObjectID | undefined): id is ObjectID => {
|
|
286
|
+
return Boolean(id);
|
|
287
|
+
}),
|
|
288
|
+
),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
} catch (err) {
|
|
292
|
+
logger.error(
|
|
293
|
+
"Error capturing schedule members before delete (best-effort).",
|
|
294
|
+
);
|
|
295
|
+
logger.error(err);
|
|
296
|
+
}
|
|
297
|
+
|
|
117
298
|
return {
|
|
118
299
|
deleteBy: deleteBy,
|
|
119
|
-
carryForward: {},
|
|
300
|
+
carryForward: { deletedSchedules },
|
|
120
301
|
};
|
|
121
302
|
}
|
|
122
303
|
|
|
304
|
+
/*
|
|
305
|
+
* A deleted schedule takes its shared feed with it (FK cascade) and drops
|
|
306
|
+
* out of its members' personal feeds; purge what was rendered from it and
|
|
307
|
+
* let the shift-change listeners (reminders) re-plan for those users. No
|
|
308
|
+
* version bump — the row is gone.
|
|
309
|
+
*/
|
|
310
|
+
protected override async onDeleteSuccess(
|
|
311
|
+
onDelete: OnDelete<OnCallDutyPolicySchedule>,
|
|
312
|
+
_itemIdsBeforeDelete: Array<ObjectID>,
|
|
313
|
+
): Promise<OnDelete<OnCallDutyPolicySchedule>> {
|
|
314
|
+
const deletedSchedules: Array<{
|
|
315
|
+
scheduleId: ObjectID;
|
|
316
|
+
projectId: ObjectID | null;
|
|
317
|
+
userIds: Array<ObjectID>;
|
|
318
|
+
}> = onDelete.carryForward?.deletedSchedules || [];
|
|
319
|
+
|
|
320
|
+
for (const deleted of deletedSchedules) {
|
|
321
|
+
await this.propagateShiftConfigChange({
|
|
322
|
+
scheduleIds: [deleted.scheduleId],
|
|
323
|
+
projectId: deleted.projectId,
|
|
324
|
+
userIds: deleted.userIds,
|
|
325
|
+
reason: OnCallShiftChangeReason.ScheduleDeleted,
|
|
326
|
+
skipVersionBump: true,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return onDelete;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/*
|
|
334
|
+
* A rename or a timezone change alters every calendar event of the
|
|
335
|
+
* schedule (its title, and for a timezone change the instants themselves),
|
|
336
|
+
* so it must bump the schedule's shiftConfigVersion like any other
|
|
337
|
+
* configuration edit. The roster refresh writes through updateOneById with
|
|
338
|
+
* ignoreHooks, and the version bump itself is a raw statement, so neither
|
|
339
|
+
* re-enters this hook.
|
|
340
|
+
*/
|
|
341
|
+
protected override async onUpdateSuccess(
|
|
342
|
+
onUpdate: OnUpdate<OnCallDutyPolicySchedule>,
|
|
343
|
+
updatedItemIds: Array<ObjectID>,
|
|
344
|
+
): Promise<OnUpdate<OnCallDutyPolicySchedule>> {
|
|
345
|
+
const data: Record<string, unknown> = (onUpdate.updateBy?.data ||
|
|
346
|
+
{}) as unknown as Record<string, unknown>;
|
|
347
|
+
|
|
348
|
+
const touchesShiftConfiguration: boolean =
|
|
349
|
+
data["name"] !== undefined || data["timezone"] !== undefined;
|
|
350
|
+
|
|
351
|
+
if (!touchesShiftConfiguration || updatedItemIds.length === 0) {
|
|
352
|
+
return onUpdate;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const schedules: Array<OnCallDutyPolicySchedule> = await this.findBy({
|
|
357
|
+
query: {
|
|
358
|
+
_id: QueryHelper.any(updatedItemIds),
|
|
359
|
+
},
|
|
360
|
+
select: {
|
|
361
|
+
_id: true,
|
|
362
|
+
projectId: true,
|
|
363
|
+
},
|
|
364
|
+
limit: LIMIT_PER_PROJECT,
|
|
365
|
+
skip: 0,
|
|
366
|
+
props: {
|
|
367
|
+
isRoot: true,
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const byProject: Map<string, Array<ObjectID>> = new Map<
|
|
372
|
+
string,
|
|
373
|
+
Array<ObjectID>
|
|
374
|
+
>();
|
|
375
|
+
|
|
376
|
+
for (const schedule of schedules) {
|
|
377
|
+
if (!schedule.id) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const projectKey: string = schedule.projectId?.toString() || "";
|
|
381
|
+
const list: Array<ObjectID> = byProject.get(projectKey) || [];
|
|
382
|
+
list.push(schedule.id);
|
|
383
|
+
byProject.set(projectKey, list);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
for (const [projectKey, scheduleIds] of byProject) {
|
|
387
|
+
await this.propagateShiftConfigChange({
|
|
388
|
+
scheduleIds,
|
|
389
|
+
projectId: projectKey ? new ObjectID(projectKey) : null,
|
|
390
|
+
reason: OnCallShiftChangeReason.ScheduleChanged,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.error(
|
|
395
|
+
"Error propagating a schedule rename / timezone change to the calendar feeds (best-effort).",
|
|
396
|
+
);
|
|
397
|
+
logger.error(err);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return onUpdate;
|
|
401
|
+
}
|
|
402
|
+
|
|
123
403
|
public async getOnCallSchedulesWhereUserIsOnCallDuty(data: {
|
|
124
404
|
projectId: ObjectID;
|
|
125
405
|
userId: ObjectID;
|
|
@@ -1321,12 +1601,38 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
1321
1601
|
const scheduleTimezone: string | undefined =
|
|
1322
1602
|
schedule?.timezone?.toString() || undefined;
|
|
1323
1603
|
|
|
1604
|
+
const { layerProps, scheduleUserIds } = this.buildLayerPropsFromRows({
|
|
1605
|
+
layers,
|
|
1606
|
+
layerUsers,
|
|
1607
|
+
scheduleTimezone,
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
const projectId: ObjectID | null = layers[0]?.projectId || null;
|
|
1611
|
+
|
|
1612
|
+
return { layerProps, projectId, scheduleUserIds };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
/*
|
|
1616
|
+
* Compose LayerProps from persisted layer + layer-user rows. Shared by the
|
|
1617
|
+
* single-schedule loader the paging/roster path uses and the batched loader
|
|
1618
|
+
* the calendar feeds use, so both feed the engine byte-identical input.
|
|
1619
|
+
* Layers must already be sorted by order; layer users by order.
|
|
1620
|
+
*
|
|
1621
|
+
* Each LayerProps is stamped with the layer's id and name (informational;
|
|
1622
|
+
* the engine copies them onto its events) so consumers can tell which
|
|
1623
|
+
* layer a merged segment came from.
|
|
1624
|
+
*/
|
|
1625
|
+
private buildLayerPropsFromRows(data: {
|
|
1626
|
+
layers: Array<OnCallDutyPolicyScheduleLayer>;
|
|
1627
|
+
layerUsers: Array<OnCallDutyPolicyScheduleLayerUser>;
|
|
1628
|
+
scheduleTimezone: string | undefined;
|
|
1629
|
+
}): { layerProps: Array<LayerProps>; scheduleUserIds: Array<ObjectID> } {
|
|
1324
1630
|
const layerProps: Array<LayerProps> = [];
|
|
1325
1631
|
const scheduleUserIds: Array<ObjectID> = [];
|
|
1326
1632
|
const seenUserIds: Set<string> = new Set<string>();
|
|
1327
1633
|
|
|
1328
|
-
for (const layer of layers) {
|
|
1329
|
-
const usersForLayer: Array<User> = layerUsers
|
|
1634
|
+
for (const layer of data.layers) {
|
|
1635
|
+
const usersForLayer: Array<User> = data.layerUsers
|
|
1330
1636
|
.filter((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
1331
1637
|
return (
|
|
1332
1638
|
layerUser.onCallDutyPolicyScheduleLayerId?.toString() ===
|
|
@@ -1348,171 +1654,1154 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
1348
1654
|
}
|
|
1349
1655
|
}
|
|
1350
1656
|
|
|
1351
|
-
|
|
1657
|
+
const props: LayerProps = {
|
|
1352
1658
|
users: usersForLayer,
|
|
1353
1659
|
startDateTimeOfLayer: layer.startsAt!,
|
|
1354
1660
|
restrictionTimes: layer.restrictionTimes!,
|
|
1355
1661
|
rotation: layer.rotation!,
|
|
1356
1662
|
handOffTime: layer.handOffTime!,
|
|
1357
|
-
timezone: scheduleTimezone,
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1663
|
+
timezone: data.scheduleTimezone,
|
|
1664
|
+
};
|
|
1360
1665
|
|
|
1361
|
-
|
|
1666
|
+
const layerId: string | undefined = layer.id?.toString();
|
|
1667
|
+
if (layerId) {
|
|
1668
|
+
props.layerId = layerId;
|
|
1669
|
+
}
|
|
1362
1670
|
|
|
1363
|
-
|
|
1364
|
-
|
|
1671
|
+
if (typeof layer.name === "string" && layer.name) {
|
|
1672
|
+
props.layerName = layer.name;
|
|
1673
|
+
}
|
|
1365
1674
|
|
|
1366
|
-
|
|
1367
|
-
projectId: ObjectID;
|
|
1368
|
-
scheduleUserIds: Array<ObjectID>;
|
|
1369
|
-
windowStart: Date;
|
|
1370
|
-
windowEnd: Date;
|
|
1371
|
-
currentOnCallDutyPolicyId?: ObjectID | undefined;
|
|
1372
|
-
}): Promise<Array<UserOverrideRecord>> {
|
|
1373
|
-
if (data.scheduleUserIds.length === 0) {
|
|
1374
|
-
return [];
|
|
1675
|
+
layerProps.push(props);
|
|
1375
1676
|
}
|
|
1376
1677
|
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
* policy plus global overrides. Without a policy context (e.g. schedule
|
|
1380
|
-
* roster refresh, dashboard preview, incoming-call routing), only global
|
|
1381
|
-
* overrides apply — a policy-specific override from one policy must not
|
|
1382
|
-
* affect schedule resolution for a different policy.
|
|
1383
|
-
*/
|
|
1384
|
-
const overrides: Array<OnCallDutyPolicyUserOverride> =
|
|
1385
|
-
await OnCallDutyPolicyUserOverrideService.findBy({
|
|
1386
|
-
query: {
|
|
1387
|
-
projectId: data.projectId,
|
|
1388
|
-
startsAt: QueryHelper.lessThanEqualTo(data.windowEnd),
|
|
1389
|
-
endsAt: QueryHelper.greaterThanEqualTo(data.windowStart),
|
|
1390
|
-
onCallDutyPolicyId: data.currentOnCallDutyPolicyId
|
|
1391
|
-
? QueryHelper.equalToOrNull(data.currentOnCallDutyPolicyId)
|
|
1392
|
-
: QueryHelper.isNull(),
|
|
1393
|
-
},
|
|
1394
|
-
select: {
|
|
1395
|
-
startsAt: true,
|
|
1396
|
-
endsAt: true,
|
|
1397
|
-
overrideUserId: true,
|
|
1398
|
-
routeAlertsToUserId: true,
|
|
1399
|
-
onCallDutyPolicyId: true,
|
|
1400
|
-
},
|
|
1401
|
-
sort: {
|
|
1402
|
-
startsAt: SortOrder.Ascending,
|
|
1403
|
-
},
|
|
1404
|
-
limit: LIMIT_PER_PROJECT,
|
|
1405
|
-
skip: 0,
|
|
1406
|
-
props: {
|
|
1407
|
-
isRoot: true,
|
|
1408
|
-
},
|
|
1409
|
-
});
|
|
1678
|
+
return { layerProps, scheduleUserIds };
|
|
1679
|
+
}
|
|
1410
1680
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1681
|
+
/*
|
|
1682
|
+
* ---------------------------------------------------------------------------
|
|
1683
|
+
* Shift resolver — the source of truth for the calendar feeds, /my-shifts
|
|
1684
|
+
* and the shift reminders.
|
|
1685
|
+
* ---------------------------------------------------------------------------
|
|
1686
|
+
*/
|
|
1416
1687
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1688
|
+
/**
|
|
1689
|
+
* Resolve every on-call segment of ONE schedule that overlaps
|
|
1690
|
+
* [windowStart, windowEnd), in the exact order the paging and roster paths
|
|
1691
|
+
* use: layers -> engine -> overrides. Returns null when the schedule does
|
|
1692
|
+
* not exist. See getResolvedShiftSegmentsForSchedules for the rules.
|
|
1693
|
+
*/
|
|
1694
|
+
@CaptureSpan()
|
|
1695
|
+
public async getResolvedShiftSegments(
|
|
1696
|
+
options: ResolveShiftSegmentsOptions,
|
|
1697
|
+
): Promise<ResolvedShiftSegments | null> {
|
|
1698
|
+
const resolved: Array<ResolvedShiftSegments> =
|
|
1699
|
+
await this.getResolvedShiftSegmentsForSchedules({
|
|
1700
|
+
scheduleIds: [options.scheduleId],
|
|
1701
|
+
windowStart: options.windowStart,
|
|
1702
|
+
windowEnd: options.windowEnd,
|
|
1703
|
+
maxSimulationIterations: options.maxSimulationIterations,
|
|
1430
1704
|
});
|
|
1705
|
+
|
|
1706
|
+
return resolved[0] || null;
|
|
1431
1707
|
}
|
|
1432
1708
|
|
|
1433
|
-
|
|
1434
|
-
*
|
|
1435
|
-
*
|
|
1436
|
-
*
|
|
1437
|
-
*
|
|
1709
|
+
/**
|
|
1710
|
+
* Batched resolver: loads the layers, layer users, policy attachments and
|
|
1711
|
+
* overrides of every schedule with a handful of IN queries, then resolves
|
|
1712
|
+
* each schedule in memory.
|
|
1713
|
+
*
|
|
1714
|
+
* Per schedule:
|
|
1715
|
+
* 1. LayerProps exactly as the roster path builds them (plus layer ids).
|
|
1716
|
+
* 2. Expansion window = 2 rotation periods before windowStart .. 2 after
|
|
1717
|
+
* windowEnd, so the segments overlapping the window carry their TRUE
|
|
1718
|
+
* start and end rather than the engine's clamp to the window edge.
|
|
1719
|
+
* 3. ONE multi-layer engine expansion, bounded by maxSimulationIterations
|
|
1720
|
+
* (default: the engine's own caps); `truncated` reports a cap hit.
|
|
1721
|
+
* 4. Overrides in the roster's policy context: the single attached
|
|
1722
|
+
* policy's scoped overrides plus globals when exactly one policy is
|
|
1723
|
+
* attached, globals only otherwise — the same rule as
|
|
1724
|
+
* refreshCurrentUserIdAndHandoffTimeInSchedule (getSingleAttachedPolicyId).
|
|
1725
|
+
* 5. With two or more distinct attached policies AND a policy-scoped
|
|
1726
|
+
* override for a member overlapping the window, the same base events
|
|
1727
|
+
* are re-resolved once per such policy -> policyVariants. The
|
|
1728
|
+
* expensive expansion runs once; the override pass is cheap.
|
|
1729
|
+
* 6. lastModifiedAt = max(updatedAt) over layers, layer users,
|
|
1730
|
+
* participating overrides and attachments — never the schedule row.
|
|
1731
|
+
*
|
|
1732
|
+
* Schedules that no longer exist are skipped; the result is in the order
|
|
1733
|
+
* of the input ids.
|
|
1438
1734
|
*/
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1735
|
+
@CaptureSpan()
|
|
1736
|
+
public async getResolvedShiftSegmentsForSchedules(
|
|
1737
|
+
options: ResolveShiftSegmentsForSchedulesOptions,
|
|
1738
|
+
): Promise<Array<ResolvedShiftSegments>> {
|
|
1739
|
+
const scheduleIds: Array<ObjectID> = OnCallShiftChangeListeners.dedupe(
|
|
1740
|
+
options.scheduleIds,
|
|
1741
|
+
);
|
|
1445
1742
|
|
|
1446
|
-
|
|
1447
|
-
|
|
1743
|
+
if (scheduleIds.length === 0) {
|
|
1744
|
+
return [];
|
|
1745
|
+
}
|
|
1448
1746
|
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1747
|
+
if (
|
|
1748
|
+
!OneUptimeDate.isBefore(options.windowStart, options.windowEnd) &&
|
|
1749
|
+
options.windowStart.getTime() !== options.windowEnd.getTime()
|
|
1750
|
+
) {
|
|
1751
|
+
throw new BadDataException("windowStart must be before windowEnd");
|
|
1752
|
+
}
|
|
1453
1753
|
|
|
1454
|
-
|
|
1754
|
+
const inputsById: Map<string, ScheduleResolutionInputs> =
|
|
1755
|
+
await this.loadResolutionInputsForSchedules(scheduleIds);
|
|
1756
|
+
|
|
1757
|
+
/*
|
|
1758
|
+
* Build the layer props and the per-schedule expansion window first so
|
|
1759
|
+
* the override query below can cover every schedule in one statement
|
|
1760
|
+
* (overrides are project-scoped, not schedule-scoped).
|
|
1761
|
+
*/
|
|
1762
|
+
interface PreparedSchedule {
|
|
1763
|
+
inputs: ScheduleResolutionInputs;
|
|
1764
|
+
layerProps: Array<LayerProps>;
|
|
1765
|
+
scheduleUserIds: Array<ObjectID>;
|
|
1766
|
+
expansionStart: Date;
|
|
1767
|
+
expansionEnd: Date;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
const prepared: Array<PreparedSchedule> = [];
|
|
1771
|
+
const projectIds: Set<string> = new Set<string>();
|
|
1772
|
+
let earliestStart: Date | null = null;
|
|
1773
|
+
let latestEnd: Date | null = null;
|
|
1774
|
+
|
|
1775
|
+
for (const scheduleId of scheduleIds) {
|
|
1776
|
+
const inputs: ScheduleResolutionInputs | undefined = inputsById.get(
|
|
1777
|
+
scheduleId.toString(),
|
|
1778
|
+
);
|
|
1779
|
+
|
|
1780
|
+
if (!inputs) {
|
|
1455
1781
|
continue;
|
|
1456
1782
|
}
|
|
1457
1783
|
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1784
|
+
const { layerProps, scheduleUserIds } = this.buildLayerPropsFromRows({
|
|
1785
|
+
layers: inputs.layers,
|
|
1786
|
+
layerUsers: inputs.layerUsers,
|
|
1787
|
+
scheduleTimezone: inputs.schedule.timezone?.toString() || undefined,
|
|
1788
|
+
});
|
|
1789
|
+
|
|
1790
|
+
const expansionStart: Date =
|
|
1791
|
+
layerProps.length > 0
|
|
1792
|
+
? this.computeResolutionWindowStart(
|
|
1793
|
+
layerProps,
|
|
1794
|
+
options.windowStart,
|
|
1795
|
+
2,
|
|
1796
|
+
)
|
|
1797
|
+
: options.windowStart;
|
|
1798
|
+
|
|
1799
|
+
const expansionEnd: Date =
|
|
1800
|
+
layerProps.length > 0
|
|
1801
|
+
? this.computeResolutionWindowEndByPeriods(
|
|
1802
|
+
layerProps,
|
|
1803
|
+
options.windowEnd,
|
|
1804
|
+
2,
|
|
1805
|
+
)
|
|
1806
|
+
: options.windowEnd;
|
|
1807
|
+
|
|
1808
|
+
if (inputs.schedule.projectId) {
|
|
1809
|
+
projectIds.add(inputs.schedule.projectId.toString());
|
|
1466
1810
|
}
|
|
1467
1811
|
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1812
|
+
if (
|
|
1813
|
+
!earliestStart ||
|
|
1814
|
+
OneUptimeDate.isBefore(expansionStart, earliestStart)
|
|
1815
|
+
) {
|
|
1816
|
+
earliestStart = expansionStart;
|
|
1471
1817
|
}
|
|
1472
1818
|
|
|
1473
|
-
if (OneUptimeDate.isAfter(
|
|
1474
|
-
|
|
1819
|
+
if (!latestEnd || OneUptimeDate.isAfter(expansionEnd, latestEnd)) {
|
|
1820
|
+
latestEnd = expansionEnd;
|
|
1475
1821
|
}
|
|
1822
|
+
|
|
1823
|
+
prepared.push({
|
|
1824
|
+
inputs,
|
|
1825
|
+
layerProps,
|
|
1826
|
+
scheduleUserIds,
|
|
1827
|
+
expansionStart,
|
|
1828
|
+
expansionEnd,
|
|
1829
|
+
});
|
|
1476
1830
|
}
|
|
1477
1831
|
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
* be far past the naive (getNumberOfEvents + 1)-period horizon — keep a
|
|
1482
|
-
* generous 1-year floor for them so a current/next user is still found.
|
|
1483
|
-
* UNRESTRICTED layers produce exactly one event per rotation period, so the
|
|
1484
|
-
* per-layer candidate above already spans the events we need; flooring those
|
|
1485
|
-
* at a full year forced a fast (hourly/daily) rotation to materialize
|
|
1486
|
-
* thousands of events on every resolution, and getMultiLayerEvents then ran
|
|
1487
|
-
* an O(n^2) overlap merge over them — a real worker stall (audit H2). For
|
|
1488
|
-
* unrestricted layers a small 1-day floor is ample margin.
|
|
1489
|
-
*/
|
|
1490
|
-
const floor: Date = anyLayerRestricted
|
|
1491
|
-
? OneUptimeDate.addRemoveYears(from, 1)
|
|
1492
|
-
: OneUptimeDate.addRemoveDays(from, 1);
|
|
1832
|
+
if (prepared.length === 0) {
|
|
1833
|
+
return [];
|
|
1834
|
+
}
|
|
1493
1835
|
|
|
1494
|
-
|
|
1495
|
-
|
|
1836
|
+
const allOverrides: Array<OnCallDutyPolicyUserOverride> =
|
|
1837
|
+
projectIds.size > 0 && earliestStart && latestEnd
|
|
1838
|
+
? await this.loadOverridesForProjects({
|
|
1839
|
+
projectIds: Array.from(projectIds).map((id: string) => {
|
|
1840
|
+
return new ObjectID(id);
|
|
1841
|
+
}),
|
|
1842
|
+
windowStart: earliestStart,
|
|
1843
|
+
windowEnd: latestEnd,
|
|
1844
|
+
})
|
|
1845
|
+
: [];
|
|
1846
|
+
|
|
1847
|
+
const results: Array<ResolvedShiftSegments> = [];
|
|
1848
|
+
|
|
1849
|
+
for (const item of prepared) {
|
|
1850
|
+
results.push(
|
|
1851
|
+
this.resolveScheduleInMemory({
|
|
1852
|
+
schedule: item.inputs.schedule,
|
|
1853
|
+
layers: item.inputs.layers,
|
|
1854
|
+
layerUsers: item.inputs.layerUsers,
|
|
1855
|
+
attachments: item.inputs.attachments,
|
|
1856
|
+
layerProps: item.layerProps,
|
|
1857
|
+
scheduleUserIds: item.scheduleUserIds,
|
|
1858
|
+
overrides: allOverrides,
|
|
1859
|
+
expansionStart: item.expansionStart,
|
|
1860
|
+
expansionEnd: item.expansionEnd,
|
|
1861
|
+
windowStart: options.windowStart,
|
|
1862
|
+
windowEnd: options.windowEnd,
|
|
1863
|
+
maxSimulationIterations: options.maxSimulationIterations,
|
|
1864
|
+
}),
|
|
1865
|
+
);
|
|
1496
1866
|
}
|
|
1497
1867
|
|
|
1498
|
-
return
|
|
1868
|
+
return results;
|
|
1499
1869
|
}
|
|
1500
1870
|
|
|
1501
1871
|
/*
|
|
1502
|
-
*
|
|
1503
|
-
*
|
|
1504
|
-
* serialized JSON form ({ _type, value: { restictionType } }) so it is safe
|
|
1505
|
-
* regardless of where the LayerProps came from.
|
|
1872
|
+
* Everything the resolver needs for a batch of schedules, in four queries.
|
|
1873
|
+
* Schedules that do not exist simply have no entry.
|
|
1506
1874
|
*/
|
|
1507
|
-
private
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1875
|
+
private async loadResolutionInputsForSchedules(
|
|
1876
|
+
scheduleIds: Array<ObjectID>,
|
|
1877
|
+
): Promise<Map<string, ScheduleResolutionInputs>> {
|
|
1878
|
+
const schedules: Array<OnCallDutyPolicySchedule> = await this.findBy({
|
|
1879
|
+
query: {
|
|
1880
|
+
_id: QueryHelper.any(scheduleIds),
|
|
1881
|
+
},
|
|
1882
|
+
select: {
|
|
1883
|
+
_id: true,
|
|
1884
|
+
name: true,
|
|
1885
|
+
timezone: true,
|
|
1886
|
+
projectId: true,
|
|
1887
|
+
shiftConfigVersion: true,
|
|
1888
|
+
createdAt: true,
|
|
1889
|
+
},
|
|
1890
|
+
limit: LIMIT_PER_PROJECT,
|
|
1891
|
+
skip: 0,
|
|
1892
|
+
props: {
|
|
1893
|
+
isRoot: true,
|
|
1894
|
+
},
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
const byId: Map<string, ScheduleResolutionInputs> = new Map<
|
|
1898
|
+
string,
|
|
1899
|
+
ScheduleResolutionInputs
|
|
1900
|
+
>();
|
|
1901
|
+
|
|
1902
|
+
for (const schedule of schedules) {
|
|
1903
|
+
if (!schedule.id) {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
byId.set(schedule.id.toString(), {
|
|
1907
|
+
schedule,
|
|
1908
|
+
layers: [],
|
|
1909
|
+
layerUsers: [],
|
|
1910
|
+
attachments: [],
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (byId.size === 0) {
|
|
1915
|
+
return byId;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const existingIds: Array<ObjectID> = Array.from(byId.keys()).map(
|
|
1919
|
+
(id: string) => {
|
|
1920
|
+
return new ObjectID(id);
|
|
1921
|
+
},
|
|
1922
|
+
);
|
|
1923
|
+
|
|
1924
|
+
const [layers, layerUsers, attachments]: [
|
|
1925
|
+
Array<OnCallDutyPolicyScheduleLayer>,
|
|
1926
|
+
Array<OnCallDutyPolicyScheduleLayerUser>,
|
|
1927
|
+
Array<OnCallDutyPolicyEscalationRuleSchedule>,
|
|
1928
|
+
] = await Promise.all([
|
|
1929
|
+
OnCallDutyPolicyScheduleLayerService.findBy({
|
|
1930
|
+
query: {
|
|
1931
|
+
onCallDutyPolicyScheduleId: QueryHelper.any(existingIds),
|
|
1932
|
+
},
|
|
1933
|
+
select: {
|
|
1934
|
+
_id: true,
|
|
1935
|
+
order: true,
|
|
1936
|
+
name: true,
|
|
1937
|
+
description: true,
|
|
1938
|
+
startsAt: true,
|
|
1939
|
+
restrictionTimes: true,
|
|
1940
|
+
rotation: true,
|
|
1941
|
+
onCallDutyPolicyScheduleId: true,
|
|
1942
|
+
projectId: true,
|
|
1943
|
+
handOffTime: true,
|
|
1944
|
+
updatedAt: true,
|
|
1945
|
+
},
|
|
1946
|
+
sort: {
|
|
1947
|
+
order: SortOrder.Ascending,
|
|
1948
|
+
},
|
|
1949
|
+
limit: LIMIT_PER_PROJECT,
|
|
1950
|
+
skip: 0,
|
|
1951
|
+
props: {
|
|
1952
|
+
isRoot: true,
|
|
1953
|
+
},
|
|
1954
|
+
}),
|
|
1955
|
+
OnCallDutyPolicyScheduleLayerUserService.findBy({
|
|
1956
|
+
query: {
|
|
1957
|
+
onCallDutyPolicyScheduleId: QueryHelper.any(existingIds),
|
|
1958
|
+
},
|
|
1959
|
+
select: {
|
|
1960
|
+
_id: true,
|
|
1961
|
+
user: {
|
|
1962
|
+
_id: true,
|
|
1963
|
+
},
|
|
1964
|
+
userId: true,
|
|
1965
|
+
order: true,
|
|
1966
|
+
onCallDutyPolicyScheduleLayerId: true,
|
|
1967
|
+
onCallDutyPolicyScheduleId: true,
|
|
1968
|
+
updatedAt: true,
|
|
1969
|
+
},
|
|
1970
|
+
sort: {
|
|
1971
|
+
order: SortOrder.Ascending,
|
|
1972
|
+
},
|
|
1973
|
+
limit: LIMIT_PER_PROJECT,
|
|
1974
|
+
skip: 0,
|
|
1975
|
+
props: {
|
|
1976
|
+
isRoot: true,
|
|
1977
|
+
},
|
|
1978
|
+
}),
|
|
1979
|
+
OnCallDutyPolicyEscalationRuleScheduleService.findBy({
|
|
1980
|
+
query: {
|
|
1981
|
+
onCallDutyPolicyScheduleId: QueryHelper.any(existingIds),
|
|
1982
|
+
},
|
|
1983
|
+
select: {
|
|
1984
|
+
_id: true,
|
|
1985
|
+
onCallDutyPolicyScheduleId: true,
|
|
1986
|
+
onCallDutyPolicyId: true,
|
|
1987
|
+
onCallDutyPolicyEscalationRuleId: true,
|
|
1988
|
+
updatedAt: true,
|
|
1989
|
+
onCallDutyPolicy: {
|
|
1990
|
+
name: true,
|
|
1991
|
+
_id: true,
|
|
1992
|
+
},
|
|
1993
|
+
onCallDutyPolicyEscalationRule: {
|
|
1994
|
+
name: true,
|
|
1995
|
+
_id: true,
|
|
1996
|
+
order: true,
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
limit: LIMIT_PER_PROJECT,
|
|
2000
|
+
skip: 0,
|
|
2001
|
+
props: {
|
|
2002
|
+
isRoot: true,
|
|
2003
|
+
},
|
|
2004
|
+
}),
|
|
2005
|
+
]);
|
|
2006
|
+
|
|
2007
|
+
for (const layer of layers) {
|
|
2008
|
+
const entry: ScheduleResolutionInputs | undefined = byId.get(
|
|
2009
|
+
layer.onCallDutyPolicyScheduleId?.toString() || "",
|
|
2010
|
+
);
|
|
2011
|
+
if (entry) {
|
|
2012
|
+
entry.layers.push(layer);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
for (const layerUser of layerUsers) {
|
|
2017
|
+
const entry: ScheduleResolutionInputs | undefined = byId.get(
|
|
2018
|
+
layerUser.onCallDutyPolicyScheduleId?.toString() || "",
|
|
2019
|
+
);
|
|
2020
|
+
if (entry) {
|
|
2021
|
+
entry.layerUsers.push(layerUser);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
for (const attachment of attachments) {
|
|
2026
|
+
const entry: ScheduleResolutionInputs | undefined = byId.get(
|
|
2027
|
+
attachment.onCallDutyPolicyScheduleId?.toString() || "",
|
|
2028
|
+
);
|
|
2029
|
+
if (entry) {
|
|
2030
|
+
entry.attachments.push(attachment);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
return byId;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/*
|
|
2038
|
+
* Every override (global AND policy-scoped) of the given projects that
|
|
2039
|
+
* overlaps [windowStart, windowEnd]. The per-schedule policy scoping and
|
|
2040
|
+
* member filtering happen in memory (see selectOverridesForSchedule), which
|
|
2041
|
+
* mirrors fetchOverridesForSchedule's query exactly but for many schedules
|
|
2042
|
+
* at once.
|
|
2043
|
+
*/
|
|
2044
|
+
private async loadOverridesForProjects(data: {
|
|
2045
|
+
projectIds: Array<ObjectID>;
|
|
2046
|
+
windowStart: Date;
|
|
2047
|
+
windowEnd: Date;
|
|
2048
|
+
}): Promise<Array<OnCallDutyPolicyUserOverride>> {
|
|
2049
|
+
if (data.projectIds.length === 0) {
|
|
2050
|
+
return [];
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
return await OnCallDutyPolicyUserOverrideService.findBy({
|
|
2054
|
+
query: {
|
|
2055
|
+
projectId: QueryHelper.any(data.projectIds),
|
|
2056
|
+
startsAt: QueryHelper.lessThanEqualTo(data.windowEnd),
|
|
2057
|
+
endsAt: QueryHelper.greaterThanEqualTo(data.windowStart),
|
|
2058
|
+
},
|
|
2059
|
+
select: {
|
|
2060
|
+
_id: true,
|
|
2061
|
+
projectId: true,
|
|
2062
|
+
startsAt: true,
|
|
2063
|
+
endsAt: true,
|
|
2064
|
+
overrideUserId: true,
|
|
2065
|
+
routeAlertsToUserId: true,
|
|
2066
|
+
onCallDutyPolicyId: true,
|
|
2067
|
+
updatedAt: true,
|
|
2068
|
+
},
|
|
2069
|
+
sort: {
|
|
2070
|
+
startsAt: SortOrder.Ascending,
|
|
2071
|
+
},
|
|
2072
|
+
limit: LIMIT_PER_PROJECT,
|
|
2073
|
+
skip: 0,
|
|
2074
|
+
props: {
|
|
2075
|
+
isRoot: true,
|
|
2076
|
+
},
|
|
2077
|
+
});
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
/*
|
|
2081
|
+
* The in-memory twin of fetchOverridesForSchedule: the overrides of this
|
|
2082
|
+
* schedule's project that overlap the window, whose overridden user is a
|
|
2083
|
+
* schedule member, scoped to `policyId` (that policy's plus globals) or to
|
|
2084
|
+
* globals only when no policy context is given.
|
|
2085
|
+
*/
|
|
2086
|
+
private selectOverridesForSchedule(data: {
|
|
2087
|
+
overrides: Array<OnCallDutyPolicyUserOverride>;
|
|
2088
|
+
projectId: string;
|
|
2089
|
+
scheduleUserIds: Set<string>;
|
|
2090
|
+
windowStart: Date;
|
|
2091
|
+
windowEnd: Date;
|
|
2092
|
+
policyId: string | undefined;
|
|
2093
|
+
}): Array<OnCallDutyPolicyUserOverride> {
|
|
2094
|
+
return data.overrides.filter((override: OnCallDutyPolicyUserOverride) => {
|
|
2095
|
+
if (override.projectId?.toString() !== data.projectId) {
|
|
2096
|
+
return false;
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
if (!override.startsAt || !override.endsAt) {
|
|
2100
|
+
return false;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (
|
|
2104
|
+
OneUptimeDate.isAfter(override.startsAt, data.windowEnd) ||
|
|
2105
|
+
OneUptimeDate.isBefore(override.endsAt, data.windowStart)
|
|
2106
|
+
) {
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
if (
|
|
2111
|
+
!data.scheduleUserIds.has(override.overrideUserId?.toString() || "")
|
|
2112
|
+
) {
|
|
2113
|
+
return false;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const overridePolicyId: string | undefined =
|
|
2117
|
+
override.onCallDutyPolicyId?.toString() || undefined;
|
|
2118
|
+
|
|
2119
|
+
if (!overridePolicyId) {
|
|
2120
|
+
return true; // global
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
return Boolean(data.policyId) && overridePolicyId === data.policyId;
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
private toUserOverrideRecord(
|
|
2128
|
+
override: OnCallDutyPolicyUserOverride,
|
|
2129
|
+
): UserOverrideRecord {
|
|
2130
|
+
return {
|
|
2131
|
+
overrideUserId: override.overrideUserId?.toString() || "",
|
|
2132
|
+
routeAlertsToUserId: override.routeAlertsToUserId?.toString() || "",
|
|
2133
|
+
startsAt: override.startsAt!,
|
|
2134
|
+
endsAt: override.endsAt!,
|
|
2135
|
+
onCallDutyPolicyId: override.onCallDutyPolicyId?.toString() || null,
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
/*
|
|
2140
|
+
* The pure part of the resolver: given everything already loaded, expand
|
|
2141
|
+
* once and resolve the base context plus any policy variants.
|
|
2142
|
+
*/
|
|
2143
|
+
private resolveScheduleInMemory(data: {
|
|
2144
|
+
schedule: OnCallDutyPolicySchedule;
|
|
2145
|
+
layers: Array<OnCallDutyPolicyScheduleLayer>;
|
|
2146
|
+
layerUsers: Array<OnCallDutyPolicyScheduleLayerUser>;
|
|
2147
|
+
attachments: Array<OnCallDutyPolicyEscalationRuleSchedule>;
|
|
2148
|
+
layerProps: Array<LayerProps>;
|
|
2149
|
+
scheduleUserIds: Array<ObjectID>;
|
|
2150
|
+
overrides: Array<OnCallDutyPolicyUserOverride>;
|
|
2151
|
+
expansionStart: Date;
|
|
2152
|
+
expansionEnd: Date;
|
|
2153
|
+
windowStart: Date;
|
|
2154
|
+
windowEnd: Date;
|
|
2155
|
+
maxSimulationIterations?: number | undefined;
|
|
2156
|
+
}): ResolvedShiftSegments {
|
|
2157
|
+
const scheduleId: string = data.schedule.id!.toString();
|
|
2158
|
+
const projectId: string = data.schedule.projectId?.toString() || "";
|
|
2159
|
+
|
|
2160
|
+
// Distinct (policy, rule) attachments, stable order.
|
|
2161
|
+
const attachedPolicies: Array<MaterializedShiftPolicy> =
|
|
2162
|
+
this.toAttachedPolicies(data.attachments);
|
|
2163
|
+
|
|
2164
|
+
const distinctPolicyIds: Array<string> = [];
|
|
2165
|
+
for (const policy of attachedPolicies) {
|
|
2166
|
+
if (!distinctPolicyIds.includes(policy.policyId)) {
|
|
2167
|
+
distinctPolicyIds.push(policy.policyId);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
// The roster rule: exactly one attached policy => resolve in its context.
|
|
2172
|
+
const baseContextPolicyId: string | undefined =
|
|
2173
|
+
distinctPolicyIds.length === 1 ? distinctPolicyIds[0] : undefined;
|
|
2174
|
+
|
|
2175
|
+
// 1 + 2 + 3: one engine expansion over the widened window.
|
|
2176
|
+
let baseEvents: Array<CalendarEvent> = [];
|
|
2177
|
+
let truncated: boolean = false;
|
|
2178
|
+
|
|
2179
|
+
if (data.layerProps.length > 0) {
|
|
2180
|
+
const expansion: LayerEventsResult =
|
|
2181
|
+
this.layerUtil.getMultiLayerEventsWithMeta(
|
|
2182
|
+
{
|
|
2183
|
+
layers: data.layerProps,
|
|
2184
|
+
calendarStartDate: data.expansionStart,
|
|
2185
|
+
calendarEndDate: data.expansionEnd,
|
|
2186
|
+
},
|
|
2187
|
+
data.maxSimulationIterations !== undefined
|
|
2188
|
+
? { maxSimulationIterations: data.maxSimulationIterations }
|
|
2189
|
+
: undefined,
|
|
2190
|
+
);
|
|
2191
|
+
|
|
2192
|
+
baseEvents = expansion.events;
|
|
2193
|
+
truncated = expansion.truncated;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const scheduleUserIdSet: Set<string> = new Set<string>(
|
|
2197
|
+
data.scheduleUserIds.map((id: ObjectID) => {
|
|
2198
|
+
return id.toString();
|
|
2199
|
+
}),
|
|
2200
|
+
);
|
|
2201
|
+
|
|
2202
|
+
// 4: overrides in the base (roster) context.
|
|
2203
|
+
const baseOverrideRows: Array<OnCallDutyPolicyUserOverride> =
|
|
2204
|
+
this.selectOverridesForSchedule({
|
|
2205
|
+
overrides: data.overrides,
|
|
2206
|
+
projectId,
|
|
2207
|
+
scheduleUserIds: scheduleUserIdSet,
|
|
2208
|
+
windowStart: data.expansionStart,
|
|
2209
|
+
windowEnd: data.expansionEnd,
|
|
2210
|
+
policyId: baseContextPolicyId,
|
|
2211
|
+
});
|
|
2212
|
+
|
|
2213
|
+
const baseOverrides: Array<UserOverrideRecord> = baseOverrideRows.map(
|
|
2214
|
+
(override: OnCallDutyPolicyUserOverride) => {
|
|
2215
|
+
return this.toUserOverrideRecord(override);
|
|
2216
|
+
},
|
|
2217
|
+
);
|
|
2218
|
+
|
|
2219
|
+
const participatingOverrideRows: Array<OnCallDutyPolicyUserOverride> = [
|
|
2220
|
+
...baseOverrideRows,
|
|
2221
|
+
];
|
|
2222
|
+
const participatingOverrides: Array<UserOverrideRecord> = [
|
|
2223
|
+
...baseOverrides,
|
|
2224
|
+
];
|
|
2225
|
+
|
|
2226
|
+
let segments: Array<CalendarEvent> = baseEvents;
|
|
2227
|
+
|
|
2228
|
+
if (baseOverrides.length > 0 && baseEvents.length > 0) {
|
|
2229
|
+
segments = UserOverrideUtil.applyOverridesToEvents({
|
|
2230
|
+
events: baseEvents,
|
|
2231
|
+
overrides: baseOverrides,
|
|
2232
|
+
currentOnCallDutyPolicyId: baseContextPolicyId,
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
segments = this.filterEventsOverlappingWindow(
|
|
2237
|
+
segments,
|
|
2238
|
+
data.windowStart,
|
|
2239
|
+
data.windowEnd,
|
|
2240
|
+
);
|
|
2241
|
+
|
|
2242
|
+
// 5: policy variants.
|
|
2243
|
+
const policyVariants: Array<ResolvedPolicyVariant> = [];
|
|
2244
|
+
|
|
2245
|
+
if (distinctPolicyIds.length >= 2 && baseEvents.length > 0) {
|
|
2246
|
+
for (const policyId of distinctPolicyIds) {
|
|
2247
|
+
const scopedRows: Array<OnCallDutyPolicyUserOverride> =
|
|
2248
|
+
this.selectOverridesForSchedule({
|
|
2249
|
+
overrides: data.overrides,
|
|
2250
|
+
projectId,
|
|
2251
|
+
scheduleUserIds: scheduleUserIdSet,
|
|
2252
|
+
windowStart: data.expansionStart,
|
|
2253
|
+
windowEnd: data.expansionEnd,
|
|
2254
|
+
policyId,
|
|
2255
|
+
}).filter((override: OnCallDutyPolicyUserOverride) => {
|
|
2256
|
+
// Only the policy-scoped ones make this context differ.
|
|
2257
|
+
return Boolean(override.onCallDutyPolicyId);
|
|
2258
|
+
});
|
|
2259
|
+
|
|
2260
|
+
if (scopedRows.length === 0) {
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const variantOverrideRows: Array<OnCallDutyPolicyUserOverride> = [
|
|
2265
|
+
...scopedRows,
|
|
2266
|
+
...baseOverrideRows,
|
|
2267
|
+
];
|
|
2268
|
+
|
|
2269
|
+
const variantOverrides: Array<UserOverrideRecord> =
|
|
2270
|
+
variantOverrideRows.map((override: OnCallDutyPolicyUserOverride) => {
|
|
2271
|
+
return this.toUserOverrideRecord(override);
|
|
2272
|
+
});
|
|
2273
|
+
|
|
2274
|
+
for (const row of scopedRows) {
|
|
2275
|
+
participatingOverrideRows.push(row);
|
|
2276
|
+
}
|
|
2277
|
+
for (const record of variantOverrides.slice(0, scopedRows.length)) {
|
|
2278
|
+
participatingOverrides.push(record);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
const variantSegments: Array<CalendarEvent> =
|
|
2282
|
+
this.filterEventsOverlappingWindow(
|
|
2283
|
+
UserOverrideUtil.applyOverridesToEvents({
|
|
2284
|
+
events: baseEvents,
|
|
2285
|
+
overrides: variantOverrides,
|
|
2286
|
+
currentOnCallDutyPolicyId: policyId,
|
|
2287
|
+
}),
|
|
2288
|
+
data.windowStart,
|
|
2289
|
+
data.windowEnd,
|
|
2290
|
+
);
|
|
2291
|
+
|
|
2292
|
+
const policyName: string =
|
|
2293
|
+
attachedPolicies.find((policy: MaterializedShiftPolicy) => {
|
|
2294
|
+
return policy.policyId === policyId;
|
|
2295
|
+
})?.policyName || "";
|
|
2296
|
+
|
|
2297
|
+
policyVariants.push({
|
|
2298
|
+
policyId,
|
|
2299
|
+
policyName,
|
|
2300
|
+
segments: variantSegments,
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// 6: lastModifiedAt — schedule row excluded.
|
|
2306
|
+
const lastModifiedAt: Date = this.computeLastModifiedAt({
|
|
2307
|
+
schedule: data.schedule,
|
|
2308
|
+
layers: data.layers,
|
|
2309
|
+
layerUsers: data.layerUsers,
|
|
2310
|
+
attachments: data.attachments,
|
|
2311
|
+
overrides: participatingOverrideRows,
|
|
2312
|
+
});
|
|
2313
|
+
|
|
2314
|
+
const scheduleInfo: ResolvedScheduleInfo = {
|
|
2315
|
+
id: scheduleId,
|
|
2316
|
+
name: data.schedule.name || "",
|
|
2317
|
+
projectId,
|
|
2318
|
+
shiftConfigVersion: Service.toVersionNumber(
|
|
2319
|
+
data.schedule.shiftConfigVersion,
|
|
2320
|
+
),
|
|
2321
|
+
};
|
|
2322
|
+
|
|
2323
|
+
const timezone: string | undefined =
|
|
2324
|
+
data.schedule.timezone?.toString() || undefined;
|
|
2325
|
+
if (timezone) {
|
|
2326
|
+
scheduleInfo.timezone = timezone;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
return {
|
|
2330
|
+
schedule: scheduleInfo,
|
|
2331
|
+
segments,
|
|
2332
|
+
policyVariants,
|
|
2333
|
+
attachedPolicies,
|
|
2334
|
+
layerProps: data.layerProps,
|
|
2335
|
+
scheduleUserIds: Array.from(scheduleUserIdSet),
|
|
2336
|
+
overrides: participatingOverrides,
|
|
2337
|
+
lastModifiedAt,
|
|
2338
|
+
truncated,
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
private toAttachedPolicies(
|
|
2343
|
+
attachments: Array<OnCallDutyPolicyEscalationRuleSchedule>,
|
|
2344
|
+
): Array<MaterializedShiftPolicy> {
|
|
2345
|
+
const seen: Set<string> = new Set<string>();
|
|
2346
|
+
const policies: Array<MaterializedShiftPolicy> = [];
|
|
2347
|
+
|
|
2348
|
+
for (const attachment of attachments) {
|
|
2349
|
+
const policyId: string =
|
|
2350
|
+
attachment.onCallDutyPolicyId?.toString() ||
|
|
2351
|
+
attachment.onCallDutyPolicy?.id?.toString() ||
|
|
2352
|
+
"";
|
|
2353
|
+
const ruleId: string =
|
|
2354
|
+
attachment.onCallDutyPolicyEscalationRuleId?.toString() ||
|
|
2355
|
+
attachment.onCallDutyPolicyEscalationRule?.id?.toString() ||
|
|
2356
|
+
"";
|
|
2357
|
+
|
|
2358
|
+
if (!policyId) {
|
|
2359
|
+
continue;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
const key: string = `${policyId}:${ruleId}`;
|
|
2363
|
+
if (seen.has(key)) {
|
|
2364
|
+
continue;
|
|
2365
|
+
}
|
|
2366
|
+
seen.add(key);
|
|
2367
|
+
|
|
2368
|
+
policies.push({
|
|
2369
|
+
policyId,
|
|
2370
|
+
policyName: attachment.onCallDutyPolicy?.name || "",
|
|
2371
|
+
ruleId,
|
|
2372
|
+
ruleName: attachment.onCallDutyPolicyEscalationRule?.name || "",
|
|
2373
|
+
ruleOrder: Service.toVersionNumber(
|
|
2374
|
+
attachment.onCallDutyPolicyEscalationRule?.order,
|
|
2375
|
+
),
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
policies.sort((a: MaterializedShiftPolicy, b: MaterializedShiftPolicy) => {
|
|
2380
|
+
if (a.policyName !== b.policyName) {
|
|
2381
|
+
return a.policyName < b.policyName ? -1 : 1;
|
|
2382
|
+
}
|
|
2383
|
+
if (a.policyId !== b.policyId) {
|
|
2384
|
+
return a.policyId < b.policyId ? -1 : 1;
|
|
2385
|
+
}
|
|
2386
|
+
if (a.ruleOrder !== b.ruleOrder) {
|
|
2387
|
+
return a.ruleOrder - b.ruleOrder;
|
|
2388
|
+
}
|
|
2389
|
+
return a.ruleId < b.ruleId ? -1 : a.ruleId > b.ruleId ? 1 : 0;
|
|
2390
|
+
});
|
|
2391
|
+
|
|
2392
|
+
return policies;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
private computeLastModifiedAt(data: {
|
|
2396
|
+
schedule: OnCallDutyPolicySchedule;
|
|
2397
|
+
layers: Array<{ updatedAt?: Date | undefined }>;
|
|
2398
|
+
layerUsers: Array<{ updatedAt?: Date | undefined }>;
|
|
2399
|
+
attachments: Array<{ updatedAt?: Date | undefined }>;
|
|
2400
|
+
overrides: Array<{ updatedAt?: Date | undefined }>;
|
|
2401
|
+
}): Date {
|
|
2402
|
+
let latest: Date | null = null;
|
|
2403
|
+
|
|
2404
|
+
const consider: (value: Date | undefined) => void = (
|
|
2405
|
+
value: Date | undefined,
|
|
2406
|
+
): void => {
|
|
2407
|
+
if (!value) {
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
const asDate: Date = value instanceof Date ? value : new Date(value);
|
|
2411
|
+
if (Number.isNaN(asDate.getTime())) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (!latest || asDate.getTime() > latest.getTime()) {
|
|
2415
|
+
latest = asDate;
|
|
2416
|
+
}
|
|
2417
|
+
};
|
|
2418
|
+
|
|
2419
|
+
for (const rows of [
|
|
2420
|
+
data.layers,
|
|
2421
|
+
data.layerUsers,
|
|
2422
|
+
data.attachments,
|
|
2423
|
+
data.overrides,
|
|
2424
|
+
]) {
|
|
2425
|
+
for (const row of rows) {
|
|
2426
|
+
consider(row.updatedAt);
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
if (latest) {
|
|
2431
|
+
return latest;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
/*
|
|
2435
|
+
* Nothing configured yet (no layers, no attachments): fall back to when
|
|
2436
|
+
* the schedule was CREATED — stable, unlike its updatedAt — and finally
|
|
2437
|
+
* to the epoch so the value is always a valid instant.
|
|
2438
|
+
*/
|
|
2439
|
+
if (data.schedule.createdAt) {
|
|
2440
|
+
const created: Date = new Date(data.schedule.createdAt);
|
|
2441
|
+
if (!Number.isNaN(created.getTime())) {
|
|
2442
|
+
return created;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
return new Date(0);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
private filterEventsOverlappingWindow(
|
|
2450
|
+
events: Array<CalendarEvent>,
|
|
2451
|
+
windowStart: Date,
|
|
2452
|
+
windowEnd: Date,
|
|
2453
|
+
): Array<CalendarEvent> {
|
|
2454
|
+
return events.filter((event: CalendarEvent) => {
|
|
2455
|
+
if (!event || !event.start || !event.end) {
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
return (
|
|
2459
|
+
event.start.getTime() < windowEnd.getTime() &&
|
|
2460
|
+
event.end.getTime() > windowStart.getTime()
|
|
2461
|
+
);
|
|
2462
|
+
});
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
private static toVersionNumber(value: unknown): number {
|
|
2466
|
+
const parsed: number =
|
|
2467
|
+
typeof value === "number" ? value : parseInt(String(value ?? ""), 10);
|
|
2468
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
/*
|
|
2472
|
+
* ---------------------------------------------------------------------------
|
|
2473
|
+
* Change propagation — shiftConfigVersion, feed caches, listeners.
|
|
2474
|
+
* ---------------------------------------------------------------------------
|
|
2475
|
+
*/
|
|
2476
|
+
|
|
2477
|
+
/**
|
|
2478
|
+
* Atomically add one to shiftConfigVersion of each schedule, in a single
|
|
2479
|
+
* UPDATE per schedule (`SET col = COALESCE(col, 0) + 1`, no hooks, no
|
|
2480
|
+
* optimistic-lock bump — see atomicAddToColumnsByIdWithoutHooks). Best
|
|
2481
|
+
* effort: a failure is logged and never thrown, because this runs inside
|
|
2482
|
+
* the CRUD hooks of the user's edit and must not fail it.
|
|
2483
|
+
*
|
|
2484
|
+
* NEVER call this from refreshCurrentUserIdAndHandoffTimeInSchedule: the
|
|
2485
|
+
* roster refresh runs every handoff and touches no configuration, and the
|
|
2486
|
+
* version is what calendar clients see as SEQUENCE — bumping it there would
|
|
2487
|
+
* make every feed look edited every rotation period.
|
|
2488
|
+
*/
|
|
2489
|
+
@CaptureSpan()
|
|
2490
|
+
public async bumpShiftConfigVersion(
|
|
2491
|
+
scheduleIds: Array<ObjectID>,
|
|
2492
|
+
): Promise<void> {
|
|
2493
|
+
for (const scheduleId of OnCallShiftChangeListeners.dedupe(scheduleIds)) {
|
|
2494
|
+
try {
|
|
2495
|
+
await this.atomicAddToColumnsByIdWithoutHooks({
|
|
2496
|
+
id: scheduleId,
|
|
2497
|
+
add: { shiftConfigVersion: 1 },
|
|
2498
|
+
});
|
|
2499
|
+
} catch (err) {
|
|
2500
|
+
logger.error(
|
|
2501
|
+
`Error bumping shiftConfigVersion for schedule ${scheduleId.toString()} (best-effort).`,
|
|
2502
|
+
);
|
|
2503
|
+
logger.error(err);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
/**
|
|
2509
|
+
* The schedules in `projectId` whose layers include any of `userIds` —
|
|
2510
|
+
* the query refreshRostersForUserInProject uses, batched. Root read.
|
|
2511
|
+
*/
|
|
2512
|
+
@CaptureSpan()
|
|
2513
|
+
public async getScheduleIdsForUsersInProject(data: {
|
|
2514
|
+
projectId: ObjectID;
|
|
2515
|
+
userIds: Array<ObjectID>;
|
|
2516
|
+
}): Promise<Array<ObjectID>> {
|
|
2517
|
+
const userIds: Array<ObjectID> = OnCallShiftChangeListeners.dedupe(
|
|
2518
|
+
data.userIds,
|
|
2519
|
+
);
|
|
2520
|
+
|
|
2521
|
+
if (userIds.length === 0) {
|
|
2522
|
+
return [];
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
const layerUsers: Array<OnCallDutyPolicyScheduleLayerUser> =
|
|
2526
|
+
await OnCallDutyPolicyScheduleLayerUserService.findBy({
|
|
2527
|
+
query: {
|
|
2528
|
+
projectId: data.projectId,
|
|
2529
|
+
userId: QueryHelper.any(userIds),
|
|
2530
|
+
},
|
|
2531
|
+
select: {
|
|
2532
|
+
onCallDutyPolicyScheduleId: true,
|
|
2533
|
+
},
|
|
2534
|
+
limit: LIMIT_PER_PROJECT,
|
|
2535
|
+
skip: 0,
|
|
2536
|
+
props: {
|
|
2537
|
+
isRoot: true,
|
|
2538
|
+
},
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2541
|
+
return OnCallShiftChangeListeners.dedupe(
|
|
2542
|
+
layerUsers
|
|
2543
|
+
.map((layerUser: OnCallDutyPolicyScheduleLayerUser) => {
|
|
2544
|
+
return layerUser.onCallDutyPolicyScheduleId;
|
|
2545
|
+
})
|
|
2546
|
+
.filter((id: ObjectID | undefined): id is ObjectID => {
|
|
2547
|
+
return Boolean(id);
|
|
2548
|
+
}),
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
/**
|
|
2553
|
+
* What every configuration hook calls after the roster refresh:
|
|
2554
|
+
* 1. bump shiftConfigVersion of the affected schedules (SEQUENCE / cache key),
|
|
2555
|
+
* 2. purge the calendar-feed caches of those schedules and of the users
|
|
2556
|
+
* the change names directly,
|
|
2557
|
+
* 3. tell the shift-change listeners (the reminder change pass) which
|
|
2558
|
+
* schedules and users to look at again — delivered in the background,
|
|
2559
|
+
* never awaited, never allowed to fail the edit.
|
|
2560
|
+
* Never throws.
|
|
2561
|
+
*/
|
|
2562
|
+
@CaptureSpan()
|
|
2563
|
+
public async propagateShiftConfigChange(
|
|
2564
|
+
change: ShiftConfigChange,
|
|
2565
|
+
): Promise<void> {
|
|
2566
|
+
try {
|
|
2567
|
+
const scheduleIds: Array<ObjectID> = OnCallShiftChangeListeners.dedupe(
|
|
2568
|
+
change.scheduleIds,
|
|
2569
|
+
);
|
|
2570
|
+
const explicitUserIds: Array<ObjectID> =
|
|
2571
|
+
OnCallShiftChangeListeners.dedupe(change.userIds || []);
|
|
2572
|
+
|
|
2573
|
+
if (scheduleIds.length > 0 && !change.skipVersionBump) {
|
|
2574
|
+
await this.bumpShiftConfigVersion(scheduleIds);
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
for (const scheduleId of scheduleIds) {
|
|
2578
|
+
try {
|
|
2579
|
+
await OnCallCalendarFeedCache.purgeForSchedule(scheduleId.toString());
|
|
2580
|
+
} catch (err) {
|
|
2581
|
+
logger.error(err);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
if (change.projectId) {
|
|
2586
|
+
for (const userId of explicitUserIds) {
|
|
2587
|
+
try {
|
|
2588
|
+
await OnCallCalendarFeedCache.purgeForUser(
|
|
2589
|
+
change.projectId.toString(),
|
|
2590
|
+
userId.toString(),
|
|
2591
|
+
);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
logger.error(err);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
|
|
2598
|
+
let userIds: Array<ObjectID> = explicitUserIds;
|
|
2599
|
+
|
|
2600
|
+
if (scheduleIds.length > 0) {
|
|
2601
|
+
try {
|
|
2602
|
+
const members: Array<OnCallDutyPolicyScheduleLayerUser> =
|
|
2603
|
+
await OnCallDutyPolicyScheduleLayerUserService.findBy({
|
|
2604
|
+
query: {
|
|
2605
|
+
onCallDutyPolicyScheduleId: QueryHelper.any(scheduleIds),
|
|
2606
|
+
},
|
|
2607
|
+
select: {
|
|
2608
|
+
userId: true,
|
|
2609
|
+
},
|
|
2610
|
+
limit: LIMIT_PER_PROJECT,
|
|
2611
|
+
skip: 0,
|
|
2612
|
+
props: {
|
|
2613
|
+
isRoot: true,
|
|
2614
|
+
},
|
|
2615
|
+
});
|
|
2616
|
+
|
|
2617
|
+
userIds = OnCallShiftChangeListeners.dedupe([
|
|
2618
|
+
...explicitUserIds,
|
|
2619
|
+
...members
|
|
2620
|
+
.map((member: OnCallDutyPolicyScheduleLayerUser) => {
|
|
2621
|
+
return member.userId;
|
|
2622
|
+
})
|
|
2623
|
+
.filter((id: ObjectID | undefined): id is ObjectID => {
|
|
2624
|
+
return Boolean(id);
|
|
2625
|
+
}),
|
|
2626
|
+
]);
|
|
2627
|
+
} catch (err) {
|
|
2628
|
+
logger.error(
|
|
2629
|
+
"Error loading schedule members for the shift-change event (best-effort).",
|
|
2630
|
+
);
|
|
2631
|
+
logger.error(err);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
const event: OnCallShiftChangeEvent =
|
|
2636
|
+
OnCallShiftChangeListeners.buildEvent({
|
|
2637
|
+
projectId: change.projectId,
|
|
2638
|
+
scheduleIds,
|
|
2639
|
+
userIds,
|
|
2640
|
+
reason: change.reason,
|
|
2641
|
+
});
|
|
2642
|
+
|
|
2643
|
+
// Background delivery; notify never rejects, the catch is belt-and-braces.
|
|
2644
|
+
OnCallShiftChangeListeners.notify(event).catch((err: Error) => {
|
|
2645
|
+
logger.error(err);
|
|
2646
|
+
});
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
logger.error(
|
|
2649
|
+
"Error propagating an on-call configuration change (best-effort).",
|
|
2650
|
+
);
|
|
2651
|
+
logger.error(err);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
private async fetchOverridesForSchedule(data: {
|
|
2656
|
+
projectId: ObjectID;
|
|
2657
|
+
scheduleUserIds: Array<ObjectID>;
|
|
2658
|
+
windowStart: Date;
|
|
2659
|
+
windowEnd: Date;
|
|
2660
|
+
currentOnCallDutyPolicyId?: ObjectID | undefined;
|
|
2661
|
+
}): Promise<Array<UserOverrideRecord>> {
|
|
2662
|
+
if (data.scheduleUserIds.length === 0) {
|
|
2663
|
+
return [];
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
/*
|
|
2667
|
+
* When a policy context is provided, include overrides scoped to that
|
|
2668
|
+
* policy plus global overrides. Without a policy context (e.g. schedule
|
|
2669
|
+
* roster refresh, dashboard preview, incoming-call routing), only global
|
|
2670
|
+
* overrides apply — a policy-specific override from one policy must not
|
|
2671
|
+
* affect schedule resolution for a different policy.
|
|
2672
|
+
*/
|
|
2673
|
+
const overrides: Array<OnCallDutyPolicyUserOverride> =
|
|
2674
|
+
await OnCallDutyPolicyUserOverrideService.findBy({
|
|
2675
|
+
query: {
|
|
2676
|
+
projectId: data.projectId,
|
|
2677
|
+
startsAt: QueryHelper.lessThanEqualTo(data.windowEnd),
|
|
2678
|
+
endsAt: QueryHelper.greaterThanEqualTo(data.windowStart),
|
|
2679
|
+
onCallDutyPolicyId: data.currentOnCallDutyPolicyId
|
|
2680
|
+
? QueryHelper.equalToOrNull(data.currentOnCallDutyPolicyId)
|
|
2681
|
+
: QueryHelper.isNull(),
|
|
2682
|
+
},
|
|
2683
|
+
select: {
|
|
2684
|
+
startsAt: true,
|
|
2685
|
+
endsAt: true,
|
|
2686
|
+
overrideUserId: true,
|
|
2687
|
+
routeAlertsToUserId: true,
|
|
2688
|
+
onCallDutyPolicyId: true,
|
|
2689
|
+
},
|
|
2690
|
+
sort: {
|
|
2691
|
+
startsAt: SortOrder.Ascending,
|
|
2692
|
+
},
|
|
2693
|
+
limit: LIMIT_PER_PROJECT,
|
|
2694
|
+
skip: 0,
|
|
2695
|
+
props: {
|
|
2696
|
+
isRoot: true,
|
|
2697
|
+
},
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2700
|
+
const scheduleUserIdSet: Set<string> = new Set<string>(
|
|
2701
|
+
data.scheduleUserIds.map((id: ObjectID) => {
|
|
2702
|
+
return id.toString();
|
|
2703
|
+
}),
|
|
2704
|
+
);
|
|
2705
|
+
|
|
2706
|
+
return overrides
|
|
2707
|
+
.filter((o: OnCallDutyPolicyUserOverride) => {
|
|
2708
|
+
const overrideUserId: string = o.overrideUserId?.toString() || "";
|
|
2709
|
+
return scheduleUserIdSet.has(overrideUserId);
|
|
2710
|
+
})
|
|
2711
|
+
.map((o: OnCallDutyPolicyUserOverride): UserOverrideRecord => {
|
|
2712
|
+
return {
|
|
2713
|
+
overrideUserId: o.overrideUserId?.toString() || "",
|
|
2714
|
+
routeAlertsToUserId: o.routeAlertsToUserId?.toString() || "",
|
|
2715
|
+
startsAt: o.startsAt!,
|
|
2716
|
+
endsAt: o.endsAt!,
|
|
2717
|
+
onCallDutyPolicyId: o.onCallDutyPolicyId?.toString() || null,
|
|
2718
|
+
};
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
/*
|
|
2723
|
+
* Compute how far out to resolve calendar events. Returns at least 1 year
|
|
2724
|
+
* from `from`, but extends far enough to contain (getNumberOfEvents + 1)
|
|
2725
|
+
* rotation periods for the slowest layer, so multi-year rotations still yield
|
|
2726
|
+
* a current and next event.
|
|
2727
|
+
*/
|
|
2728
|
+
private computeResolutionWindowEnd(
|
|
2729
|
+
layerProps: Array<LayerProps>,
|
|
2730
|
+
from: Date,
|
|
2731
|
+
getNumberOfEvents: number,
|
|
2732
|
+
): Date {
|
|
2733
|
+
const periodsNeeded: number = Math.max(2, getNumberOfEvents + 1);
|
|
2734
|
+
|
|
2735
|
+
let windowEnd: Date = from;
|
|
2736
|
+
let anyLayerRestricted: boolean = false;
|
|
2737
|
+
|
|
2738
|
+
for (const layer of layerProps) {
|
|
2739
|
+
if (this.isLayerRestricted(layer)) {
|
|
2740
|
+
anyLayerRestricted = true;
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
if (!layer.rotation) {
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
let recurring: Recurring;
|
|
2748
|
+
try {
|
|
2749
|
+
recurring =
|
|
2750
|
+
layer.rotation instanceof Recurring
|
|
2751
|
+
? layer.rotation
|
|
2752
|
+
: Recurring.fromJSON(layer.rotation as any);
|
|
2753
|
+
} catch {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
let candidate: Date = from;
|
|
2758
|
+
for (let i: number = 0; i < periodsNeeded; i++) {
|
|
2759
|
+
candidate = Recurring.getNextDateInterval(candidate, recurring);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
if (OneUptimeDate.isAfter(candidate, windowEnd)) {
|
|
2763
|
+
windowEnd = candidate;
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/*
|
|
2768
|
+
* Floor the window. Restricted layers can have long coverage gaps (a
|
|
2769
|
+
* weekend-only or business-hours restriction), so the next COVERED event may
|
|
2770
|
+
* be far past the naive (getNumberOfEvents + 1)-period horizon — keep a
|
|
2771
|
+
* generous 1-year floor for them so a current/next user is still found.
|
|
2772
|
+
* UNRESTRICTED layers produce exactly one event per rotation period, so the
|
|
2773
|
+
* per-layer candidate above already spans the events we need; flooring those
|
|
2774
|
+
* at a full year forced a fast (hourly/daily) rotation to materialize
|
|
2775
|
+
* thousands of events on every resolution, and getMultiLayerEvents then ran
|
|
2776
|
+
* an O(n^2) overlap merge over them — a real worker stall (audit H2). For
|
|
2777
|
+
* unrestricted layers a small 1-day floor is ample margin.
|
|
2778
|
+
*/
|
|
2779
|
+
const floor: Date = anyLayerRestricted
|
|
2780
|
+
? OneUptimeDate.addRemoveYears(from, 1)
|
|
2781
|
+
: OneUptimeDate.addRemoveDays(from, 1);
|
|
2782
|
+
|
|
2783
|
+
if (OneUptimeDate.isBefore(windowEnd, floor)) {
|
|
2784
|
+
windowEnd = floor;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
return windowEnd;
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
/*
|
|
2791
|
+
* True when the layer carries an active (non-None) restriction. Handles both a
|
|
2792
|
+
* hydrated RestrictionTimes instance (exposes `restictionType`) and the raw
|
|
2793
|
+
* serialized JSON form ({ _type, value: { restictionType } }) so it is safe
|
|
2794
|
+
* regardless of where the LayerProps came from.
|
|
2795
|
+
*/
|
|
2796
|
+
private isLayerRestricted(layer: LayerProps): boolean {
|
|
2797
|
+
const rt: unknown = layer.restrictionTimes;
|
|
2798
|
+
if (!rt) {
|
|
2799
|
+
return false;
|
|
2800
|
+
}
|
|
2801
|
+
const anyRt: {
|
|
2802
|
+
restictionType?: RestrictionType | string;
|
|
2803
|
+
value?: { restictionType?: RestrictionType | string };
|
|
2804
|
+
} = rt as {
|
|
1516
2805
|
restictionType?: RestrictionType | string;
|
|
1517
2806
|
value?: { restictionType?: RestrictionType | string };
|
|
1518
2807
|
};
|
|
@@ -1562,6 +2851,48 @@ export class Service extends DatabaseService<OnCallDutyPolicySchedule> {
|
|
|
1562
2851
|
return windowStart;
|
|
1563
2852
|
}
|
|
1564
2853
|
|
|
2854
|
+
/*
|
|
2855
|
+
* Mirror of computeResolutionWindowStart, stepping FORWARDS: the latest of
|
|
2856
|
+
* `from` plus `periodsForward` rotation periods across the layers (at least
|
|
2857
|
+
* a day). The calendar-feed resolver widens its expansion this far past the
|
|
2858
|
+
* requested window so the last overlapping segment carries its true end
|
|
2859
|
+
* instead of the engine's clamp to the window edge.
|
|
2860
|
+
*/
|
|
2861
|
+
private computeResolutionWindowEndByPeriods(
|
|
2862
|
+
layerProps: Array<LayerProps>,
|
|
2863
|
+
from: Date,
|
|
2864
|
+
periodsForward: number,
|
|
2865
|
+
): Date {
|
|
2866
|
+
let windowEnd: Date = OneUptimeDate.addRemoveDays(from, 1);
|
|
2867
|
+
|
|
2868
|
+
for (const layer of layerProps) {
|
|
2869
|
+
if (!layer.rotation) {
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
let recurring: Recurring;
|
|
2874
|
+
try {
|
|
2875
|
+
recurring =
|
|
2876
|
+
layer.rotation instanceof Recurring
|
|
2877
|
+
? layer.rotation
|
|
2878
|
+
: Recurring.fromJSON(layer.rotation as any);
|
|
2879
|
+
} catch {
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
let candidate: Date = from;
|
|
2884
|
+
for (let i: number = 0; i < periodsForward; i++) {
|
|
2885
|
+
candidate = Recurring.getNextDateInterval(candidate, recurring);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (OneUptimeDate.isAfter(candidate, windowEnd)) {
|
|
2889
|
+
windowEnd = candidate;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
return windowEnd;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
1565
2896
|
/*
|
|
1566
2897
|
* Recover the TRUE start of the current on-call coverage window for
|
|
1567
2898
|
* `currentUserId`. getEventByIndexInSchedule always expands from `now`, and
|