@oneuptime/common 14.0.2 → 14.0.5

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 (765) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/AlertFeed.ts +2 -0
  3. package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
  4. package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
  5. package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
  6. package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
  7. package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
  8. package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
  9. package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
  10. package/Models/DatabaseModels/Incident.ts +55 -0
  11. package/Models/DatabaseModels/IncidentAlert.ts +540 -0
  12. package/Models/DatabaseModels/IncidentFeed.ts +2 -0
  13. package/Models/DatabaseModels/Index.ts +18 -0
  14. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
  15. package/Models/DatabaseModels/Project.ts +60 -0
  16. package/Models/DatabaseModels/RumApplication.ts +52 -1
  17. package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
  18. package/Models/DatabaseModels/TelemetryException.ts +2 -0
  19. package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.ts +28 -0
  21. package/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.ts +89 -0
  22. package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
  23. package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
  24. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  25. package/Server/Middleware/MultipartFormData.ts +1 -1
  26. package/Server/Middleware/TelemetryIngest.ts +15 -8
  27. package/Server/Services/AlertReminderRuleService.ts +100 -24
  28. package/Server/Services/DatabaseServerEndpointService.ts +895 -0
  29. package/Server/Services/DatabaseServerFeedService.ts +128 -0
  30. package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
  31. package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
  32. package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
  33. package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
  34. package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
  35. package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
  36. package/Server/Services/DatabaseServerService.ts +4546 -0
  37. package/Server/Services/DatabaseService.ts +14 -8
  38. package/Server/Services/DockerResourceService.ts +43 -2
  39. package/Server/Services/IncidentAlertService.ts +1811 -0
  40. package/Server/Services/IncidentReminderRuleService.ts +103 -23
  41. package/Server/Services/IncidentService.ts +210 -10
  42. package/Server/Services/IncidentStateTimelineService.ts +24 -0
  43. package/Server/Services/Index.ts +18 -0
  44. package/Server/Services/InventoryItemService.ts +113 -5
  45. package/Server/Services/IoTDeviceService.ts +145 -37
  46. package/Server/Services/OpenTelemetryIngestService.ts +19 -0
  47. package/Server/Services/PodmanResourceService.ts +43 -2
  48. package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
  49. package/Server/Services/ServiceService.ts +1 -1
  50. package/Server/Services/TelemetryUsageBillingService.ts +29 -2
  51. package/Server/Services/UserProjectSsoConsentService.ts +74 -0
  52. package/Server/Services/UserService.ts +14 -1
  53. package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
  54. package/Server/Types/Database/QueryHelper.ts +30 -1
  55. package/Server/Utils/AI/Toolbox/ResourceTools.ts +69 -1
  56. package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
  57. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +188 -6
  58. package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
  59. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2262 -755
  60. package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
  61. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
  62. package/Server/Utils/Monitor/MonitorResource.ts +47 -11
  63. package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
  64. package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
  65. package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
  66. package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
  67. package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
  68. package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
  69. package/Server/Utils/SessionReplay/SessionReplayGateCache.ts +24 -2
  70. package/Server/Utils/StartServer.ts +26 -6
  71. package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
  72. package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
  73. package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
  74. package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
  75. package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
  76. package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
  77. package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
  78. package/Server/Utils/Telemetry/Telemetry.ts +16 -0
  79. package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
  80. package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
  81. package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
  82. package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
  83. package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
  84. package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
  85. package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
  86. package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
  87. package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
  88. package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
  89. package/Tests/App/Dashboard/BulkIncidentLinkActions.test.tsx +869 -0
  90. package/Tests/App/Dashboard/CephMetricTooltips.test.tsx +1777 -0
  91. package/Tests/App/Dashboard/CloudFleetSummary.test.ts +8 -0
  92. package/Tests/App/Dashboard/CloudMetricTooltips.test.tsx +887 -0
  93. package/Tests/App/Dashboard/ContainerHostMetricTooltips.test.tsx +1309 -0
  94. package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
  95. package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
  96. package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
  97. package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
  98. package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
  99. package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
  100. package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
  101. package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
  102. package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
  103. package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
  104. package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
  105. package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
  106. package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
  107. package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
  108. package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
  109. package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
  110. package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
  111. package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
  112. package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
  113. package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
  114. package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
  115. package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
  116. package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
  117. package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
  118. package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
  119. package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
  120. package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
  121. package/Tests/App/Dashboard/DiscoveryScanLivePage.test.tsx +39 -0
  122. package/Tests/App/Dashboard/DockerSwarmClustersListTooltips.test.tsx +414 -0
  123. package/Tests/App/Dashboard/DockerSwarmMetricTooltips.test.ts +796 -0
  124. package/Tests/App/Dashboard/DockerSwarmOverviewTooltips.test.tsx +747 -0
  125. package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
  126. package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
  127. package/Tests/App/Dashboard/ExceptionMonitorEnvironmentFilter.test.tsx +38 -18
  128. package/Tests/App/Dashboard/HostDetailViewTooltips.test.tsx +725 -0
  129. package/Tests/App/Dashboard/HostListColumnTooltips.test.tsx +472 -0
  130. package/Tests/App/Dashboard/HostMetricTooltips.test.ts +709 -0
  131. package/Tests/App/Dashboard/HostOverviewTooltips.test.tsx +698 -0
  132. package/Tests/App/Dashboard/HostTooltipHarness.ts +124 -0
  133. package/Tests/App/Dashboard/HostsListTooltips.test.tsx +395 -0
  134. package/Tests/App/Dashboard/IncidentAlertLinkDialog.test.tsx +691 -0
  135. package/Tests/App/Dashboard/IncidentAlertLinkHelpers.test.ts +458 -0
  136. package/Tests/App/Dashboard/IncidentAlertLinkPages.test.tsx +828 -0
  137. package/Tests/App/Dashboard/IncidentAlertLinkSideMenus.test.tsx +231 -0
  138. package/Tests/App/Dashboard/IncidentCreateFromAlerts.test.tsx +689 -0
  139. package/Tests/App/Dashboard/IoTMetricTooltips.test.tsx +1123 -0
  140. package/Tests/App/Dashboard/KubernetesClusterMetricTooltips.test.ts +837 -0
  141. package/Tests/App/Dashboard/KubernetesClusterOverviewTooltips.test.tsx +699 -0
  142. package/Tests/App/Dashboard/KubernetesContainersTabTooltips.test.tsx +320 -0
  143. package/Tests/App/Dashboard/KubernetesCostTooltips.test.tsx +502 -0
  144. package/Tests/App/Dashboard/KubernetesInsightsTooltips.test.tsx +222 -0
  145. package/Tests/App/Dashboard/KubernetesResourceMetricTooltips.test.ts +561 -0
  146. package/Tests/App/Dashboard/KubernetesResourcePagesTooltips.test.tsx +1168 -0
  147. package/Tests/App/Dashboard/LinkIncidentAlertModal.test.tsx +154 -0
  148. package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
  149. package/Tests/App/Dashboard/MetricDescriptionRules.ts +170 -0
  150. package/Tests/App/Dashboard/MetricDescriptionsCatalog.test.ts +206 -0
  151. package/Tests/App/Dashboard/MetricDescriptionsConsistency.test.ts +275 -0
  152. package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
  153. package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
  154. package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
  155. package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
  156. package/Tests/App/Dashboard/NetworkDeviceTooltips.test.tsx +852 -0
  157. package/Tests/App/Dashboard/NetworkDiagnosticsTooltips.test.tsx +556 -0
  158. package/Tests/App/Dashboard/NetworkMetricTooltips.test.ts +1348 -0
  159. package/Tests/App/Dashboard/NetworkSiteTooltips.test.tsx +679 -0
  160. package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
  161. package/Tests/App/Dashboard/ProxmoxMetricTooltips.test.ts +592 -0
  162. package/Tests/App/Dashboard/ProxmoxOverviewTooltips.test.tsx +1002 -0
  163. package/Tests/App/Dashboard/ProxmoxResourcePageTooltips.test.tsx +923 -0
  164. package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
  165. package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
  166. package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
  167. package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
  168. package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +1066 -0
  169. package/Tests/App/Dashboard/RumOverviewPage.test.tsx +1195 -0
  170. package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
  171. package/Tests/App/Dashboard/ServerlessMetricTooltips.test.tsx +474 -0
  172. package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
  173. package/Tests/App/Dashboard/ServiceMetricTooltips.test.tsx +772 -0
  174. package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +56 -1
  175. package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +1207 -0
  176. package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
  177. package/Tests/App/Dashboard/TelemetryResourceTabTooltips.test.tsx +437 -0
  178. package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
  179. package/Tests/App/Dashboard/VMwareMetricTooltips.test.ts +801 -0
  180. package/Tests/App/Dashboard/VMwareOverviewTooltips.test.tsx +601 -0
  181. package/Tests/App/Dashboard/VMwareResourcePagesTooltips.test.tsx +646 -0
  182. package/Tests/App/Dashboard/VMwareTooltipHarness.ts +145 -0
  183. package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
  184. package/Tests/ClassStrings.ts +178 -0
  185. package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
  186. package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
  187. package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
  188. package/Tests/Models/DatabaseModels/IncidentAlertModel.test.ts +554 -0
  189. package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
  190. package/Tests/Models/DatabaseModels/SessionReplayModels.test.ts +117 -0
  191. package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
  192. package/Tests/RenderedMarkup.ts +518 -0
  193. package/Tests/ResponsiveVisibility.test.ts +490 -4
  194. package/Tests/ResponsiveVisibility.ts +505 -52
  195. package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
  196. package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
  197. package/Tests/Server/Infrastructure/Postgres/AddIncidentAlertMigration.test.ts +341 -0
  198. package/Tests/Server/Infrastructure/Postgres/AddSessionReplaySameOriginTracePropagationMigration.test.ts +247 -0
  199. package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
  200. package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
  201. package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
  202. package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
  203. package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
  204. package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
  205. package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
  206. package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
  207. package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
  208. package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
  209. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
  210. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
  211. package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
  212. package/Tests/Server/Services/IncidentAlertPostgres.test.ts +903 -0
  213. package/Tests/Server/Services/IncidentAlertRegistration.test.ts +96 -0
  214. package/Tests/Server/Services/IncidentAlertService.test.ts +2225 -0
  215. package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
  216. package/Tests/Server/Services/IncidentAlertStateCascade.test.ts +970 -0
  217. package/Tests/Server/Services/IncidentCreateFromAlerts.test.ts +1284 -0
  218. package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
  219. package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
  220. package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
  221. package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
  222. package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
  223. package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
  224. package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
  225. package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
  226. package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
  227. package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
  228. package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
  229. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
  230. package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
  231. package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
  232. package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
  233. package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
  234. package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
  235. package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
  236. package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
  237. package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
  238. package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
  239. package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
  240. package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
  241. package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
  242. package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
  243. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
  244. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
  245. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
  246. package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
  247. package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
  248. package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
  249. package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
  250. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
  251. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
  252. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1719 -0
  253. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
  254. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
  255. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
  256. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
  257. package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
  258. package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
  259. package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
  260. package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
  261. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
  262. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
  263. package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
  264. package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
  265. package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
  266. package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
  267. package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
  268. package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
  269. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
  270. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
  271. package/Tests/Server/Utils/SessionReplay/SessionReplayGateCachePolicy.test.ts +103 -0
  272. package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
  273. package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
  274. package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
  275. package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
  276. package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
  277. package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
  278. package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
  279. package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
  280. package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
  281. package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
  282. package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
  283. package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
  284. package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
  285. package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
  286. package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
  287. package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
  288. package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
  289. package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
  290. package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
  291. package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
  292. package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
  293. package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +298 -0
  294. package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +271 -0
  295. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
  296. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
  297. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
  298. package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
  299. package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
  300. package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
  301. package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +181 -0
  302. package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
  303. package/Tests/Types/Rum/WebVitals.test.ts +173 -0
  304. package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
  305. package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
  306. package/Tests/UI/Components/Card.test.tsx +93 -2
  307. package/Tests/UI/Components/FeedOptions.test.ts +6 -0
  308. package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
  309. package/Tests/UI/Components/FieldLabelInfoCardTooltip.test.tsx +413 -0
  310. package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
  311. package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
  312. package/Tests/UI/Components/InfoTooltip.test.tsx +242 -0
  313. package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
  314. package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
  315. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +207 -3
  316. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDeleteVerb.test.tsx +367 -0
  317. package/Tests/UI/Components/ModelTable/BaseModelTableCreateButtonTitle.test.tsx +275 -0
  318. package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
  319. package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
  320. package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
  321. package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
  322. package/Tests/UI/Components/TableHeaderTooltip.test.tsx +662 -0
  323. package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
  324. package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
  325. package/Tests/UI/Rum/PrivacySummaryCard.test.tsx +166 -9
  326. package/Tests/UI/Rum/RecordingHealthTooltips.test.tsx +447 -0
  327. package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +164 -0
  328. package/Tests/UI/Rum/ReplayFrameCapture.test.ts +6750 -0
  329. package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
  330. package/Tests/UI/Rum/ReplayRail.test.tsx +723 -12
  331. package/Tests/UI/Rum/ReplayRailDetail.test.tsx +86 -0
  332. package/Tests/UI/Rum/ReplayScreenshot.test.ts +2124 -0
  333. package/Tests/UI/Rum/ReplayScreenshotActions.test.tsx +3288 -0
  334. package/Tests/UI/Rum/ReplayStage.test.tsx +62 -0
  335. package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +983 -2
  336. package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
  337. package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +149 -0
  338. package/Tests/UI/Rum/SessionReplaySetupGuide.test.tsx +107 -6
  339. package/Tests/UI/Rum/SessionReplayUsersTableTooltips.test.tsx +330 -0
  340. package/Tests/UI/Rum/UserFlowTooltips.test.tsx +886 -0
  341. package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
  342. package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
  343. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +80 -0
  344. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +23 -0
  345. package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
  346. package/Tests/UI/Utils/Navigation.test.ts +102 -0
  347. package/Tests/UI/Utils/PermissionGate.test.ts +76 -0
  348. package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
  349. package/Tests/Utils/Incident/IncidentFromAlerts.test.ts +696 -0
  350. package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
  351. package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
  352. package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
  353. package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
  354. package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
  355. package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
  356. package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
  357. package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
  358. package/Tests/Utils/Rum/SessionTraceState.test.ts +239 -0
  359. package/Tests/Utils/ValueFormatter.test.ts +50 -0
  360. package/Tests/tsconfig.json +16 -0
  361. package/Types/AI/AIResourceContext.ts +7 -0
  362. package/Types/Dashboard/DashboardTemplates.ts +2 -1
  363. package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
  364. package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
  365. package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
  366. package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
  367. package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
  368. package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
  369. package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
  370. package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
  371. package/Types/Email/EmailTemplateType.ts +1 -0
  372. package/Types/Incident/IncidentAlertLink.ts +38 -0
  373. package/Types/Monitor/CephAlertTemplates.ts +18 -0
  374. package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
  375. package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
  376. package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
  377. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
  378. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
  379. package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
  380. package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
  381. package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
  382. package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
  383. package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
  384. package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
  385. package/Types/Permission.ts +315 -0
  386. package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
  387. package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
  388. package/Types/Rules/RuleRun.ts +12 -0
  389. package/Types/Rum/SessionReplay.ts +25 -6
  390. package/Types/Rum/WebVitals.ts +52 -0
  391. package/Types/Telemetry/EntityType.ts +23 -0
  392. package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
  393. package/Types/Telemetry/ServiceType.ts +8 -0
  394. package/UI/Components/Banner/Banner.tsx +1 -1
  395. package/UI/Components/Card/Card.tsx +1 -1
  396. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
  397. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  398. package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
  399. package/UI/Components/Date/StartAndEndDate.tsx +5 -5
  400. package/UI/Components/Detail/FieldLabel.tsx +8 -0
  401. package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
  402. package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
  403. package/UI/Components/Forms/BasicForm.tsx +1 -1
  404. package/UI/Components/Header/Header.tsx +1 -1
  405. package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
  406. package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
  407. package/UI/Components/InfoCard/InfoCard.tsx +47 -1
  408. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
  409. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
  410. package/UI/Components/Modal/ModalFooter.tsx +1 -1
  411. package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
  412. package/UI/Components/ModelTable/BaseModelTable.tsx +99 -21
  413. package/UI/Components/ModelTable/Column.ts +5 -0
  414. package/UI/Components/Navbar/NavBar.tsx +9 -2
  415. package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
  416. package/UI/Components/Page/Page.tsx +1 -1
  417. package/UI/Components/Pagination/Pagination.tsx +3 -3
  418. package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
  419. package/UI/Components/SideMenu/SideMenu.tsx +4 -1
  420. package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
  421. package/UI/Components/StatusPage/ResourceList.tsx +6 -6
  422. package/UI/Components/Table/TableHeader.tsx +47 -2
  423. package/UI/Components/Table/Types/Column.ts +7 -0
  424. package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
  425. package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
  426. package/UI/Components/Tooltip/InfoTooltip.tsx +78 -0
  427. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
  428. package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
  429. package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
  430. package/UI/Styles/Theme.css +8 -1
  431. package/UI/Utils/Navigation.ts +89 -42
  432. package/UI/Utils/PermissionGate.ts +11 -2
  433. package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
  434. package/UI/Utils/TelemetryService.ts +4 -3
  435. package/Utils/Incident/IncidentFromAlerts.ts +516 -0
  436. package/Utils/LabelRuleImportExport.ts +2 -0
  437. package/Utils/MetricUnitUtil.ts +103 -11
  438. package/Utils/Monitor/MetricValueFormatter.ts +66 -13
  439. package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
  440. package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
  441. package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
  442. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
  443. package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
  444. package/Utils/Rum/SessionTraceState.ts +212 -0
  445. package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
  446. package/Utils/Telemetry/EntityKey.ts +176 -0
  447. package/Utils/ValueFormatter.ts +16 -0
  448. package/build/dist/Models/DatabaseModels/Alert.js +54 -0
  449. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  450. package/build/dist/Models/DatabaseModels/AlertFeed.js +2 -0
  451. package/build/dist/Models/DatabaseModels/AlertFeed.js.map +1 -1
  452. package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
  453. package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
  454. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
  455. package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
  456. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
  457. package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
  458. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
  459. package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
  460. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
  461. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
  462. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
  463. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
  464. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
  465. package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
  466. package/build/dist/Models/DatabaseModels/Incident.js +54 -0
  467. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  468. package/build/dist/Models/DatabaseModels/IncidentAlert.js +553 -0
  469. package/build/dist/Models/DatabaseModels/IncidentAlert.js.map +1 -0
  470. package/build/dist/Models/DatabaseModels/IncidentFeed.js +2 -0
  471. package/build/dist/Models/DatabaseModels/IncidentFeed.js.map +1 -1
  472. package/build/dist/Models/DatabaseModels/Index.js +18 -0
  473. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  474. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  475. package/build/dist/Models/DatabaseModels/Project.js +62 -0
  476. package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
  477. package/build/dist/Models/DatabaseModels/RumApplication.js +53 -1
  478. package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
  479. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
  480. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  481. package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
  482. package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
  483. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
  484. package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
  485. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js +20 -0
  486. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794800000000-AddSessionReplaySameOriginTracePropagation.js.map +1 -0
  487. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js +42 -0
  488. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794900000000-AddIncidentAlert.js.map +1 -0
  489. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
  490. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
  491. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
  492. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
  493. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  494. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  495. package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
  496. package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
  497. package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
  498. package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
  499. package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
  500. package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
  501. package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
  502. package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
  503. package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
  504. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
  505. package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
  506. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
  507. package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
  508. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
  509. package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
  510. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
  511. package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
  512. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
  513. package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
  514. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
  515. package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
  516. package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
  517. package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
  518. package/build/dist/Server/Services/DatabaseService.js +8 -4
  519. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  520. package/build/dist/Server/Services/DockerResourceService.js +30 -3
  521. package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
  522. package/build/dist/Server/Services/IncidentAlertService.js +1370 -0
  523. package/build/dist/Server/Services/IncidentAlertService.js.map +1 -0
  524. package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
  525. package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
  526. package/build/dist/Server/Services/IncidentService.js +164 -12
  527. package/build/dist/Server/Services/IncidentService.js.map +1 -1
  528. package/build/dist/Server/Services/IncidentStateTimelineService.js +21 -0
  529. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  530. package/build/dist/Server/Services/Index.js +18 -0
  531. package/build/dist/Server/Services/Index.js.map +1 -1
  532. package/build/dist/Server/Services/InventoryItemService.js +88 -5
  533. package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
  534. package/build/dist/Server/Services/IoTDeviceService.js +111 -24
  535. package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
  536. package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
  537. package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
  538. package/build/dist/Server/Services/PodmanResourceService.js +30 -3
  539. package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
  540. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
  541. package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
  542. package/build/dist/Server/Services/ServiceService.js +1 -1
  543. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  544. package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
  545. package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
  546. package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
  547. package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
  548. package/build/dist/Server/Services/UserService.js +13 -1
  549. package/build/dist/Server/Services/UserService.js.map +1 -1
  550. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
  551. package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
  552. package/build/dist/Server/Types/Database/QueryHelper.js +26 -1
  553. package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
  554. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +46 -0
  555. package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
  556. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
  557. package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
  558. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +134 -13
  559. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  560. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
  561. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  562. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
  563. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  564. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
  565. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  566. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
  567. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  568. package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
  569. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  570. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
  571. package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
  572. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
  573. package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
  574. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
  575. package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
  576. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
  577. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  578. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
  579. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
  580. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
  581. package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
  582. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js +8 -0
  583. package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCache.js.map +1 -1
  584. package/build/dist/Server/Utils/StartServer.js +20 -4
  585. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  586. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
  587. package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
  588. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
  589. package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
  590. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
  591. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  592. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
  593. package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
  594. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
  595. package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
  596. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
  597. package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
  598. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
  599. package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
  600. package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
  601. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  602. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
  603. package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
  604. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
  605. package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
  606. package/build/dist/Types/AI/AIResourceContext.js +7 -0
  607. package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
  608. package/build/dist/Types/Dashboard/DashboardTemplates.js +2 -1
  609. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  610. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
  611. package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
  612. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
  613. package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
  614. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
  615. package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
  616. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
  617. package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
  618. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
  619. package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
  620. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
  621. package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
  622. package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
  623. package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
  624. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
  625. package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
  626. package/build/dist/Types/Email/EmailTemplateType.js +1 -0
  627. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  628. package/build/dist/Types/Incident/IncidentAlertLink.js +34 -0
  629. package/build/dist/Types/Incident/IncidentAlertLink.js.map +1 -0
  630. package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
  631. package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
  632. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
  633. package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
  634. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
  635. package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
  636. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
  637. package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
  638. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
  639. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
  640. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
  641. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
  642. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
  643. package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
  644. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
  645. package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
  646. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
  647. package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
  648. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
  649. package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
  650. package/build/dist/Types/Permission.js +276 -0
  651. package/build/dist/Types/Permission.js.map +1 -1
  652. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
  653. package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
  654. package/build/dist/Types/Rules/RuleRun.js +4 -0
  655. package/build/dist/Types/Rules/RuleRun.js.map +1 -1
  656. package/build/dist/Types/Rum/WebVitals.js +25 -0
  657. package/build/dist/Types/Rum/WebVitals.js.map +1 -1
  658. package/build/dist/Types/Telemetry/EntityType.js +23 -0
  659. package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
  660. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
  661. package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
  662. package/build/dist/Types/Telemetry/ServiceType.js +8 -0
  663. package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
  664. package/build/dist/UI/Components/Banner/Banner.js +1 -1
  665. package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
  666. package/build/dist/UI/Components/Card/Card.js +1 -1
  667. package/build/dist/UI/Components/Card/Card.js.map +1 -1
  668. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
  669. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  670. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  671. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  672. package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
  673. package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
  674. package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
  675. package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
  676. package/build/dist/UI/Components/Detail/FieldLabel.js +2 -0
  677. package/build/dist/UI/Components/Detail/FieldLabel.js.map +1 -1
  678. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
  679. package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
  680. package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
  681. package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
  682. package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
  683. package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
  684. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
  685. package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
  686. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
  687. package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
  688. package/build/dist/UI/Components/InfoCard/InfoCard.js +22 -1
  689. package/build/dist/UI/Components/InfoCard/InfoCard.js.map +1 -1
  690. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
  691. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
  692. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
  693. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  694. package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
  695. package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
  696. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
  697. package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
  698. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +70 -18
  699. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  700. package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
  701. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  702. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  703. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  704. package/build/dist/UI/Components/Page/Page.js +1 -1
  705. package/build/dist/UI/Components/Page/Page.js.map +1 -1
  706. package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
  707. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  708. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
  709. package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
  710. package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
  711. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  712. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
  713. package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
  714. package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
  715. package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
  716. package/build/dist/UI/Components/Table/TableHeader.js +18 -2
  717. package/build/dist/UI/Components/Table/TableHeader.js.map +1 -1
  718. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
  719. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
  720. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
  721. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
  722. package/build/dist/UI/Components/Tooltip/InfoTooltip.js +28 -0
  723. package/build/dist/UI/Components/Tooltip/InfoTooltip.js.map +1 -0
  724. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
  725. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
  726. package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
  727. package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
  728. package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
  729. package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
  730. package/build/dist/UI/Utils/Navigation.js +66 -33
  731. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  732. package/build/dist/UI/Utils/PermissionGate.js +4 -2
  733. package/build/dist/UI/Utils/PermissionGate.js.map +1 -1
  734. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
  735. package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
  736. package/build/dist/UI/Utils/TelemetryService.js +3 -2
  737. package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
  738. package/build/dist/Utils/Incident/IncidentFromAlerts.js +306 -0
  739. package/build/dist/Utils/Incident/IncidentFromAlerts.js.map +1 -0
  740. package/build/dist/Utils/LabelRuleImportExport.js +2 -0
  741. package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
  742. package/build/dist/Utils/MetricUnitUtil.js +77 -13
  743. package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
  744. package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
  745. package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
  746. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
  747. package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
  748. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
  749. package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
  750. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
  751. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
  752. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
  753. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
  754. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
  755. package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
  756. package/build/dist/Utils/Rum/SessionTraceState.js +154 -0
  757. package/build/dist/Utils/Rum/SessionTraceState.js.map +1 -0
  758. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
  759. package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
  760. package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
  761. package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
  762. package/build/dist/Utils/ValueFormatter.js +15 -0
  763. package/build/dist/Utils/ValueFormatter.js.map +1 -1
  764. package/package.json +1 -1
  765. package/tsconfig.tests.json +7 -5
