@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,2033 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
3
+
4
+ /*
5
+ * PasswordHash carries a pre-existing TS5.9 diagnostic that fails any suite
6
+ * whose runtime require graph reaches it, and DatabaseService imports it.
7
+ */
8
+ jest.mock("../../../Server/Utils/PasswordHash", () => {
9
+ return {
10
+ __esModule: true,
11
+ default: {
12
+ hash: jest.fn(),
13
+ verify: jest.fn(),
14
+ generateSalt: jest.fn(),
15
+ needsUpgrade: jest.fn(),
16
+ applyPepper: jest.fn(),
17
+ },
18
+ };
19
+ });
20
+
21
+ import DatabaseServerEndpointService, {
22
+ DatabaseServerEndpointClaimResult,
23
+ DatabaseServerEndpointOwner,
24
+ } from "../../../Server/Services/DatabaseServerEndpointService";
25
+ import DatabaseServerFeedService from "../../../Server/Services/DatabaseServerFeedService";
26
+ import DatabaseServerService from "../../../Server/Services/DatabaseServerService";
27
+ import DatabaseServer from "../../../Models/DatabaseModels/DatabaseServer";
28
+ import DatabaseServerEndpoint from "../../../Models/DatabaseModels/DatabaseServerEndpoint";
29
+ import { DatabaseServerFeedEventType } from "../../../Models/DatabaseModels/DatabaseServerFeed";
30
+ import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
31
+ import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
32
+ import logger from "../../../Server/Utils/Logger";
33
+ import OwnedScopePermission from "../../../Server/Types/Database/Permissions/OwnedScopePermission";
34
+ import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
35
+ import SortOrder from "../../../Types/BaseDatabase/SortOrder";
36
+ import PermissionScope from "../../../Types/Database/AccessControl/PermissionScope";
37
+ import BadDataException from "../../../Types/Exception/BadDataException";
38
+ import NotAuthorizedException from "../../../Types/Exception/NotAuthorizedException";
39
+ import ObjectID from "../../../Types/ObjectID";
40
+ import Permission, { UserPermission } from "../../../Types/Permission";
41
+ import PositiveNumber from "../../../Types/PositiveNumber";
42
+ import { getJestSpyOn } from "../../Spy";
43
+
44
+ /*
45
+ * DatabaseServerEndpointService - the one-owner-per-endpoint table.
46
+ *
47
+ * Pinned here:
48
+ * - findOwnerByEndpoint / claimEndpoint / listEndpoints, the root API the
49
+ * discovery paths code against: a claim never takes an endpoint from
50
+ * another database, and a lost unique-index race re-reads the owner
51
+ * instead of throwing;
52
+ * - a person adding an alias goes through the REAL create pipeline,
53
+ * column permission check included: the endpoint is canonicalized with
54
+ * the parent database's engine / namespace / cluster, source and
55
+ * isPrimary are forced, and a collision names the database that owns the
56
+ * endpoint;
57
+ * - removing an endpoint is an edit of its database, label and Owned
58
+ * scopes included, and a person can never remove the primary endpoint.
59
+ *
60
+ * The repository is faked - no Postgres, no Redis.
61
+ */
62
+
63
+ const PROJECT_ID: ObjectID = new ObjectID(
64
+ "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
65
+ );
66
+ const OTHER_PROJECT_ID: ObjectID = new ObjectID(
67
+ "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
68
+ );
69
+ const DATABASE_ID: ObjectID = new ObjectID(
70
+ "11111111-1111-4111-8111-111111111111",
71
+ );
72
+ const OTHER_DATABASE_ID: ObjectID = new ObjectID(
73
+ "22222222-2222-4222-8222-222222222222",
74
+ );
75
+ const USER_ID: ObjectID = new ObjectID("dddddddd-dddd-4ddd-8ddd-dddddddddddd");
76
+
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ const service: any = DatabaseServerEndpointService;
79
+
80
+ function userPermission(permission: Permission): UserPermission {
81
+ return {
82
+ permission,
83
+ labelIds: [],
84
+ isBlockPermission: false,
85
+ _type: "UserPermission",
86
+ };
87
+ }
88
+
89
+ /* The shape getUserMiddleware builds for a signed-in member of the project. */
90
+ function memberProps(
91
+ permissions: Array<Permission> = [Permission.ProjectMember],
92
+ ): DatabaseCommonInteractionProps {
93
+ return {
94
+ userId: USER_ID,
95
+ tenantId: PROJECT_ID,
96
+ userGlobalAccessPermission: {
97
+ projectIds: [PROJECT_ID],
98
+ globalPermissions: [Permission.Public, Permission.User],
99
+ _type: "UserGlobalAccessPermission",
100
+ },
101
+ userTenantAccessPermission: {
102
+ [PROJECT_ID.toString()]: {
103
+ projectId: PROJECT_ID,
104
+ permissions: permissions.map(userPermission),
105
+ _type: "UserTenantAccessPermission",
106
+ },
107
+ },
108
+ };
109
+ }
110
+
111
+ function owner(
112
+ databaseServerId: ObjectID,
113
+ isPrimary: boolean = false,
114
+ ): DatabaseServerEndpointOwner {
115
+ return { databaseServerId: databaseServerId, isPrimary: isPrimary };
116
+ }
117
+
118
+ function parentDatabase(
119
+ overrides: {
120
+ dbSystem?: string | undefined;
121
+ kubernetesNamespace?: string | undefined;
122
+ clusterIdentifier?: string | undefined;
123
+ } = {},
124
+ ): DatabaseServer {
125
+ const database: DatabaseServer = new DatabaseServer(DATABASE_ID);
126
+ database.projectId = PROJECT_ID;
127
+ database.dbSystem = overrides.dbSystem || "postgresql";
128
+
129
+ if (overrides.kubernetesNamespace) {
130
+ database.kubernetesNamespace = overrides.kubernetesNamespace;
131
+ }
132
+
133
+ if (overrides.clusterIdentifier) {
134
+ const cluster: KubernetesCluster = new KubernetesCluster();
135
+ cluster.clusterIdentifier = overrides.clusterIdentifier;
136
+ database.kubernetesCluster = cluster;
137
+ }
138
+
139
+ return database;
140
+ }
141
+
142
+ function aliasRequest(
143
+ endpoint: unknown,
144
+ extra: Partial<DatabaseServerEndpoint> = {},
145
+ ): DatabaseServerEndpoint {
146
+ const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
147
+ data.databaseServerId = DATABASE_ID;
148
+ data.endpoint = endpoint as string;
149
+ Object.assign(data, extra);
150
+ return data;
151
+ }
152
+
153
+ afterEach(() => {
154
+ jest.restoreAllMocks();
155
+ });
156
+
157
+ describe("DatabaseServerEndpointService.findOwnerByEndpoint", () => {
158
+ test("reads the owner of the exact canonical endpoint in the project, as root", async () => {
159
+ const row: DatabaseServerEndpoint = new DatabaseServerEndpoint();
160
+ row.databaseServerId = DATABASE_ID;
161
+ row.isPrimary = true;
162
+ const findOneBy: jest.SpyInstance = getJestSpyOn(
163
+ service,
164
+ "findOneBy",
165
+ ).mockResolvedValue(row);
166
+
167
+ const result: DatabaseServerEndpointOwner | null =
168
+ await DatabaseServerEndpointService.findOwnerByEndpoint(
169
+ PROJECT_ID,
170
+ " orders-db.example.com:5432 ",
171
+ );
172
+
173
+ expect(result).not.toBeNull();
174
+ expect(result!.databaseServerId.toString()).toBe(DATABASE_ID.toString());
175
+ expect(result!.isPrimary).toBe(true);
176
+
177
+ const call: any = findOneBy.mock.calls[0]![0];
178
+ expect(call.query.projectId).toBe(PROJECT_ID);
179
+ // Stored canonical, so compared byte for byte - trimmed, not lowercased again.
180
+ expect(call.query.endpoint).toBe("orders-db.example.com:5432");
181
+ expect(call.props.isRoot).toBe(true);
182
+ expect(call.select).toMatchObject({
183
+ databaseServerId: true,
184
+ isPrimary: true,
185
+ });
186
+ });
187
+
188
+ test("null when no database lists the endpoint", async () => {
189
+ getJestSpyOn(service, "findOneBy").mockResolvedValue(null);
190
+
191
+ await expect(
192
+ DatabaseServerEndpointService.findOwnerByEndpoint(
193
+ PROJECT_ID,
194
+ "orders-db.example.com:5432",
195
+ ),
196
+ ).resolves.toBeNull();
197
+ });
198
+
199
+ test("a blank endpoint answers null without a query", async () => {
200
+ const findOneBy: jest.SpyInstance = getJestSpyOn(service, "findOneBy");
201
+
202
+ await expect(
203
+ DatabaseServerEndpointService.findOwnerByEndpoint(PROJECT_ID, " "),
204
+ ).resolves.toBeNull();
205
+ expect(findOneBy).not.toHaveBeenCalled();
206
+ });
207
+
208
+ test("an isPrimary column that was not read comes back as false", async () => {
209
+ const row: DatabaseServerEndpoint = new DatabaseServerEndpoint();
210
+ row.databaseServerId = DATABASE_ID;
211
+ getJestSpyOn(service, "findOneBy").mockResolvedValue(row);
212
+
213
+ const result: DatabaseServerEndpointOwner | null =
214
+ await DatabaseServerEndpointService.findOwnerByEndpoint(
215
+ PROJECT_ID,
216
+ "orders-db.example.com:5432",
217
+ );
218
+
219
+ expect(result!.isPrimary).toBe(false);
220
+ });
221
+ });
222
+
223
+ describe("DatabaseServerEndpointService.claimEndpoint", () => {
224
+ const CLAIM: {
225
+ projectId: ObjectID;
226
+ databaseServerId: ObjectID;
227
+ endpoint: string;
228
+ isPrimary: boolean;
229
+ source: "auto";
230
+ } = {
231
+ projectId: PROJECT_ID,
232
+ databaseServerId: DATABASE_ID,
233
+ endpoint: "orders-db.data.svc.cluster.local:5432@prod",
234
+ isPrimary: true,
235
+ source: "auto",
236
+ };
237
+
238
+ test("inserts an unowned endpoint as root, stamped matched now", async () => {
239
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(null);
240
+ const create: jest.SpyInstance = getJestSpyOn(
241
+ service,
242
+ "create",
243
+ ).mockImplementation(async (input: any) => {
244
+ return input.data;
245
+ });
246
+
247
+ const before: number = Date.now();
248
+ const result: DatabaseServerEndpointClaimResult =
249
+ await DatabaseServerEndpointService.claimEndpoint(CLAIM);
250
+
251
+ expect(result).toBe("claimed");
252
+ expect(create).toHaveBeenCalledTimes(1);
253
+ const call: any = create.mock.calls[0]![0];
254
+ expect(call.props).toEqual({ isRoot: true });
255
+ const row: DatabaseServerEndpoint = call.data;
256
+ expect(row.projectId!.toString()).toBe(PROJECT_ID.toString());
257
+ expect(row.databaseServerId!.toString()).toBe(DATABASE_ID.toString());
258
+ expect(row.endpoint).toBe(CLAIM.endpoint);
259
+ expect(row.isPrimary).toBe(true);
260
+ expect(row.source).toBe("auto");
261
+ expect(row.lastMatchedAt!.getTime()).toBeGreaterThanOrEqual(before);
262
+ });
263
+
264
+ test("an endpoint this database already owns is not written again", async () => {
265
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(
266
+ owner(DATABASE_ID, true),
267
+ );
268
+ const create: jest.SpyInstance = getJestSpyOn(service, "create");
269
+
270
+ await expect(
271
+ DatabaseServerEndpointService.claimEndpoint(CLAIM),
272
+ ).resolves.toBe("already-owned-by-this");
273
+ expect(create).not.toHaveBeenCalled();
274
+ });
275
+
276
+ test("never takes an endpoint away from another database", async () => {
277
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(
278
+ owner(OTHER_DATABASE_ID),
279
+ );
280
+ const create: jest.SpyInstance = getJestSpyOn(service, "create");
281
+
282
+ await expect(
283
+ DatabaseServerEndpointService.claimEndpoint(CLAIM),
284
+ ).resolves.toBe("owned-by-other");
285
+ expect(create).not.toHaveBeenCalled();
286
+ });
287
+
288
+ test("a claim lost to a racing writer reports the winner instead of throwing", async () => {
289
+ getJestSpyOn(service, "findOwnerByEndpoint")
290
+ .mockResolvedValueOnce(null)
291
+ .mockResolvedValueOnce(owner(OTHER_DATABASE_ID));
292
+ getJestSpyOn(service, "create").mockRejectedValue(
293
+ new BadDataException("duplicate key value violates unique constraint"),
294
+ );
295
+
296
+ await expect(
297
+ DatabaseServerEndpointService.claimEndpoint(CLAIM),
298
+ ).resolves.toBe("owned-by-other");
299
+ });
300
+
301
+ test("a race won by another claim for the SAME database is a success", async () => {
302
+ getJestSpyOn(service, "findOwnerByEndpoint")
303
+ .mockResolvedValueOnce(null)
304
+ .mockResolvedValueOnce(owner(DATABASE_ID));
305
+ getJestSpyOn(service, "create").mockRejectedValue(
306
+ new BadDataException("duplicate key value violates unique constraint"),
307
+ );
308
+
309
+ await expect(
310
+ DatabaseServerEndpointService.claimEndpoint(CLAIM),
311
+ ).resolves.toBe("already-owned-by-this");
312
+ });
313
+
314
+ test("a failure that is not a lost race is surfaced", async () => {
315
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(null);
316
+ getJestSpyOn(service, "create").mockRejectedValue(
317
+ new Error("insert or update violates foreign key constraint"),
318
+ );
319
+
320
+ await expect(
321
+ DatabaseServerEndpointService.claimEndpoint(CLAIM),
322
+ ).rejects.toThrow("foreign key");
323
+ });
324
+
325
+ test("a blank endpoint is refused", async () => {
326
+ await expect(
327
+ DatabaseServerEndpointService.claimEndpoint({ ...CLAIM, endpoint: " " }),
328
+ ).rejects.toThrow(BadDataException);
329
+ });
330
+ });
331
+
332
+ describe("DatabaseServerEndpointService.listEndpoints", () => {
333
+ test("lists one database's endpoints, primary first, deduped", async () => {
334
+ const rows: Array<DatabaseServerEndpoint> = [
335
+ "orders-db.example.com:5432",
336
+ "orders-replica.internal:5432",
337
+ "orders-replica.internal:5432",
338
+ "",
339
+ ].map((endpoint: string, index: number) => {
340
+ const row: DatabaseServerEndpoint = new DatabaseServerEndpoint();
341
+ if (endpoint) {
342
+ row.endpoint = endpoint;
343
+ }
344
+ row.isPrimary = index === 0;
345
+ return row;
346
+ });
347
+ const findBy: jest.SpyInstance = getJestSpyOn(
348
+ service,
349
+ "findBy",
350
+ ).mockResolvedValue(rows);
351
+
352
+ const endpoints: Array<string> =
353
+ await DatabaseServerEndpointService.listEndpoints(DATABASE_ID);
354
+
355
+ expect(endpoints).toEqual([
356
+ "orders-db.example.com:5432",
357
+ "orders-replica.internal:5432",
358
+ ]);
359
+ const call: any = findBy.mock.calls[0]![0];
360
+ expect(call.query.databaseServerId).toBe(DATABASE_ID);
361
+ expect(call.sort).toEqual({
362
+ isPrimary: SortOrder.Descending,
363
+ endpoint: SortOrder.Ascending,
364
+ });
365
+ expect(call.props.isRoot).toBe(true);
366
+ });
367
+ });
368
+
369
+ /*
370
+ * A fake DatabaseServer table for the parent lookups. A root lookup is
371
+ * matched as the service built it (after the REAL update-permission check
372
+ * scoped it); a caller's own lookup first goes through the REAL
373
+ * read-permission check, exactly as DatabaseService would run it. So the
374
+ * project, label and Owned scoping under test is the framework's - the fake
375
+ * only evaluates the resulting operators.
376
+ */
377
+ interface FakeDatabase {
378
+ row: DatabaseServer;
379
+ labelIds: Array<string>;
380
+ }
381
+
382
+ function operatorValues(value: unknown): Array<string> | null {
383
+ if (value === undefined || value === null) {
384
+ return null;
385
+ }
386
+
387
+ if (typeof value === "string" || value instanceof ObjectID) {
388
+ return [value.toString().toLowerCase()];
389
+ }
390
+
391
+ const operator: any = value;
392
+
393
+ if (operator._type === "and" && Array.isArray(operator._value)) {
394
+ let result: Array<string> | null = null;
395
+ for (const child of operator._value) {
396
+ const values: Array<string> | null = operatorValues(child);
397
+ if (values === null) {
398
+ continue;
399
+ }
400
+ result =
401
+ result === null
402
+ ? values
403
+ : result.filter((item: string) => {
404
+ return values.includes(item);
405
+ });
406
+ }
407
+ return result;
408
+ }
409
+
410
+ if (operator._type === "equal") {
411
+ return operatorValues(operator._value);
412
+ }
413
+
414
+ const parameters: unknown =
415
+ operator._objectLiteralParameters || operator.objectLiteralParameters;
416
+
417
+ if (parameters && typeof parameters === "object") {
418
+ const values: Array<string> = [];
419
+ for (const parameter of Object.values(parameters)) {
420
+ for (const item of Array.isArray(parameter) ? parameter : [parameter]) {
421
+ values.push(String(item).toLowerCase());
422
+ }
423
+ }
424
+ return values;
425
+ }
426
+
427
+ throw new Error(`The fake cannot evaluate ${JSON.stringify(value)}`);
428
+ }
429
+
430
+ function fakeRowMatches(database: FakeDatabase, query: any): boolean {
431
+ const ids: Array<string> | null = operatorValues(query._id);
432
+ if (ids && !ids.includes(database.row.id!.toString())) {
433
+ return false;
434
+ }
435
+
436
+ const projects: Array<string> | null = operatorValues(query.projectId);
437
+ if (projects && !projects.includes(database.row.projectId!.toString())) {
438
+ return false;
439
+ }
440
+
441
+ if (query.labels) {
442
+ const labelIds: Array<string> | null = operatorValues(query.labels._id);
443
+ if (
444
+ labelIds &&
445
+ !database.labelIds.some((labelId: string) => {
446
+ return labelIds.includes(labelId);
447
+ })
448
+ ) {
449
+ return false;
450
+ }
451
+ }
452
+
453
+ return true;
454
+ }
455
+
456
+ function permissionRow(
457
+ permission: Permission,
458
+ overrides: Partial<UserPermission> = {},
459
+ ): UserPermission {
460
+ return {
461
+ permission,
462
+ labelIds: [],
463
+ isBlockPermission: false,
464
+ _type: "UserPermission",
465
+ ...overrides,
466
+ };
467
+ }
468
+
469
+ function propsWith(
470
+ permissions: Array<UserPermission>,
471
+ ): DatabaseCommonInteractionProps {
472
+ return {
473
+ userId: USER_ID,
474
+ tenantId: PROJECT_ID,
475
+ userGlobalAccessPermission: {
476
+ projectIds: [PROJECT_ID],
477
+ globalPermissions: [Permission.Public, Permission.User],
478
+ _type: "UserGlobalAccessPermission",
479
+ },
480
+ userTenantAccessPermission: {
481
+ [PROJECT_ID.toString()]: {
482
+ projectId: PROJECT_ID,
483
+ permissions: permissions,
484
+ _type: "UserTenantAccessPermission",
485
+ },
486
+ },
487
+ userTeamIds: [],
488
+ };
489
+ }
490
+
491
+ // Edit + Read Database, both scoped to one label: a team's own databases.
492
+ function labelScopedEditorProps(
493
+ labelId: string,
494
+ ): DatabaseCommonInteractionProps {
495
+ return propsWith([
496
+ permissionRow(Permission.EditDatabaseServer, {
497
+ labelIds: [new ObjectID(labelId)],
498
+ scope: PermissionScope.Labels,
499
+ }),
500
+ permissionRow(Permission.ReadDatabaseServer, {
501
+ labelIds: [new ObjectID(labelId)],
502
+ scope: PermissionScope.Labels,
503
+ }),
504
+ ]);
505
+ }
506
+
507
+ // Edit + Read Database, both only on databases the caller owns.
508
+ function ownedScopeEditorProps(): DatabaseCommonInteractionProps {
509
+ return propsWith([
510
+ permissionRow(Permission.EditDatabaseServer, {
511
+ scope: PermissionScope.Owned,
512
+ }),
513
+ permissionRow(Permission.ReadDatabaseServer, {
514
+ scope: PermissionScope.Owned,
515
+ }),
516
+ ]);
517
+ }
518
+
519
+ const LABEL_TEAM_A: string = "a1a1a1a1-a1a1-4a1a-8a1a-a1a1a1a1a1a1";
520
+ const LABEL_TEAM_B: string = "b2b2b2b2-b2b2-4b2b-8b2b-b2b2b2b2b2b2";
521
+
522
+ describe("DatabaseServerEndpointService - a person adding an alias (real create pipeline)", () => {
523
+ let save: jest.Mock;
524
+ let findParent: jest.SpyInstance;
525
+ let findOwner: jest.SpyInstance;
526
+ let databases: Array<FakeDatabase>;
527
+
528
+ function addDatabase(
529
+ overrides: {
530
+ id?: ObjectID;
531
+ projectId?: ObjectID;
532
+ name?: string;
533
+ labelIds?: Array<string>;
534
+ dbSystem?: string;
535
+ kubernetesNamespace?: string;
536
+ clusterIdentifier?: string;
537
+ } = {},
538
+ ): FakeDatabase {
539
+ const row: DatabaseServer = parentDatabase({
540
+ dbSystem: overrides.dbSystem,
541
+ kubernetesNamespace: overrides.kubernetesNamespace,
542
+ clusterIdentifier: overrides.clusterIdentifier,
543
+ });
544
+ row._id = (overrides.id || DATABASE_ID).toString();
545
+ row.projectId = overrides.projectId || PROJECT_ID;
546
+ row.name = overrides.name || "PostgreSQL orders-db.example.com:5432";
547
+ const database: FakeDatabase = {
548
+ row: row,
549
+ labelIds: overrides.labelIds || [],
550
+ };
551
+ databases = databases.filter((item: FakeDatabase) => {
552
+ return item.row.id!.toString() !== row.id!.toString();
553
+ });
554
+ databases.push(database);
555
+ return database;
556
+ }
557
+
558
+ beforeEach(() => {
559
+ databases = [];
560
+ addDatabase();
561
+ save = jest.fn(async (entity: any) => {
562
+ entity._id = ObjectID.generate().toString();
563
+ return entity;
564
+ });
565
+ getJestSpyOn(service, "getRepository").mockReturnValue({ save } as never);
566
+ getJestSpyOn(service, "countBy").mockResolvedValue(
567
+ new PositiveNumber(0) as never,
568
+ );
569
+ getJestSpyOn(service, "onTriggerWorkflow").mockResolvedValue(
570
+ undefined as never,
571
+ );
572
+ getJestSpyOn(service, "onTriggerRealtime").mockResolvedValue(
573
+ undefined as never,
574
+ );
575
+ findParent = getJestSpyOn(DatabaseServerService, "findOneBy");
576
+ findParent.mockImplementation(async (findOneBy: any) => {
577
+ let query: any = findOneBy.query;
578
+ if (!findOneBy.props?.isRoot) {
579
+ query = (
580
+ await ModelPermission.checkReadQueryPermission(
581
+ DatabaseServer,
582
+ { ...findOneBy.query },
583
+ findOneBy.select || null,
584
+ findOneBy.props,
585
+ )
586
+ ).query;
587
+ }
588
+ const found: FakeDatabase | undefined = databases.find(
589
+ (database: FakeDatabase) => {
590
+ return fakeRowMatches(database, query);
591
+ },
592
+ );
593
+ return found ? found.row : null;
594
+ });
595
+ findOwner = getJestSpyOn(service, "findOwnerByEndpoint");
596
+ findOwner.mockResolvedValue(null);
597
+ // The Feed item an added alias writes - see "the database's Feed".
598
+ getJestSpyOn(
599
+ DatabaseServerFeedService,
600
+ "createDatabaseServerFeedItem",
601
+ ).mockResolvedValue(undefined);
602
+ });
603
+
604
+ test("canonicalizes what was typed and forces a removable user alias", async () => {
605
+ const created: DatabaseServerEndpoint =
606
+ await DatabaseServerEndpointService.create({
607
+ data: aliasRequest(" Orders-DB.Example.com. ", {
608
+ // Whatever the caller claims, a person only adds a removable alias.
609
+ isPrimary: true,
610
+ source: "auto",
611
+ }),
612
+ props: memberProps(),
613
+ });
614
+
615
+ expect(save).toHaveBeenCalledTimes(1);
616
+ expect(created.endpoint).toBe("orders-db.example.com:5432");
617
+ expect(created.isPrimary).toBe(false);
618
+ expect(created.source).toBe("user");
619
+ expect(created.projectId!.toString()).toBe(PROJECT_ID.toString());
620
+ expect(created.databaseServerId!.toString()).toBe(DATABASE_ID.toString());
621
+ expect(created.createdByUserId!.toString()).toBe(USER_ID.toString());
622
+
623
+ // Ownership was checked against the canonical form, in the caller's project.
624
+ expect(findOwner).toHaveBeenCalledWith(
625
+ PROJECT_ID,
626
+ "orders-db.example.com:5432",
627
+ );
628
+ });
629
+
630
+ test("an explicit port is kept, the engine default fills a missing one", async () => {
631
+ addDatabase({ dbSystem: "mysql" });
632
+
633
+ const withPort: DatabaseServerEndpoint =
634
+ await DatabaseServerEndpointService.create({
635
+ data: aliasRequest("orders-db.example.com:3307"),
636
+ props: memberProps(),
637
+ });
638
+ const withoutPort: DatabaseServerEndpoint =
639
+ await DatabaseServerEndpointService.create({
640
+ data: aliasRequest("orders-db.example.com"),
641
+ props: memberProps(),
642
+ });
643
+
644
+ expect(withPort.endpoint).toBe("orders-db.example.com:3307");
645
+ expect(withoutPort.endpoint).toBe("orders-db.example.com:3306");
646
+ });
647
+
648
+ test("a bare service name on a Kubernetes database expands in its namespace and cluster", async () => {
649
+ addDatabase({
650
+ kubernetesNamespace: "data",
651
+ clusterIdentifier: "prod-cluster",
652
+ });
653
+
654
+ const created: DatabaseServerEndpoint =
655
+ await DatabaseServerEndpointService.create({
656
+ data: aliasRequest("orders-db"),
657
+ props: memberProps(),
658
+ });
659
+
660
+ expect(created.endpoint).toBe(
661
+ "orders-db.data.svc.cluster.local:5432@prod-cluster",
662
+ );
663
+ });
664
+
665
+ test("reads the parent as root, through the caller's EDIT scope, pinned to the caller's project", async () => {
666
+ await DatabaseServerEndpointService.create({
667
+ data: aliasRequest("orders-db.example.com"),
668
+ props: memberProps(),
669
+ });
670
+
671
+ const call: any = findParent.mock.calls[0]![0];
672
+ expect(call.props).toEqual({ isRoot: true });
673
+ expect(operatorValues(call.query._id)).toEqual([DATABASE_ID.toString()]);
674
+ expect(operatorValues(call.query.projectId)).toEqual([
675
+ PROJECT_ID.toString(),
676
+ ]);
677
+ expect(call.select.kubernetesCluster).toEqual({ clusterIdentifier: true });
678
+ });
679
+
680
+ test("a database in another project is refused, and nothing is written", async () => {
681
+ addDatabase({ projectId: OTHER_PROJECT_ID });
682
+
683
+ await expect(
684
+ DatabaseServerEndpointService.create({
685
+ data: aliasRequest("orders-db.example.com"),
686
+ props: memberProps(),
687
+ }),
688
+ ).rejects.toThrow(
689
+ "Database not found, or you do not have permission to edit it.",
690
+ );
691
+ expect(save).not.toHaveBeenCalled();
692
+ expect(findOwner).not.toHaveBeenCalled();
693
+ });
694
+
695
+ test("an alias without a database is refused", async () => {
696
+ const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
697
+ data.endpoint = "orders-db.example.com";
698
+
699
+ await expect(
700
+ DatabaseServerEndpointService.create({ data, props: memberProps() }),
701
+ ).rejects.toThrow("Select the database this endpoint belongs to.");
702
+ expect(save).not.toHaveBeenCalled();
703
+ });
704
+
705
+ /*
706
+ * TypeORM persists a relation object's id over the FK column's, so the
707
+ * relation object must be validated exactly like the column.
708
+ */
709
+ describe("the database reference", () => {
710
+ test("a relation object pointing elsewhere than the FK column is refused", async () => {
711
+ addDatabase({ id: OTHER_DATABASE_ID, projectId: OTHER_PROJECT_ID });
712
+ const foreign: DatabaseServer = new DatabaseServer(OTHER_DATABASE_ID);
713
+
714
+ await expect(
715
+ DatabaseServerEndpointService.create({
716
+ data: aliasRequest("orders-db.example.com", {
717
+ databaseServer: foreign,
718
+ }),
719
+ props: memberProps(),
720
+ }),
721
+ ).rejects.toThrow("Conflicting database references were provided.");
722
+ expect(save).not.toHaveBeenCalled();
723
+ expect(findParent).not.toHaveBeenCalled();
724
+ });
725
+
726
+ test("a relation object alone is validated like the FK column: another project's database is refused", async () => {
727
+ addDatabase({ id: OTHER_DATABASE_ID, projectId: OTHER_PROJECT_ID });
728
+ const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
729
+ data.endpoint = "orders-db.example.com";
730
+ data.databaseServer = new DatabaseServer(OTHER_DATABASE_ID);
731
+
732
+ await expect(
733
+ DatabaseServerEndpointService.create({ data, props: memberProps() }),
734
+ ).rejects.toThrow(
735
+ "Database not found, or you do not have permission to edit it.",
736
+ );
737
+ expect(save).not.toHaveBeenCalled();
738
+ });
739
+
740
+ test("a relation object alone for the caller's own database becomes the FK column - and only that reaches the insert", async () => {
741
+ const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
742
+ data.endpoint = "orders-db.example.com";
743
+ data.databaseServer = new DatabaseServer(DATABASE_ID);
744
+
745
+ await DatabaseServerEndpointService.create({
746
+ data,
747
+ props: memberProps(),
748
+ });
749
+
750
+ expect(save).toHaveBeenCalledTimes(1);
751
+ const saved: any = save.mock.calls[0]![0];
752
+ expect(saved.databaseServerId.toString()).toBe(DATABASE_ID.toString());
753
+ expect(saved.databaseServer).toBeUndefined();
754
+ });
755
+
756
+ test("a relation object that agrees with the FK column is dropped, the validated column kept", async () => {
757
+ await DatabaseServerEndpointService.create({
758
+ data: aliasRequest("orders-db.example.com", {
759
+ databaseServer: new DatabaseServer(DATABASE_ID),
760
+ }),
761
+ props: memberProps(),
762
+ });
763
+
764
+ const saved: any = save.mock.calls[0]![0];
765
+ expect(saved.databaseServerId.toString()).toBe(DATABASE_ID.toString());
766
+ expect(saved.databaseServer).toBeUndefined();
767
+ });
768
+ });
769
+
770
+ /*
771
+ * Adding an endpoint decides which traffic a database's pages show, and
772
+ * that no other database can claim it: it is an edit of that database, so
773
+ * a label- or Owned-scoped editor may only do it on the rows they edit.
774
+ */
775
+ describe("scoped edit permission", () => {
776
+ test("an editor scoped to label A adds an alias to a database labelled A", async () => {
777
+ addDatabase({ labelIds: [LABEL_TEAM_A] });
778
+
779
+ const created: DatabaseServerEndpoint =
780
+ await DatabaseServerEndpointService.create({
781
+ data: aliasRequest("orders-db.example.com"),
782
+ props: labelScopedEditorProps(LABEL_TEAM_A),
783
+ });
784
+
785
+ expect(save).toHaveBeenCalledTimes(1);
786
+ expect(created.databaseServerId!.toString()).toBe(DATABASE_ID.toString());
787
+ });
788
+
789
+ test("an editor scoped to label A cannot add an alias to a database labelled only B", async () => {
790
+ addDatabase({ labelIds: [LABEL_TEAM_B] });
791
+
792
+ const error: unknown = await DatabaseServerEndpointService.create({
793
+ data: aliasRequest("payments-db.example.com"),
794
+ props: labelScopedEditorProps(LABEL_TEAM_A),
795
+ }).catch((e: unknown) => {
796
+ return e;
797
+ });
798
+
799
+ expect(error).toBeInstanceOf(NotAuthorizedException);
800
+ expect((error as Error).message).toBe(
801
+ "Database not found, or you do not have permission to edit it. Adding an endpoint to a database needs permission to edit that database.",
802
+ );
803
+ expect(save).not.toHaveBeenCalled();
804
+ // Refused before anything about the endpoint's owner is looked up.
805
+ expect(findOwner).not.toHaveBeenCalled();
806
+ });
807
+
808
+ test("an unlabelled database is outside a label-scoped editor's reach", async () => {
809
+ addDatabase({ labelIds: [] });
810
+
811
+ await expect(
812
+ DatabaseServerEndpointService.create({
813
+ data: aliasRequest("orders-db.example.com"),
814
+ props: labelScopedEditorProps(LABEL_TEAM_A),
815
+ }),
816
+ ).rejects.toThrow("Database not found, or you do not have permission");
817
+ expect(save).not.toHaveBeenCalled();
818
+ });
819
+
820
+ test("an Owned-scoped editor adds an alias to a database they own", async () => {
821
+ const allowed: jest.SpyInstance = jest
822
+ .spyOn(OwnedScopePermission as any, "getAllowedResourceIds")
823
+ .mockResolvedValue([DATABASE_ID]);
824
+
825
+ await DatabaseServerEndpointService.create({
826
+ data: aliasRequest("orders-db.example.com"),
827
+ props: ownedScopeEditorProps(),
828
+ });
829
+
830
+ expect(allowed).toHaveBeenCalled();
831
+ expect(save).toHaveBeenCalledTimes(1);
832
+ });
833
+
834
+ test("an Owned-scoped editor cannot add an alias to a database they do not own", async () => {
835
+ jest
836
+ .spyOn(OwnedScopePermission as any, "getAllowedResourceIds")
837
+ .mockResolvedValue([OTHER_DATABASE_ID]);
838
+
839
+ await expect(
840
+ DatabaseServerEndpointService.create({
841
+ data: aliasRequest("orders-db.example.com"),
842
+ props: ownedScopeEditorProps(),
843
+ }),
844
+ ).rejects.toThrow("Database not found, or you do not have permission");
845
+ expect(save).not.toHaveBeenCalled();
846
+ expect(findOwner).not.toHaveBeenCalled();
847
+ });
848
+
849
+ test("a Viewer is refused by the permission check before any lookup", async () => {
850
+ const error: unknown = await DatabaseServerEndpointService.create({
851
+ data: aliasRequest("orders-db.example.com"),
852
+ props: memberProps([Permission.Viewer]),
853
+ }).catch((e: unknown) => {
854
+ return e;
855
+ });
856
+
857
+ expect(error).toBeInstanceOf(NotAuthorizedException);
858
+ // Neither "Database not found" nor an owner's name: nothing to probe with.
859
+ expect((error as Error).message).not.toContain("Database not found");
860
+ expect(findParent).not.toHaveBeenCalled();
861
+ expect(findOwner).not.toHaveBeenCalled();
862
+ expect(save).not.toHaveBeenCalled();
863
+ });
864
+
865
+ test("an endpoint owned by a database the scoped caller can read names it", async () => {
866
+ addDatabase({ labelIds: [LABEL_TEAM_A] });
867
+ addDatabase({
868
+ id: OTHER_DATABASE_ID,
869
+ name: "PostgreSQL payments (team A)",
870
+ labelIds: [LABEL_TEAM_A],
871
+ });
872
+ findOwner.mockResolvedValue(owner(OTHER_DATABASE_ID, true));
873
+
874
+ await expect(
875
+ DatabaseServerEndpointService.create({
876
+ data: aliasRequest("orders-db.example.com"),
877
+ props: labelScopedEditorProps(LABEL_TEAM_A),
878
+ }),
879
+ ).rejects.toThrow(
880
+ 'orders-db.example.com:5432 already belongs to the database "PostgreSQL payments (team A)".',
881
+ );
882
+ });
883
+
884
+ test("an endpoint owned by a database the scoped caller cannot read does not reveal its name", async () => {
885
+ addDatabase({ labelIds: [LABEL_TEAM_A] });
886
+ addDatabase({
887
+ id: OTHER_DATABASE_ID,
888
+ name: "PostgreSQL payments (team B)",
889
+ labelIds: [LABEL_TEAM_B],
890
+ });
891
+ findOwner.mockResolvedValue(owner(OTHER_DATABASE_ID, true));
892
+
893
+ const error: unknown = await DatabaseServerEndpointService.create({
894
+ data: aliasRequest("orders-db.example.com"),
895
+ props: labelScopedEditorProps(LABEL_TEAM_A),
896
+ }).catch((e: unknown) => {
897
+ return e;
898
+ });
899
+
900
+ expect((error as Error).message).toContain(
901
+ "orders-db.example.com:5432 already belongs to another database.",
902
+ );
903
+ expect((error as Error).message).not.toContain("team B");
904
+ expect(save).not.toHaveBeenCalled();
905
+ });
906
+ });
907
+
908
+ /*
909
+ * Every refusal says what to change. The example is a public name:
910
+ * `.internal` names are network-scoped now, so they are no longer the
911
+ * example of an address that works everywhere.
912
+ */
913
+ test.each([
914
+ [
915
+ "an empty value",
916
+ "",
917
+ "Endpoint is required. Enter the host name or IP address applications use to reach this database, with an optional port, for example orders-db.example.com:5432.",
918
+ ],
919
+ [
920
+ "a blank value",
921
+ " ",
922
+ "Endpoint is required. Enter the host name or IP address applications use to reach this database, with an optional port, for example orders-db.example.com:5432.",
923
+ ],
924
+ [
925
+ "localhost",
926
+ "localhost:5432",
927
+ '"localhost:5432" is a loopback or host-local address, such as localhost or host.docker.internal. Every application reaches its own, so it cannot identify one database.',
928
+ ],
929
+ ["a loopback IP", "127.0.0.1", '"127.0.0.1" is a loopback or host-local'],
930
+ [
931
+ "a host-relative name",
932
+ "host.minikube.internal:5432",
933
+ '"host.minikube.internal:5432" is a loopback or host-local',
934
+ ],
935
+ [
936
+ "something that is not an address",
937
+ "orders db",
938
+ '"orders db" is not a valid host[:port] endpoint. Enter a host name or IP address with an optional port, for example orders-db.example.com:5432.',
939
+ ],
940
+ [
941
+ "user@host (psql habit)",
942
+ "admin@10.0.0.5:5432",
943
+ '"admin@10.0.0.5:5432" looks like user@host. Remove "admin@": the database user does not belong in an endpoint.',
944
+ ],
945
+ [
946
+ "a dotted user name before a public host",
947
+ "john.doe@orders-db.example.com",
948
+ 'Remove "john.doe@"',
949
+ ],
950
+ [
951
+ "a cluster qualifier on a public host",
952
+ "orders-db.example.com@prod",
953
+ '"orders-db.example.com" resolves the same way everywhere, so it cannot take a Kubernetes cluster qualifier. Remove "@prod".',
954
+ ],
955
+ ])(
956
+ "refuses %s with a message saying what to change",
957
+ async (_label: string, value: string, message: string) => {
958
+ const error: unknown = await DatabaseServerEndpointService.create({
959
+ data: aliasRequest(value),
960
+ props: memberProps(),
961
+ }).catch((e: unknown) => {
962
+ return e;
963
+ });
964
+
965
+ expect(error).toBeInstanceOf(BadDataException);
966
+ expect((error as Error).message).toContain(message);
967
+ expect((error as Error).message).not.toContain("orders-db.internal");
968
+ expect(save).not.toHaveBeenCalled();
969
+ expect(findOwner).not.toHaveBeenCalled();
970
+ },
971
+ );
972
+
973
+ /*
974
+ * A port a person typed is kept or refused - never read as "no port" and
975
+ * quietly replaced by the engine default, which is right for telemetry
976
+ * and wrong for a typed value.
977
+ */
978
+ test.each([
979
+ ["orders-db.example.com:99999"],
980
+ ["orders-db.example.com:0"],
981
+ ["orders-db.example.com:65536"],
982
+ ["[2001:db8::1]:70000"],
983
+ ["pg.shop.svc.cluster.local:70000@prod"],
984
+ ["postgresql://app@orders-db.example.com:99999/orders"],
985
+ ])("refuses the out-of-range port in %s", async (value: string) => {
986
+ const error: unknown = await DatabaseServerEndpointService.create({
987
+ data: aliasRequest(value),
988
+ props: memberProps(),
989
+ }).catch((e: unknown) => {
990
+ return e;
991
+ });
992
+
993
+ expect(error).toBeInstanceOf(BadDataException);
994
+ expect((error as Error).message).toContain(
995
+ "has a port outside 1-65535. Enter a port between 1 and 65535.",
996
+ );
997
+ expect(save).not.toHaveBeenCalled();
998
+ });
999
+
1000
+ test.each([
1001
+ ["orders-db.example.com:65535", "orders-db.example.com:65535"],
1002
+ ["orders-db.example.com:1", "orders-db.example.com:1"],
1003
+ ["[2001:db8::1]:5432", "[2001:db8::1]:5432"],
1004
+ // A bare IPv6 address carries no port: the engine default fills it.
1005
+ ["2001:db8::1", "[2001:db8::1]:5432"],
1006
+ [
1007
+ "postgresql://app:secret@orders-db.example.com:6432/orders",
1008
+ "orders-db.example.com:6432",
1009
+ ],
1010
+ [
1011
+ "pg.shop.svc.cluster.local:6432@prod",
1012
+ "pg.shop.svc.cluster.local:6432@prod",
1013
+ ],
1014
+ ])("keeps the port typed in %s", async (value: string, expected: string) => {
1015
+ const created: DatabaseServerEndpoint =
1016
+ await DatabaseServerEndpointService.create({
1017
+ data: aliasRequest(value),
1018
+ props: memberProps(),
1019
+ });
1020
+
1021
+ expect(created.endpoint).toBe(expected);
1022
+ });
1023
+
1024
+ test("`<service>.<namespace>` on a Kubernetes database is the Service's full name in its cluster", async () => {
1025
+ addDatabase({
1026
+ kubernetesNamespace: "data",
1027
+ clusterIdentifier: "prod-cluster",
1028
+ });
1029
+
1030
+ const created: DatabaseServerEndpoint =
1031
+ await DatabaseServerEndpointService.create({
1032
+ data: aliasRequest("orders-db.billing:5433"),
1033
+ props: memberProps(),
1034
+ });
1035
+
1036
+ expect(created.endpoint).toBe(
1037
+ "orders-db.billing.svc.cluster.local:5433@prod-cluster",
1038
+ );
1039
+ });
1040
+
1041
+ test("a cluster-local name typed on a Kubernetes database is qualified with its cluster", async () => {
1042
+ addDatabase({
1043
+ kubernetesNamespace: "data",
1044
+ clusterIdentifier: "prod-cluster",
1045
+ });
1046
+
1047
+ const created: DatabaseServerEndpoint =
1048
+ await DatabaseServerEndpointService.create({
1049
+ data: aliasRequest("10.0.4.12"),
1050
+ props: memberProps(),
1051
+ });
1052
+
1053
+ expect(created.endpoint).toBe("10.0.4.12:5432@prod-cluster");
1054
+ });
1055
+
1056
+ /*
1057
+ * The manual-create form refuses an unqualified Kubernetes Service name in
1058
+ * a project with clusters and suggests adding it here as well: an alias
1059
+ * is the explicit way to also match a Database Agent or applications that
1060
+ * do not report their cluster.
1061
+ */
1062
+ test("an unqualified cluster-local alias on a database outside Kubernetes is kept as typed", async () => {
1063
+ const created: DatabaseServerEndpoint =
1064
+ await DatabaseServerEndpointService.create({
1065
+ data: aliasRequest("pg.shop.svc.cluster.local"),
1066
+ props: memberProps(),
1067
+ });
1068
+
1069
+ expect(created.endpoint).toBe("pg.shop.svc.cluster.local:5432");
1070
+ });
1071
+
1072
+ test("a SQL Server named instance alias keeps its instance and gets no default port", async () => {
1073
+ addDatabase({ dbSystem: "mssql" });
1074
+
1075
+ const created: DatabaseServerEndpoint =
1076
+ await DatabaseServerEndpointService.create({
1077
+ data: aliasRequest("SQL1.corp.example.com\\INST01"),
1078
+ props: memberProps(),
1079
+ });
1080
+
1081
+ expect(created.endpoint).toBe("sql1.corp.example.com\\inst01");
1082
+ });
1083
+
1084
+ test("an endpoint another database owns is refused, naming that database", async () => {
1085
+ addDatabase({
1086
+ id: OTHER_DATABASE_ID,
1087
+ name: "PostgreSQL payments-db.example.com:5432",
1088
+ });
1089
+ findOwner.mockResolvedValue(owner(OTHER_DATABASE_ID, true));
1090
+
1091
+ await expect(
1092
+ DatabaseServerEndpointService.create({
1093
+ data: aliasRequest("orders-db.example.com"),
1094
+ props: memberProps(),
1095
+ }),
1096
+ ).rejects.toThrow(
1097
+ 'orders-db.example.com:5432 already belongs to the database "PostgreSQL payments-db.example.com:5432". An endpoint can belong to only one database in a project - remove it from that database first.',
1098
+ );
1099
+ expect(save).not.toHaveBeenCalled();
1100
+ });
1101
+
1102
+ test("a nameless owner still produces a readable refusal", async () => {
1103
+ findOwner.mockResolvedValue(owner(OTHER_DATABASE_ID));
1104
+ getJestSpyOn(
1105
+ DatabaseServerService,
1106
+ "getDatabaseServerNameIfReadable",
1107
+ ).mockRejectedValue(new Error("row gone"));
1108
+
1109
+ await expect(
1110
+ DatabaseServerEndpointService.create({
1111
+ data: aliasRequest("orders-db.example.com"),
1112
+ props: memberProps(),
1113
+ }),
1114
+ ).rejects.toThrow(
1115
+ "orders-db.example.com:5432 already belongs to another database.",
1116
+ );
1117
+ });
1118
+
1119
+ test("an endpoint this database already lists is refused as a duplicate", async () => {
1120
+ findOwner.mockResolvedValue(owner(DATABASE_ID));
1121
+
1122
+ await expect(
1123
+ DatabaseServerEndpointService.create({
1124
+ data: aliasRequest("ORDERS-DB.example.com:5432"),
1125
+ props: memberProps(),
1126
+ }),
1127
+ ).rejects.toThrow(
1128
+ "orders-db.example.com:5432 is already an endpoint of this database.",
1129
+ );
1130
+ expect(save).not.toHaveBeenCalled();
1131
+ });
1132
+
1133
+ /*
1134
+ * The hook forces the columns it owns; everything else still goes through
1135
+ * the model's column ACLs - lastMatchedAt is root-only.
1136
+ */
1137
+ test("a root-only column is still refused by the column permission check", async () => {
1138
+ await expect(
1139
+ DatabaseServerEndpointService.create({
1140
+ data: aliasRequest("orders-db.example.com", {
1141
+ lastMatchedAt: new Date(),
1142
+ }),
1143
+ props: memberProps(),
1144
+ }),
1145
+ ).rejects.toThrow("lastMatchedAt");
1146
+ expect(save).not.toHaveBeenCalled();
1147
+ // The column check now runs before the hook reads anything.
1148
+ expect(findParent).not.toHaveBeenCalled();
1149
+ });
1150
+
1151
+ test("a caller without edit permission on databases cannot add one", async () => {
1152
+ await expect(
1153
+ DatabaseServerEndpointService.create({
1154
+ data: aliasRequest("orders-db.example.com"),
1155
+ props: memberProps([Permission.ReadDatabaseServer]),
1156
+ }),
1157
+ ).rejects.toThrow();
1158
+ expect(save).not.toHaveBeenCalled();
1159
+ expect(findParent).not.toHaveBeenCalled();
1160
+ });
1161
+
1162
+ test("a root create passes through untouched - discovery owns its own canonical form", async () => {
1163
+ const created: DatabaseServerEndpoint =
1164
+ await DatabaseServerEndpointService.create({
1165
+ data: aliasRequest("orders-db.example.com:5432", {
1166
+ projectId: PROJECT_ID,
1167
+ isPrimary: true,
1168
+ source: "auto",
1169
+ }),
1170
+ props: { isRoot: true },
1171
+ });
1172
+
1173
+ expect(findParent).not.toHaveBeenCalled();
1174
+ expect(findOwner).not.toHaveBeenCalled();
1175
+ expect(created.isPrimary).toBe(true);
1176
+ expect(created.source).toBe("auto");
1177
+ });
1178
+
1179
+ /*
1180
+ * e2e: five aliases added and one removed on a database left its Feed
1181
+ * with nothing but "created automatically" - although the Feed promises
1182
+ * every change, and the docs call an alias an edit of the database.
1183
+ */
1184
+ describe("the database's Feed", () => {
1185
+ let feed: jest.SpyInstance;
1186
+
1187
+ beforeEach(() => {
1188
+ feed = getJestSpyOn(
1189
+ DatabaseServerFeedService,
1190
+ "createDatabaseServerFeedItem",
1191
+ ).mockResolvedValue(undefined);
1192
+ getJestSpyOn(
1193
+ DatabaseServerService,
1194
+ "getDatabaseServerMarkdownLink",
1195
+ ).mockResolvedValue(
1196
+ "[Database PostgreSQL orders-db.example.com:5432](/db)",
1197
+ );
1198
+ });
1199
+
1200
+ test("an alias a person adds is an edit on the database's Feed, naming the endpoint and who added it", async () => {
1201
+ await DatabaseServerEndpointService.create({
1202
+ data: aliasRequest("Orders-Replica.Example.com"),
1203
+ props: memberProps(),
1204
+ });
1205
+
1206
+ expect(feed).toHaveBeenCalledTimes(1);
1207
+ const item: any = feed.mock.calls[0]![0];
1208
+ expect(item.databaseServerId.toString()).toBe(DATABASE_ID.toString());
1209
+ expect(item.projectId.toString()).toBe(PROJECT_ID.toString());
1210
+ expect(item.databaseServerFeedEventType).toBe(
1211
+ DatabaseServerFeedEventType.DatabaseServerUpdated,
1212
+ );
1213
+ expect(item.feedInfoInMarkdown).toBe(
1214
+ "🔗 Added the endpoint `orders-replica.example.com:5432` to [Database PostgreSQL orders-db.example.com:5432](/db).",
1215
+ );
1216
+ expect(item.userId.toString()).toBe(USER_ID.toString());
1217
+ });
1218
+
1219
+ test("a refused alias writes nothing", async () => {
1220
+ findOwner.mockResolvedValue({
1221
+ databaseServerId: OTHER_DATABASE_ID,
1222
+ isPrimary: true,
1223
+ });
1224
+ getJestSpyOn(
1225
+ DatabaseServerService,
1226
+ "getDatabaseServerNameIfReadable",
1227
+ ).mockResolvedValue("");
1228
+
1229
+ await expect(
1230
+ DatabaseServerEndpointService.create({
1231
+ data: aliasRequest("orders-replica.example.com:5432"),
1232
+ props: memberProps(),
1233
+ }),
1234
+ ).rejects.toThrow();
1235
+ expect(feed).not.toHaveBeenCalled();
1236
+ });
1237
+
1238
+ test("discovery's claims (root) never reach the Feed", async () => {
1239
+ await DatabaseServerEndpointService.create({
1240
+ data: aliasRequest("orders-db.example.com:5432", {
1241
+ projectId: PROJECT_ID,
1242
+ isPrimary: true,
1243
+ source: "auto",
1244
+ }),
1245
+ props: { isRoot: true },
1246
+ });
1247
+
1248
+ expect(feed).not.toHaveBeenCalled();
1249
+ });
1250
+
1251
+ test("a Feed that cannot be written never fails the alias", async () => {
1252
+ const warn: jest.SpyInstance = jest
1253
+ .spyOn(logger, "warn")
1254
+ .mockImplementation(() => {
1255
+ return undefined as never;
1256
+ });
1257
+ feed.mockRejectedValue(new Error("connection terminated"));
1258
+
1259
+ await expect(
1260
+ DatabaseServerEndpointService.create({
1261
+ data: aliasRequest("orders-replica.example.com"),
1262
+ props: memberProps(),
1263
+ }),
1264
+ ).resolves.toBeDefined();
1265
+ expect(warn).toHaveBeenCalledWith(
1266
+ expect.stringContaining("connection terminated"),
1267
+ );
1268
+ });
1269
+ });
1270
+ });
1271
+
1272
+ /*
1273
+ * Removing an endpoint is an EDIT of the database it belongs to, exactly
1274
+ * like adding one: it decides which traffic that database's pages show, and
1275
+ * frees the endpoint for another database to claim. So a label- or
1276
+ * Owned-scoped editor may only remove the endpoints of the rows they edit,
1277
+ * and a person can never remove a database's primary endpoint.
1278
+ */
1279
+ describe("DatabaseServerEndpointService - removing endpoints", () => {
1280
+ let findBy: jest.SpyInstance;
1281
+ let findParent: jest.SpyInstance;
1282
+ let repositoryDelete: jest.Mock;
1283
+ let databases: Array<FakeDatabase>;
1284
+ let endpoints: Array<DatabaseServerEndpoint>;
1285
+
1286
+ function addDatabase(overrides: {
1287
+ id: ObjectID;
1288
+ labelIds?: Array<string>;
1289
+ }): void {
1290
+ const row: DatabaseServer = parentDatabase();
1291
+ row._id = overrides.id.toString();
1292
+ row.projectId = PROJECT_ID;
1293
+ databases.push({ row: row, labelIds: overrides.labelIds || [] });
1294
+ }
1295
+
1296
+ function addEndpoint(overrides: {
1297
+ databaseServerId: ObjectID;
1298
+ endpoint: string;
1299
+ isPrimary?: boolean;
1300
+ projectId?: ObjectID;
1301
+ }): DatabaseServerEndpoint {
1302
+ const row: DatabaseServerEndpoint = new DatabaseServerEndpoint(
1303
+ ObjectID.generate(),
1304
+ );
1305
+ row.projectId = overrides.projectId || PROJECT_ID;
1306
+ row.databaseServerId = overrides.databaseServerId;
1307
+ row.endpoint = overrides.endpoint;
1308
+ row.isPrimary = Boolean(overrides.isPrimary);
1309
+ row.source = overrides.isPrimary ? "auto" : "user";
1310
+ endpoints.push(row);
1311
+ return row;
1312
+ }
1313
+
1314
+ function removeEndpoint(
1315
+ endpoint: DatabaseServerEndpoint,
1316
+ props: DatabaseCommonInteractionProps,
1317
+ ): Promise<number> {
1318
+ return DatabaseServerEndpointService.deleteBy({
1319
+ query: { _id: endpoint.id!.toString() },
1320
+ limit: 1,
1321
+ skip: 0,
1322
+ props: props,
1323
+ });
1324
+ }
1325
+
1326
+ beforeEach(() => {
1327
+ databases = [];
1328
+ endpoints = [];
1329
+ repositoryDelete = jest.fn(async () => {
1330
+ return { affected: 1 };
1331
+ }) as any;
1332
+ getJestSpyOn(service, "getRepository").mockReturnValue({
1333
+ delete: repositoryDelete,
1334
+ } as never);
1335
+ getJestSpyOn(service, "onTriggerWorkflow").mockResolvedValue(
1336
+ undefined as never,
1337
+ );
1338
+ getJestSpyOn(service, "onTriggerRealtime").mockResolvedValue(
1339
+ undefined as never,
1340
+ );
1341
+ // The endpoint table: matched by id, project and isPrimary, as root reads it.
1342
+ findBy = getJestSpyOn(service, "findBy").mockImplementation(
1343
+ async (findBy: any) => {
1344
+ return endpoints.filter((endpoint: DatabaseServerEndpoint) => {
1345
+ const ids: Array<string> | null = operatorValues(findBy.query._id);
1346
+ const projects: Array<string> | null = operatorValues(
1347
+ findBy.query.projectId,
1348
+ );
1349
+ return (
1350
+ (!ids || ids.includes(endpoint.id!.toString())) &&
1351
+ (!projects || projects.includes(endpoint.projectId!.toString())) &&
1352
+ (findBy.query.isPrimary === undefined ||
1353
+ endpoint.isPrimary === findBy.query.isPrimary)
1354
+ );
1355
+ });
1356
+ },
1357
+ );
1358
+ // The delete's own read, after DatabaseService scoped it.
1359
+ getJestSpyOn(service, "_findBy").mockImplementation(async (input: any) => {
1360
+ return endpoints.filter((endpoint: DatabaseServerEndpoint) => {
1361
+ const ids: Array<string> | null = operatorValues(input.query._id);
1362
+ const projects: Array<string> | null = operatorValues(
1363
+ input.query.projectId,
1364
+ );
1365
+ return (
1366
+ (!ids || ids.includes(endpoint.id!.toString())) &&
1367
+ (!projects || projects.includes(endpoint.projectId!.toString()))
1368
+ );
1369
+ });
1370
+ });
1371
+ findParent = getJestSpyOn(DatabaseServerService, "findOneBy");
1372
+ findParent.mockImplementation(async (findOneBy: any) => {
1373
+ const found: FakeDatabase | undefined = databases.find(
1374
+ (database: FakeDatabase) => {
1375
+ return fakeRowMatches(database, findOneBy.query);
1376
+ },
1377
+ );
1378
+ return found ? found.row : null;
1379
+ });
1380
+ // The Feed item a removed alias writes - see "the database's Feed".
1381
+ getJestSpyOn(
1382
+ DatabaseServerFeedService,
1383
+ "createDatabaseServerFeedItem",
1384
+ ).mockResolvedValue(undefined);
1385
+ });
1386
+
1387
+ test("a person cannot remove the primary endpoint", async () => {
1388
+ addDatabase({ id: DATABASE_ID });
1389
+ const primary: DatabaseServerEndpoint = addEndpoint({
1390
+ databaseServerId: DATABASE_ID,
1391
+ endpoint: "orders-db.example.com:5432",
1392
+ isPrimary: true,
1393
+ });
1394
+
1395
+ await expect(removeEndpoint(primary, memberProps())).rejects.toThrow(
1396
+ "orders-db.example.com:5432 is the primary endpoint of this database and cannot be removed.",
1397
+ );
1398
+ expect(repositoryDelete).not.toHaveBeenCalled();
1399
+ });
1400
+
1401
+ test("the lookup only looks inside the caller's project, as root", async () => {
1402
+ const endpointId: string = ObjectID.generate().toString();
1403
+
1404
+ await DatabaseServerEndpointService.deleteBy({
1405
+ query: { _id: endpointId, projectId: OTHER_PROJECT_ID },
1406
+ limit: 1,
1407
+ skip: 0,
1408
+ props: memberProps(),
1409
+ }).catch(() => {
1410
+ return 0;
1411
+ });
1412
+
1413
+ const call: any = findBy.mock.calls[0]![0];
1414
+ expect(call.query._id).toBe(endpointId);
1415
+ // The request tenant wins over whatever project the query named.
1416
+ expect(call.query.projectId).toEqual(PROJECT_ID);
1417
+ expect(call.props.isRoot).toBe(true);
1418
+ });
1419
+
1420
+ test("an endpoint in another project is never matched, so nothing is removed or revealed", async () => {
1421
+ addDatabase({ id: DATABASE_ID });
1422
+ const foreign: DatabaseServerEndpoint = addEndpoint({
1423
+ databaseServerId: DATABASE_ID,
1424
+ endpoint: "orders-db.example.com:5432",
1425
+ isPrimary: true,
1426
+ projectId: OTHER_PROJECT_ID,
1427
+ });
1428
+
1429
+ await expect(removeEndpoint(foreign, memberProps())).resolves.toBe(0);
1430
+ expect(findParent).not.toHaveBeenCalled();
1431
+ });
1432
+
1433
+ test("an alias of a database the caller may edit is removed", async () => {
1434
+ addDatabase({ id: DATABASE_ID });
1435
+ const alias: DatabaseServerEndpoint = addEndpoint({
1436
+ databaseServerId: DATABASE_ID,
1437
+ endpoint: "orders-replica.example.com:5432",
1438
+ });
1439
+
1440
+ await expect(removeEndpoint(alias, memberProps())).resolves.toBe(1);
1441
+ expect(repositoryDelete).toHaveBeenCalledTimes(1);
1442
+ });
1443
+
1444
+ describe("scoped edit permission", () => {
1445
+ test("an editor scoped to label A removes an alias of a database labelled A", async () => {
1446
+ addDatabase({ id: DATABASE_ID, labelIds: [LABEL_TEAM_A] });
1447
+ const alias: DatabaseServerEndpoint = addEndpoint({
1448
+ databaseServerId: DATABASE_ID,
1449
+ endpoint: "orders-replica.example.com:5432",
1450
+ });
1451
+
1452
+ await expect(
1453
+ removeEndpoint(alias, labelScopedEditorProps(LABEL_TEAM_A)),
1454
+ ).resolves.toBe(1);
1455
+ expect(repositoryDelete).toHaveBeenCalledTimes(1);
1456
+ });
1457
+
1458
+ test("an editor scoped to label A cannot remove an alias of a database labelled only B", async () => {
1459
+ addDatabase({ id: OTHER_DATABASE_ID, labelIds: [LABEL_TEAM_B] });
1460
+ const alias: DatabaseServerEndpoint = addEndpoint({
1461
+ databaseServerId: OTHER_DATABASE_ID,
1462
+ endpoint: "payments-db.example.com:5432",
1463
+ });
1464
+
1465
+ const error: unknown = await removeEndpoint(
1466
+ alias,
1467
+ labelScopedEditorProps(LABEL_TEAM_A),
1468
+ ).catch((e: unknown) => {
1469
+ return e;
1470
+ });
1471
+
1472
+ expect(error).toBeInstanceOf(NotAuthorizedException);
1473
+ expect((error as Error).message).toBe(
1474
+ "Database not found, or you do not have permission to edit it. Removing an endpoint from a database needs permission to edit that database.",
1475
+ );
1476
+ expect(repositoryDelete).not.toHaveBeenCalled();
1477
+ });
1478
+
1479
+ test("another team's PRIMARY endpoint is refused as not editable - its endpoint is not echoed", async () => {
1480
+ addDatabase({ id: OTHER_DATABASE_ID, labelIds: [LABEL_TEAM_B] });
1481
+ const primary: DatabaseServerEndpoint = addEndpoint({
1482
+ databaseServerId: OTHER_DATABASE_ID,
1483
+ endpoint: "payments-db.example.com:5432",
1484
+ isPrimary: true,
1485
+ });
1486
+
1487
+ const error: unknown = await removeEndpoint(
1488
+ primary,
1489
+ labelScopedEditorProps(LABEL_TEAM_A),
1490
+ ).catch((e: unknown) => {
1491
+ return e;
1492
+ });
1493
+
1494
+ expect(error).toBeInstanceOf(NotAuthorizedException);
1495
+ expect((error as Error).message).not.toContain("payments-db");
1496
+ });
1497
+
1498
+ test("an unlabelled database is outside a label-scoped editor's reach", async () => {
1499
+ addDatabase({ id: DATABASE_ID, labelIds: [] });
1500
+ const alias: DatabaseServerEndpoint = addEndpoint({
1501
+ databaseServerId: DATABASE_ID,
1502
+ endpoint: "orders-replica.example.com:5432",
1503
+ });
1504
+
1505
+ await expect(
1506
+ removeEndpoint(alias, labelScopedEditorProps(LABEL_TEAM_A)),
1507
+ ).rejects.toThrow("Database not found, or you do not have permission");
1508
+ expect(repositoryDelete).not.toHaveBeenCalled();
1509
+ });
1510
+
1511
+ test("an Owned-scoped editor removes an alias of a database they own", async () => {
1512
+ addDatabase({ id: DATABASE_ID });
1513
+ const alias: DatabaseServerEndpoint = addEndpoint({
1514
+ databaseServerId: DATABASE_ID,
1515
+ endpoint: "orders-replica.example.com:5432",
1516
+ });
1517
+ jest
1518
+ .spyOn(OwnedScopePermission as any, "getAllowedResourceIds")
1519
+ .mockResolvedValue([DATABASE_ID]);
1520
+
1521
+ await expect(
1522
+ removeEndpoint(alias, ownedScopeEditorProps()),
1523
+ ).resolves.toBe(1);
1524
+ });
1525
+
1526
+ test("an Owned-scoped editor cannot remove an alias of a database they do not own", async () => {
1527
+ addDatabase({ id: DATABASE_ID });
1528
+ const alias: DatabaseServerEndpoint = addEndpoint({
1529
+ databaseServerId: DATABASE_ID,
1530
+ endpoint: "orders-replica.example.com:5432",
1531
+ });
1532
+ jest
1533
+ .spyOn(OwnedScopePermission as any, "getAllowedResourceIds")
1534
+ .mockResolvedValue([OTHER_DATABASE_ID]);
1535
+
1536
+ await expect(
1537
+ removeEndpoint(alias, ownedScopeEditorProps()),
1538
+ ).rejects.toThrow("Database not found, or you do not have permission");
1539
+ expect(repositoryDelete).not.toHaveBeenCalled();
1540
+ });
1541
+
1542
+ test("a Viewer is refused by the permission check before any lookup", async () => {
1543
+ addDatabase({ id: DATABASE_ID });
1544
+ const alias: DatabaseServerEndpoint = addEndpoint({
1545
+ databaseServerId: DATABASE_ID,
1546
+ endpoint: "orders-replica.example.com:5432",
1547
+ });
1548
+
1549
+ const error: unknown = await removeEndpoint(
1550
+ alias,
1551
+ memberProps([Permission.Viewer]),
1552
+ ).catch((e: unknown) => {
1553
+ return e;
1554
+ });
1555
+
1556
+ expect(error).toBeInstanceOf(NotAuthorizedException);
1557
+ expect((error as Error).message).not.toContain("Database not found");
1558
+ expect(findBy).not.toHaveBeenCalled();
1559
+ expect(findParent).not.toHaveBeenCalled();
1560
+ expect(repositoryDelete).not.toHaveBeenCalled();
1561
+ });
1562
+ });
1563
+
1564
+ test("root deletes (project cleanup, discovery) are not guarded", async () => {
1565
+ const onBeforeDelete: (deleteBy: any) => Promise<any> =
1566
+ service.onBeforeDelete.bind(service);
1567
+
1568
+ await onBeforeDelete({
1569
+ query: {},
1570
+ limit: 1,
1571
+ skip: 0,
1572
+ props: { isRoot: true },
1573
+ });
1574
+
1575
+ expect(findBy).not.toHaveBeenCalled();
1576
+ expect(findParent).not.toHaveBeenCalled();
1577
+ });
1578
+
1579
+ describe("the database's Feed", () => {
1580
+ let feed: jest.SpyInstance;
1581
+
1582
+ beforeEach(() => {
1583
+ feed = getJestSpyOn(
1584
+ DatabaseServerFeedService,
1585
+ "createDatabaseServerFeedItem",
1586
+ ).mockResolvedValue(undefined);
1587
+ getJestSpyOn(
1588
+ DatabaseServerService,
1589
+ "getDatabaseServerMarkdownLink",
1590
+ ).mockResolvedValue(
1591
+ "[Database PostgreSQL orders-db.example.com:5432](/db)",
1592
+ );
1593
+ });
1594
+
1595
+ test("an alias a person removes is an edit on its database's Feed, naming the endpoint and who removed it", async () => {
1596
+ addDatabase({ id: DATABASE_ID });
1597
+ const alias: DatabaseServerEndpoint = addEndpoint({
1598
+ databaseServerId: DATABASE_ID,
1599
+ endpoint: "orders-replica.example.com:5432",
1600
+ });
1601
+
1602
+ await expect(removeEndpoint(alias, memberProps())).resolves.toBe(1);
1603
+
1604
+ expect(feed).toHaveBeenCalledTimes(1);
1605
+ const item: any = feed.mock.calls[0]![0];
1606
+ expect(item.databaseServerId.toString()).toBe(DATABASE_ID.toString());
1607
+ expect(item.projectId.toString()).toBe(PROJECT_ID.toString());
1608
+ expect(item.databaseServerFeedEventType).toBe(
1609
+ DatabaseServerFeedEventType.DatabaseServerUpdated,
1610
+ );
1611
+ expect(item.feedInfoInMarkdown).toBe(
1612
+ "🔗 Removed the endpoint `orders-replica.example.com:5432` from [Database PostgreSQL orders-db.example.com:5432](/db).",
1613
+ );
1614
+ expect(item.userId.toString()).toBe(USER_ID.toString());
1615
+ });
1616
+
1617
+ test("a refused removal (the primary endpoint) writes nothing", async () => {
1618
+ addDatabase({ id: DATABASE_ID });
1619
+ const primary: DatabaseServerEndpoint = addEndpoint({
1620
+ databaseServerId: DATABASE_ID,
1621
+ endpoint: "orders-db.example.com:5432",
1622
+ isPrimary: true,
1623
+ });
1624
+
1625
+ await expect(removeEndpoint(primary, memberProps())).rejects.toThrow();
1626
+ expect(feed).not.toHaveBeenCalled();
1627
+ });
1628
+
1629
+ test("an endpoint the delete did not actually remove gets no item", async () => {
1630
+ addDatabase({ id: DATABASE_ID });
1631
+ const alias: DatabaseServerEndpoint = addEndpoint({
1632
+ databaseServerId: DATABASE_ID,
1633
+ endpoint: "orders-replica.example.com:5432",
1634
+ });
1635
+ // A racing delete took it first: the pipeline's own read finds nothing.
1636
+ getJestSpyOn(service, "_findBy").mockResolvedValue([] as never);
1637
+
1638
+ await removeEndpoint(alias, memberProps());
1639
+ expect(feed).not.toHaveBeenCalled();
1640
+ });
1641
+
1642
+ test("discovery's releases (root) never reach the Feed", async () => {
1643
+ addDatabase({ id: DATABASE_ID });
1644
+ const alias: DatabaseServerEndpoint = addEndpoint({
1645
+ databaseServerId: DATABASE_ID,
1646
+ endpoint: "orders-replica.example.com:5432",
1647
+ });
1648
+
1649
+ await removeEndpoint(alias, { isRoot: true });
1650
+ expect(feed).not.toHaveBeenCalled();
1651
+ });
1652
+ });
1653
+ });
1654
+
1655
+ /*
1656
+ * ---------------------------------------------------------------------------
1657
+ * The alias lifecycle: "last matched" kept honest, discovered endpoints moved
1658
+ * or released - never a person's.
1659
+ * ---------------------------------------------------------------------------
1660
+ */
1661
+ describe("DatabaseServerEndpointService - endpoint lifecycle", () => {
1662
+ const ENDPOINT_ID: ObjectID = new ObjectID(
1663
+ "33333333-3333-4333-8333-333333333333",
1664
+ );
1665
+ const HOUR_MS: number = 60 * 60 * 1000;
1666
+
1667
+ function ownerRow(
1668
+ overrides: Partial<DatabaseServerEndpointOwner> = {},
1669
+ ): DatabaseServerEndpointOwner {
1670
+ return {
1671
+ databaseServerId: DATABASE_ID,
1672
+ isPrimary: false,
1673
+ endpointId: ENDPOINT_ID,
1674
+ source: "auto",
1675
+ lastMatchedAt: new Date(Date.now() - 2 * HOUR_MS),
1676
+ ...overrides,
1677
+ };
1678
+ }
1679
+
1680
+ function mockRawQuery(result: unknown = []): jest.Mock {
1681
+ const query: jest.Mock = jest.fn(async () => {
1682
+ return result;
1683
+ });
1684
+ getJestSpyOn(service, "getRepository").mockReturnValue({
1685
+ manager: { query },
1686
+ } as never);
1687
+ return query;
1688
+ }
1689
+
1690
+ describe("findOwnerByEndpoint", () => {
1691
+ test("returns the endpoint row itself: id, source and when it last matched", async () => {
1692
+ const row: DatabaseServerEndpoint = new DatabaseServerEndpoint(
1693
+ ENDPOINT_ID,
1694
+ );
1695
+ const lastMatchedAt: Date = new Date("2026-09-01T10:00:00Z");
1696
+ row.databaseServerId = DATABASE_ID;
1697
+ row.isPrimary = false;
1698
+ row.source = "workload";
1699
+ row.lastMatchedAt = lastMatchedAt;
1700
+ const findOneBy: jest.SpyInstance = getJestSpyOn(
1701
+ service,
1702
+ "findOneBy",
1703
+ ).mockResolvedValue(row);
1704
+
1705
+ const result: DatabaseServerEndpointOwner | null =
1706
+ await DatabaseServerEndpointService.findOwnerByEndpoint(
1707
+ PROJECT_ID,
1708
+ "orders-db.example.com:5432",
1709
+ );
1710
+
1711
+ expect(result!.endpointId!.toString()).toBe(ENDPOINT_ID.toString());
1712
+ expect(result!.source).toBe("workload");
1713
+ expect(result!.lastMatchedAt).toBe(lastMatchedAt);
1714
+ expect(findOneBy.mock.calls[0]![0].select).toMatchObject({
1715
+ source: true,
1716
+ lastMatchedAt: true,
1717
+ });
1718
+ });
1719
+ });
1720
+
1721
+ describe("markEndpointMatched", () => {
1722
+ let write: jest.SpyInstance;
1723
+
1724
+ beforeEach(() => {
1725
+ write = getJestSpyOn(
1726
+ service,
1727
+ "updateColumnsByIdWithoutHooks",
1728
+ ).mockResolvedValue(undefined);
1729
+ });
1730
+
1731
+ test("a match after more than an hour moves lastMatchedAt to now, without bumping updatedAt", async () => {
1732
+ const matched: DatabaseServerEndpointOwner = ownerRow();
1733
+ const before: number = Date.now();
1734
+
1735
+ await DatabaseServerEndpointService.markEndpointMatched(matched);
1736
+
1737
+ expect(write).toHaveBeenCalledTimes(1);
1738
+ const call: any = write.mock.calls[0]![0];
1739
+ expect(call.id).toBe(ENDPOINT_ID);
1740
+ expect(call.skipUpdateDateColumn).toBe(true);
1741
+ expect(
1742
+ (call.data.lastMatchedAt as Date).getTime(),
1743
+ ).toBeGreaterThanOrEqual(before);
1744
+ // The caller's copy is refreshed too, so a second call is free.
1745
+ await DatabaseServerEndpointService.markEndpointMatched(matched);
1746
+ expect(write).toHaveBeenCalledTimes(1);
1747
+ });
1748
+
1749
+ test("an endpoint never matched since it was added (a person's alias) gets its first match", async () => {
1750
+ await DatabaseServerEndpointService.markEndpointMatched(
1751
+ ownerRow({ source: "user", lastMatchedAt: undefined }),
1752
+ );
1753
+
1754
+ expect(write).toHaveBeenCalledTimes(1);
1755
+ });
1756
+
1757
+ test("a match within the hour costs no write", async () => {
1758
+ await DatabaseServerEndpointService.markEndpointMatched(
1759
+ ownerRow({ lastMatchedAt: new Date(Date.now() - 10 * 60 * 1000) }),
1760
+ );
1761
+
1762
+ expect(write).not.toHaveBeenCalled();
1763
+ });
1764
+
1765
+ test("a workload alias is left to discovery: its lastMatchedAt says when the workload last produced it", async () => {
1766
+ await DatabaseServerEndpointService.markEndpointMatched(
1767
+ ownerRow({ source: "workload", lastMatchedAt: undefined }),
1768
+ );
1769
+
1770
+ expect(write).not.toHaveBeenCalled();
1771
+ });
1772
+
1773
+ test("nothing to write without the endpoint row's id", async () => {
1774
+ await DatabaseServerEndpointService.markEndpointMatched(
1775
+ owner(DATABASE_ID),
1776
+ );
1777
+ await DatabaseServerEndpointService.markEndpointMatched(null);
1778
+
1779
+ expect(write).not.toHaveBeenCalled();
1780
+ });
1781
+
1782
+ test("a failing write is logged, never thrown", async () => {
1783
+ write.mockRejectedValue(new Error("connection terminated"));
1784
+ const warn: jest.SpyInstance = jest
1785
+ .spyOn(logger, "warn")
1786
+ .mockImplementation(() => {
1787
+ return undefined as never;
1788
+ });
1789
+
1790
+ await expect(
1791
+ DatabaseServerEndpointService.markEndpointMatched(ownerRow()),
1792
+ ).resolves.toBeUndefined();
1793
+ expect(warn).toHaveBeenCalled();
1794
+ });
1795
+ });
1796
+
1797
+ describe("claimEndpoint re-reported by its owner", () => {
1798
+ test("records the match instead of writing the endpoint again", async () => {
1799
+ const existing: DatabaseServerEndpointOwner = ownerRow();
1800
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(existing);
1801
+ const create: jest.SpyInstance = getJestSpyOn(service, "create");
1802
+ const mark: jest.SpyInstance = getJestSpyOn(
1803
+ service,
1804
+ "markEndpointMatched",
1805
+ ).mockResolvedValue(undefined);
1806
+
1807
+ await expect(
1808
+ DatabaseServerEndpointService.claimEndpoint({
1809
+ projectId: PROJECT_ID,
1810
+ databaseServerId: DATABASE_ID,
1811
+ endpoint: "orders-db.example.com:5432",
1812
+ isPrimary: false,
1813
+ source: "auto",
1814
+ }),
1815
+ ).resolves.toBe("already-owned-by-this");
1816
+ expect(mark).toHaveBeenCalledWith(existing);
1817
+ expect(create).not.toHaveBeenCalled();
1818
+ });
1819
+
1820
+ test("an endpoint another database owns is not marked for the claimant", async () => {
1821
+ getJestSpyOn(service, "findOwnerByEndpoint").mockResolvedValue(
1822
+ ownerRow({ databaseServerId: OTHER_DATABASE_ID }),
1823
+ );
1824
+ const mark: jest.SpyInstance = getJestSpyOn(
1825
+ service,
1826
+ "markEndpointMatched",
1827
+ ).mockResolvedValue(undefined);
1828
+
1829
+ await expect(
1830
+ DatabaseServerEndpointService.claimEndpoint({
1831
+ projectId: PROJECT_ID,
1832
+ databaseServerId: DATABASE_ID,
1833
+ endpoint: "orders-db.example.com:5432",
1834
+ isPrimary: false,
1835
+ source: "auto",
1836
+ }),
1837
+ ).resolves.toBe("owned-by-other");
1838
+ expect(mark).not.toHaveBeenCalled();
1839
+ });
1840
+ });
1841
+
1842
+ describe("refreshMatchedEndpoints", () => {
1843
+ test("one statement, only for this database's stale endpoints among those given", async () => {
1844
+ const query: jest.Mock = mockRawQuery([]);
1845
+ const now: Date = new Date("2026-09-24T12:00:00Z");
1846
+ const staleBefore: Date = new Date("2026-09-24T11:00:00Z");
1847
+
1848
+ await DatabaseServerEndpointService.refreshMatchedEndpoints({
1849
+ projectId: PROJECT_ID,
1850
+ databaseServerId: DATABASE_ID,
1851
+ endpoints: ["a.example.com:5432", "b.example.com:5432"],
1852
+ now: now,
1853
+ staleBefore: staleBefore,
1854
+ });
1855
+
1856
+ expect(query).toHaveBeenCalledTimes(1);
1857
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
1858
+ expect(sql).toContain(`UPDATE "DatabaseServerEndpoint"`);
1859
+ expect(sql).toContain(`SET "lastMatchedAt" = $1`);
1860
+ expect(sql).toContain(`"projectId" = $2`);
1861
+ expect(sql).toContain(`"databaseServerId" = $3`);
1862
+ expect(sql).toContain(`"endpoint" = ANY($4::text[])`);
1863
+ expect(sql).toContain(
1864
+ `("lastMatchedAt" IS NULL OR "lastMatchedAt" < $5)`,
1865
+ );
1866
+ expect(sql).not.toContain(`"updatedAt"`);
1867
+ expect(params).toEqual([
1868
+ now,
1869
+ PROJECT_ID.toString(),
1870
+ DATABASE_ID.toString(),
1871
+ ["a.example.com:5432", "b.example.com:5432"],
1872
+ staleBefore,
1873
+ ]);
1874
+ });
1875
+
1876
+ test("no endpoints, no statement", async () => {
1877
+ const query: jest.Mock = mockRawQuery([]);
1878
+
1879
+ await DatabaseServerEndpointService.refreshMatchedEndpoints({
1880
+ projectId: PROJECT_ID,
1881
+ databaseServerId: DATABASE_ID,
1882
+ endpoints: [],
1883
+ now: new Date(),
1884
+ staleBefore: new Date(),
1885
+ });
1886
+
1887
+ expect(query).not.toHaveBeenCalled();
1888
+ });
1889
+ });
1890
+
1891
+ describe("transferEndpoint", () => {
1892
+ test("moves a discovered endpoint with a compare-and-set on its owner - never a person's", async () => {
1893
+ const query: jest.Mock = mockRawQuery([{ _id: ENDPOINT_ID.toString() }]);
1894
+ const now: Date = new Date();
1895
+
1896
+ await expect(
1897
+ DatabaseServerEndpointService.transferEndpoint({
1898
+ projectId: PROJECT_ID,
1899
+ endpointId: ENDPOINT_ID,
1900
+ fromDatabaseServerId: OTHER_DATABASE_ID,
1901
+ toDatabaseServerId: DATABASE_ID,
1902
+ isPrimary: true,
1903
+ now: now,
1904
+ }),
1905
+ ).resolves.toBe(true);
1906
+
1907
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
1908
+ expect(sql).toContain(`SET "databaseServerId" = $1`);
1909
+ expect(sql).toContain(`"source" = 'workload'`);
1910
+ expect(sql).toContain(`"isPrimary" = $2`);
1911
+ expect(sql).toContain(`"lastMatchedAt" = $3`);
1912
+ expect(sql).toContain(`WHERE "_id" = $4`);
1913
+ expect(sql).toContain(`"projectId" = $5`);
1914
+ expect(sql).toContain(`"databaseServerId" = $6`);
1915
+ expect(sql).toContain(`COALESCE("source", '') <> 'user'`);
1916
+ expect(sql).toContain(`RETURNING "_id"`);
1917
+ /*
1918
+ * A top-level SELECT over the UPDATE: TypeORM answers a bare UPDATE
1919
+ * with [rows, rowCount], which would always read as "moved".
1920
+ */
1921
+ expect(sql.trim().startsWith(`WITH "moved" AS (`)).toBe(true);
1922
+ expect(sql).toContain(`SELECT "_id" FROM "moved"`);
1923
+ expect(params).toEqual([
1924
+ DATABASE_ID.toString(),
1925
+ true,
1926
+ now,
1927
+ ENDPOINT_ID.toString(),
1928
+ PROJECT_ID.toString(),
1929
+ OTHER_DATABASE_ID.toString(),
1930
+ ]);
1931
+ });
1932
+
1933
+ test("a lost compare-and-set (moved or re-owned meanwhile) answers false", async () => {
1934
+ mockRawQuery([]);
1935
+
1936
+ await expect(
1937
+ DatabaseServerEndpointService.transferEndpoint({
1938
+ projectId: PROJECT_ID,
1939
+ endpointId: ENDPOINT_ID,
1940
+ fromDatabaseServerId: OTHER_DATABASE_ID,
1941
+ toDatabaseServerId: DATABASE_ID,
1942
+ isPrimary: false,
1943
+ now: new Date(),
1944
+ }),
1945
+ ).resolves.toBe(false);
1946
+ });
1947
+ });
1948
+
1949
+ describe("releaseUnproducedWorkloadEndpoints", () => {
1950
+ test("deletes only this database's stale non-primary workload aliases outside the kept set", async () => {
1951
+ const query: jest.Mock = mockRawQuery([
1952
+ { endpoint: "redis.cache.svc.cluster.local:6379" },
1953
+ ]);
1954
+ const staleBefore: Date = new Date("2026-09-24T10:00:00Z");
1955
+
1956
+ await expect(
1957
+ DatabaseServerEndpointService.releaseUnproducedWorkloadEndpoints({
1958
+ projectId: PROJECT_ID,
1959
+ databaseServerId: DATABASE_ID,
1960
+ keepEndpoints: ["redis.cache.svc.cluster.local:6379@prod"],
1961
+ staleBefore: staleBefore,
1962
+ }),
1963
+ ).resolves.toEqual(["redis.cache.svc.cluster.local:6379"]);
1964
+
1965
+ const [sql, params] = query.mock.calls[0] as [string, Array<unknown>];
1966
+ expect(sql).toContain(`DELETE FROM "DatabaseServerEndpoint"`);
1967
+ expect(sql.trim().startsWith(`WITH "released" AS (`)).toBe(true);
1968
+ expect(sql).toContain(`SELECT "endpoint" FROM "released"`);
1969
+ expect(sql).toContain(`"projectId" = $1`);
1970
+ expect(sql).toContain(`"databaseServerId" = $2`);
1971
+ expect(sql).toContain(`"source" = 'workload'`);
1972
+ expect(sql).toContain(`"isPrimary" = false`);
1973
+ expect(sql).toContain(`NOT ("endpoint" = ANY($3::text[]))`);
1974
+ expect(sql).toContain(
1975
+ `("lastMatchedAt" IS NULL OR "lastMatchedAt" < $4)`,
1976
+ );
1977
+ expect(params).toEqual([
1978
+ PROJECT_ID.toString(),
1979
+ DATABASE_ID.toString(),
1980
+ ["redis.cache.svc.cluster.local:6379@prod"],
1981
+ staleBefore,
1982
+ ]);
1983
+ });
1984
+
1985
+ test("an unexpected driver answer releases nothing", async () => {
1986
+ mockRawQuery(undefined);
1987
+
1988
+ await expect(
1989
+ DatabaseServerEndpointService.releaseUnproducedWorkloadEndpoints({
1990
+ projectId: PROJECT_ID,
1991
+ databaseServerId: DATABASE_ID,
1992
+ keepEndpoints: [],
1993
+ staleBefore: new Date(),
1994
+ }),
1995
+ ).resolves.toEqual([]);
1996
+ });
1997
+ });
1998
+
1999
+ describe("hasPrimaryEndpoint", () => {
2000
+ test("asks for a primary endpoint of this database in this project, as root", async () => {
2001
+ const findOneBy: jest.SpyInstance = getJestSpyOn(
2002
+ service,
2003
+ "findOneBy",
2004
+ ).mockResolvedValue(new DatabaseServerEndpoint());
2005
+
2006
+ await expect(
2007
+ DatabaseServerEndpointService.hasPrimaryEndpoint({
2008
+ projectId: PROJECT_ID,
2009
+ databaseServerId: DATABASE_ID,
2010
+ }),
2011
+ ).resolves.toBe(true);
2012
+
2013
+ const call: any = findOneBy.mock.calls[0]![0];
2014
+ expect(call.query).toEqual({
2015
+ projectId: PROJECT_ID,
2016
+ databaseServerId: DATABASE_ID,
2017
+ isPrimary: true,
2018
+ });
2019
+ expect(call.props).toEqual({ isRoot: true });
2020
+ });
2021
+
2022
+ test("false when it has none", async () => {
2023
+ getJestSpyOn(service, "findOneBy").mockResolvedValue(null);
2024
+
2025
+ await expect(
2026
+ DatabaseServerEndpointService.hasPrimaryEndpoint({
2027
+ projectId: PROJECT_ID,
2028
+ databaseServerId: DATABASE_ID,
2029
+ }),
2030
+ ).resolves.toBe(false);
2031
+ });
2032
+ });
2033
+ });