@oneuptime/common 12.0.13 → 12.0.14
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/OnCallDutyPolicySchedule.ts +14 -0
- package/Models/DatabaseModels/UserCall.ts +94 -3
- package/Models/DatabaseModels/UserEmail.ts +94 -3
- package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
- package/Models/DatabaseModels/UserPush.ts +37 -0
- package/Models/DatabaseModels/UserSMS.ts +94 -3
- package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
- package/Server/API/BaseAPI.ts +79 -14
- package/Server/API/UserCallAPI.ts +53 -45
- package/Server/API/UserEmailAPI.ts +53 -44
- package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
- package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
- package/Server/API/UserPushAPI.ts +126 -2
- package/Server/API/UserSmsAPI.ts +53 -44
- package/Server/API/UserWhatsAppAPI.ts +53 -50
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
- package/Server/Services/AnalyticsDatabaseService.ts +169 -0
- package/Server/Services/DatabaseService.ts +101 -0
- package/Server/Services/LogAggregationService.ts +5 -0
- package/Server/Services/PushNotificationService.ts +127 -12
- package/Server/Services/TeamMemberService.ts +53 -13
- package/Server/Services/UserCallService.ts +55 -24
- package/Server/Services/UserEmailService.ts +83 -17
- package/Server/Services/UserIncomingCallNumberService.ts +49 -85
- package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
- package/Server/Services/UserNotificationRuleService.ts +26 -0
- package/Server/Services/UserPushService.ts +89 -0
- package/Server/Services/UserSmsService.ts +50 -17
- package/Server/Services/UserWhatsAppService.ts +48 -15
- package/Server/Types/Database/QueryHelper.ts +30 -0
- package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
- package/Server/Utils/ChannelVerification.ts +510 -0
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
- package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
- package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
- package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
- package/Server/Utils/TotpAuth.ts +137 -9
- package/Server/Utils/UserRegistrationToken.ts +182 -0
- package/Server/Utils/VerificationCode.ts +134 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
- package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
- package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
- package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
- package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
- package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
- package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
- package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
- package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
- package/Tests/Server/API/UserSmsApi.test.ts +107 -14
- package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
- package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
- package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
- package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
- package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
- package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
- package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
- package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
- package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
- package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
- package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
- package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
- package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
- package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
- package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
- package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
- package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
- package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
- package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
- package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
- package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
- package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
- package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
- package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
- package/Tests/Server/Utils/Browser.test.ts +162 -0
- package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
- package/Tests/Server/Utils/Express.test.ts +371 -0
- package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
- package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
- package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
- package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
- package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
- package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
- package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
- package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
- package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
- package/Tests/Types/Call/CallRequest.test.ts +189 -0
- package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
- package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
- package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
- package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
- package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
- package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
- package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
- package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
- package/Tests/Types/TextRandomGeneration.test.ts +211 -0
- package/Tests/UI/Components/Button.test.tsx +175 -1
- package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
- package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
- package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
- package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
- package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
- package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
- package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
- package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
- package/Tests/UI/Components/List.test.tsx +9 -2
- package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
- package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
- package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
- package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
- package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
- package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
- package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
- package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
- package/Tests/UI/Components/Skeleton.test.tsx +71 -0
- package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
- package/Tests/UI/Components/Tabs.test.tsx +24 -0
- package/Tests/UI/Components/Toast.test.tsx +130 -7
- package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
- package/Tests/UI/Utils/Dropdown.test.ts +423 -0
- package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
- package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
- package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
- package/Tests/Utils/API.test.ts +16 -2
- package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
- package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
- package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
- package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
- package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
- package/Types/Permission.ts +35 -0
- package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
- package/Types/PushNotification/PushNotificationMessage.ts +15 -0
- package/Types/PushNotification/PushNotificationRequest.ts +8 -16
- package/Types/Service/ServiceLanguage.ts +144 -0
- package/Types/Text.ts +111 -19
- package/UI/Components/Button/Button.tsx +27 -14
- package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
- package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
- package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
- package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
- package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
- package/UI/Components/CommandPalette/Types.ts +49 -0
- package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
- package/UI/Components/Dropdown/Dropdown.tsx +6 -1
- package/UI/Components/Feed/FeedItem.tsx +1 -1
- package/UI/Components/List/List.tsx +37 -5
- package/UI/Components/List/ListSkeleton.tsx +54 -0
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
- package/UI/Components/Modal/Modal.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
- package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
- package/UI/Components/Navbar/NavBar.tsx +14 -1
- package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
- package/UI/Components/SideOver/SideOver.tsx +37 -2
- package/UI/Components/Skeleton/Skeleton.tsx +53 -0
- package/UI/Components/Table/Table.tsx +74 -44
- package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
- package/UI/Components/Tabs/Tab.tsx +1 -1
- package/UI/Components/Toast/Toast.tsx +118 -53
- package/UI/Components/Toast/ToastInit.tsx +82 -32
- package/UI/Styles/Theme.css +40 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
- package/UI/Utils/ModelListCache.ts +188 -0
- package/UI/Utils/Project.ts +29 -0
- package/Utils/API.ts +6 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
- package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
- package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
- package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
- package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
- package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
- package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +69 -13
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +42 -38
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +42 -38
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
- package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
- package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
- package/build/dist/Server/API/UserPushAPI.js +95 -2
- package/build/dist/Server/API/UserPushAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +42 -38
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.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/VerificationCodeRateLimit.js +393 -0
- package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +74 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +5 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/PushNotificationService.js +69 -30
- package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +43 -7
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Services/UserCallService.js +54 -22
- package/build/dist/Server/Services/UserCallService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +78 -17
- package/build/dist/Server/Services/UserEmailService.js.map +1 -1
- package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
- package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
- package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
- package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Services/UserPushService.js +77 -0
- package/build/dist/Server/Services/UserPushService.js.map +1 -1
- package/build/dist/Server/Services/UserSmsService.js +50 -17
- package/build/dist/Server/Services/UserSmsService.js.map +1 -1
- package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
- package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
- package/build/dist/Server/Utils/ChannelVerification.js +348 -0
- package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/TotpAuth.js +127 -8
- package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
- package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
- package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
- package/build/dist/Server/Utils/VerificationCode.js +120 -0
- package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
- package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
- package/build/dist/Types/Permission.js +33 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
- package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
- package/build/dist/Types/Service/ServiceLanguage.js +96 -0
- package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
- package/build/dist/Types/Text.js +82 -17
- package/build/dist/Types/Text.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +22 -14
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
- package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
- package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
- package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
- package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
- package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
- package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
- package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
- package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
- package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
- package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
- package/build/dist/UI/Components/List/List.js +19 -5
- package/build/dist/UI/Components/List/List.js.map +1 -1
- package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
- package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +2 -2
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
- package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
- package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
- package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
- package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
- package/build/dist/UI/Components/Table/Table.js +35 -24
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
- package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
- package/build/dist/UI/Components/Tabs/Tab.js +1 -1
- package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
- package/build/dist/UI/Components/Toast/Toast.js +77 -23
- package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
- package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
- package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelListCache.js +126 -0
- package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
- package/build/dist/UI/Utils/Project.js +24 -1
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/Utils/API.js +6 -1
- package/build/dist/Utils/API.js.map +1 -1
- package/jest.config.json +1 -1
- package/package.json +2 -3
- package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
- package/Tests/__mocks__/otpauth.js +0 -30
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
- package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js.map +0 -1
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import AnalyticsDatabaseService from "../../../Server/Services/AnalyticsDatabaseService";
|
|
2
|
+
import {
|
|
3
|
+
SQL,
|
|
4
|
+
Statement,
|
|
5
|
+
} from "../../../Server/Utils/AnalyticsDatabase/Statement";
|
|
6
|
+
import logger from "../../../Server/Utils/Logger";
|
|
7
|
+
import "../TestingUtils/Init";
|
|
8
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
9
|
+
import Route from "../../../Types/API/Route";
|
|
10
|
+
import AnalyticsTableEngine from "../../../Types/AnalyticsDatabase/AnalyticsTableEngine";
|
|
11
|
+
import AnalyticsTableColumn from "../../../Types/AnalyticsDatabase/TableColumn";
|
|
12
|
+
import TableColumnType from "../../../Types/AnalyticsDatabase/TableColumnType";
|
|
13
|
+
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
14
|
+
import GenericObject from "../../../Types/GenericObject";
|
|
15
|
+
import {
|
|
16
|
+
describe,
|
|
17
|
+
expect,
|
|
18
|
+
beforeEach,
|
|
19
|
+
afterEach,
|
|
20
|
+
test,
|
|
21
|
+
jest,
|
|
22
|
+
} from "@jest/globals";
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* The sort-key boundary filter.
|
|
26
|
+
*
|
|
27
|
+
* ClickHouse reads in sorting-key order — and stops early — only when the
|
|
28
|
+
* ORDER BY it is handed lines up with the table's physical key. The `_id`
|
|
29
|
+
* tiebreaker appended by toPaginationStableSort is deliberately NOT a
|
|
30
|
+
* sorting-key column, so it denies that plan: the engine reads every row
|
|
31
|
+
* matching the WHERE, sorts the lot in memory, and discards all but one page.
|
|
32
|
+
* On the Logs viewer that turned a 100-row page into a full-window scan and
|
|
33
|
+
* tripped the 3 GiB max_memory_usage ceiling (Code 241, surfacing to users as
|
|
34
|
+
* "Server Error").
|
|
35
|
+
*
|
|
36
|
+
* Measured on ClickHouse 26.7 against a 3M-row table shaped like LogItemV3
|
|
37
|
+
* (140 rows per timestamp, fat attributes Map), one 100-row page:
|
|
38
|
+
*
|
|
39
|
+
* ORDER BY time DESC 107k rows read, 14.11 MiB peak
|
|
40
|
+
* ORDER BY time DESC, _id DESC 1.91M rows read, 114.92 MiB peak
|
|
41
|
+
* ... with the boundary filter 114k rows read, 8.68 MiB peak
|
|
42
|
+
*
|
|
43
|
+
* The fix does NOT weaken the tiebreaker — that would reintroduce the
|
|
44
|
+
* skip/repeat paging bug it was added for. It bounds the rows the sort has to
|
|
45
|
+
* consider, using a predicate that is result-preserving by construction:
|
|
46
|
+
* `min(k)` over the top `skip + limit` values of the leading sort key is a
|
|
47
|
+
* rank statistic of k's MULTISET, so it does not depend on which of several
|
|
48
|
+
* tied rows the inner LIMIT kept, and every row tied at the boundary value is
|
|
49
|
+
* admitted.
|
|
50
|
+
*
|
|
51
|
+
* Because the predicate cannot change results, every gate below is a
|
|
52
|
+
* PERFORMANCE gate: getting one wrong makes a query slower, never wrong. The
|
|
53
|
+
* suites assert that (a) the bound is emitted where it pays, (b) it is
|
|
54
|
+
* withheld where it cannot be proven safe or cannot help, and (c) the page
|
|
55
|
+
* the caller gets back is unchanged either way.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
const TIME_COLUMN: string = "time";
|
|
59
|
+
const PROJECT_ID_COLUMN: string = "projectId";
|
|
60
|
+
const BODY_COLUMN: string = "body";
|
|
61
|
+
const SEVERITY_COLUMN: string = "severityText";
|
|
62
|
+
const NULLABLE_SORT_COLUMN: string = "endedAt";
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* Mirrors LogItemV3: sorting key (projectId, time, primaryEntityId), a
|
|
66
|
+
* non-key column the table header can sort by, and a nullable column that
|
|
67
|
+
* is a legal sort target but NOT safe to bound on.
|
|
68
|
+
*/
|
|
69
|
+
class LogLikeModel extends AnalyticsBaseModel {
|
|
70
|
+
public constructor() {
|
|
71
|
+
super({
|
|
72
|
+
tableName: "<log-like-table>",
|
|
73
|
+
singularName: "<singular-name>",
|
|
74
|
+
pluralName: "<plural-name>",
|
|
75
|
+
tableColumns: [
|
|
76
|
+
new AnalyticsTableColumn({
|
|
77
|
+
key: PROJECT_ID_COLUMN,
|
|
78
|
+
title: "<title>",
|
|
79
|
+
description: "<description>",
|
|
80
|
+
required: true,
|
|
81
|
+
type: TableColumnType.ObjectID,
|
|
82
|
+
}),
|
|
83
|
+
new AnalyticsTableColumn({
|
|
84
|
+
key: TIME_COLUMN,
|
|
85
|
+
title: "<title>",
|
|
86
|
+
description: "<description>",
|
|
87
|
+
required: true,
|
|
88
|
+
type: TableColumnType.DateTime64,
|
|
89
|
+
}),
|
|
90
|
+
new AnalyticsTableColumn({
|
|
91
|
+
key: SEVERITY_COLUMN,
|
|
92
|
+
title: "<title>",
|
|
93
|
+
description: "<description>",
|
|
94
|
+
required: true,
|
|
95
|
+
type: TableColumnType.Text,
|
|
96
|
+
}),
|
|
97
|
+
new AnalyticsTableColumn({
|
|
98
|
+
key: NULLABLE_SORT_COLUMN,
|
|
99
|
+
title: "<title>",
|
|
100
|
+
description: "<description>",
|
|
101
|
+
required: false,
|
|
102
|
+
type: TableColumnType.DateTime64,
|
|
103
|
+
}),
|
|
104
|
+
new AnalyticsTableColumn({
|
|
105
|
+
key: BODY_COLUMN,
|
|
106
|
+
title: "<title>",
|
|
107
|
+
description: "<description>",
|
|
108
|
+
required: false,
|
|
109
|
+
type: TableColumnType.Text,
|
|
110
|
+
}),
|
|
111
|
+
],
|
|
112
|
+
crudApiPath: new Route("route"),
|
|
113
|
+
primaryKeys: [PROJECT_ID_COLUMN, TIME_COLUMN],
|
|
114
|
+
/*
|
|
115
|
+
* `endedAt` is deliberately IN sortKeys so the nullable-column test
|
|
116
|
+
* isolates the `required` gate rather than passing for the wrong
|
|
117
|
+
* reason (failing the sortKeys membership gate instead).
|
|
118
|
+
*/
|
|
119
|
+
sortKeys: [PROJECT_ID_COLUMN, TIME_COLUMN, NULLABLE_SORT_COLUMN],
|
|
120
|
+
partitionKey: TIME_COLUMN,
|
|
121
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
122
|
+
defaultSortColumn: TIME_COLUMN,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/*
|
|
128
|
+
* Stands in for a derived aggregate target. `includeBaseColumns: false`
|
|
129
|
+
* omits `_id`, so there is no off-key tiebreaker to pay for and nothing to
|
|
130
|
+
* bound.
|
|
131
|
+
*/
|
|
132
|
+
class NoIdModel extends AnalyticsBaseModel {
|
|
133
|
+
public constructor() {
|
|
134
|
+
super({
|
|
135
|
+
tableName: "<no-id-table>",
|
|
136
|
+
singularName: "<singular-name>",
|
|
137
|
+
pluralName: "<plural-name>",
|
|
138
|
+
includeBaseColumns: false,
|
|
139
|
+
tableColumns: [
|
|
140
|
+
new AnalyticsTableColumn({
|
|
141
|
+
key: TIME_COLUMN,
|
|
142
|
+
title: "<title>",
|
|
143
|
+
description: "<description>",
|
|
144
|
+
required: true,
|
|
145
|
+
type: TableColumnType.DateTime64,
|
|
146
|
+
}),
|
|
147
|
+
],
|
|
148
|
+
crudApiPath: new Route("route"),
|
|
149
|
+
primaryKeys: [TIME_COLUMN],
|
|
150
|
+
sortKeys: [TIME_COLUMN],
|
|
151
|
+
partitionKey: TIME_COLUMN,
|
|
152
|
+
tableEngine: AnalyticsTableEngine.MergeTree,
|
|
153
|
+
defaultSortColumn: TIME_COLUMN,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/*
|
|
159
|
+
* The boundary subquery is the only place a find statement nests a SELECT,
|
|
160
|
+
* so its presence is the signal that the bound was emitted.
|
|
161
|
+
*/
|
|
162
|
+
function hasBoundary(statement: Statement): boolean {
|
|
163
|
+
return statement.query.includes("(SELECT ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* The bound sits in the WHERE, so it precedes the outer ORDER BY. */
|
|
167
|
+
function boundaryClause(statement: Statement): string {
|
|
168
|
+
const query: string = statement.query;
|
|
169
|
+
return query.slice(query.indexOf(" AND "), query.lastIndexOf("ORDER BY "));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function outerOrderBy(statement: Statement): string {
|
|
173
|
+
return statement.query
|
|
174
|
+
.slice(statement.query.lastIndexOf("ORDER BY "))
|
|
175
|
+
.replace("ORDER BY ", "")
|
|
176
|
+
.split(" LIMIT")[0]!;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Resolves rendered `{pN:Identifier}` terms back to the bound column names. */
|
|
180
|
+
function orderByColumns(statement: Statement): Array<string> {
|
|
181
|
+
return outerOrderBy(statement)
|
|
182
|
+
.split(", ")
|
|
183
|
+
.map((term: string): string => {
|
|
184
|
+
const param: string = term.split(":")[0]!.replace("{", "");
|
|
185
|
+
return statement.query_params[param] as string;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function paramValues(statement: Statement): Array<unknown> {
|
|
190
|
+
return Object.values(statement.query_params);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
describe("AnalyticsDatabaseService sort key boundary", () => {
|
|
194
|
+
let service: AnalyticsDatabaseService<LogLikeModel>;
|
|
195
|
+
|
|
196
|
+
type FindArgs = {
|
|
197
|
+
sort?: GenericObject | undefined;
|
|
198
|
+
query?: GenericObject | undefined;
|
|
199
|
+
groupBy?: GenericObject | undefined;
|
|
200
|
+
limit?: unknown;
|
|
201
|
+
skip?: unknown;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const runFind: (args: FindArgs) => Statement = (
|
|
205
|
+
args: FindArgs,
|
|
206
|
+
): Statement => {
|
|
207
|
+
return service.toFindStatement({
|
|
208
|
+
select: {} as GenericObject,
|
|
209
|
+
query: args.query ?? ({} as GenericObject),
|
|
210
|
+
props: {} as GenericObject,
|
|
211
|
+
sort: args.sort ?? { [TIME_COLUMN]: SortOrder.Descending },
|
|
212
|
+
groupBy: args.groupBy,
|
|
213
|
+
/*
|
|
214
|
+
* `in` rather than a nullish default, so a test can pass an EXPLICIT
|
|
215
|
+
* undefined limit/skip and actually exercise that path.
|
|
216
|
+
*/
|
|
217
|
+
limit: "limit" in args ? args.limit : 50,
|
|
218
|
+
skip: "skip" in args ? args.skip : 0,
|
|
219
|
+
} as never).statement;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
service = new AnalyticsDatabaseService({ modelType: LogLikeModel });
|
|
224
|
+
|
|
225
|
+
service.statementGenerator.toSelectStatement = jest.fn(() => {
|
|
226
|
+
return { statement: SQL`<select>`, columns: ["<column>"] };
|
|
227
|
+
});
|
|
228
|
+
service.statementGenerator.toWhereStatement = jest.fn(() => {
|
|
229
|
+
return SQL`<where>`;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
jest.spyOn(logger, "debug").mockImplementation(() => {
|
|
233
|
+
return undefined!;
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
afterEach(() => {
|
|
238
|
+
jest.restoreAllMocks();
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("emitted shape", () => {
|
|
242
|
+
test("bounds the scan by the leading sort key", () => {
|
|
243
|
+
const statement: Statement = runFind({
|
|
244
|
+
sort: { [TIME_COLUMN]: SortOrder.Descending },
|
|
245
|
+
limit: 50,
|
|
246
|
+
skip: 0,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
expect(hasBoundary(statement)).toBe(true);
|
|
250
|
+
expect(boundaryClause(statement)).toContain("min(");
|
|
251
|
+
expect(paramValues(statement)).toContain(TIME_COLUMN);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("the bound is a lower bound for a descending sort", () => {
|
|
255
|
+
const clause: string = boundaryClause(
|
|
256
|
+
runFind({ sort: { [TIME_COLUMN]: SortOrder.Descending } }),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
expect(clause).toContain(">=");
|
|
260
|
+
expect(clause).toContain("min(");
|
|
261
|
+
expect(clause).not.toContain("max(");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("the bound flips to an upper bound for an ascending sort", () => {
|
|
265
|
+
const clause: string = boundaryClause(
|
|
266
|
+
runFind({ sort: { [TIME_COLUMN]: SortOrder.Ascending } }),
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
expect(clause).toContain("<=");
|
|
270
|
+
expect(clause).toContain("max(");
|
|
271
|
+
expect(clause).not.toContain("min(");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("the inner sort direction matches the outer one", () => {
|
|
275
|
+
expect(
|
|
276
|
+
boundaryClause(
|
|
277
|
+
runFind({ sort: { [TIME_COLUMN]: SortOrder.Descending } }),
|
|
278
|
+
),
|
|
279
|
+
).toContain("DESC");
|
|
280
|
+
expect(
|
|
281
|
+
boundaryClause(
|
|
282
|
+
runFind({ sort: { [TIME_COLUMN]: SortOrder.Ascending } }),
|
|
283
|
+
),
|
|
284
|
+
).toContain("ASC");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* The bound admits every row that can reach position skip + limit. A
|
|
289
|
+
* short inner LIMIT would cut the page off; a long one wastes the read.
|
|
290
|
+
*/
|
|
291
|
+
test("the inner LIMIT is skip + limit", () => {
|
|
292
|
+
expect(paramValues(runFind({ limit: 50, skip: 100 }))).toContain(150);
|
|
293
|
+
expect(paramValues(runFind({ limit: 100, skip: 0 }))).toContain(100);
|
|
294
|
+
expect(paramValues(runFind({ limit: 25, skip: 975 }))).toContain(1000);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
/*
|
|
298
|
+
* The whole point: the tiebreaker that makes paging deterministic is
|
|
299
|
+
* still there, and still last. The bound only makes it affordable.
|
|
300
|
+
*/
|
|
301
|
+
test("the _id tiebreaker is untouched", () => {
|
|
302
|
+
const statement: Statement = runFind({
|
|
303
|
+
sort: { [TIME_COLUMN]: SortOrder.Descending },
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(orderByColumns(statement)).toStrictEqual([TIME_COLUMN, "_id"]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("the bound sits in the WHERE, before the outer ORDER BY", () => {
|
|
310
|
+
const query: string = runFind({}).query;
|
|
311
|
+
|
|
312
|
+
expect(query.indexOf("(SELECT ")).toBeLessThan(
|
|
313
|
+
query.lastIndexOf("ORDER BY "),
|
|
314
|
+
);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
* A find carries exactly one bound. A second nested SELECT would mean
|
|
319
|
+
* the filter was appended twice and the parameters bound twice.
|
|
320
|
+
*/
|
|
321
|
+
test("emits exactly one boundary subquery", () => {
|
|
322
|
+
const query: string = runFind({}).query;
|
|
323
|
+
|
|
324
|
+
expect(query.split("(SELECT ").length - 1).toBe(2);
|
|
325
|
+
expect(query.split("min(").length - 1).toBe(1);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
/* The bound must read the same rows the outer query filters on. */
|
|
329
|
+
test("the boundary subquery repeats the caller's WHERE", () => {
|
|
330
|
+
const query: string = runFind({}).query;
|
|
331
|
+
|
|
332
|
+
expect(query.split("<where>").length - 1).toBe(2);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("renders valid parameter placeholders throughout", () => {
|
|
336
|
+
const statement: Statement = runFind({ limit: 50, skip: 100 });
|
|
337
|
+
const placeholders: Array<string> =
|
|
338
|
+
statement.query.match(/\{p\d+:[A-Za-z0-9]+\}/g) ?? [];
|
|
339
|
+
|
|
340
|
+
expect(placeholders.length).toBeGreaterThan(0);
|
|
341
|
+
|
|
342
|
+
for (const placeholder of placeholders) {
|
|
343
|
+
const name: string = placeholder.slice(1).split(":")[0]!;
|
|
344
|
+
expect(statement.query_params).toHaveProperty(name);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe("gates: the bound is withheld when it cannot help or cannot be proven safe", () => {
|
|
350
|
+
/*
|
|
351
|
+
* Under GROUP BY the predicate would restrict the aggregation INPUT
|
|
352
|
+
* rather than the page, which WOULD change results.
|
|
353
|
+
*/
|
|
354
|
+
test("no bound on a GROUP BY query", () => {
|
|
355
|
+
const statement: Statement = runFind({
|
|
356
|
+
groupBy: { [SEVERITY_COLUMN]: true },
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
/*
|
|
363
|
+
* A non-required column is Nullable in the DDL, and `k >= NULL` is
|
|
364
|
+
* NULL — the bound would DROP those rows rather than narrow the scan.
|
|
365
|
+
* `endedAt` IS in sortKeys, so only the `required` gate can stop this.
|
|
366
|
+
*/
|
|
367
|
+
test("no bound on a nullable leading sort column", () => {
|
|
368
|
+
const statement: Statement = runFind({
|
|
369
|
+
sort: { [NULLABLE_SORT_COLUMN]: SortOrder.Descending },
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
/*
|
|
376
|
+
* Off-key leads already plan a bounded top-N, so the extra pass would
|
|
377
|
+
* be pure waste.
|
|
378
|
+
*/
|
|
379
|
+
test("no bound when the leading sort key is not in sortKeys", () => {
|
|
380
|
+
const statement: Statement = runFind({
|
|
381
|
+
sort: { [SEVERITY_COLUMN]: SortOrder.Descending },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
test("no bound when the model has no _id column to pay for", () => {
|
|
388
|
+
const noIdService: AnalyticsDatabaseService<NoIdModel> =
|
|
389
|
+
new AnalyticsDatabaseService({ modelType: NoIdModel });
|
|
390
|
+
|
|
391
|
+
noIdService.statementGenerator.toSelectStatement = jest.fn(() => {
|
|
392
|
+
return { statement: SQL`<select>`, columns: ["<column>"] };
|
|
393
|
+
});
|
|
394
|
+
noIdService.statementGenerator.toWhereStatement = jest.fn(() => {
|
|
395
|
+
return SQL`<where>`;
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const { statement } = noIdService.toFindStatement({
|
|
399
|
+
select: {} as GenericObject,
|
|
400
|
+
query: {} as GenericObject,
|
|
401
|
+
props: {} as GenericObject,
|
|
402
|
+
sort: { [TIME_COLUMN]: SortOrder.Descending },
|
|
403
|
+
limit: 50,
|
|
404
|
+
skip: 0,
|
|
405
|
+
} as never);
|
|
406
|
+
|
|
407
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
/*
|
|
411
|
+
* findOneById funnels a point lookup through findOneBy carrying the
|
|
412
|
+
* model's default sort. At most one row matches, so bounding it would
|
|
413
|
+
* double a query with no ordering problem.
|
|
414
|
+
*/
|
|
415
|
+
test("no bound when the query already pins _id", () => {
|
|
416
|
+
const statement: Statement = runFind({
|
|
417
|
+
query: { _id: "<some-id>" } as GenericObject,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("no bound on an empty sort", () => {
|
|
424
|
+
const statement: Statement = runFind({ sort: {} as GenericObject });
|
|
425
|
+
|
|
426
|
+
expect(hasBoundary(statement)).toBe(false);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
/*
|
|
430
|
+
* The predicate is valid only BECAUSE of LIMIT/OFFSET. A non-finite
|
|
431
|
+
* bound would admit an unbounded set, so fail back to today's SQL.
|
|
432
|
+
*/
|
|
433
|
+
test.each([
|
|
434
|
+
["NaN limit", NaN, 0],
|
|
435
|
+
["NaN skip", 50, NaN],
|
|
436
|
+
["undefined limit", undefined, 0],
|
|
437
|
+
["Infinity limit", Infinity, 0],
|
|
438
|
+
["zero limit with zero skip", 0, 0],
|
|
439
|
+
["negative limit", -10, 0],
|
|
440
|
+
])("no bound for %s", (_label: string, limit: unknown, skip: unknown) => {
|
|
441
|
+
expect(hasBoundary(runFind({ limit, skip }))).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
/*
|
|
445
|
+
* Every gate returns today's statement verbatim, so a withheld bound is
|
|
446
|
+
* indistinguishable from the pre-fix SQL.
|
|
447
|
+
*/
|
|
448
|
+
test("a withheld bound leaves the statement byte-identical to the unbounded form", () => {
|
|
449
|
+
const bounded: Statement = runFind({
|
|
450
|
+
sort: { [TIME_COLUMN]: SortOrder.Descending },
|
|
451
|
+
});
|
|
452
|
+
const withheld: Statement = runFind({
|
|
453
|
+
sort: { [SEVERITY_COLUMN]: SortOrder.Descending },
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
expect(hasBoundary(bounded)).toBe(true);
|
|
457
|
+
expect(withheld.query).not.toContain("(SELECT ");
|
|
458
|
+
expect(withheld.query).toContain("ORDER BY ");
|
|
459
|
+
expect(orderByColumns(withheld)).toStrictEqual([SEVERITY_COLUMN, "_id"]);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
/*
|
|
464
|
+
* The bound is a rank statistic over the leading key's MULTISET, so it
|
|
465
|
+
* cannot depend on tie order and cannot exclude a row the unbounded query
|
|
466
|
+
* would have returned. These tests model that directly: they page a fixed
|
|
467
|
+
* dataset with and without the bound, under adversarial tie orderings,
|
|
468
|
+
* and require the pages to match exactly.
|
|
469
|
+
*
|
|
470
|
+
* Mirrors the "page boundary behaviour" suite in
|
|
471
|
+
* AnalyticsDatabasePaginationStability.test.ts, which models the engine's
|
|
472
|
+
* freedom to reorder ties rather than asserting SQL text.
|
|
473
|
+
*/
|
|
474
|
+
describe("result preservation", () => {
|
|
475
|
+
type Row = { time: number; _id: string };
|
|
476
|
+
|
|
477
|
+
/* 12 rows over 3 timestamps, 4 tied on each. */
|
|
478
|
+
const ROWS: Array<Row> = [
|
|
479
|
+
{ time: 300, _id: "a1" },
|
|
480
|
+
{ time: 300, _id: "a2" },
|
|
481
|
+
{ time: 300, _id: "a3" },
|
|
482
|
+
{ time: 300, _id: "a4" },
|
|
483
|
+
{ time: 200, _id: "b1" },
|
|
484
|
+
{ time: 200, _id: "b2" },
|
|
485
|
+
{ time: 200, _id: "b3" },
|
|
486
|
+
{ time: 200, _id: "b4" },
|
|
487
|
+
{ time: 100, _id: "c1" },
|
|
488
|
+
{ time: 100, _id: "c2" },
|
|
489
|
+
{ time: 100, _id: "c3" },
|
|
490
|
+
{ time: 100, _id: "c4" },
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
/*
|
|
494
|
+
* What the engine is ALLOWED to do: return rows in any order that is
|
|
495
|
+
* consistent with the requested sort. `tieSeed` picks a different legal
|
|
496
|
+
* tie ordering per fetch, standing in for part order and thread
|
|
497
|
+
* scheduling varying between executions.
|
|
498
|
+
*/
|
|
499
|
+
function scan(rows: Array<Row>, tieSeed: number): Array<Row> {
|
|
500
|
+
const shuffled: Array<Row> = [...rows];
|
|
501
|
+
|
|
502
|
+
/* Rotate within each tie group so the order differs per fetch. */
|
|
503
|
+
const byTime: Map<number, Array<Row>> = new Map();
|
|
504
|
+
for (const row of shuffled) {
|
|
505
|
+
byTime.set(row.time, [...(byTime.get(row.time) ?? []), row]);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const out: Array<Row> = [];
|
|
509
|
+
for (const time of [...byTime.keys()].sort((a: number, b: number) => {
|
|
510
|
+
return b - a;
|
|
511
|
+
})) {
|
|
512
|
+
const group: Array<Row> = byTime.get(time)!;
|
|
513
|
+
const offset: number = tieSeed % group.length;
|
|
514
|
+
out.push(...group.slice(offset), ...group.slice(0, offset));
|
|
515
|
+
}
|
|
516
|
+
return out;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* ORDER BY time DESC, _id DESC — a total order, tie-seed independent. */
|
|
520
|
+
function sortTotal(rows: Array<Row>): Array<Row> {
|
|
521
|
+
return [...rows].sort((left: Row, right: Row) => {
|
|
522
|
+
return right.time - left.time || right._id.localeCompare(left._id);
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/* Today's SQL: sort everything the WHERE matched, then slice. */
|
|
527
|
+
function unboundedPage(
|
|
528
|
+
skip: number,
|
|
529
|
+
limit: number,
|
|
530
|
+
tieSeed: number,
|
|
531
|
+
): Array<Row> {
|
|
532
|
+
return sortTotal(scan(ROWS, tieSeed)).slice(skip, skip + limit);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/*
|
|
536
|
+
* The fix: min(time) over the top skip+limit rows by time DESC, admit
|
|
537
|
+
* every row at or after it, then apply the same total order and slice.
|
|
538
|
+
*/
|
|
539
|
+
function boundedPage(
|
|
540
|
+
skip: number,
|
|
541
|
+
limit: number,
|
|
542
|
+
tieSeed: number,
|
|
543
|
+
): Array<Row> {
|
|
544
|
+
const scanned: Array<Row> = scan(ROWS, tieSeed);
|
|
545
|
+
const topN: Array<Row> = scanned.slice(0, skip + limit);
|
|
546
|
+
|
|
547
|
+
if (topN.length === 0) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const boundary: number = Math.min(
|
|
552
|
+
...topN.map((row: Row): number => {
|
|
553
|
+
return row.time;
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
const admitted: Array<Row> = scanned.filter((row: Row): boolean => {
|
|
558
|
+
return row.time >= boundary;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
return sortTotal(admitted).slice(skip, skip + limit);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
test("the bound admits every row tied at the boundary value", () => {
|
|
565
|
+
/* Top 2 rows by time DESC both sit at 300; the bound is 300. */
|
|
566
|
+
const scanned: Array<Row> = scan(ROWS, 0);
|
|
567
|
+
const boundary: number = Math.min(
|
|
568
|
+
...scanned.slice(0, 2).map((row: Row): number => {
|
|
569
|
+
return row.time;
|
|
570
|
+
}),
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
const admitted: Array<Row> = scanned.filter((row: Row): boolean => {
|
|
574
|
+
return row.time >= boundary;
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
/* All FOUR rows at t=300 survive, not just the two that were scanned. */
|
|
578
|
+
expect(admitted).toHaveLength(4);
|
|
579
|
+
expect(
|
|
580
|
+
admitted
|
|
581
|
+
.map((row: Row): string => {
|
|
582
|
+
return row._id;
|
|
583
|
+
})
|
|
584
|
+
.sort(),
|
|
585
|
+
).toStrictEqual(["a1", "a2", "a3", "a4"]);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test.each([
|
|
589
|
+
[0, 5],
|
|
590
|
+
[5, 5],
|
|
591
|
+
[10, 5],
|
|
592
|
+
[0, 1],
|
|
593
|
+
[3, 4],
|
|
594
|
+
[11, 1],
|
|
595
|
+
])(
|
|
596
|
+
"bounded and unbounded pages are identical (skip %i, limit %i)",
|
|
597
|
+
(skip: number, limit: number) => {
|
|
598
|
+
for (let tieSeed: number = 0; tieSeed < 4; tieSeed++) {
|
|
599
|
+
expect(boundedPage(skip, limit, tieSeed)).toStrictEqual(
|
|
600
|
+
unboundedPage(skip, limit, tieSeed),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
test("paging the whole set with the bound returns every row exactly once", () => {
|
|
607
|
+
const seen: Array<string> = [];
|
|
608
|
+
|
|
609
|
+
for (let page: number = 0; page < 3; page++) {
|
|
610
|
+
/* A different tie ordering per fetch, as the engine may do. */
|
|
611
|
+
const rows: Array<Row> = boundedPage(page * 5, 5, page + 1);
|
|
612
|
+
seen.push(
|
|
613
|
+
...rows.map((row: Row): string => {
|
|
614
|
+
return row._id;
|
|
615
|
+
}),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
expect(seen).toHaveLength(12);
|
|
620
|
+
expect(new Set(seen).size).toBe(12);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
/*
|
|
624
|
+
* The final page matches fewer rows than skip + limit. This is why the
|
|
625
|
+
* bound is min() over a subquery rather than the N-th value directly:
|
|
626
|
+
* `ORDER BY time DESC LIMIT 1 OFFSET n-1` returns no row here, the
|
|
627
|
+
* scalar is NULL, `time >= NULL` is NULL, and the page comes back empty.
|
|
628
|
+
*/
|
|
629
|
+
test("a final page shorter than skip + limit is not truncated", () => {
|
|
630
|
+
expect(boundedPage(10, 5, 0)).toStrictEqual(unboundedPage(10, 5, 0));
|
|
631
|
+
expect(boundedPage(10, 5, 0)).toHaveLength(2);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("an offset past the end returns nothing, not a full page", () => {
|
|
635
|
+
expect(boundedPage(50, 5, 0)).toStrictEqual([]);
|
|
636
|
+
expect(boundedPage(50, 5, 0)).toStrictEqual(unboundedPage(50, 5, 0));
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
test("the bound never narrows the page when every row ties", () => {
|
|
640
|
+
const allTied: Array<Row> = ROWS.map((row: Row): Row => {
|
|
641
|
+
return { time: 500, _id: row._id };
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const scanned: Array<Row> = [...allTied];
|
|
645
|
+
const boundary: number = Math.min(
|
|
646
|
+
...scanned.slice(0, 5).map((row: Row): number => {
|
|
647
|
+
return row.time;
|
|
648
|
+
}),
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(
|
|
652
|
+
scanned.filter((row: Row): boolean => {
|
|
653
|
+
return row.time >= boundary;
|
|
654
|
+
}),
|
|
655
|
+
).toHaveLength(12);
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
});
|