@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.
Files changed (376) hide show
  1. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +14 -0
  2. package/Models/DatabaseModels/UserCall.ts +94 -3
  3. package/Models/DatabaseModels/UserEmail.ts +94 -3
  4. package/Models/DatabaseModels/UserIncomingCallNumber.ts +94 -3
  5. package/Models/DatabaseModels/UserPush.ts +37 -0
  6. package/Models/DatabaseModels/UserSMS.ts +94 -3
  7. package/Models/DatabaseModels/UserWhatsApp.ts +93 -2
  8. package/Server/API/BaseAPI.ts +79 -14
  9. package/Server/API/UserCallAPI.ts +53 -45
  10. package/Server/API/UserEmailAPI.ts +53 -44
  11. package/Server/API/UserIncomingCallNumberAPI.ts +48 -38
  12. package/Server/API/UserNotificationMethodAdminAPI.ts +481 -0
  13. package/Server/API/UserPushAPI.ts +126 -2
  14. package/Server/API/UserSmsAPI.ts +53 -44
  15. package/Server/API/UserWhatsAppAPI.ts +53 -50
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.ts +154 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.ts +17 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.ts +45 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  20. package/Server/Middleware/VerificationCodeRateLimit.ts +586 -0
  21. package/Server/Services/AnalyticsDatabaseService.ts +169 -0
  22. package/Server/Services/DatabaseService.ts +101 -0
  23. package/Server/Services/LogAggregationService.ts +5 -0
  24. package/Server/Services/PushNotificationService.ts +127 -12
  25. package/Server/Services/TeamMemberService.ts +53 -13
  26. package/Server/Services/UserCallService.ts +55 -24
  27. package/Server/Services/UserEmailService.ts +83 -17
  28. package/Server/Services/UserIncomingCallNumberService.ts +49 -85
  29. package/Server/Services/UserNotificationMethodAdminService.ts +1527 -0
  30. package/Server/Services/UserNotificationRuleService.ts +26 -0
  31. package/Server/Services/UserPushService.ts +89 -0
  32. package/Server/Services/UserSmsService.ts +50 -17
  33. package/Server/Services/UserWhatsAppService.ts +48 -15
  34. package/Server/Types/Database/QueryHelper.ts +30 -0
  35. package/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.ts +59 -0
  36. package/Server/Utils/ChannelVerification.ts +510 -0
  37. package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +13 -16
  38. package/Server/Utils/Monitor/MonitorAlert.ts +24 -21
  39. package/Server/Utils/Monitor/MonitorIncident.ts +21 -18
  40. package/Server/Utils/Monitor/MonitorResourceContext.ts +107 -0
  41. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +442 -0
  42. package/Server/Utils/Monitor/SeriesResourceLabels.ts +3 -3
  43. package/Server/Utils/Monitor/SeriesResourceLinker.ts +96 -28
  44. package/Server/Utils/TotpAuth.ts +137 -9
  45. package/Server/Utils/UserRegistrationToken.ts +182 -0
  46. package/Server/Utils/VerificationCode.ts +134 -0
  47. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +173 -94
  48. package/Tests/App/Dashboard/AdminUserNotificationMethodsPage.test.tsx +1151 -0
  49. package/Tests/App/Dashboard/AdminUserOnCallPages.test.tsx +905 -0
  50. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +338 -0
  51. package/Tests/App/Dashboard/InvestigationPanel.test.tsx +1 -1
  52. package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +28 -0
  53. package/Tests/Models/UserPushCriticalAlertColumn.test.ts +172 -0
  54. package/Tests/Server/API/BaseAPIGetListParallel.test.ts +519 -0
  55. package/Tests/Server/API/NotificationChannelVerificationAPI.test.ts +543 -0
  56. package/Tests/Server/API/UserIncomingCallNumberApi.test.ts +108 -18
  57. package/Tests/Server/API/UserNotificationMethodAdminAPI.test.ts +963 -0
  58. package/Tests/Server/API/UserPushCriticalAlertsApi.test.ts +114 -0
  59. package/Tests/Server/API/UserSmsApi.test.ts +107 -14
  60. package/Tests/Server/API/UserTotpAuthAPI.test.ts +427 -0
  61. package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +315 -0
  62. package/Tests/Server/Middleware/VerificationCodeRateLimit.test.ts +775 -0
  63. package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +10 -62
  64. package/Tests/Server/Services/AnalyticsDatabasePaginationStability.test.ts +33 -13
  65. package/Tests/Server/Services/AnalyticsDatabaseSortKeyBoundary.test.ts +658 -0
  66. package/Tests/Server/Services/CriticalOnCallAlertDelivery.test.ts +458 -0
  67. package/Tests/Server/Services/CriticalPushAlertPayload.test.ts +301 -0
  68. package/Tests/Server/Services/DatabaseServiceAtomicIncrement.test.ts +143 -0
  69. package/Tests/Server/Services/DeliverNotificationForRuleExtraction.test.ts +1 -0
  70. package/Tests/Server/Services/LogAggregationScanMemory.test.ts +421 -0
  71. package/Tests/Server/Services/LogAggregationService.test.ts +2 -2
  72. package/Tests/Server/Services/NotificationChannelCodeIssuance.test.ts +401 -0
  73. package/Tests/Server/Services/PushNotificationDeliveryOptions.test.ts +204 -0
  74. package/Tests/Server/Services/TeamMemberAutoAcceptInvitation.test.ts +21 -1
  75. package/Tests/Server/Services/TeamMemberInviteRegistrationToken.test.ts +290 -0
  76. package/Tests/Server/Services/UserNotificationMethodAdminService.test.ts +948 -0
  77. package/Tests/Server/Services/UserNotificationRuleExecuteItem.test.ts +1 -0
  78. package/Tests/Server/Services/UserPushCriticalAlertToggle.test.ts +255 -0
  79. package/Tests/Server/Services/UserTotpAuthEnrolment.test.ts +218 -0
  80. package/Tests/Server/TestingUtils/AuthenticatorApp.ts +248 -0
  81. package/Tests/Server/Types/Database/Permissions/NotificationChannelVerificationColumns.test.ts +135 -0
  82. package/Tests/Server/Types/Database/Permissions/OnCallScheduleRelationSelect.test.ts +504 -0
  83. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAnyOf.test.ts +91 -0
  84. package/Tests/Server/Types/Workflow/Components/JsonToText.test.ts +384 -0
  85. package/Tests/Server/Types/Workflow/Components/MergeJson.test.ts +403 -0
  86. package/Tests/Server/Utils/Browser.test.ts +162 -0
  87. package/Tests/Server/Utils/ChannelVerification.test.ts +689 -0
  88. package/Tests/Server/Utils/Express.test.ts +371 -0
  89. package/Tests/Server/Utils/Monitor/Criteria/CustomCodeMonitorCriteria.test.ts +707 -0
  90. package/Tests/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.test.ts +698 -0
  91. package/Tests/Server/Utils/Monitor/Criteria/IncomingEmailBodyCriteria.test.ts +243 -0
  92. package/Tests/Server/Utils/Monitor/Criteria/IncomingRequestBodyCriteria.test.ts +278 -0
  93. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +160 -20
  94. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +13 -8
  95. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +162 -18
  96. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +349 -0
  97. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +690 -0
  98. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +7 -2
  99. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +92 -18
  100. package/Tests/Server/Utils/PushNotificationUtilCreators.test.ts +267 -0
  101. package/Tests/Server/Utils/Telemetry/OneuptimeLabel.test.ts +439 -0
  102. package/Tests/Server/Utils/TotpAuth.test.ts +719 -0
  103. package/Tests/Server/Utils/UserRegistrationToken.test.ts +322 -0
  104. package/Tests/Server/Utils/VerificationCode.test.ts +296 -0
  105. package/Tests/Types/AutoRemediation/AutoRemediationSuggestionStatus.test.ts +192 -0
  106. package/Tests/Types/Call/CallRequest.test.ts +189 -0
  107. package/Tests/Types/Dashboard/DashboardSize.test.ts +226 -0
  108. package/Tests/Types/Incident/IncidentSlaStatus.test.ts +191 -0
  109. package/Tests/Types/Metrics/MetricDashboardMetricType.test.ts +278 -0
  110. package/Tests/Types/Metrics/MetricPipelineRuleType.test.ts +517 -0
  111. package/Tests/Types/Monitor/IncomingMonitorDefaultCriteria.test.ts +470 -0
  112. package/Tests/Types/Monitor/MonitorCriteriaInstance.test.ts +6 -4
  113. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +262 -5
  114. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +1 -1
  115. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +1 -1
  116. package/Tests/Types/Monitor/ServiceAlertTemplates.test.ts +905 -0
  117. package/Tests/Types/Service/ServiceLanguage.test.ts +255 -0
  118. package/Tests/Types/TextRandomGeneration.test.ts +211 -0
  119. package/Tests/UI/Components/Button.test.tsx +175 -1
  120. package/Tests/UI/Components/CodeBlockLanguages.test.tsx +236 -0
  121. package/Tests/UI/Components/CommandPalette/CommandPalette.test.tsx +568 -0
  122. package/Tests/UI/Components/CommandPalette/CommandPaletteKeyboard.test.tsx +239 -0
  123. package/Tests/UI/Components/CommandPalette/CommandPaletteProviders.test.tsx +400 -0
  124. package/Tests/UI/Components/CommandPalette/PaletteFilter.test.ts +234 -0
  125. package/Tests/UI/Components/CommandPalette/RecentCommands.test.ts +97 -0
  126. package/Tests/UI/Components/CountModelSideMenuItemGuard.test.tsx +198 -0
  127. package/Tests/UI/Components/FeedItemSafeMode.test.tsx +1 -1
  128. package/Tests/UI/Components/Forms/Utils/FormFieldSchemaTypeUtil.test.ts +267 -0
  129. package/Tests/UI/Components/List.test.tsx +9 -2
  130. package/Tests/UI/Components/ListLoadingStates.test.tsx +173 -0
  131. package/Tests/UI/Components/MarkdownLazy.test.tsx +113 -0
  132. package/Tests/UI/Components/MarkdownMermaidRetry.test.tsx +110 -0
  133. package/Tests/UI/Components/ModelTable/useCustomFieldColumns.test.tsx +8 -0
  134. package/Tests/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.test.ts +692 -0
  135. package/Tests/UI/Components/MoreMenuMotion.test.tsx +118 -0
  136. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +178 -0
  137. package/Tests/UI/Components/OrderedStatesList.test.tsx +19 -2
  138. package/Tests/UI/Components/SideOverMotion.test.tsx +109 -0
  139. package/Tests/UI/Components/Skeleton.test.tsx +71 -0
  140. package/Tests/UI/Components/TableLoadingStates.test.tsx +279 -0
  141. package/Tests/UI/Components/Tabs.test.tsx +24 -0
  142. package/Tests/UI/Components/Toast.test.tsx +130 -7
  143. package/Tests/UI/Components/ToastStacking.test.tsx +198 -0
  144. package/Tests/UI/Utils/Dropdown.test.ts +423 -0
  145. package/Tests/UI/Utils/ModelAPITenantHeader.test.ts +122 -0
  146. package/Tests/UI/Utils/ModelListCache.test.ts +338 -0
  147. package/Tests/UI/Utils/ProjectListCacheInvalidation.test.ts +225 -0
  148. package/Tests/Utils/API.test.ts +16 -2
  149. package/Tests/Utils/Dashboard/Components/DashboardKubernetesResourceListShared.test.ts +347 -0
  150. package/Tests/Utils/Dashboard/Components/DashboardListSharedArgs.test.ts +259 -0
  151. package/Tests/Utils/Dashboard/Components/DashboardMonitorListComponent.test.ts +466 -0
  152. package/Tests/Utils/Rum/UrlScrubberSegments.test.ts +362 -0
  153. package/Types/Email/EmailTemplateType.ts +1 -0
  154. package/Types/Monitor/MonitorCriteriaInstance.ts +41 -20
  155. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +131 -8
  156. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +48 -8
  157. package/Types/Monitor/ServiceAlertTemplates.ts +1352 -0
  158. package/Types/Permission.ts +35 -0
  159. package/Types/PushNotification/AndroidNotificationChannel.ts +26 -0
  160. package/Types/PushNotification/PushNotificationMessage.ts +15 -0
  161. package/Types/PushNotification/PushNotificationRequest.ts +8 -16
  162. package/Types/Service/ServiceLanguage.ts +144 -0
  163. package/Types/Text.ts +111 -19
  164. package/UI/Components/Button/Button.tsx +27 -14
  165. package/UI/Components/CodeBlock/CodeBlock.tsx +54 -7
  166. package/UI/Components/CodeBlock/LanguageRegistry.ts +103 -0
  167. package/UI/Components/CommandPalette/CommandPalette.tsx +790 -0
  168. package/UI/Components/CommandPalette/PaletteFilter.ts +196 -0
  169. package/UI/Components/CommandPalette/PaletteRow.tsx +173 -0
  170. package/UI/Components/CommandPalette/RecentCommands.ts +57 -0
  171. package/UI/Components/CommandPalette/Types.ts +49 -0
  172. package/UI/Components/CommandPalette/UseProviderSearch.ts +129 -0
  173. package/UI/Components/Dropdown/Dropdown.tsx +6 -1
  174. package/UI/Components/Feed/FeedItem.tsx +1 -1
  175. package/UI/Components/List/List.tsx +37 -5
  176. package/UI/Components/List/ListSkeleton.tsx +54 -0
  177. package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +6 -1
  178. package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +71 -26
  179. package/UI/Components/Modal/Modal.tsx +2 -2
  180. package/UI/Components/ModelTable/BaseModelTable.tsx +1 -1
  181. package/UI/Components/ModelTable/useCustomFieldColumns.ts +11 -5
  182. package/UI/Components/MoreMenu/MoreMenu.tsx +39 -1
  183. package/UI/Components/Navbar/NavBar.tsx +14 -1
  184. package/UI/Components/OrderedStatesList/OrderedStatesList.tsx +50 -5
  185. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +40 -1
  186. package/UI/Components/SideOver/SideOver.tsx +37 -2
  187. package/UI/Components/Skeleton/Skeleton.tsx +53 -0
  188. package/UI/Components/Table/Table.tsx +74 -44
  189. package/UI/Components/Table/TableSkeletonRows.tsx +141 -0
  190. package/UI/Components/Tabs/Tab.tsx +1 -1
  191. package/UI/Components/Toast/Toast.tsx +118 -53
  192. package/UI/Components/Toast/ToastInit.tsx +82 -32
  193. package/UI/Styles/Theme.css +40 -0
  194. package/UI/Utils/ModelAPI/ModelAPI.ts +20 -10
  195. package/UI/Utils/ModelListCache.ts +188 -0
  196. package/UI/Utils/Project.ts +29 -0
  197. package/Utils/API.ts +6 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +14 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  200. package/build/dist/Models/DatabaseModels/UserCall.js +101 -3
  201. package/build/dist/Models/DatabaseModels/UserCall.js.map +1 -1
  202. package/build/dist/Models/DatabaseModels/UserEmail.js +101 -3
  203. package/build/dist/Models/DatabaseModels/UserEmail.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js +101 -3
  205. package/build/dist/Models/DatabaseModels/UserIncomingCallNumber.js.map +1 -1
  206. package/build/dist/Models/DatabaseModels/UserPush.js +38 -0
  207. package/build/dist/Models/DatabaseModels/UserPush.js.map +1 -1
  208. package/build/dist/Models/DatabaseModels/UserSMS.js +101 -3
  209. package/build/dist/Models/DatabaseModels/UserSMS.js.map +1 -1
  210. package/build/dist/Models/DatabaseModels/UserWhatsApp.js +100 -2
  211. package/build/dist/Models/DatabaseModels/UserWhatsApp.js.map +1 -1
  212. package/build/dist/Server/API/BaseAPI.js +69 -13
  213. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  214. package/build/dist/Server/API/UserCallAPI.js +42 -38
  215. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  216. package/build/dist/Server/API/UserEmailAPI.js +42 -38
  217. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  218. package/build/dist/Server/API/UserIncomingCallNumberAPI.js +37 -32
  219. package/build/dist/Server/API/UserIncomingCallNumberAPI.js.map +1 -1
  220. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js +341 -0
  221. package/build/dist/Server/API/UserNotificationMethodAdminAPI.js.map +1 -0
  222. package/build/dist/Server/API/UserPushAPI.js +95 -2
  223. package/build/dist/Server/API/UserPushAPI.js.map +1 -1
  224. package/build/dist/Server/API/UserSmsAPI.js +42 -38
  225. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  226. package/build/dist/Server/API/UserWhatsAppAPI.js +42 -40
  227. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js +89 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787142779538-MigrationName.js.map +1 -0
  230. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js +12 -0
  231. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787156982416-MigrationName.js.map +1 -0
  232. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js +40 -0
  233. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787700000000-FixTotpOtpUrlAlgorithm.js.map +1 -0
  234. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  235. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  236. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js +393 -0
  237. package/build/dist/Server/Middleware/VerificationCodeRateLimit.js.map +1 -0
  238. package/build/dist/Server/Services/AnalyticsDatabaseService.js +136 -0
  239. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  240. package/build/dist/Server/Services/DatabaseService.js +74 -0
  241. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  242. package/build/dist/Server/Services/LogAggregationService.js +5 -0
  243. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  244. package/build/dist/Server/Services/PushNotificationService.js +69 -30
  245. package/build/dist/Server/Services/PushNotificationService.js.map +1 -1
  246. package/build/dist/Server/Services/TeamMemberService.js +43 -7
  247. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  248. package/build/dist/Server/Services/UserCallService.js +54 -22
  249. package/build/dist/Server/Services/UserCallService.js.map +1 -1
  250. package/build/dist/Server/Services/UserEmailService.js +78 -17
  251. package/build/dist/Server/Services/UserEmailService.js.map +1 -1
  252. package/build/dist/Server/Services/UserIncomingCallNumberService.js +45 -74
  253. package/build/dist/Server/Services/UserIncomingCallNumberService.js.map +1 -1
  254. package/build/dist/Server/Services/UserNotificationMethodAdminService.js +1117 -0
  255. package/build/dist/Server/Services/UserNotificationMethodAdminService.js.map +1 -0
  256. package/build/dist/Server/Services/UserNotificationRuleService.js +20 -0
  257. package/build/dist/Server/Services/UserNotificationRuleService.js.map +1 -1
  258. package/build/dist/Server/Services/UserPushService.js +77 -0
  259. package/build/dist/Server/Services/UserPushService.js.map +1 -1
  260. package/build/dist/Server/Services/UserSmsService.js +50 -17
  261. package/build/dist/Server/Services/UserSmsService.js.map +1 -1
  262. package/build/dist/Server/Services/UserWhatsAppService.js +48 -15
  263. package/build/dist/Server/Services/UserWhatsAppService.js.map +1 -1
  264. package/build/dist/Server/Types/Database/QueryHelper.js +27 -0
  265. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  266. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js +46 -0
  267. package/build/dist/Server/Utils/AnalyticsDatabase/QuerySettingsHelper.js.map +1 -1
  268. package/build/dist/Server/Utils/ChannelVerification.js +348 -0
  269. package/build/dist/Server/Utils/ChannelVerification.js.map +1 -0
  270. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +13 -12
  271. package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
  272. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +20 -17
  273. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  274. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +17 -14
  275. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  276. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +95 -0
  277. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -0
  278. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +313 -0
  279. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -0
  280. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +3 -3
  281. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +67 -26
  282. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  283. package/build/dist/Server/Utils/TotpAuth.js +127 -8
  284. package/build/dist/Server/Utils/TotpAuth.js.map +1 -1
  285. package/build/dist/Server/Utils/UserRegistrationToken.js +170 -0
  286. package/build/dist/Server/Utils/UserRegistrationToken.js.map +1 -0
  287. package/build/dist/Server/Utils/VerificationCode.js +120 -0
  288. package/build/dist/Server/Utils/VerificationCode.js.map +1 -0
  289. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  290. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  291. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +42 -21
  292. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  293. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +77 -5
  294. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  295. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +18 -6
  296. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  297. package/build/dist/Types/Monitor/ServiceAlertTemplates.js +1028 -0
  298. package/build/dist/Types/Monitor/ServiceAlertTemplates.js.map +1 -0
  299. package/build/dist/Types/Permission.js +33 -0
  300. package/build/dist/Types/Permission.js.map +1 -1
  301. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js +27 -0
  302. package/build/dist/Types/PushNotification/AndroidNotificationChannel.js.map +1 -0
  303. package/build/dist/Types/Service/ServiceLanguage.js +96 -0
  304. package/build/dist/Types/Service/ServiceLanguage.js.map +1 -0
  305. package/build/dist/Types/Text.js +82 -17
  306. package/build/dist/Types/Text.js.map +1 -1
  307. package/build/dist/UI/Components/Button/Button.js +22 -14
  308. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  309. package/build/dist/UI/Components/CodeBlock/CodeBlock.js +35 -3
  310. package/build/dist/UI/Components/CodeBlock/CodeBlock.js.map +1 -1
  311. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js +57 -0
  312. package/build/dist/UI/Components/CodeBlock/LanguageRegistry.js.map +1 -0
  313. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +439 -0
  314. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -0
  315. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js +135 -0
  316. package/build/dist/UI/Components/CommandPalette/PaletteFilter.js.map +1 -0
  317. package/build/dist/UI/Components/CommandPalette/PaletteRow.js +83 -0
  318. package/build/dist/UI/Components/CommandPalette/PaletteRow.js.map +1 -0
  319. package/build/dist/UI/Components/CommandPalette/RecentCommands.js +47 -0
  320. package/build/dist/UI/Components/CommandPalette/RecentCommands.js.map +1 -0
  321. package/build/dist/UI/Components/CommandPalette/Types.js +2 -0
  322. package/build/dist/UI/Components/CommandPalette/Types.js.map +1 -0
  323. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js +88 -0
  324. package/build/dist/UI/Components/CommandPalette/UseProviderSearch.js.map +1 -0
  325. package/build/dist/UI/Components/Dropdown/Dropdown.js +6 -1
  326. package/build/dist/UI/Components/Dropdown/Dropdown.js.map +1 -1
  327. package/build/dist/UI/Components/Feed/FeedItem.js +1 -1
  328. package/build/dist/UI/Components/Feed/FeedItem.js.map +1 -1
  329. package/build/dist/UI/Components/List/List.js +19 -5
  330. package/build/dist/UI/Components/List/List.js.map +1 -1
  331. package/build/dist/UI/Components/List/ListSkeleton.js +21 -0
  332. package/build/dist/UI/Components/List/ListSkeleton.js.map +1 -0
  333. package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
  334. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +52 -25
  335. package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
  336. package/build/dist/UI/Components/Modal/Modal.js +2 -2
  337. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +1 -1
  338. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  339. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +9 -2
  340. package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js.map +1 -1
  341. package/build/dist/UI/Components/MoreMenu/MoreMenu.js +32 -1
  342. package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
  343. package/build/dist/UI/Components/Navbar/NavBar.js +5 -1
  344. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  345. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js +29 -5
  346. package/build/dist/UI/Components/OrderedStatesList/OrderedStatesList.js.map +1 -1
  347. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +23 -1
  348. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  349. package/build/dist/UI/Components/SideOver/SideOver.js +23 -2
  350. package/build/dist/UI/Components/SideOver/SideOver.js.map +1 -1
  351. package/build/dist/UI/Components/Skeleton/Skeleton.js +23 -0
  352. package/build/dist/UI/Components/Skeleton/Skeleton.js.map +1 -0
  353. package/build/dist/UI/Components/Table/Table.js +35 -24
  354. package/build/dist/UI/Components/Table/Table.js.map +1 -1
  355. package/build/dist/UI/Components/Table/TableSkeletonRows.js +52 -0
  356. package/build/dist/UI/Components/Table/TableSkeletonRows.js.map +1 -0
  357. package/build/dist/UI/Components/Tabs/Tab.js +1 -1
  358. package/build/dist/UI/Components/Tabs/Tab.js.map +1 -1
  359. package/build/dist/UI/Components/Toast/Toast.js +77 -23
  360. package/build/dist/UI/Components/Toast/Toast.js.map +1 -1
  361. package/build/dist/UI/Components/Toast/ToastInit.js +37 -15
  362. package/build/dist/UI/Components/Toast/ToastInit.js.map +1 -1
  363. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +18 -9
  364. package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
  365. package/build/dist/UI/Utils/ModelListCache.js +126 -0
  366. package/build/dist/UI/Utils/ModelListCache.js.map +1 -0
  367. package/build/dist/UI/Utils/Project.js +24 -1
  368. package/build/dist/UI/Utils/Project.js.map +1 -1
  369. package/build/dist/Utils/API.js +6 -1
  370. package/build/dist/Utils/API.js.map +1 -1
  371. package/jest.config.json +1 -1
  372. package/package.json +2 -3
  373. package/Server/Utils/Monitor/MonitorClusterContext.ts +0 -182
  374. package/Tests/__mocks__/otpauth.js +0 -30
  375. package/build/dist/Server/Utils/Monitor/MonitorClusterContext.js +0 -141
  376. 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
+ });