@@ -0,0 +1,2250 @@
1
+ import AlertReminderRule from "../../../Models/DatabaseModels/AlertReminderRule";
2
+ import IncidentReminderRule from "../../../Models/DatabaseModels/IncidentReminderRule";
3
+ import Entities from "../../../Models/DatabaseModels/Index";
4
+ import ScheduledMaintenanceReminderRule from "../../../Models/DatabaseModels/ScheduledMaintenanceReminderRule";
5
+ import PostgresAppInstance from "../../../Server/Infrastructure/PostgresDatabase";
6
+ import AlertReminderRuleService from "../../../Server/Services/AlertReminderRuleService";
7
+ import AlertService from "../../../Server/Services/AlertService";
8
+ import DatabaseService from "../../../Server/Services/DatabaseService";
9
+ import IncidentReminderRuleService from "../../../Server/Services/IncidentReminderRuleService";
10
+ import IncidentService from "../../../Server/Services/IncidentService";
11
+ import ScheduledMaintenanceReminderRuleService from "../../../Server/Services/ScheduledMaintenanceReminderRuleService";
12
+ import ScheduledMaintenanceService from "../../../Server/Services/ScheduledMaintenanceService";
13
+ import Query from "../../../Server/Types/Database/Query";
14
+ import QueryHelper from "../../../Server/Types/Database/QueryHelper";
15
+ import Select from "../../../Server/Types/Database/Select";
16
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
17
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
18
+ import LIMIT_MAX from "../../../Types/Database/LimitMax";
19
+ import PartialEntity from "../../../Types/Database/PartialEntity";
20
+ import FilterCondition from "../../../Types/Filter/FilterCondition";
21
+ import ObjectID from "../../../Types/ObjectID";
22
+ import Permission from "../../../Types/Permission";
23
+ import RuleCriteria, {
24
+ RULE_CRITERIA_SCHEMA_VERSION,
25
+ RuleCriteriaFilter,
26
+ RuleCriteriaOperator,
27
+ } from "../../../Types/Rules/RuleCriteria";
28
+ import UserType from "../../../Types/UserType";
29
+ import { DataSource, Logger } from "typeorm";
30
+
31
+ /*
32
+ * Opt in with RUN_POSTGRES_REMINDER_RULE_TESTS=true against a database the
33
+ * registered migrations have been applied to, e.g.
34
+ *
35
+ * RUN_POSTGRES_REMINDER_RULE_TESTS=true \
36
+ * REMINDER_RULE_TEST_DATABASE_HOST=127.0.0.1 \
37
+ * REMINDER_RULE_TEST_DATABASE_PORT=5400 \
38
+ * DATABASE_PASSWORD=... npx jest Tests/Server/Services/ReminderRuleLookupPostgres.test.ts
39
+ *
40
+ * CI runs it in .github/workflows/postgres-schema-drift.yaml ("Test reminder
41
+ * rule lookup on migrated Postgres"), right after that job has applied every
42
+ * registered migration to an empty database. The Common test job's Postgres
43
+ * is not migrated, so the suite is skipped there.
44
+ *
45
+ * Why a real Postgres (#4030): the criteria-backed `isEnabled` filter of the
46
+ * Incident, Alert and Scheduled Maintenance reminder rules is a TypeORM Raw
47
+ * whose SQL only Postgres judges. It once named its sibling column as
48
+ * `IncidentReminderRule."criteria"`; the unquoted alias folded to lowercase
49
+ * and every lookup failed with `missing FROM-clause entry for table
50
+ * "incidentreminderrule"` - whether or not the project had rules - so no
51
+ * reminder was ever scheduled. No mock notices that.
52
+ *
53
+ * Everything runs in a uniquely named schema holding structure-only clones
54
+ * (LIKE ... INCLUDING ALL: columns, defaults, CHECK constraints, indexes) of
55
+ * the migrated tables the lookup and the reminder refresh read or write,
56
+ * listed in TABLES. LIKE does not copy triggers, so the migrated triggers of
57
+ * those tables - on the rule tables, the one that derives the stored
58
+ * `isEnabled` from `criteria` - are recreated on the clones from their
59
+ * migrated definitions. The clones carry no foreign keys, so seeded rows only
60
+ * need their NOT NULL columns. The DataSource's search path is that schema,
61
+ * then public; no row is ever written outside the schema, and the schema is
62
+ * dropped afterwards. A signed-in project owner's read (projectOwnerProps)
63
+ * needs no further clone: on the rule tables, which have no access-control,
64
+ * owner or label-block relation, the permission layer only adds the tenant
65
+ * scope and reads no table itself.
66
+ *
67
+ * The production services run against the clones. Only true side effects are
68
+ * stubbed: workflow triggers (an HTTP call to the workflow service) and
69
+ * realtime events. Every statement Postgres rejects is recorded through the
70
+ * DataSource's logger and fails the test, even when a caller catches and logs
71
+ * the error - which is how #4030 surfaced: "Reminder scheduling failed in
72
+ * IncidentService.onCreateSuccess", logged and swallowed.
73
+ */
74
+ const describePostgres: typeof describe =
75
+ process.env["RUN_POSTGRES_REMINDER_RULE_TESTS"] === "true"
76
+ ? describe
77
+ : describe.skip;
78
+
79
+ const TABLES: Array<string> = [
80
+ "Label",
81
+ "IncidentReminderRule",
82
+ "IncidentReminderRuleIncidentSeverity",
83
+ "IncidentReminderRuleLabel",
84
+ "IncidentSeverity",
85
+ "IncidentState",
86
+ "Incident",
87
+ "IncidentLabel",
88
+ "AlertReminderRule",
89
+ "AlertReminderRuleAlertSeverity",
90
+ "AlertReminderRuleLabel",
91
+ "AlertSeverity",
92
+ "AlertState",
93
+ "Alert",
94
+ "AlertLabel",
95
+ "ScheduledMaintenanceReminderRule",
96
+ "ScheduledMaintenanceReminderRuleLabel",
97
+ "ScheduledMaintenanceState",
98
+ "ScheduledMaintenance",
99
+ "ScheduledMaintenanceLabel",
100
+ ];
101
+
102
+ const MINUTE_IN_MS: number = 60 * 1000;
103
+
104
+ type ReminderRule =
105
+ | IncidentReminderRule
106
+ | AlertReminderRule
107
+ | ScheduledMaintenanceReminderRule;
108
+
109
+ type SqlRow = Record<string, unknown>;
110
+
111
+ /*
112
+ * The options of the three rule services' refreshSchedulesForOpen*, which
113
+ * take the same shape. Typed from one of them, so a change to that signature
114
+ * fails the type-check here rather than passing silently.
115
+ */
116
+ type RefreshOpenSubjectsOptions = Parameters<
117
+ typeof IncidentReminderRuleService.refreshSchedulesForOpenIncidents
118
+ >[1];
119
+
120
+ // The side-effect hooks every DatabaseService has, whatever its model.
121
+ type SideEffectHooks = Pick<
122
+ DatabaseService<ReminderRule>,
123
+ "onTriggerWorkflow" | "onTriggerRealtime"
124
+ >;
125
+
126
+ // A rule row as Postgres stores it.
127
+ interface StoredRule {
128
+ isEnabled: boolean | null;
129
+ // criteria.isEnabled, or null for a legacy rule.
130
+ criteriaEnabled: boolean | null;
131
+ }
132
+
133
+ // A rule as a caller of the DatabaseService reads it back.
134
+ interface ReadRule {
135
+ id: string;
136
+ isEnabled: boolean | null | undefined;
137
+ }
138
+
139
+ // A rule as the backfill's paged read returns it.
140
+ interface PagedRule {
141
+ id: string;
142
+ projectId: string;
143
+ }
144
+
145
+ /*
146
+ * The DatabaseService paths that take an isEnabled filter, on one rule table.
147
+ * Reads run as root unless given the props of a signed-in caller.
148
+ */
149
+ interface RuleOperations {
150
+ findByEnabled(
151
+ projectId: ObjectID,
152
+ isEnabled: boolean,
153
+ props?: DatabaseCommonInteractionProps,
154
+ ): Promise<Array<ReadRule>>;
155
+ countByEnabled(
156
+ projectId: ObjectID,
157
+ isEnabled: boolean,
158
+ props?: DatabaseCommonInteractionProps,
159
+ ): Promise<number>;
160
+ findEnabledById(id: ObjectID): Promise<boolean | null | undefined>;
161
+ /*
162
+ * One `_id`-ordered page of the enabled rules of every project, after
163
+ * `cursor`: the read the ScheduleRemindersMissedByReminderRuleLookup data
164
+ * migration (App) pages with.
165
+ */
166
+ findEnabledPageAfter(
167
+ cursor: ObjectID | null,
168
+ limit: number,
169
+ ): Promise<Array<PagedRule>>;
170
+ updateByEnabled(data: {
171
+ projectId: ObjectID;
172
+ isEnabled: boolean;
173
+ setEnabled: boolean;
174
+ props: DatabaseCommonInteractionProps;
175
+ }): Promise<number>;
176
+ deleteByEnabled(projectId: ObjectID, isEnabled: boolean): Promise<number>;
177
+ hardDeleteByEnabled(projectId: ObjectID, isEnabled: boolean): Promise<number>;
178
+ create(data: {
179
+ projectId: ObjectID;
180
+ name: string;
181
+ reminderIntervalInMinutes: number;
182
+ criteria: RuleCriteria | null;
183
+ }): Promise<ObjectID>;
184
+ }
185
+
186
+ interface ReminderKind {
187
+ name: "Incident" | "Alert" | "ScheduledMaintenance";
188
+ ruleTable: string;
189
+ // The column naming the rule in its junction tables.
190
+ ruleJunctionColumn: string;
191
+ ruleLabelTable: string;
192
+ // Scheduled maintenance rules have no severity restriction.
193
+ severity: {
194
+ table: string;
195
+ ruleJunctionTable: string;
196
+ junctionColumn: string;
197
+ } | null;
198
+ subjectTable: string;
199
+ subjectLabelTable: string;
200
+ subjectLabelColumn: string;
201
+ stateTable: string;
202
+ rules: RuleOperations;
203
+ stateRow(data: {
204
+ id: ObjectID;
205
+ projectId: ObjectID;
206
+ order: number;
207
+ isResolvedState: boolean;
208
+ }): SqlRow;
209
+ subjectRow(data: {
210
+ id: ObjectID;
211
+ projectId: ObjectID;
212
+ stateId: ObjectID;
213
+ severityId: ObjectID;
214
+ }): SqlRow;
215
+ findMatchingRule(data: {
216
+ projectId: ObjectID;
217
+ severityId?: ObjectID | undefined;
218
+ labelIds?: Array<ObjectID> | undefined;
219
+ }): Promise<ReminderRule | null>;
220
+ refreshReminderSchedule(data: {
221
+ subjectId: ObjectID;
222
+ projectId: ObjectID;
223
+ }): Promise<void>;
224
+ // The rule service's re-evaluation of every open subject of a project.
225
+ refreshOpenSubjects(data: {
226
+ projectId: ObjectID;
227
+ options?: RefreshOpenSubjectsOptions;
228
+ }): Promise<void>;
229
+ /*
230
+ * Replaces the subject service's refreshReminderSchedule with
231
+ * `onRefresh`, for a test about which subjects the rule service hands to
232
+ * it rather than what it writes. The caller restores the returned spy.
233
+ */
234
+ stubRefreshReminderSchedule(onRefresh: (subjectId: string) => void): {
235
+ mockRestore(): void;
236
+ };
237
+ }
238
+
239
+ /*
240
+ * Records every statement Postgres rejected. PostgresQueryRunner hands each
241
+ * failure to the logger before it throws, whatever the caller then does with
242
+ * the error.
243
+ */
244
+ class FailedQueryRecorder implements Logger {
245
+ public failures: Array<string> = [];
246
+
247
+ public logQuery(): void {
248
+ return;
249
+ }
250
+
251
+ public logQueryError(error: string | Error, query: string): void {
252
+ this.failures.push(
253
+ `${error instanceof Error ? error.message : error} in: ${query}`,
254
+ );
255
+ }
256
+
257
+ public logQuerySlow(): void {
258
+ return;
259
+ }
260
+
261
+ public logSchemaBuild(): void {
262
+ return;
263
+ }
264
+
265
+ public logMigration(): void {
266
+ return;
267
+ }
268
+
269
+ public log(): void {
270
+ return;
271
+ }
272
+ }
273
+
274
+ function ruleOperations<TRule extends ReminderRule>(
275
+ service: DatabaseService<TRule>,
276
+ ): RuleOperations {
277
+ function enabledQuery(projectId: ObjectID, isEnabled: boolean): Query<TRule> {
278
+ return { projectId: projectId, isEnabled: isEnabled } as Query<TRule>;
279
+ }
280
+
281
+ return {
282
+ findByEnabled: async (
283
+ projectId: ObjectID,
284
+ isEnabled: boolean,
285
+ props: DatabaseCommonInteractionProps = { isRoot: true },
286
+ ): Promise<Array<ReadRule>> => {
287
+ const rules: Array<TRule> = await service.findBy({
288
+ query: enabledQuery(projectId, isEnabled),
289
+ select: { _id: true, isEnabled: true } as Select<TRule>,
290
+ limit: 100,
291
+ skip: 0,
292
+ props: props,
293
+ });
294
+
295
+ return rules
296
+ .map((rule: TRule): ReadRule => {
297
+ return { id: rule.id!.toString(), isEnabled: rule.isEnabled };
298
+ })
299
+ .sort((a: ReadRule, b: ReadRule): number => {
300
+ return a.id.localeCompare(b.id);
301
+ });
302
+ },
303
+
304
+ countByEnabled: async (
305
+ projectId: ObjectID,
306
+ isEnabled: boolean,
307
+ props: DatabaseCommonInteractionProps = { isRoot: true },
308
+ ): Promise<number> => {
309
+ return (
310
+ await service.countBy({
311
+ query: enabledQuery(projectId, isEnabled),
312
+ props: props,
313
+ })
314
+ ).toNumber();
315
+ },
316
+
317
+ findEnabledById: async (
318
+ id: ObjectID,
319
+ ): Promise<boolean | null | undefined> => {
320
+ const rule: TRule | null = await service.findOneById({
321
+ id: id,
322
+ select: { isEnabled: true } as Select<TRule>,
323
+ props: { isRoot: true },
324
+ });
325
+
326
+ return rule?.isEnabled;
327
+ },
328
+
329
+ findEnabledPageAfter: async (
330
+ cursor: ObjectID | null,
331
+ limit: number,
332
+ ): Promise<Array<PagedRule>> => {
333
+ const rules: Array<TRule> = await service.findBy({
334
+ query: {
335
+ isEnabled: true,
336
+ ...(cursor ? { _id: QueryHelper.greaterThan(cursor) } : {}),
337
+ } as Query<TRule>,
338
+ select: { _id: true, projectId: true } as Select<TRule>,
339
+ sort: { _id: SortOrder.Ascending },
340
+ skip: 0,
341
+ limit: limit,
342
+ props: { isRoot: true },
343
+ });
344
+
345
+ return rules.map((rule: TRule): PagedRule => {
346
+ return {
347
+ id: rule.id!.toString(),
348
+ projectId: rule.projectId!.toString(),
349
+ };
350
+ });
351
+ },
352
+
353
+ updateByEnabled: async (data: {
354
+ projectId: ObjectID;
355
+ isEnabled: boolean;
356
+ setEnabled: boolean;
357
+ props: DatabaseCommonInteractionProps;
358
+ }): Promise<number> => {
359
+ return await service.updateBy({
360
+ query: enabledQuery(data.projectId, data.isEnabled),
361
+ // Every rule model has isEnabled; TypeScript cannot see it through TRule.
362
+ data: { isEnabled: data.setEnabled } as unknown as PartialEntity<TRule>,
363
+ limit: 100,
364
+ skip: 0,
365
+ props: data.props,
366
+ });
367
+ },
368
+
369
+ deleteByEnabled: async (
370
+ projectId: ObjectID,
371
+ isEnabled: boolean,
372
+ ): Promise<number> => {
373
+ return await service.deleteBy({
374
+ query: enabledQuery(projectId, isEnabled),
375
+ limit: 100,
376
+ skip: 0,
377
+ props: { isRoot: true },
378
+ });
379
+ },
380
+
381
+ hardDeleteByEnabled: async (
382
+ projectId: ObjectID,
383
+ isEnabled: boolean,
384
+ ): Promise<number> => {
385
+ return await service.hardDeleteBy({
386
+ query: enabledQuery(projectId, isEnabled),
387
+ limit: 100,
388
+ skip: 0,
389
+ props: { isRoot: true },
390
+ });
391
+ },
392
+
393
+ create: async (data: {
394
+ projectId: ObjectID;
395
+ name: string;
396
+ reminderIntervalInMinutes: number;
397
+ criteria: RuleCriteria | null;
398
+ }): Promise<ObjectID> => {
399
+ const rule: TRule = new service.modelType();
400
+ rule.projectId = data.projectId;
401
+ rule.name = data.name;
402
+ rule.reminderIntervalInMinutes = data.reminderIntervalInMinutes;
403
+ rule.isEnabled = true;
404
+ if (data.criteria) {
405
+ rule.criteria = data.criteria;
406
+ }
407
+
408
+ const created: TRule = await service.create({
409
+ data: rule,
410
+ props: { isRoot: true },
411
+ });
412
+
413
+ return created.id!;
414
+ },
415
+ };
416
+ }
417
+
418
+ function criteria(
419
+ isEnabled: boolean,
420
+ filters: Array<RuleCriteriaFilter> = [],
421
+ filterCondition: FilterCondition = FilterCondition.All,
422
+ ): RuleCriteria {
423
+ return {
424
+ schemaVersion: RULE_CRITERIA_SCHEMA_VERSION,
425
+ filterCondition: filterCondition,
426
+ filters: filters,
427
+ isEnabled: isEnabled,
428
+ };
429
+ }
430
+
431
+ function hasAnyOfLabels(labelIds: Array<ObjectID>): RuleCriteriaFilter {
432
+ return {
433
+ field: "labels",
434
+ operator: RuleCriteriaOperator.HasAnyOf,
435
+ value: labelIds.map((id: ObjectID): string => {
436
+ return id.toString();
437
+ }),
438
+ };
439
+ }
440
+
441
+ function hasNoneOfLabels(labelIds: Array<ObjectID>): RuleCriteriaFilter {
442
+ return {
443
+ field: "labels",
444
+ operator: RuleCriteriaOperator.HasNoneOf,
445
+ value: labelIds.map((id: ObjectID): string => {
446
+ return id.toString();
447
+ }),
448
+ };
449
+ }
450
+
451
+ const KINDS: Array<ReminderKind> = [
452
+ {
453
+ name: "Incident",
454
+ ruleTable: "IncidentReminderRule",
455
+ ruleJunctionColumn: "incidentReminderRuleId",
456
+ ruleLabelTable: "IncidentReminderRuleLabel",
457
+ severity: {
458
+ table: "IncidentSeverity",
459
+ ruleJunctionTable: "IncidentReminderRuleIncidentSeverity",
460
+ junctionColumn: "incidentSeverityId",
461
+ },
462
+ subjectTable: "Incident",
463
+ subjectLabelTable: "IncidentLabel",
464
+ subjectLabelColumn: "incidentId",
465
+ stateTable: "IncidentState",
466
+ rules: ruleOperations(IncidentReminderRuleService),
467
+ stateRow: (data: {
468
+ id: ObjectID;
469
+ projectId: ObjectID;
470
+ order: number;
471
+ isResolvedState: boolean;
472
+ }): SqlRow => {
473
+ return {
474
+ _id: data.id.toString(),
475
+ projectId: data.projectId.toString(),
476
+ name: `State ${data.order}`,
477
+ slug: `state-${data.id.toString()}`,
478
+ color: "#000000",
479
+ order: data.order,
480
+ isResolvedState: data.isResolvedState,
481
+ version: 1,
482
+ };
483
+ },
484
+ subjectRow: (data: {
485
+ id: ObjectID;
486
+ projectId: ObjectID;
487
+ stateId: ObjectID;
488
+ severityId: ObjectID;
489
+ }): SqlRow => {
490
+ return {
491
+ _id: data.id.toString(),
492
+ projectId: data.projectId.toString(),
493
+ title: "Checkout is failing",
494
+ slug: `incident-${data.id.toString()}`,
495
+ currentIncidentStateId: data.stateId.toString(),
496
+ incidentSeverityId: data.severityId.toString(),
497
+ version: 1,
498
+ };
499
+ },
500
+ findMatchingRule: (data: {
501
+ projectId: ObjectID;
502
+ severityId?: ObjectID | undefined;
503
+ labelIds?: Array<ObjectID> | undefined;
504
+ }): Promise<ReminderRule | null> => {
505
+ return IncidentReminderRuleService.findMatchingRule({
506
+ projectId: data.projectId,
507
+ incidentSeverityId: data.severityId,
508
+ labelIds: data.labelIds,
509
+ });
510
+ },
511
+ refreshReminderSchedule: (data: {
512
+ subjectId: ObjectID;
513
+ projectId: ObjectID;
514
+ }): Promise<void> => {
515
+ return IncidentService.refreshReminderSchedule({
516
+ incidentId: data.subjectId,
517
+ projectId: data.projectId,
518
+ });
519
+ },
520
+ refreshOpenSubjects: (data: {
521
+ projectId: ObjectID;
522
+ options?: RefreshOpenSubjectsOptions;
523
+ }): Promise<void> => {
524
+ return IncidentReminderRuleService.refreshSchedulesForOpenIncidents(
525
+ data.projectId,
526
+ data.options,
527
+ );
528
+ },
529
+ stubRefreshReminderSchedule: (
530
+ onRefresh: (subjectId: string) => void,
531
+ ): { mockRestore(): void } => {
532
+ return jest
533
+ .spyOn(IncidentService, "refreshReminderSchedule")
534
+ .mockImplementation((data: { incidentId: ObjectID }): Promise<void> => {
535
+ onRefresh(data.incidentId.toString());
536
+ return Promise.resolve();
537
+ });
538
+ },
539
+ },
540
+ {
541
+ name: "Alert",
542
+ ruleTable: "AlertReminderRule",
543
+ ruleJunctionColumn: "alertReminderRuleId",
544
+ ruleLabelTable: "AlertReminderRuleLabel",
545
+ severity: {
546
+ table: "AlertSeverity",
547
+ ruleJunctionTable: "AlertReminderRuleAlertSeverity",
548
+ junctionColumn: "alertSeverityId",
549
+ },
550
+ subjectTable: "Alert",
551
+ subjectLabelTable: "AlertLabel",
552
+ subjectLabelColumn: "alertId",
553
+ stateTable: "AlertState",
554
+ rules: ruleOperations(AlertReminderRuleService),
555
+ stateRow: (data: {
556
+ id: ObjectID;
557
+ projectId: ObjectID;
558
+ order: number;
559
+ isResolvedState: boolean;
560
+ }): SqlRow => {
561
+ return {
562
+ _id: data.id.toString(),
563
+ projectId: data.projectId.toString(),
564
+ name: `State ${data.order}`,
565
+ color: "#000000",
566
+ order: data.order,
567
+ isResolvedState: data.isResolvedState,
568
+ version: 1,
569
+ };
570
+ },
571
+ subjectRow: (data: {
572
+ id: ObjectID;
573
+ projectId: ObjectID;
574
+ stateId: ObjectID;
575
+ severityId: ObjectID;
576
+ }): SqlRow => {
577
+ return {
578
+ _id: data.id.toString(),
579
+ projectId: data.projectId.toString(),
580
+ title: "Checkout p95 latency is high",
581
+ currentAlertStateId: data.stateId.toString(),
582
+ alertSeverityId: data.severityId.toString(),
583
+ version: 1,
584
+ };
585
+ },
586
+ findMatchingRule: (data: {
587
+ projectId: ObjectID;
588
+ severityId?: ObjectID | undefined;
589
+ labelIds?: Array<ObjectID> | undefined;
590
+ }): Promise<ReminderRule | null> => {
591
+ return AlertReminderRuleService.findMatchingRule({
592
+ projectId: data.projectId,
593
+ alertSeverityId: data.severityId,
594
+ labelIds: data.labelIds,
595
+ });
596
+ },
597
+ refreshReminderSchedule: (data: {
598
+ subjectId: ObjectID;
599
+ projectId: ObjectID;
600
+ }): Promise<void> => {
601
+ return AlertService.refreshReminderSchedule({
602
+ alertId: data.subjectId,
603
+ projectId: data.projectId,
604
+ });
605
+ },
606
+ refreshOpenSubjects: (data: {
607
+ projectId: ObjectID;
608
+ options?: RefreshOpenSubjectsOptions;
609
+ }): Promise<void> => {
610
+ return AlertReminderRuleService.refreshSchedulesForOpenAlerts(
611
+ data.projectId,
612
+ data.options,
613
+ );
614
+ },
615
+ stubRefreshReminderSchedule: (
616
+ onRefresh: (subjectId: string) => void,
617
+ ): { mockRestore(): void } => {
618
+ return jest
619
+ .spyOn(AlertService, "refreshReminderSchedule")
620
+ .mockImplementation((data: { alertId: ObjectID }): Promise<void> => {
621
+ onRefresh(data.alertId.toString());
622
+ return Promise.resolve();
623
+ });
624
+ },
625
+ },
626
+ {
627
+ name: "ScheduledMaintenance",
628
+ ruleTable: "ScheduledMaintenanceReminderRule",
629
+ ruleJunctionColumn: "scheduledMaintenanceReminderRuleId",
630
+ ruleLabelTable: "ScheduledMaintenanceReminderRuleLabel",
631
+ severity: null,
632
+ subjectTable: "ScheduledMaintenance",
633
+ subjectLabelTable: "ScheduledMaintenanceLabel",
634
+ subjectLabelColumn: "scheduledMaintenanceId",
635
+ stateTable: "ScheduledMaintenanceState",
636
+ rules: ruleOperations(ScheduledMaintenanceReminderRuleService),
637
+ stateRow: (data: {
638
+ id: ObjectID;
639
+ projectId: ObjectID;
640
+ order: number;
641
+ isResolvedState: boolean;
642
+ }): SqlRow => {
643
+ return {
644
+ _id: data.id.toString(),
645
+ projectId: data.projectId.toString(),
646
+ name: `State ${data.order}`,
647
+ slug: `state-${data.id.toString()}`,
648
+ color: "#000000",
649
+ order: data.order,
650
+ isResolvedState: data.isResolvedState,
651
+ version: 1,
652
+ };
653
+ },
654
+ subjectRow: (data: {
655
+ id: ObjectID;
656
+ projectId: ObjectID;
657
+ stateId: ObjectID;
658
+ }): SqlRow => {
659
+ /*
660
+ * Already started, so a rule that does not remind while the event is
661
+ * scheduled still schedules from now; the deferred case has its own
662
+ * test.
663
+ */
664
+ return {
665
+ _id: data.id.toString(),
666
+ projectId: data.projectId.toString(),
667
+ title: "Database upgrade",
668
+ slug: `maintenance-${data.id.toString()}`,
669
+ currentScheduledMaintenanceStateId: data.stateId.toString(),
670
+ startsAt: new Date(Date.now() - 60 * MINUTE_IN_MS),
671
+ endsAt: new Date(Date.now() + 60 * MINUTE_IN_MS),
672
+ version: 1,
673
+ };
674
+ },
675
+ findMatchingRule: (data: {
676
+ projectId: ObjectID;
677
+ labelIds?: Array<ObjectID> | undefined;
678
+ }): Promise<ReminderRule | null> => {
679
+ return ScheduledMaintenanceReminderRuleService.findMatchingRule({
680
+ projectId: data.projectId,
681
+ labelIds: data.labelIds,
682
+ });
683
+ },
684
+ refreshReminderSchedule: (data: {
685
+ subjectId: ObjectID;
686
+ projectId: ObjectID;
687
+ }): Promise<void> => {
688
+ return ScheduledMaintenanceService.refreshReminderSchedule({
689
+ scheduledMaintenanceId: data.subjectId,
690
+ projectId: data.projectId,
691
+ });
692
+ },
693
+ refreshOpenSubjects: (data: {
694
+ projectId: ObjectID;
695
+ options?: RefreshOpenSubjectsOptions;
696
+ }): Promise<void> => {
697
+ return ScheduledMaintenanceReminderRuleService.refreshSchedulesForOpenScheduledMaintenances(
698
+ data.projectId,
699
+ data.options,
700
+ );
701
+ },
702
+ stubRefreshReminderSchedule: (
703
+ onRefresh: (subjectId: string) => void,
704
+ ): { mockRestore(): void } => {
705
+ return jest
706
+ .spyOn(ScheduledMaintenanceService, "refreshReminderSchedule")
707
+ .mockImplementation(
708
+ (data: { scheduledMaintenanceId: ObjectID }): Promise<void> => {
709
+ onRefresh(data.scheduledMaintenanceId.toString());
710
+ return Promise.resolve();
711
+ },
712
+ );
713
+ },
714
+ },
715
+ ];
716
+
717
+ describePostgres("reminder rule lookup against a migrated Postgres", () => {
718
+ const schema: string = `reminder_rule_${ObjectID.generate()
719
+ .toString()
720
+ .replace(/-/g, "")}`;
721
+
722
+ const projectId: ObjectID = ObjectID.generate();
723
+ const otherProjectId: ObjectID = ObjectID.generate();
724
+ // A signed-in user who owns the test project; see projectOwnerProps.
725
+ const ownerUserId: ObjectID = ObjectID.generate();
726
+
727
+ const failedQueries: FailedQueryRecorder = new FailedQueryRecorder();
728
+ let database: DataSource;
729
+
730
+ // Per kind, the open and the resolved state of the test project.
731
+ let openStateIds: Record<string, ObjectID> = {};
732
+ let resolvedStateIds: Record<string, ObjectID> = {};
733
+
734
+ beforeAll(async () => {
735
+ database = new DataSource({
736
+ type: "postgres",
737
+ host: process.env["REMINDER_RULE_TEST_DATABASE_HOST"] || "localhost",
738
+ port: Number(process.env["REMINDER_RULE_TEST_DATABASE_PORT"] || "5400"),
739
+ username: process.env["DATABASE_USERNAME"] || "postgres",
740
+ password: process.env["DATABASE_PASSWORD"] || "password",
741
+ database:
742
+ process.env["REMINDER_RULE_TEST_DATABASE_NAME"] ||
743
+ process.env["DATABASE_NAME"] ||
744
+ "oneuptimedb",
745
+ entities: Entities,
746
+ schema,
747
+ synchronize: false,
748
+ logger: failedQueries,
749
+ extra: { options: `-c search_path=${schema},public` },
750
+ });
751
+ await database.initialize();
752
+
753
+ // Read the migrated triggers before the clones exist.
754
+ const migratedTriggers: Array<{ table: string; definition: string }> =
755
+ await database.query(
756
+ `SELECT c.relname AS "table", pg_get_triggerdef(t.oid) AS "definition"
757
+ FROM pg_trigger t
758
+ JOIN pg_class c ON c.oid = t.tgrelid
759
+ JOIN pg_namespace n ON n.oid = c.relnamespace
760
+ WHERE n.nspname = 'public' AND NOT t.tgisinternal
761
+ AND c.relname = ANY($1)
762
+ ORDER BY c.relname, t.tgname`,
763
+ [TABLES],
764
+ );
765
+
766
+ await database.query(`CREATE SCHEMA "${schema}"`);
767
+
768
+ for (const table of TABLES) {
769
+ await database.query(
770
+ `CREATE TABLE "${schema}"."${table}" (LIKE public."${table}" INCLUDING ALL)`,
771
+ );
772
+ }
773
+
774
+ for (const trigger of migratedTriggers) {
775
+ const definition: string = trigger.definition.replace(
776
+ ` ON public."${trigger.table}" `,
777
+ ` ON "${schema}"."${trigger.table}" `,
778
+ );
779
+ expect(definition).toContain(`ON "${schema}"."${trigger.table}"`);
780
+ await database.query(definition);
781
+ }
782
+
783
+ const currentSchema: Array<{ current_schema: string }> =
784
+ await database.query("SELECT current_schema()");
785
+ expect(currentSchema[0]?.current_schema).toBe(schema);
786
+
787
+ jest.spyOn(PostgresAppInstance, "isConnected").mockReturnValue(true);
788
+ jest.spyOn(PostgresAppInstance, "getDataSource").mockReturnValue(database);
789
+
790
+ /*
791
+ * Workflow triggers POST to the workflow service and realtime events go
792
+ * to the socket server: the only side effects of these paths that leave
793
+ * Postgres. The subjects' updates fire them, and so do the rules'
794
+ * creates, updates and deletes.
795
+ */
796
+ const servicesWithSideEffects: Array<SideEffectHooks> = [
797
+ IncidentService,
798
+ AlertService,
799
+ ScheduledMaintenanceService,
800
+ IncidentReminderRuleService,
801
+ AlertReminderRuleService,
802
+ ScheduledMaintenanceReminderRuleService,
803
+ ];
804
+
805
+ for (const service of servicesWithSideEffects) {
806
+ jest.spyOn(service, "onTriggerWorkflow").mockResolvedValue(undefined);
807
+ jest.spyOn(service, "onTriggerRealtime").mockResolvedValue(undefined);
808
+ }
809
+ });
810
+
811
+ beforeEach(async () => {
812
+ failedQueries.failures = [];
813
+
814
+ await database.query(
815
+ TABLES.map((table: string): string => {
816
+ return `DELETE FROM "${schema}"."${table}"`;
817
+ }).join("; "),
818
+ );
819
+
820
+ openStateIds = {};
821
+ resolvedStateIds = {};
822
+
823
+ for (const kind of KINDS) {
824
+ openStateIds[kind.name] = ObjectID.generate();
825
+ resolvedStateIds[kind.name] = ObjectID.generate();
826
+
827
+ await insert(
828
+ kind.stateTable,
829
+ kind.stateRow({
830
+ id: openStateIds[kind.name]!,
831
+ projectId: projectId,
832
+ order: 1,
833
+ isResolvedState: false,
834
+ }),
835
+ );
836
+ await insert(
837
+ kind.stateTable,
838
+ kind.stateRow({
839
+ id: resolvedStateIds[kind.name]!,
840
+ projectId: projectId,
841
+ order: 2,
842
+ isResolvedState: true,
843
+ }),
844
+ );
845
+ }
846
+ });
847
+
848
+ afterEach(() => {
849
+ // A failed statement fails the test, even if a caller swallowed it.
850
+ expect(failedQueries.failures).toEqual([]);
851
+ });
852
+
853
+ afterAll(async () => {
854
+ jest.restoreAllMocks();
855
+ if (database?.isInitialized) {
856
+ await database.query(`DROP SCHEMA IF EXISTS "${schema}" CASCADE`);
857
+ await database.destroy();
858
+ }
859
+ });
860
+
861
+ async function insert(table: string, row: SqlRow): Promise<void> {
862
+ const columns: Array<string> = Object.keys(row);
863
+ await database.query(
864
+ `INSERT INTO "${schema}"."${table}" (${columns
865
+ .map((column: string): string => {
866
+ return `"${column}"`;
867
+ })
868
+ .join(", ")}) VALUES (${columns
869
+ .map((_column: string, index: number): string => {
870
+ return `$${index + 1}`;
871
+ })
872
+ .join(", ")})`,
873
+ columns.map((column: string): unknown => {
874
+ return row[column];
875
+ }),
876
+ );
877
+ }
878
+
879
+ async function seedLabel(project: ObjectID = projectId): Promise<ObjectID> {
880
+ const id: ObjectID = ObjectID.generate();
881
+ await insert("Label", {
882
+ _id: id.toString(),
883
+ projectId: project.toString(),
884
+ name: `Label ${id.toString()}`,
885
+ slug: `label-${id.toString()}`,
886
+ color: "#000000",
887
+ version: 1,
888
+ });
889
+ return id;
890
+ }
891
+
892
+ async function seedSeverity(kind: ReminderKind): Promise<ObjectID> {
893
+ const id: ObjectID = ObjectID.generate();
894
+ await insert(kind.severity!.table, {
895
+ _id: id.toString(),
896
+ projectId: projectId.toString(),
897
+ name: `Severity ${id.toString()}`,
898
+ slug: `severity-${id.toString()}`,
899
+ color: "#000000",
900
+ order: 1,
901
+ version: 1,
902
+ });
903
+ return id;
904
+ }
905
+
906
+ interface RuleSeed {
907
+ name: string;
908
+ order: number;
909
+ /*
910
+ * The legacy column as a legacy row stores it. A criteria-backed row's
911
+ * stored value is derived from criteria.isEnabled by the migrated
912
+ * trigger, whatever is written here.
913
+ */
914
+ isEnabled?: boolean | null;
915
+ criteria?: RuleCriteria | null;
916
+ reminderIntervalInMinutes?: number;
917
+ project?: ObjectID;
918
+ deletedAt?: Date | null;
919
+ labelIds?: Array<ObjectID>;
920
+ severityIds?: Array<ObjectID>;
921
+ remindWhileScheduled?: boolean;
922
+ }
923
+
924
+ async function seedRule(
925
+ kind: ReminderKind,
926
+ seed: RuleSeed,
927
+ ): Promise<ObjectID> {
928
+ const id: ObjectID = ObjectID.generate();
929
+ const row: SqlRow = {
930
+ _id: id.toString(),
931
+ projectId: (seed.project || projectId).toString(),
932
+ name: seed.name,
933
+ order: seed.order,
934
+ isEnabled: seed.isEnabled === undefined ? true : seed.isEnabled,
935
+ criteria: seed.criteria ? JSON.stringify(seed.criteria) : null,
936
+ reminderIntervalInMinutes: seed.reminderIntervalInMinutes || 30,
937
+ deletedAt: seed.deletedAt || null,
938
+ version: 1,
939
+ };
940
+
941
+ if (seed.remindWhileScheduled !== undefined) {
942
+ row["remindWhileScheduled"] = seed.remindWhileScheduled;
943
+ }
944
+
945
+ await insert(kind.ruleTable, row);
946
+
947
+ for (const labelId of seed.labelIds || []) {
948
+ await insert(kind.ruleLabelTable, {
949
+ [kind.ruleJunctionColumn]: id.toString(),
950
+ labelId: labelId.toString(),
951
+ });
952
+ }
953
+
954
+ for (const severityId of seed.severityIds || []) {
955
+ await insert(kind.severity!.ruleJunctionTable, {
956
+ [kind.ruleJunctionColumn]: id.toString(),
957
+ [kind.severity!.junctionColumn]: severityId.toString(),
958
+ });
959
+ }
960
+
961
+ return id;
962
+ }
963
+
964
+ // Every enabled/disabled encoding a rule table holds, plus decoys.
965
+ interface SeededRuleSet {
966
+ legacyEnabled: ObjectID;
967
+ legacyDisabled: ObjectID;
968
+ legacyNull: ObjectID;
969
+ criteriaEnabled: ObjectID;
970
+ criteriaDisabled: ObjectID;
971
+ otherProjectEnabled: ObjectID;
972
+ softDeletedEnabled: ObjectID;
973
+ }
974
+
975
+ async function seedEveryEncoding(kind: ReminderKind): Promise<SeededRuleSet> {
976
+ return {
977
+ legacyDisabled: await seedRule(kind, {
978
+ name: "legacy disabled",
979
+ order: 1,
980
+ isEnabled: false,
981
+ }),
982
+ legacyNull: await seedRule(kind, {
983
+ name: "legacy with a NULL isEnabled",
984
+ order: 2,
985
+ isEnabled: null,
986
+ }),
987
+ criteriaDisabled: await seedRule(kind, {
988
+ name: "criteria-backed disabled",
989
+ order: 3,
990
+ criteria: criteria(false),
991
+ }),
992
+ legacyEnabled: await seedRule(kind, {
993
+ name: "legacy enabled",
994
+ order: 4,
995
+ isEnabled: true,
996
+ }),
997
+ criteriaEnabled: await seedRule(kind, {
998
+ name: "criteria-backed enabled",
999
+ order: 5,
1000
+ criteria: criteria(true),
1001
+ }),
1002
+ otherProjectEnabled: await seedRule(kind, {
1003
+ name: "another project's enabled rule",
1004
+ order: 0,
1005
+ isEnabled: true,
1006
+ project: otherProjectId,
1007
+ }),
1008
+ softDeletedEnabled: await seedRule(kind, {
1009
+ name: "soft-deleted enabled rule",
1010
+ order: 0,
1011
+ isEnabled: true,
1012
+ deletedAt: new Date(),
1013
+ }),
1014
+ };
1015
+ }
1016
+
1017
+ async function storedRules(
1018
+ kind: ReminderKind,
1019
+ ): Promise<Record<string, StoredRule>> {
1020
+ const rows: Array<{
1021
+ _id: string;
1022
+ isEnabled: boolean | null;
1023
+ criteria: RuleCriteria | null;
1024
+ }> = await database.query(
1025
+ `SELECT "_id", "isEnabled", "criteria" FROM "${schema}"."${kind.ruleTable}"`,
1026
+ );
1027
+
1028
+ const stored: Record<string, StoredRule> = {};
1029
+
1030
+ for (const row of rows) {
1031
+ stored[row._id] = {
1032
+ isEnabled: row.isEnabled,
1033
+ criteriaEnabled:
1034
+ row.criteria && typeof row.criteria.isEnabled === "boolean"
1035
+ ? row.criteria.isEnabled
1036
+ : null,
1037
+ };
1038
+ }
1039
+
1040
+ return stored;
1041
+ }
1042
+
1043
+ async function countRules(kind: ReminderKind): Promise<number> {
1044
+ const rows: Array<{ count: string }> = await database.query(
1045
+ `SELECT count(*)::text AS "count" FROM "${schema}"."${kind.ruleTable}"`,
1046
+ );
1047
+ return Number(rows[0]!.count);
1048
+ }
1049
+
1050
+ async function seedSubject(
1051
+ kind: ReminderKind,
1052
+ options: {
1053
+ severityId?: ObjectID;
1054
+ labelIds?: Array<ObjectID>;
1055
+ enableReminders?: boolean;
1056
+ resolved?: boolean;
1057
+ nextReminderNotificationAt?: Date | null;
1058
+ // Scheduled maintenance only; give both to keep the window valid.
1059
+ startsAt?: Date;
1060
+ endsAt?: Date;
1061
+ } = {},
1062
+ ): Promise<ObjectID> {
1063
+ const id: ObjectID = ObjectID.generate();
1064
+ const row: SqlRow = kind.subjectRow({
1065
+ id: id,
1066
+ projectId: projectId,
1067
+ stateId: options.resolved
1068
+ ? resolvedStateIds[kind.name]!
1069
+ : openStateIds[kind.name]!,
1070
+ severityId: options.severityId || ObjectID.generate(),
1071
+ });
1072
+
1073
+ row["enableReminders"] =
1074
+ options.enableReminders === undefined ? true : options.enableReminders;
1075
+ row["nextReminderNotificationAt"] =
1076
+ options.nextReminderNotificationAt || null;
1077
+
1078
+ if (options.startsAt) {
1079
+ row["startsAt"] = options.startsAt;
1080
+ }
1081
+
1082
+ if (options.endsAt) {
1083
+ row["endsAt"] = options.endsAt;
1084
+ }
1085
+
1086
+ await insert(kind.subjectTable, row);
1087
+
1088
+ for (const labelId of options.labelIds || []) {
1089
+ await insert(kind.subjectLabelTable, {
1090
+ [kind.subjectLabelColumn]: id.toString(),
1091
+ labelId: labelId.toString(),
1092
+ });
1093
+ }
1094
+
1095
+ return id;
1096
+ }
1097
+
1098
+ /*
1099
+ * `count` open subjects of the test project with no reminder, in one
1100
+ * statement, as seedSubject would write each; returns their ids. For
1101
+ * seeding more subjects than one read returns.
1102
+ */
1103
+ async function seedOpenSubjectsWithoutReminder(
1104
+ kind: ReminderKind,
1105
+ count: number,
1106
+ ): Promise<Array<string>> {
1107
+ const rows: Array<SqlRow> = Array.from({ length: count }, (): SqlRow => {
1108
+ return {
1109
+ ...kind.subjectRow({
1110
+ id: ObjectID.generate(),
1111
+ projectId: projectId,
1112
+ stateId: openStateIds[kind.name]!,
1113
+ severityId: ObjectID.generate(),
1114
+ }),
1115
+ enableReminders: true,
1116
+ nextReminderNotificationAt: null,
1117
+ };
1118
+ });
1119
+
1120
+ const columns: string = Object.keys(rows[0]!)
1121
+ .map((column: string): string => {
1122
+ return `"${column}"`;
1123
+ })
1124
+ .join(", ");
1125
+
1126
+ await database.query(
1127
+ `INSERT INTO "${schema}"."${kind.subjectTable}" (${columns})
1128
+ SELECT ${columns}
1129
+ FROM json_populate_recordset(NULL::"${schema}"."${kind.subjectTable}", $1)`,
1130
+ [JSON.stringify(rows)],
1131
+ );
1132
+
1133
+ return rows.map((row: SqlRow): string => {
1134
+ return row["_id"] as string;
1135
+ });
1136
+ }
1137
+
1138
+ async function nextReminderAt(
1139
+ kind: ReminderKind,
1140
+ subjectId: ObjectID,
1141
+ ): Promise<Date | null> {
1142
+ const rows: Array<{ nextReminderNotificationAt: Date | null }> =
1143
+ await database.query(
1144
+ `SELECT "nextReminderNotificationAt" FROM "${schema}"."${kind.subjectTable}" WHERE "_id" = $1`,
1145
+ [subjectId.toString()],
1146
+ );
1147
+ expect(rows).toHaveLength(1);
1148
+ return rows[0]!.nextReminderNotificationAt;
1149
+ }
1150
+
1151
+ // A subject row as a refresh leaves it.
1152
+ interface StoredSubject {
1153
+ nextReminderNotificationAt: Date | null;
1154
+ /*
1155
+ * TypeORM bumps the version column in every UPDATE it issues, so a row
1156
+ * still at the seeded version 1 was never written.
1157
+ */
1158
+ version: number;
1159
+ }
1160
+
1161
+ async function storedSubject(
1162
+ kind: ReminderKind,
1163
+ subjectId: ObjectID,
1164
+ ): Promise<StoredSubject> {
1165
+ const rows: Array<StoredSubject> = await database.query(
1166
+ `SELECT "nextReminderNotificationAt", "version" FROM "${schema}"."${kind.subjectTable}" WHERE "_id" = $1`,
1167
+ [subjectId.toString()],
1168
+ );
1169
+ expect(rows).toHaveLength(1);
1170
+ return rows[0]!;
1171
+ }
1172
+
1173
+ /*
1174
+ * Runs the refresh and returns the window its "now" fell into, so the
1175
+ * scheduled time is checked without depending on the clock.
1176
+ */
1177
+ async function refresh(
1178
+ kind: ReminderKind,
1179
+ subjectId: ObjectID,
1180
+ ): Promise<{ startedAt: number; finishedAt: number }> {
1181
+ const startedAt: number = Date.now();
1182
+ await kind.refreshReminderSchedule({
1183
+ subjectId: subjectId,
1184
+ projectId: projectId,
1185
+ });
1186
+ const finishedAt: number = Date.now();
1187
+ return { startedAt, finishedAt };
1188
+ }
1189
+
1190
+ // As refresh, for the rule service's refresh of every open subject.
1191
+ async function refreshOpenSubjects(
1192
+ kind: ReminderKind,
1193
+ options?: RefreshOpenSubjectsOptions,
1194
+ ): Promise<{ startedAt: number; finishedAt: number }> {
1195
+ const startedAt: number = Date.now();
1196
+ await kind.refreshOpenSubjects({
1197
+ projectId: projectId,
1198
+ options: options,
1199
+ });
1200
+ const finishedAt: number = Date.now();
1201
+ return { startedAt, finishedAt };
1202
+ }
1203
+
1204
+ /*
1205
+ * The props a signed-in owner of the test project brings to a dashboard or
1206
+ * API read, the global permissions being those AccessTokenService grants
1207
+ * every user: with the project as the tenant, or without one, when the
1208
+ * permission layer scopes the query to each project the user belongs to.
1209
+ */
1210
+ function projectOwnerProps(
1211
+ withTenantId: boolean,
1212
+ ): DatabaseCommonInteractionProps {
1213
+ return {
1214
+ userId: ownerUserId,
1215
+ userType: UserType.User,
1216
+ ...(withTenantId ? { tenantId: projectId } : {}),
1217
+ userGlobalAccessPermission: {
1218
+ _type: "UserGlobalAccessPermission",
1219
+ projectIds: [projectId],
1220
+ globalPermissions: [
1221
+ Permission.Public,
1222
+ Permission.User,
1223
+ Permission.CurrentUser,
1224
+ ],
1225
+ },
1226
+ userTenantAccessPermission: {
1227
+ [projectId.toString()]: {
1228
+ _type: "UserTenantAccessPermission",
1229
+ projectId: projectId,
1230
+ permissions: [
1231
+ {
1232
+ _type: "UserPermission",
1233
+ permission: Permission.ProjectOwner,
1234
+ labelIds: [],
1235
+ isBlockPermission: false,
1236
+ },
1237
+ ],
1238
+ },
1239
+ },
1240
+ };
1241
+ }
1242
+
1243
+ function expectScheduledIn(
1244
+ scheduled: Date | null,
1245
+ window: { startedAt: number; finishedAt: number },
1246
+ intervalInMinutes: number,
1247
+ ): void {
1248
+ expect(scheduled).toBeInstanceOf(Date);
1249
+ expect(scheduled!.getTime()).toBeGreaterThanOrEqual(
1250
+ window.startedAt + intervalInMinutes * MINUTE_IN_MS,
1251
+ );
1252
+ expect(scheduled!.getTime()).toBeLessThanOrEqual(
1253
+ window.finishedAt + intervalInMinutes * MINUTE_IN_MS,
1254
+ );
1255
+ }
1256
+
1257
+ function ids(values: Array<ObjectID>): Array<string> {
1258
+ return values
1259
+ .map((value: ObjectID): string => {
1260
+ return value.toString();
1261
+ })
1262
+ .sort();
1263
+ }
1264
+
1265
+ describe("the migrated rule tables", () => {
1266
+ test.each(KINDS)(
1267
+ "$name: the stored isEnabled of a criteria-backed rule is derived from criteria.isEnabled",
1268
+ async (kind: ReminderKind) => {
1269
+ /*
1270
+ * The rollout encoding the lookup decodes: a criteria-backed rule
1271
+ * stores NULL when enabled and false when disabled, so an older
1272
+ * worker asking for isEnabled = true sees neither. The migrated
1273
+ * trigger enforces it on every write, whatever the writer sent.
1274
+ */
1275
+ const enabled: ObjectID = await seedRule(kind, {
1276
+ name: "criteria-backed, sent as true",
1277
+ order: 1,
1278
+ isEnabled: true,
1279
+ criteria: criteria(true),
1280
+ });
1281
+ const disabled: ObjectID = await seedRule(kind, {
1282
+ name: "criteria-backed, sent as true but disabled",
1283
+ order: 2,
1284
+ isEnabled: true,
1285
+ criteria: criteria(false),
1286
+ });
1287
+
1288
+ expect(await storedRules(kind)).toEqual({
1289
+ [enabled.toString()]: { isEnabled: null, criteriaEnabled: true },
1290
+ [disabled.toString()]: { isEnabled: false, criteriaEnabled: false },
1291
+ });
1292
+ },
1293
+ );
1294
+ });
1295
+
1296
+ describe.each(KINDS)("$name reminder rules", (kind: ReminderKind) => {
1297
+ describe("findMatchingRule", () => {
1298
+ test("with no rules at all, finds no match and raises no SQL error", async () => {
1299
+ expect(await countRules(kind)).toBe(0);
1300
+
1301
+ await expect(
1302
+ kind.findMatchingRule({ projectId: projectId }),
1303
+ ).resolves.toBeNull();
1304
+ await expect(
1305
+ kind.findMatchingRule({
1306
+ projectId: projectId,
1307
+ severityId: ObjectID.generate(),
1308
+ labelIds: [ObjectID.generate()],
1309
+ }),
1310
+ ).resolves.toBeNull();
1311
+ });
1312
+
1313
+ test("finds the enabled legacy rule and skips disabled and NULL legacy rules", async () => {
1314
+ await seedRule(kind, { name: "disabled", order: 1, isEnabled: false });
1315
+ await seedRule(kind, { name: "NULL", order: 2, isEnabled: null });
1316
+
1317
+ await expect(
1318
+ kind.findMatchingRule({ projectId: projectId }),
1319
+ ).resolves.toBeNull();
1320
+
1321
+ const enabled: ObjectID = await seedRule(kind, {
1322
+ name: "enabled",
1323
+ order: 3,
1324
+ isEnabled: true,
1325
+ reminderIntervalInMinutes: 45,
1326
+ });
1327
+
1328
+ const match: ReminderRule | null = await kind.findMatchingRule({
1329
+ projectId: projectId,
1330
+ });
1331
+
1332
+ expect(match?.id?.toString()).toBe(enabled.toString());
1333
+ expect(match?.name).toBe("enabled");
1334
+ expect(match?.reminderIntervalInMinutes).toBe(45);
1335
+ expect(match?.criteria).toBeNull();
1336
+ });
1337
+
1338
+ test("finds an enabled criteria-backed rule, which reads back as enabled, and skips a disabled one", async () => {
1339
+ const disabled: ObjectID = await seedRule(kind, {
1340
+ name: "criteria-backed disabled",
1341
+ order: 1,
1342
+ criteria: criteria(false),
1343
+ });
1344
+
1345
+ await expect(
1346
+ kind.findMatchingRule({ projectId: projectId }),
1347
+ ).resolves.toBeNull();
1348
+
1349
+ const enabled: ObjectID = await seedRule(kind, {
1350
+ name: "criteria-backed enabled",
1351
+ order: 2,
1352
+ criteria: criteria(true),
1353
+ });
1354
+
1355
+ const match: ReminderRule | null = await kind.findMatchingRule({
1356
+ projectId: projectId,
1357
+ });
1358
+
1359
+ expect(match?.id?.toString()).toBe(enabled.toString());
1360
+ expect(match?.criteria).toEqual(criteria(true));
1361
+
1362
+ // Stored as NULL; every reader is handed the logical value.
1363
+ expect(await kind.rules.findEnabledById(enabled)).toBe(true);
1364
+ expect(await kind.rules.findEnabledById(disabled)).toBe(false);
1365
+ });
1366
+
1367
+ test("the lowest order among the matching enabled rules wins", async () => {
1368
+ await seedRule(kind, {
1369
+ name: "disabled, lowest order",
1370
+ order: 1,
1371
+ isEnabled: false,
1372
+ });
1373
+ await seedRule(kind, { name: "order 5", order: 5 });
1374
+ const winner: ObjectID = await seedRule(kind, {
1375
+ name: "order 2, criteria-backed",
1376
+ order: 2,
1377
+ criteria: criteria(true),
1378
+ });
1379
+ await seedRule(kind, { name: "order 3", order: 3 });
1380
+
1381
+ const match: ReminderRule | null = await kind.findMatchingRule({
1382
+ projectId: projectId,
1383
+ });
1384
+
1385
+ expect(match?.id?.toString()).toBe(winner.toString());
1386
+ expect(match?.order).toBe(2);
1387
+ });
1388
+
1389
+ test("a legacy label restriction is honoured", async () => {
1390
+ const matchingLabel: ObjectID = await seedLabel();
1391
+ const otherLabel: ObjectID = await seedLabel();
1392
+
1393
+ const restricted: ObjectID = await seedRule(kind, {
1394
+ name: "only for the label",
1395
+ order: 1,
1396
+ labelIds: [matchingLabel],
1397
+ });
1398
+
1399
+ // Without a fallback rule, a subject outside the label matches nothing.
1400
+ await expect(
1401
+ kind.findMatchingRule({
1402
+ projectId: projectId,
1403
+ labelIds: [otherLabel],
1404
+ }),
1405
+ ).resolves.toBeNull();
1406
+ await expect(
1407
+ kind.findMatchingRule({ projectId: projectId }),
1408
+ ).resolves.toBeNull();
1409
+
1410
+ const fallback: ObjectID = await seedRule(kind, {
1411
+ name: "everything else",
1412
+ order: 2,
1413
+ });
1414
+
1415
+ expect(
1416
+ (
1417
+ await kind.findMatchingRule({
1418
+ projectId: projectId,
1419
+ labelIds: [otherLabel, matchingLabel],
1420
+ })
1421
+ )?.id?.toString(),
1422
+ ).toBe(restricted.toString());
1423
+ expect(
1424
+ (
1425
+ await kind.findMatchingRule({
1426
+ projectId: projectId,
1427
+ labelIds: [otherLabel],
1428
+ })
1429
+ )?.id?.toString(),
1430
+ ).toBe(fallback.toString());
1431
+ expect(
1432
+ (
1433
+ await kind.findMatchingRule({ projectId: projectId })
1434
+ )?.id?.toString(),
1435
+ ).toBe(fallback.toString());
1436
+ });
1437
+
1438
+ if (kind.severity) {
1439
+ test("a legacy severity restriction is honoured", async () => {
1440
+ const critical: ObjectID = await seedSeverity(kind);
1441
+ const minor: ObjectID = await seedSeverity(kind);
1442
+
1443
+ const criticalRule: ObjectID = await seedRule(kind, {
1444
+ name: "critical only",
1445
+ order: 1,
1446
+ severityIds: [critical],
1447
+ });
1448
+ const minorRule: ObjectID = await seedRule(kind, {
1449
+ name: "minor only",
1450
+ order: 2,
1451
+ severityIds: [minor],
1452
+ });
1453
+
1454
+ expect(
1455
+ (
1456
+ await kind.findMatchingRule({
1457
+ projectId: projectId,
1458
+ severityId: critical,
1459
+ })
1460
+ )?.id?.toString(),
1461
+ ).toBe(criticalRule.toString());
1462
+ expect(
1463
+ (
1464
+ await kind.findMatchingRule({
1465
+ projectId: projectId,
1466
+ severityId: minor,
1467
+ })
1468
+ )?.id?.toString(),
1469
+ ).toBe(minorRule.toString());
1470
+ await expect(
1471
+ kind.findMatchingRule({
1472
+ projectId: projectId,
1473
+ severityId: ObjectID.generate(),
1474
+ }),
1475
+ ).resolves.toBeNull();
1476
+ await expect(
1477
+ kind.findMatchingRule({ projectId: projectId }),
1478
+ ).resolves.toBeNull();
1479
+ });
1480
+ }
1481
+
1482
+ test("a criteria-backed rule's criteria decide, not its stale legacy relations", async () => {
1483
+ const production: ObjectID = await seedLabel();
1484
+ const staging: ObjectID = await seedLabel();
1485
+
1486
+ /*
1487
+ * The junction row is what an older version saved before criteria
1488
+ * were configured; once criteria exist they alone decide.
1489
+ */
1490
+ const productionRule: ObjectID = await seedRule(kind, {
1491
+ name: "production",
1492
+ order: 1,
1493
+ criteria: criteria(true, [hasAnyOfLabels([production])]),
1494
+ labelIds: [staging],
1495
+ reminderIntervalInMinutes: 15,
1496
+ });
1497
+ const everythingElse: ObjectID = await seedRule(kind, {
1498
+ name: "everything but production",
1499
+ order: 2,
1500
+ criteria: criteria(true, [hasNoneOfLabels([production])]),
1501
+ reminderIntervalInMinutes: 120,
1502
+ });
1503
+
1504
+ expect(
1505
+ (
1506
+ await kind.findMatchingRule({
1507
+ projectId: projectId,
1508
+ labelIds: [production],
1509
+ })
1510
+ )?.id?.toString(),
1511
+ ).toBe(productionRule.toString());
1512
+ expect(
1513
+ (
1514
+ await kind.findMatchingRule({
1515
+ projectId: projectId,
1516
+ labelIds: [staging],
1517
+ })
1518
+ )?.id?.toString(),
1519
+ ).toBe(everythingElse.toString());
1520
+ expect(
1521
+ (
1522
+ await kind.findMatchingRule({ projectId: projectId })
1523
+ )?.id?.toString(),
1524
+ ).toBe(everythingElse.toString());
1525
+ });
1526
+
1527
+ test("ignores another project's rules and soft-deleted rules", async () => {
1528
+ await seedRule(kind, {
1529
+ name: "another project's",
1530
+ order: 1,
1531
+ project: otherProjectId,
1532
+ });
1533
+ await seedRule(kind, {
1534
+ name: "another project's, criteria-backed",
1535
+ order: 1,
1536
+ project: otherProjectId,
1537
+ criteria: criteria(true),
1538
+ });
1539
+ await seedRule(kind, {
1540
+ name: "deleted",
1541
+ order: 1,
1542
+ deletedAt: new Date(),
1543
+ });
1544
+ await seedRule(kind, {
1545
+ name: "deleted, criteria-backed",
1546
+ order: 1,
1547
+ deletedAt: new Date(),
1548
+ criteria: criteria(true),
1549
+ });
1550
+
1551
+ await expect(
1552
+ kind.findMatchingRule({ projectId: projectId }),
1553
+ ).resolves.toBeNull();
1554
+
1555
+ const own: ObjectID = await seedRule(kind, { name: "own", order: 9 });
1556
+
1557
+ expect(
1558
+ (
1559
+ await kind.findMatchingRule({ projectId: projectId })
1560
+ )?.id?.toString(),
1561
+ ).toBe(own.toString());
1562
+ });
1563
+ });
1564
+
1565
+ describe("the DatabaseService paths filtering on isEnabled", () => {
1566
+ test("findBy and countBy return exactly the logically enabled or disabled rules", async () => {
1567
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1568
+
1569
+ expect(await kind.rules.findByEnabled(projectId, true)).toEqual(
1570
+ ids([rules.legacyEnabled, rules.criteriaEnabled]).map(
1571
+ (id: string): ReadRule => {
1572
+ return { id: id, isEnabled: true };
1573
+ },
1574
+ ),
1575
+ );
1576
+ expect(await kind.rules.countByEnabled(projectId, true)).toBe(2);
1577
+
1578
+ // A disabled criteria-backed rule reads back as false, like a legacy one.
1579
+ const disabled: Array<ReadRule> = await kind.rules.findByEnabled(
1580
+ projectId,
1581
+ false,
1582
+ );
1583
+ expect(
1584
+ disabled.map((rule: ReadRule): string => {
1585
+ return rule.id;
1586
+ }),
1587
+ ).toEqual(
1588
+ ids([rules.legacyDisabled, rules.legacyNull, rules.criteriaDisabled]),
1589
+ );
1590
+ expect(
1591
+ disabled.find((rule: ReadRule): boolean => {
1592
+ return rule.id === rules.criteriaDisabled.toString();
1593
+ })?.isEnabled,
1594
+ ).toBe(false);
1595
+ expect(
1596
+ disabled.find((rule: ReadRule): boolean => {
1597
+ return rule.id === rules.legacyDisabled.toString();
1598
+ })?.isEnabled,
1599
+ ).toBe(false);
1600
+ expect(await kind.rules.countByEnabled(projectId, false)).toBe(3);
1601
+
1602
+ // The other project's rules are counted there, not here.
1603
+ expect(await kind.rules.countByEnabled(otherProjectId, true)).toBe(1);
1604
+ });
1605
+
1606
+ /*
1607
+ * The list path of the dashboard and the API: a signed-in user's read
1608
+ * goes through the permission layer, which scopes the rewritten filter
1609
+ * to the tenant and serializes the query before TypeORM renders it.
1610
+ * Without a tenant id the scoped query becomes one where-clause per
1611
+ * project the user belongs to, which TypeORM ORs together.
1612
+ */
1613
+ test.each([
1614
+ ["scoped to the project as the tenant", true],
1615
+ ["with no tenant, scoped to each project the user belongs to", false],
1616
+ ] as Array<[string, boolean]>)(
1617
+ "a project owner's findBy and countBy, %s, return exactly the logically enabled or disabled rules",
1618
+ async (_label: string, withTenantId: boolean) => {
1619
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1620
+ const props: DatabaseCommonInteractionProps =
1621
+ projectOwnerProps(withTenantId);
1622
+
1623
+ expect(
1624
+ await kind.rules.findByEnabled(projectId, true, props),
1625
+ ).toEqual(
1626
+ ids([rules.legacyEnabled, rules.criteriaEnabled]).map(
1627
+ (id: string): ReadRule => {
1628
+ return { id: id, isEnabled: true };
1629
+ },
1630
+ ),
1631
+ );
1632
+ expect(await kind.rules.countByEnabled(projectId, true, props)).toBe(
1633
+ 2,
1634
+ );
1635
+
1636
+ expect(
1637
+ await kind.rules.findByEnabled(projectId, false, props),
1638
+ ).toEqual(
1639
+ ids([
1640
+ rules.legacyDisabled,
1641
+ rules.legacyNull,
1642
+ rules.criteriaDisabled,
1643
+ ]).map((id: string): ReadRule => {
1644
+ return {
1645
+ id: id,
1646
+ // A legacy NULL is stored and read back as NULL.
1647
+ isEnabled: id === rules.legacyNull.toString() ? null : false,
1648
+ };
1649
+ }),
1650
+ );
1651
+ expect(await kind.rules.countByEnabled(projectId, false, props)).toBe(
1652
+ 3,
1653
+ );
1654
+ },
1655
+ );
1656
+
1657
+ /*
1658
+ * The backfill pages every project's enabled rules with the rewritten
1659
+ * filter next to a `_id > cursor` Raw of its own. Pages of two over
1660
+ * the three enabled rules: a full page, then a short one.
1661
+ */
1662
+ test("a cursor-paged findBy of every project's enabled rules returns each once, in _id order", async () => {
1663
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1664
+ const projectOf: Record<string, string> = {
1665
+ [rules.legacyEnabled.toString()]: projectId.toString(),
1666
+ [rules.criteriaEnabled.toString()]: projectId.toString(),
1667
+ [rules.otherProjectEnabled.toString()]: otherProjectId.toString(),
1668
+ };
1669
+ const expected: Array<PagedRule> = ids([
1670
+ rules.legacyEnabled,
1671
+ rules.criteriaEnabled,
1672
+ rules.otherProjectEnabled,
1673
+ ]).map((id: string): PagedRule => {
1674
+ return { id: id, projectId: projectOf[id]! };
1675
+ });
1676
+
1677
+ const firstPage: Array<PagedRule> =
1678
+ await kind.rules.findEnabledPageAfter(null, 2);
1679
+ expect(firstPage).toEqual(expected.slice(0, 2));
1680
+
1681
+ const secondPage: Array<PagedRule> =
1682
+ await kind.rules.findEnabledPageAfter(
1683
+ new ObjectID(firstPage[1]!.id),
1684
+ 2,
1685
+ );
1686
+ expect(secondPage).toEqual(expected.slice(2));
1687
+ });
1688
+
1689
+ test("updateBy changes exactly the rules the filter selects, in their own encoding", async () => {
1690
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1691
+ const before: Record<string, StoredRule> = await storedRules(kind);
1692
+
1693
+ expect(
1694
+ await kind.rules.updateByEnabled({
1695
+ projectId: projectId,
1696
+ isEnabled: true,
1697
+ setEnabled: false,
1698
+ props: { isRoot: true },
1699
+ }),
1700
+ ).toBe(2);
1701
+
1702
+ expect(await storedRules(kind)).toEqual({
1703
+ ...before,
1704
+ [rules.legacyEnabled.toString()]: {
1705
+ isEnabled: false,
1706
+ criteriaEnabled: null,
1707
+ },
1708
+ [rules.criteriaEnabled.toString()]: {
1709
+ isEnabled: false,
1710
+ criteriaEnabled: false,
1711
+ },
1712
+ });
1713
+
1714
+ // Now five disabled rules in the project, and none enabled.
1715
+ expect(
1716
+ await kind.rules.updateByEnabled({
1717
+ projectId: projectId,
1718
+ isEnabled: false,
1719
+ setEnabled: true,
1720
+ props: { isRoot: true },
1721
+ }),
1722
+ ).toBe(5);
1723
+
1724
+ expect(await storedRules(kind)).toEqual({
1725
+ ...before,
1726
+ [rules.legacyEnabled.toString()]: {
1727
+ isEnabled: true,
1728
+ criteriaEnabled: null,
1729
+ },
1730
+ [rules.legacyDisabled.toString()]: {
1731
+ isEnabled: true,
1732
+ criteriaEnabled: null,
1733
+ },
1734
+ [rules.legacyNull.toString()]: {
1735
+ isEnabled: true,
1736
+ criteriaEnabled: null,
1737
+ },
1738
+ [rules.criteriaEnabled.toString()]: {
1739
+ isEnabled: null,
1740
+ criteriaEnabled: true,
1741
+ },
1742
+ [rules.criteriaDisabled.toString()]: {
1743
+ isEnabled: null,
1744
+ criteriaEnabled: true,
1745
+ },
1746
+ });
1747
+ expect(await kind.rules.countByEnabled(projectId, true)).toBe(5);
1748
+ });
1749
+
1750
+ test("deleteBy deletes exactly the rules the filter selects", async () => {
1751
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1752
+
1753
+ expect(await kind.rules.deleteByEnabled(projectId, false)).toBe(3);
1754
+
1755
+ expect(Object.keys(await storedRules(kind)).sort()).toEqual(
1756
+ ids([
1757
+ rules.legacyEnabled,
1758
+ rules.criteriaEnabled,
1759
+ rules.otherProjectEnabled,
1760
+ rules.softDeletedEnabled,
1761
+ ]),
1762
+ );
1763
+ });
1764
+
1765
+ /*
1766
+ * Unlike the paths above, hardDeleteBy hands the filter to TypeORM's
1767
+ * DeleteQueryBuilder, which calls the Raw with a bare column name
1768
+ * instead of `Alias.column`. A hard delete also reaches soft-deleted
1769
+ * rows, so the soft-deleted enabled rule goes too.
1770
+ */
1771
+ test("hardDeleteBy, whose DELETE statement carries the filter itself, deletes exactly the rules it selects", async () => {
1772
+ const rules: SeededRuleSet = await seedEveryEncoding(kind);
1773
+
1774
+ expect(await kind.rules.hardDeleteByEnabled(projectId, true)).toBe(3);
1775
+
1776
+ expect(Object.keys(await storedRules(kind)).sort()).toEqual(
1777
+ ids([
1778
+ rules.legacyDisabled,
1779
+ rules.legacyNull,
1780
+ rules.criteriaDisabled,
1781
+ rules.otherProjectEnabled,
1782
+ ]),
1783
+ );
1784
+ });
1785
+ });
1786
+
1787
+ describe("refreshReminderSchedule", () => {
1788
+ test("with no rules at all, completes and leaves the next reminder unset", async () => {
1789
+ const subjectId: ObjectID = await seedSubject(kind);
1790
+
1791
+ await refresh(kind, subjectId);
1792
+
1793
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
1794
+ });
1795
+
1796
+ test.each([
1797
+ ["a legacy rule", null],
1798
+ ["a criteria-backed rule", criteria(true)],
1799
+ ] as Array<[string, RuleCriteria | null]>)(
1800
+ "with %s matching, schedules the next reminder one interval from now",
1801
+ async (_label: string, ruleCriteria: RuleCriteria | null) => {
1802
+ await seedRule(kind, {
1803
+ name: "disabled, shorter interval",
1804
+ order: 1,
1805
+ isEnabled: false,
1806
+ reminderIntervalInMinutes: 5,
1807
+ });
1808
+ await seedRule(kind, {
1809
+ name: "matching",
1810
+ order: 2,
1811
+ criteria: ruleCriteria,
1812
+ reminderIntervalInMinutes: 30,
1813
+ });
1814
+ const subjectId: ObjectID = await seedSubject(kind);
1815
+
1816
+ const window: { startedAt: number; finishedAt: number } =
1817
+ await refresh(kind, subjectId);
1818
+
1819
+ expectScheduledIn(await nextReminderAt(kind, subjectId), window, 30);
1820
+ },
1821
+ );
1822
+
1823
+ test("uses the rule the subject's labels select", async () => {
1824
+ const production: ObjectID = await seedLabel();
1825
+ await seedRule(kind, {
1826
+ name: "production",
1827
+ order: 1,
1828
+ labelIds: [production],
1829
+ reminderIntervalInMinutes: 15,
1830
+ });
1831
+ await seedRule(kind, {
1832
+ name: "everything else",
1833
+ order: 2,
1834
+ reminderIntervalInMinutes: 240,
1835
+ });
1836
+ const productionSubject: ObjectID = await seedSubject(kind, {
1837
+ labelIds: [production],
1838
+ });
1839
+ const otherSubject: ObjectID = await seedSubject(kind);
1840
+
1841
+ const productionWindow: { startedAt: number; finishedAt: number } =
1842
+ await refresh(kind, productionSubject);
1843
+ const otherWindow: { startedAt: number; finishedAt: number } =
1844
+ await refresh(kind, otherSubject);
1845
+
1846
+ expectScheduledIn(
1847
+ await nextReminderAt(kind, productionSubject),
1848
+ productionWindow,
1849
+ 15,
1850
+ );
1851
+ expectScheduledIn(
1852
+ await nextReminderAt(kind, otherSubject),
1853
+ otherWindow,
1854
+ 240,
1855
+ );
1856
+ });
1857
+
1858
+ test("when no rule of the project matches the subject, leaves the next reminder unset and clears a stale one", async () => {
1859
+ const production: ObjectID = await seedLabel();
1860
+ const staging: ObjectID = await seedLabel();
1861
+ await seedRule(kind, {
1862
+ name: "production only",
1863
+ order: 1,
1864
+ labelIds: [production],
1865
+ });
1866
+ await seedRule(kind, {
1867
+ name: "production only, criteria-backed",
1868
+ order: 2,
1869
+ criteria: criteria(true, [hasAnyOfLabels([production])]),
1870
+ });
1871
+ const subjectId: ObjectID = await seedSubject(kind, {
1872
+ labelIds: [staging],
1873
+ nextReminderNotificationAt: new Date(Date.now() + MINUTE_IN_MS),
1874
+ });
1875
+
1876
+ await refresh(kind, subjectId);
1877
+
1878
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
1879
+ });
1880
+
1881
+ test("with reminders turned off for the subject, leaves the next reminder unset and clears a stale one", async () => {
1882
+ await seedRule(kind, { name: "matching", order: 1 });
1883
+ const subjectId: ObjectID = await seedSubject(kind, {
1884
+ enableReminders: false,
1885
+ });
1886
+ const staleSubjectId: ObjectID = await seedSubject(kind, {
1887
+ enableReminders: false,
1888
+ nextReminderNotificationAt: new Date(Date.now() + MINUTE_IN_MS),
1889
+ });
1890
+
1891
+ await refresh(kind, subjectId);
1892
+ await refresh(kind, staleSubjectId);
1893
+
1894
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
1895
+ expect(await nextReminderAt(kind, staleSubjectId)).toBeNull();
1896
+ });
1897
+
1898
+ test("with only disabled rules, leaves the next reminder unset and clears a stale one", async () => {
1899
+ await seedRule(kind, { name: "disabled", order: 1, isEnabled: false });
1900
+ await seedRule(kind, { name: "NULL", order: 2, isEnabled: null });
1901
+ await seedRule(kind, {
1902
+ name: "criteria-backed disabled",
1903
+ order: 3,
1904
+ criteria: criteria(false),
1905
+ });
1906
+ await seedRule(kind, {
1907
+ name: "another project's",
1908
+ order: 4,
1909
+ project: otherProjectId,
1910
+ });
1911
+ const subjectId: ObjectID = await seedSubject(kind, {
1912
+ nextReminderNotificationAt: new Date(Date.now() + MINUTE_IN_MS),
1913
+ });
1914
+
1915
+ await refresh(kind, subjectId);
1916
+
1917
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
1918
+ });
1919
+
1920
+ test("for a resolved subject, leaves the next reminder unset", async () => {
1921
+ await seedRule(kind, { name: "matching", order: 1 });
1922
+ const subjectId: ObjectID = await seedSubject(kind, {
1923
+ resolved: true,
1924
+ });
1925
+
1926
+ await refresh(kind, subjectId);
1927
+
1928
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
1929
+ });
1930
+ });
1931
+
1932
+ /*
1933
+ * The rule service's refresh of every open subject of a project: the
1934
+ * path rule edits take, and, with onlyWithoutNextReminder, the backfill
1935
+ * of the reminders #4030 left unscheduled (the
1936
+ * ScheduleRemindersMissedByReminderRuleLookup data migration). The
1937
+ * backfill adds a NULL filter to the open-subject query, and must leave
1938
+ * a subject that already has a timestamp alone: re-scheduling it would
1939
+ * push its overdue reminder one full interval later.
1940
+ */
1941
+ describe("refreshing every open subject of the project", () => {
1942
+ interface OpenSubjects {
1943
+ unscheduled: ObjectID;
1944
+ pastDue: ObjectID;
1945
+ pastDueAt: Date;
1946
+ resolved: ObjectID;
1947
+ remindersOff: ObjectID;
1948
+ }
1949
+
1950
+ async function seedOpenSubjects(): Promise<OpenSubjects> {
1951
+ await seedRule(kind, {
1952
+ name: "disabled, shorter interval",
1953
+ order: 1,
1954
+ isEnabled: false,
1955
+ reminderIntervalInMinutes: 5,
1956
+ });
1957
+ await seedRule(kind, {
1958
+ name: "matching, criteria-backed",
1959
+ order: 2,
1960
+ criteria: criteria(true),
1961
+ reminderIntervalInMinutes: 30,
1962
+ });
1963
+
1964
+ const pastDueAt: Date = new Date(Date.now() - 5 * MINUTE_IN_MS);
1965
+
1966
+ return {
1967
+ unscheduled: await seedSubject(kind),
1968
+ pastDue: await seedSubject(kind, {
1969
+ nextReminderNotificationAt: pastDueAt,
1970
+ }),
1971
+ pastDueAt: pastDueAt,
1972
+ resolved: await seedSubject(kind, { resolved: true }),
1973
+ remindersOff: await seedSubject(kind, { enableReminders: false }),
1974
+ };
1975
+ }
1976
+
1977
+ test("with onlyWithoutNextReminder, schedules the open subjects that have no reminder and never writes one that has", async () => {
1978
+ const subjects: OpenSubjects = await seedOpenSubjects();
1979
+
1980
+ const window: { startedAt: number; finishedAt: number } =
1981
+ await refreshOpenSubjects(kind, { onlyWithoutNextReminder: true });
1982
+
1983
+ expectScheduledIn(
1984
+ await nextReminderAt(kind, subjects.unscheduled),
1985
+ window,
1986
+ 30,
1987
+ );
1988
+ expect(await storedSubject(kind, subjects.pastDue)).toEqual({
1989
+ nextReminderNotificationAt: subjects.pastDueAt,
1990
+ version: 1,
1991
+ });
1992
+ expect(await storedSubject(kind, subjects.resolved)).toEqual({
1993
+ nextReminderNotificationAt: null,
1994
+ version: 1,
1995
+ });
1996
+ expect(await nextReminderAt(kind, subjects.remindersOff)).toBeNull();
1997
+ });
1998
+
1999
+ test("with onlyWithoutNextReminder, a second run leaves the reminders the first one scheduled alone", async () => {
2000
+ const subjects: OpenSubjects = await seedOpenSubjects();
2001
+
2002
+ await refreshOpenSubjects(kind, { onlyWithoutNextReminder: true });
2003
+ const scheduled: StoredSubject = await storedSubject(
2004
+ kind,
2005
+ subjects.unscheduled,
2006
+ );
2007
+ expect(scheduled.nextReminderNotificationAt).toBeInstanceOf(Date);
2008
+
2009
+ await refreshOpenSubjects(kind, { onlyWithoutNextReminder: true });
2010
+
2011
+ expect(await storedSubject(kind, subjects.unscheduled)).toEqual(
2012
+ scheduled,
2013
+ );
2014
+ expect(await storedSubject(kind, subjects.pastDue)).toEqual({
2015
+ nextReminderNotificationAt: subjects.pastDueAt,
2016
+ version: 1,
2017
+ });
2018
+ expect(await nextReminderAt(kind, subjects.resolved)).toBeNull();
2019
+ expect(await nextReminderAt(kind, subjects.remindersOff)).toBeNull();
2020
+ });
2021
+
2022
+ test.each([
2023
+ ["no options", undefined],
2024
+ ["onlyWithoutNextReminder false", { onlyWithoutNextReminder: false }],
2025
+ ] as Array<[string, RefreshOpenSubjectsOptions]>)(
2026
+ "with %s, re-schedules every open subject, one already scheduled included",
2027
+ async (_label: string, options: RefreshOpenSubjectsOptions) => {
2028
+ const subjects: OpenSubjects = await seedOpenSubjects();
2029
+
2030
+ const window: { startedAt: number; finishedAt: number } =
2031
+ await refreshOpenSubjects(kind, options);
2032
+
2033
+ expectScheduledIn(
2034
+ await nextReminderAt(kind, subjects.unscheduled),
2035
+ window,
2036
+ 30,
2037
+ );
2038
+ expectScheduledIn(
2039
+ await nextReminderAt(kind, subjects.pastDue),
2040
+ window,
2041
+ 30,
2042
+ );
2043
+ expect(await storedSubject(kind, subjects.resolved)).toEqual({
2044
+ nextReminderNotificationAt: null,
2045
+ version: 1,
2046
+ });
2047
+ expect(await nextReminderAt(kind, subjects.remindersOff)).toBeNull();
2048
+ },
2049
+ );
2050
+
2051
+ /*
2052
+ * More open subjects without a reminder than one read returns, so the
2053
+ * backfill's second read runs on Postgres with the `_id > cursor`
2054
+ * filter beside the project, state and NULL filters, sorted by `_id`.
2055
+ * Every read is real; only the per-subject refresh is stubbed (what it
2056
+ * writes is covered above), which keeps LIMIT_MAX subjects cheap.
2057
+ * Without the option the same subjects get one read of LIMIT_MAX, as
2058
+ * the rule hooks have always read them.
2059
+ */
2060
+ test("with onlyWithoutNextReminder, pages through more than LIMIT_MAX open subjects without a reminder, handing each to the refresh once; without it, reads LIMIT_MAX once", async () => {
2061
+ const unscheduled: Array<string> =
2062
+ await seedOpenSubjectsWithoutReminder(kind, LIMIT_MAX + 3);
2063
+
2064
+ const scheduled: ObjectID = await seedSubject(kind, {
2065
+ nextReminderNotificationAt: new Date(Date.now() + MINUTE_IN_MS),
2066
+ });
2067
+ const resolved: ObjectID = await seedSubject(kind, { resolved: true });
2068
+ const otherProjectSubject: ObjectID = ObjectID.generate();
2069
+ await insert(kind.subjectTable, {
2070
+ ...kind.subjectRow({
2071
+ id: otherProjectSubject,
2072
+ projectId: otherProjectId,
2073
+ stateId: openStateIds[kind.name]!,
2074
+ severityId: ObjectID.generate(),
2075
+ }),
2076
+ enableReminders: true,
2077
+ nextReminderNotificationAt: null,
2078
+ });
2079
+
2080
+ const refreshed: Array<string> = [];
2081
+ const stub: { mockRestore(): void } = kind.stubRefreshReminderSchedule(
2082
+ (subjectId: string): void => {
2083
+ refreshed.push(subjectId);
2084
+ },
2085
+ );
2086
+
2087
+ try {
2088
+ await refreshOpenSubjects(kind, { onlyWithoutNextReminder: true });
2089
+
2090
+ expect([...refreshed].sort()).toEqual([...unscheduled].sort());
2091
+
2092
+ refreshed.length = 0;
2093
+ await refreshOpenSubjects(kind);
2094
+
2095
+ const openInProject: Set<string> = new Set<string>([
2096
+ ...unscheduled,
2097
+ scheduled.toString(),
2098
+ ]);
2099
+
2100
+ expect(refreshed).toHaveLength(LIMIT_MAX);
2101
+ expect(new Set<string>(refreshed).size).toBe(LIMIT_MAX);
2102
+ expect(
2103
+ refreshed.filter((subjectId: string): boolean => {
2104
+ return !openInProject.has(subjectId);
2105
+ }),
2106
+ ).toEqual([]);
2107
+ expect(refreshed).not.toContain(resolved.toString());
2108
+ expect(refreshed).not.toContain(otherProjectSubject.toString());
2109
+ } finally {
2110
+ stub.mockRestore();
2111
+ }
2112
+ });
2113
+ });
2114
+
2115
+ describe("editing rules through the service re-evaluates open subjects", () => {
2116
+ /*
2117
+ * The path a user takes: the dashboard saves the rule, and the create
2118
+ * hook re-evaluates every open subject of the project.
2119
+ */
2120
+ test.each([
2121
+ ["legacy", null, { isEnabled: true, criteriaEnabled: null }],
2122
+ [
2123
+ "criteria-backed",
2124
+ criteria(true, [], FilterCondition.Any),
2125
+ { isEnabled: null, criteriaEnabled: true },
2126
+ ],
2127
+ ] as Array<[string, RuleCriteria | null, StoredRule]>)(
2128
+ "a %s rule created through the service is found, and creating it schedules the open subjects' reminders",
2129
+ async (
2130
+ _label: string,
2131
+ ruleCriteria: RuleCriteria | null,
2132
+ expectedStored: StoredRule,
2133
+ ) => {
2134
+ const openSubject: ObjectID = await seedSubject(kind);
2135
+ const resolvedSubject: ObjectID = await seedSubject(kind, {
2136
+ resolved: true,
2137
+ });
2138
+
2139
+ const startedAt: number = Date.now();
2140
+ const ruleId: ObjectID = await kind.rules.create({
2141
+ projectId: projectId,
2142
+ name: "created through the service",
2143
+ reminderIntervalInMinutes: 20,
2144
+ criteria: ruleCriteria,
2145
+ });
2146
+ const finishedAt: number = Date.now();
2147
+
2148
+ expect(await storedRules(kind)).toEqual({
2149
+ [ruleId.toString()]: expectedStored,
2150
+ });
2151
+ expect(
2152
+ (
2153
+ await kind.findMatchingRule({ projectId: projectId })
2154
+ )?.id?.toString(),
2155
+ ).toBe(ruleId.toString());
2156
+ expectScheduledIn(
2157
+ await nextReminderAt(kind, openSubject),
2158
+ { startedAt, finishedAt },
2159
+ 20,
2160
+ );
2161
+ expect(await nextReminderAt(kind, resolvedSubject)).toBeNull();
2162
+ },
2163
+ );
2164
+
2165
+ test("disabling and re-enabling the matching rule through updateBy clears and reschedules the open subject's reminder", async () => {
2166
+ await seedRule(kind, {
2167
+ name: "matching",
2168
+ order: 1,
2169
+ criteria: criteria(true),
2170
+ reminderIntervalInMinutes: 25,
2171
+ });
2172
+ const subjectId: ObjectID = await seedSubject(kind);
2173
+ await refresh(kind, subjectId);
2174
+ expect(await nextReminderAt(kind, subjectId)).not.toBeNull();
2175
+
2176
+ // The update hook refreshes the tenant's open subjects.
2177
+ const props: DatabaseCommonInteractionProps = {
2178
+ isRoot: true,
2179
+ tenantId: projectId,
2180
+ };
2181
+
2182
+ expect(
2183
+ await kind.rules.updateByEnabled({
2184
+ projectId: projectId,
2185
+ isEnabled: true,
2186
+ setEnabled: false,
2187
+ props: props,
2188
+ }),
2189
+ ).toBe(1);
2190
+ expect(await nextReminderAt(kind, subjectId)).toBeNull();
2191
+
2192
+ const startedAt: number = Date.now();
2193
+ expect(
2194
+ await kind.rules.updateByEnabled({
2195
+ projectId: projectId,
2196
+ isEnabled: false,
2197
+ setEnabled: true,
2198
+ props: props,
2199
+ }),
2200
+ ).toBe(1);
2201
+ const finishedAt: number = Date.now();
2202
+
2203
+ expectScheduledIn(
2204
+ await nextReminderAt(kind, subjectId),
2205
+ { startedAt, finishedAt },
2206
+ 25,
2207
+ );
2208
+ });
2209
+ });
2210
+ });
2211
+
2212
+ describe("a scheduled maintenance that has not started yet", () => {
2213
+ const kind: ReminderKind = KINDS.find((candidate: ReminderKind) => {
2214
+ return candidate.name === "ScheduledMaintenance";
2215
+ })!;
2216
+
2217
+ test("is first reminded one interval after it starts, unless the rule reminds while it is scheduled", async () => {
2218
+ const startsAt: Date = new Date(Date.now() + 120 * MINUTE_IN_MS);
2219
+ const subjectId: ObjectID = await seedSubject(kind, {
2220
+ startsAt: startsAt,
2221
+ endsAt: new Date(startsAt.getTime() + 60 * MINUTE_IN_MS),
2222
+ });
2223
+
2224
+ const ruleId: ObjectID = await seedRule(kind, {
2225
+ name: "after it starts",
2226
+ order: 1,
2227
+ reminderIntervalInMinutes: 10,
2228
+ remindWhileScheduled: false,
2229
+ });
2230
+
2231
+ await refresh(kind, subjectId);
2232
+
2233
+ expect((await nextReminderAt(kind, subjectId))?.getTime()).toBe(
2234
+ startsAt.getTime() + 10 * MINUTE_IN_MS,
2235
+ );
2236
+
2237
+ await database.query(
2238
+ `UPDATE "${schema}"."${kind.ruleTable}" SET "remindWhileScheduled" = true WHERE "_id" = $1`,
2239
+ [ruleId.toString()],
2240
+ );
2241
+
2242
+ const window: { startedAt: number; finishedAt: number } = await refresh(
2243
+ kind,
2244
+ subjectId,
2245
+ );
2246
+
2247
+ expectScheduledIn(await nextReminderAt(kind, subjectId), window, 10);
2248
+ });
2249
+ });
2250
+ });