@oneuptime/common 14.0.3 → 14.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Alert.ts +55 -0
- package/Models/DatabaseModels/DatabaseServer.ts +1650 -0
- package/Models/DatabaseModels/DatabaseServerEndpoint.ts +540 -0
- package/Models/DatabaseModels/DatabaseServerFeed.ts +625 -0
- package/Models/DatabaseModels/DatabaseServerLabelRule.ts +514 -0
- package/Models/DatabaseModels/DatabaseServerOwnerRule.ts +596 -0
- package/Models/DatabaseModels/DatabaseServerOwnerTeam.ts +491 -0
- package/Models/DatabaseModels/DatabaseServerOwnerUser.ts +490 -0
- package/Models/DatabaseModels/Incident.ts +55 -0
- package/Models/DatabaseModels/Index.ts +16 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +26 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +55 -0
- package/Models/DatabaseModels/TelemetryException.ts +2 -0
- package/Models/DatabaseModels/UserProjectSsoConsent.ts +146 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.ts +673 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.ts +51 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/MultipartFormData.ts +1 -1
- package/Server/Middleware/TelemetryIngest.ts +15 -8
- package/Server/Services/AlertReminderRuleService.ts +100 -24
- package/Server/Services/DatabaseServerEndpointService.ts +895 -0
- package/Server/Services/DatabaseServerFeedService.ts +128 -0
- package/Server/Services/DatabaseServerLabelRuleEngineService.ts +340 -0
- package/Server/Services/DatabaseServerLabelRuleService.ts +14 -0
- package/Server/Services/DatabaseServerOwnerRuleEngineService.ts +425 -0
- package/Server/Services/DatabaseServerOwnerRuleService.ts +14 -0
- package/Server/Services/DatabaseServerOwnerTeamService.ts +180 -0
- package/Server/Services/DatabaseServerOwnerUserService.ts +173 -0
- package/Server/Services/DatabaseServerService.ts +4546 -0
- package/Server/Services/DatabaseService.ts +14 -8
- package/Server/Services/DockerResourceService.ts +43 -2
- package/Server/Services/IncidentReminderRuleService.ts +103 -23
- package/Server/Services/Index.ts +16 -0
- package/Server/Services/InventoryItemService.ts +113 -5
- package/Server/Services/IoTDeviceService.ts +145 -37
- package/Server/Services/MetricService.ts +1 -1
- package/Server/Services/OpenTelemetryIngestService.ts +19 -0
- package/Server/Services/PodmanResourceService.ts +43 -2
- package/Server/Services/ScheduledMaintenanceReminderRuleService.ts +107 -23
- package/Server/Services/ServiceService.ts +1 -1
- package/Server/Services/TelemetryUsageBillingService.ts +29 -2
- package/Server/Services/UserProjectSsoConsentService.ts +74 -0
- package/Server/Services/UserService.ts +14 -1
- package/Server/Types/Database/Permissions/OwnerTableRegistry.ts +18 -0
- package/Server/Types/Database/QueryHelper.ts +84 -2
- package/Server/Utils/AI/Toolbox/MetricTools.ts +13 -2
- package/Server/Utils/AI/Toolbox/ResourceTools.ts +81 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +1 -1
- package/Server/Utils/Database/AffectedResourceRelations.ts +6 -0
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +33 -0
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +192 -9
- package/Server/Utils/Monitor/MonitorAlert.ts +28 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +2264 -755
- package/Server/Utils/Monitor/MonitorIncident.ts +28 -1
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +268 -6
- package/Server/Utils/Monitor/MonitorResource.ts +47 -11
- package/Server/Utils/Monitor/MonitorResourceContext.ts +1 -0
- package/Server/Utils/Monitor/MonitorStepResourceIdentity.ts +239 -21
- package/Server/Utils/Monitor/PerSeriesResolutionRootCause.ts +43 -0
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +291 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +111 -12
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +25 -0
- package/Server/Utils/StartServer.ts +26 -6
- package/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.ts +2 -0
- package/Server/Utils/Telemetry/DatabaseEndpointDiscovery.ts +688 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +5 -1
- package/Server/Utils/Telemetry/LogExceptionExtractor.ts +18 -1
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +175 -12
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +12 -0
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +606 -36
- package/Server/Utils/Telemetry/Telemetry.ts +16 -0
- package/Server/Utils/Telemetry/TelemetryEntity.ts +22 -22
- package/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.ts +16 -10
- package/Tests/App/Accounts/SignedInRedirect.test.tsx +241 -0
- package/Tests/App/AdminDashboard/AdminHeaderSmallScreens.test.tsx +221 -4
- package/Tests/App/Dashboard/AIInsightsResourcePageContext.test.ts +42 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +100 -0
- package/Tests/App/Dashboard/AffectedResourcesPicker.test.tsx +86 -0
- package/Tests/App/Dashboard/AffectedResourcesPickerHiddenTypes.test.tsx +2 -0
- package/Tests/App/Dashboard/AffectedResourcesSummaryStep.test.tsx +19 -0
- package/Tests/App/Dashboard/BreadcrumbCoverage.test.tsx +14 -0
- package/Tests/App/Dashboard/CorrelateFilterBuilder.test.tsx +26 -1
- package/Tests/App/Dashboard/DashboardAppSsoError.test.tsx +501 -0
- package/Tests/App/Dashboard/DashboardHeaderRequests.test.tsx +64 -1
- package/Tests/App/Dashboard/DashboardHeaderSmallScreens.test.tsx +178 -18
- package/Tests/App/Dashboard/DashboardMasterPageSsoRedirect.test.tsx +444 -0
- package/Tests/App/Dashboard/DashboardNavBarForeignHiddenRule.test.tsx +573 -0
- package/Tests/App/Dashboard/DatabaseChartAxes.test.tsx +431 -0
- package/Tests/App/Dashboard/DatabaseContainerDetailBadge.test.tsx +249 -0
- package/Tests/App/Dashboard/DatabaseDashboardE2EFixes.test.ts +681 -0
- package/Tests/App/Dashboard/DatabaseDocumentationMarkdown.test.ts +1801 -0
- package/Tests/App/Dashboard/DatabaseFleetSummaryRefresh.test.tsx +280 -0
- package/Tests/App/Dashboard/DatabaseFormHelpers.test.tsx +321 -0
- package/Tests/App/Dashboard/DatabaseMetricMonitorLink.test.tsx +795 -0
- package/Tests/App/Dashboard/DatabaseOverviewHeader.test.tsx +580 -0
- package/Tests/App/Dashboard/DatabaseRecommendationsEngineMetrics.test.ts +270 -0
- package/Tests/App/Dashboard/DatabaseServerComponents.test.tsx +1486 -0
- package/Tests/App/Dashboard/DatabaseServerEngineMetricsFilter.test.ts +223 -0
- package/Tests/App/Dashboard/DatabaseServerPresentation.test.ts +435 -0
- package/Tests/App/Dashboard/DatabaseServerSummary.test.ts +186 -0
- package/Tests/App/Dashboard/DatabaseServerTelemetryQueries.test.ts +1553 -0
- package/Tests/App/Dashboard/DatabaseTableTruncation.test.tsx +326 -0
- package/Tests/App/Dashboard/DatabaseTelemetryScope.test.ts +512 -0
- package/Tests/App/Dashboard/DatabaseTelemetryTabs.test.tsx +774 -0
- package/Tests/App/Dashboard/DatabaseViewLayout.test.tsx +233 -0
- package/Tests/App/Dashboard/DatabaseWorkloadBadge.test.tsx +972 -0
- package/Tests/App/Dashboard/DiscoveryReviewInventoryRefresh.test.tsx +5 -0
- package/Tests/App/Dashboard/DiscoveryReviewUnnamedHostHint.test.tsx +1061 -0
- package/Tests/App/Dashboard/EventOverlayScope.test.ts +147 -0
- package/Tests/App/Dashboard/EventOverlayScopeHook.test.tsx +1 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +1 -0
- package/Tests/App/Dashboard/MetricsViewerDrillDown.test.tsx +310 -0
- package/Tests/App/Dashboard/MetricsViewerEntityScopedRows.test.tsx +495 -0
- package/Tests/App/Dashboard/MonitorCreateFromMonitorBackedDevice.test.tsx +132 -1
- package/Tests/App/Dashboard/NavigationSearchKeywords.test.tsx +5 -0
- package/Tests/App/Dashboard/NetworkTopologyExplorerSearch.test.tsx +1267 -0
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +4 -3
- package/Tests/App/Dashboard/RecommendationsLoadedContext.test.tsx +349 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +62 -0
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +8 -1
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +14 -0
- package/Tests/App/Dashboard/ResourceOverviewTileTooltips.test.tsx +4 -1
- package/Tests/App/Dashboard/RumOverviewPage.test.tsx +127 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +2 -0
- package/Tests/App/Dashboard/ServiceMapDatabaseLink.test.tsx +1085 -0
- package/Tests/App/Dashboard/SiteSearchBox.test.tsx +2110 -0
- package/Tests/App/Dashboard/TelemetryMetricsSignals.test.ts +168 -8
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +2 -1
- package/Tests/App/StatusPage/StatusPageHeaderForeignHiddenRule.test.tsx +284 -0
- package/Tests/ClassStrings.ts +178 -0
- package/Tests/Fixtures/MobilePasskeyUser.json +1 -2
- package/Tests/ForeignHiddenRuleGuard.ts +2152 -0
- package/Tests/Models/DatabaseModels/DatabaseServerModels.test.ts +2250 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +24 -5
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/RenderedMarkup.ts +518 -0
- package/Tests/ResponsiveVisibility.test.ts +490 -4
- package/Tests/ResponsiveVisibility.ts +505 -52
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddDatabaseServerTablesMigration.test.ts +541 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserProjectSsoConsentMigration.test.ts +234 -0
- package/Tests/Server/Services/ContainerSnapshotUpsertPostgres.test.ts +319 -0
- package/Tests/Server/Services/CrossProjectRelationWriteGuard.test.ts +13 -0
- package/Tests/Server/Services/DatabaseServerEndpointService.test.ts +2033 -0
- package/Tests/Server/Services/DatabaseServerFeedAndOwnerServices.test.ts +529 -0
- package/Tests/Server/Services/DatabaseServerLifecycle.test.ts +3302 -0
- package/Tests/Server/Services/DatabaseServerRegistries.test.ts +603 -0
- package/Tests/Server/Services/DatabaseServerRuleEngines.test.ts +754 -0
- package/Tests/Server/Services/DatabaseServerService.test.ts +3558 -0
- package/Tests/Server/Services/DatabaseServerSqlPostgres.test.ts +2045 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedPostgres.test.ts +362 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlockedReply.test.ts +210 -0
- package/Tests/Server/Services/HookFreeWriteLengthClamp.test.ts +18 -0
- package/Tests/Server/Services/IncidentAlertSloLinkSurvivesEdit.test.ts +2 -0
- package/Tests/Server/Services/IncomingCallPolicyEscalationRuleTarget.test.ts +225 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +280 -2
- package/Tests/Server/Services/InventoryUpsertSchemaParity.test.ts +8 -2
- package/Tests/Server/Services/IoTDeviceLatestMetricsPostgres.test.ts +689 -0
- package/Tests/Server/Services/IoTDeviceService.test.ts +379 -0
- package/Tests/Server/Services/LatestMetricsValuesShape.test.ts +360 -0
- package/Tests/Server/Services/LlmModelPriceService.test.ts +402 -0
- package/Tests/Server/Services/MetricServiceAggregate.test.ts +12 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +7 -1
- package/Tests/Server/Services/OwnerRowUniqueness.test.ts +61 -9
- package/Tests/Server/Services/ReminderRuleLookupPostgres.test.ts +2250 -0
- package/Tests/Server/Services/ReminderRuleRefreshOpenSubjectsPaging.test.ts +653 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +68 -20
- package/Tests/Server/Services/SavedViewDefaults.test.ts +325 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +3 -3
- package/Tests/Server/Services/StatusPageCertificateService.test.ts +169 -0
- package/Tests/Server/Services/TeamComplianceSettingService.test.ts +191 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +9 -0
- package/Tests/Server/Services/UserProjectSsoConsentPostgres.test.ts +153 -0
- package/Tests/Server/Services/UserProjectSsoConsentService.test.ts +192 -0
- package/Tests/Server/Services/UserServiceFirstMasterAdminElection.test.ts +71 -0
- package/Tests/Server/Services/UserSignupIdTakeoverPostgres.test.ts +238 -0
- package/Tests/Server/Services/WorkspaceNotificationSummaryHelpers.test.ts +592 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredAuthorization.test.ts +3 -0
- package/Tests/Server/Types/Database/EventOverlayScopePostgres.test.ts +1 -0
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +11 -0
- package/Tests/Server/Types/Database/Permissions/InfrastructureOwnershipScope.test.ts +8 -0
- package/Tests/Server/Types/Database/QueryHelperBooleanForCriteriaBackedRule.test.ts +1094 -0
- package/Tests/Server/Types/Database/QueryHelperSearchAllWords.test.ts +651 -0
- package/Tests/Server/Utils/AI/AlertWriteTools.test.ts +256 -0
- package/Tests/Server/Utils/AI/IncidentWriteTools.test.ts +482 -0
- package/Tests/Server/Utils/AI/ResourceTools.test.ts +134 -0
- package/Tests/Server/Utils/Monitor/Criteria/CompareCriteria.test.ts +54 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +41 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +279 -9
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaDisplayContext.test.ts +933 -0
- package/Tests/Server/Utils/Monitor/DatabaseMaintenanceSuppression.test.ts +582 -0
- package/Tests/Server/Utils/Monitor/DatabaseServerMonitorLinking.test.ts +753 -0
- package/Tests/Server/Utils/Monitor/MetricDecimalThreshold.test.ts +417 -0
- package/Tests/Server/Utils/Monitor/MonitorActionTimestamps.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertCriteriaReferences.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +119 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +60 -25
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorAffectedResourceEdgeCases.test.ts +1815 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesAffectedResources.test.ts +1447 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorKubernetesRatioEmail.test.ts +352 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformMetricUnits.test.ts +717 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformRawScan.test.ts +2125 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPlatformSeriesAffectedResources.test.ts +1288 -0
- package/Tests/Server/Utils/Monitor/MonitorDependencySuppressionCreatorSkip.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentCriteriaReferences.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentMemberRoleMembership.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +88 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +205 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceContext.test.ts +62 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceDatabaseMaintenance.test.ts +803 -0
- package/Tests/Server/Utils/Monitor/MonitorStepResourceIdentity.test.ts +133 -3
- package/Tests/Server/Utils/Monitor/MonitorSummaryPersistence.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/PerSeriesRecoveryResolution.test.ts +472 -1
- package/Tests/Server/Utils/Monitor/PerSeriesSecondEntityAlerting.test.ts +1 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +236 -2
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +70 -0
- package/Tests/Server/Utils/StartServerProtobufContentType.test.ts +54 -0
- package/Tests/Server/Utils/Telemetry/DatabaseEndpointDiscovery.test.ts +1895 -0
- package/Tests/Server/Utils/Telemetry/EntityRegistry.test.ts +7 -0
- package/Tests/Server/Utils/Telemetry/LogExceptionExtractorSeverityGate.test.ts +90 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +570 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +97 -2
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +1337 -0
- package/Tests/Server/Utils/Telemetry/TelemetryEntity.test.ts +295 -0
- package/Tests/Types/AI/AIResourceContext.test.ts +30 -0
- package/Tests/Types/BaseDatabase/AggregationType.test.ts +5 -1
- package/Tests/Types/Dashboard/KubernetesDashboardTemplate.test.ts +184 -0
- package/Tests/Types/Dashboard/KubernetesDashboardTemplates.test.ts +107 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerClassifier.test.ts +3644 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommand.test.ts +340 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommandCorpus.ts +826 -0
- package/Tests/Types/DatabaseServer/DatabaseContainerCommandPostgres.test.ts +150 -0
- package/Tests/Types/DatabaseServer/DatabaseEndpoint.test.ts +2657 -0
- package/Tests/Types/DatabaseServer/DatabaseServerDiscoverySource.test.ts +64 -0
- package/Tests/Types/DatabaseServer/DatabaseServerEntityKeys.test.ts +952 -0
- package/Tests/Types/DatabaseServer/DatabaseServerMetricCatalog.test.ts +1161 -0
- package/Tests/Types/DatabaseServer/DatabaseSystem.test.ts +1514 -0
- package/Tests/Types/DatabaseServer/DatabaseTelemetryResolver.test.ts +1558 -0
- package/Tests/Types/Monitor/CephAlertTemplates.test.ts +255 -6
- package/Tests/Types/Monitor/DatabaseAlertTemplates.test.ts +2254 -0
- package/Tests/Types/Monitor/KubernetesAlertTemplates.test.ts +292 -0
- package/Tests/Types/Monitor/KubernetesMetricCatalog.test.ts +272 -0
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCatalog.test.ts +253 -9
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationCoverage.test.ts +13 -5
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationNotificationMode.test.ts +4 -2
- package/Tests/Types/Monitor/Recommendation/MonitorRecommendationUtil.test.ts +111 -2
- package/Tests/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.test.ts +61 -0
- package/Tests/Types/Monitor/SeriesContext/SeriesLabelDisplay.test.ts +114 -0
- package/Tests/Types/Monitor/Utils/KubernetesAgentEmittedMetrics.ts +202 -0
- package/Tests/Types/NetworkDevice/DiscoveredHostNamingStatus.test.ts +401 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +4 -1
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +24 -2
- package/Tests/UI/Components/Card.test.tsx +93 -2
- package/Tests/UI/Components/FeedOptions.test.ts +6 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +8 -1
- package/Tests/UI/Components/ForeignHiddenRuleResilience.test.tsx +1147 -0
- package/Tests/UI/Components/HeaderRightRail.test.tsx +41 -1
- package/Tests/UI/Components/LogsEntityNames.test.ts +16 -1
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +7 -3
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +2 -1
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/SrOnlyForeignRule.test.tsx +994 -0
- package/Tests/UI/Components/StatusPage/ResourceListSelection.test.tsx +34 -1
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +4 -1
- package/Tests/UI/ForeignHiddenRuleGuard.test.ts +1951 -0
- package/Tests/UI/Rum/ChunkLoader.test.ts +76 -0
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +19 -1
- package/Tests/UI/Rum/ReplayRailDetail.test.tsx +53 -0
- package/Tests/UI/Rum/ReplaySignals.test.ts +54 -0
- package/Tests/UI/Rum/ReplayTabSwitcher.test.tsx +19 -1
- package/Tests/UI/TailwindPlayCdnForeignHiddenRule.test.ts +1005 -0
- package/Tests/UI/ThemeGroupHoverReveal.test.ts +847 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +70 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +21 -0
- package/Tests/UI/Utils/ModelAPISsoRedirect.test.ts +127 -0
- package/Tests/UI/Utils/Navigation.test.ts +102 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +23 -0
- package/Tests/Utils/Dashboard/KubernetesClusterCpuTileRepair.test.ts +370 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +5 -0
- package/Tests/Utils/MetricUnitUtil.test.ts +227 -2
- package/Tests/Utils/Monitor/MetricValueFormatter.test.ts +288 -2
- package/Tests/Utils/Monitor/PlatformResourceIdentityAndUnits.test.ts +1559 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.test.ts +2546 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredHostUtil.test.ts +315 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveryReverseDnsChain.test.ts +336 -1
- package/Tests/Utils/NetworkDiscovery/NetbiosNameUtil.test.ts +244 -0
- package/Tests/Utils/Rules/MonitorTypeRuleCriteria.test.ts +181 -0
- package/Tests/Utils/ValueFormatter.test.ts +50 -0
- package/Tests/tsconfig.json +16 -0
- package/Types/AI/AIResourceContext.ts +7 -0
- package/Types/BaseDatabase/AggregationType.ts +7 -0
- package/Types/Dashboard/DashboardTemplates.ts +22 -8
- package/Types/DatabaseServer/DatabaseConnectionSpan.ts +77 -0
- package/Types/DatabaseServer/DatabaseContainerClassifier.ts +2068 -0
- package/Types/DatabaseServer/DatabaseContainerCommand.ts +801 -0
- package/Types/DatabaseServer/DatabaseEndpoint.ts +1943 -0
- package/Types/DatabaseServer/DatabaseServerDiscoverySource.ts +68 -0
- package/Types/DatabaseServer/DatabaseServerMetricCatalog.ts +966 -0
- package/Types/DatabaseServer/DatabaseSystem.ts +2124 -0
- package/Types/DatabaseServer/DatabaseTelemetryResolver.ts +555 -0
- package/Types/Email/EmailTemplateType.ts +1 -0
- package/Types/Monitor/CephAlertTemplates.ts +18 -0
- package/Types/Monitor/DatabaseAlertTemplates.ts +2020 -0
- package/Types/Monitor/KubernetesAlertTemplates.ts +64 -24
- package/Types/Monitor/KubernetesMetricCatalog.ts +118 -19
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +14 -0
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +93 -10
- package/Types/Monitor/Recommendation/MonitorRecommendationCatalog.ts +109 -8
- package/Types/Monitor/Recommendation/MonitorRecommendationTypes.ts +36 -7
- package/Types/Monitor/Recommendation/MonitorRecommendationUtil.ts +43 -2
- package/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.ts +41 -6
- package/Types/Monitor/SeriesContext/SeriesLabelDisplay.ts +26 -0
- package/Types/NetworkDevice/DiscoveredHostNamingStatus.ts +147 -0
- package/Types/Permission.ts +267 -0
- package/Types/Probe/ProbeApiIngestResponse.ts +8 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +10 -0
- package/Types/Rules/RuleRun.ts +12 -0
- package/Types/Rum/SessionReplayCustomEvents.ts +29 -3
- package/Types/Rum/WebVitals.ts +13 -0
- package/Types/Telemetry/EntityType.ts +23 -0
- package/Types/Telemetry/ResourceFacetCatalog.ts +14 -0
- package/Types/Telemetry/ServiceType.ts +8 -0
- package/UI/Components/Banner/Banner.tsx +1 -1
- package/UI/Components/Card/Card.tsx +1 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +2 -2
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/CommandPalette/CommandPalette.tsx +1 -1
- package/UI/Components/Date/StartAndEndDate.tsx +5 -5
- package/UI/Components/Diagram/HorizontalStepChain.tsx +1 -1
- package/UI/Components/Feed/FeedOptionsButton.tsx +1 -1
- package/UI/Components/Forms/BasicForm.tsx +1 -1
- package/UI/Components/Header/Header.tsx +1 -1
- package/UI/Components/Header/HeaderIconDropdownButton.tsx +1 -1
- package/UI/Components/HeaderAlert/HeaderAlert.tsx +1 -1
- package/UI/Components/Input/Input.tsx +11 -0
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +1 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +1 -1
- package/UI/Components/Modal/ModalFooter.tsx +1 -1
- package/UI/Components/ModelDetail/CardModelDetail.tsx +2 -2
- package/UI/Components/ModelTable/BaseModelTable.tsx +4 -4
- package/UI/Components/Navbar/NavBar.tsx +9 -2
- package/UI/Components/Navbar/NavBarMenuModal.tsx +2 -2
- package/UI/Components/Page/Page.tsx +1 -1
- package/UI/Components/Pagination/Pagination.tsx +3 -3
- package/UI/Components/ProgressButtons/ProgressButtonItem.tsx +1 -1
- package/UI/Components/SideMenu/SideMenu.tsx +4 -1
- package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +4 -2
- package/UI/Components/StatusPage/ResourceList.tsx +6 -6
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +1 -1
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +1 -1
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +1 -1
- package/UI/Components/Workflow/ComponentsModal.tsx +2 -2
- package/UI/Components/Workflow/StepTraceViewer.tsx +1 -1
- package/UI/Styles/Theme.css +8 -1
- package/UI/Utils/Navigation.ts +89 -42
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +7 -0
- package/UI/Utils/TelemetryService.ts +4 -3
- package/Utils/Dashboard/KubernetesClusterCpuTileRepair.ts +147 -0
- package/Utils/LabelRuleImportExport.ts +2 -0
- package/Utils/MetricUnitUtil.ts +103 -11
- package/Utils/Monitor/MetricValueFormatter.ts +66 -13
- package/Utils/Monitor/PlatformMetricUnitUtil.ts +215 -0
- package/Utils/Monitor/PlatformResourceIdentity.ts +514 -0
- package/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.ts +425 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +32 -0
- package/Utils/NetworkDiscovery/NetbiosNameUtil.ts +73 -0
- package/Utils/Telemetry/DatabaseServerEntityKeys.ts +275 -0
- package/Utils/Telemetry/EntityKey.ts +176 -0
- package/Utils/ValueFormatter.ts +16 -0
- package/build/dist/Models/DatabaseModels/Alert.js +54 -0
- package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DatabaseServer.js +1683 -0
- package/build/dist/Models/DatabaseModels/DatabaseServer.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js +559 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerEndpoint.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js +648 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerFeed.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js +522 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js +603 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js +506 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerTeam.js.map +1 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js +505 -0
- package/build/dist/Models/DatabaseModels/DatabaseServerOwnerUser.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Incident.js +54 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +16 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +54 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +2 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js +162 -0
- package/build/dist/Models/DatabaseModels/UserProjectSsoConsent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js +240 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795000000000-AddDatabaseServerTables.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1795100000000-AddUserProjectSsoConsent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/MultipartFormData.js +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +15 -8
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AlertReminderRuleService.js +72 -23
- package/build/dist/Server/Services/AlertReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseServerEndpointService.js +694 -0
- package/build/dist/Server/Services/DatabaseServerEndpointService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerFeedService.js +110 -0
- package/build/dist/Server/Services/DatabaseServerFeedService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js +272 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleService.js +13 -0
- package/build/dist/Server/Services/DatabaseServerLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js +340 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js +13 -0
- package/build/dist/Server/Services/DatabaseServerOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js +160 -0
- package/build/dist/Server/Services/DatabaseServerOwnerTeamService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerOwnerUserService.js +153 -0
- package/build/dist/Server/Services/DatabaseServerOwnerUserService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseServerService.js +3272 -0
- package/build/dist/Server/Services/DatabaseServerService.js.map +1 -0
- package/build/dist/Server/Services/DatabaseService.js +8 -4
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerResourceService.js +30 -3
- package/build/dist/Server/Services/DockerResourceService.js.map +1 -1
- package/build/dist/Server/Services/IncidentReminderRuleService.js +73 -23
- package/build/dist/Server/Services/IncidentReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +16 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +88 -5
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/IoTDeviceService.js +111 -24
- package/build/dist/Server/Services/IoTDeviceService.js.map +1 -1
- package/build/dist/Server/Services/MetricService.js +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +24 -9
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/PodmanResourceService.js +30 -3
- package/build/dist/Server/Services/PodmanResourceService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js +75 -23
- package/build/dist/Server/Services/ScheduledMaintenanceReminderRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +1 -1
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryUsageBillingService.js +23 -2
- package/build/dist/Server/Services/TelemetryUsageBillingService.js.map +1 -1
- package/build/dist/Server/Services/UserProjectSsoConsentService.js +79 -0
- package/build/dist/Server/Services/UserProjectSsoConsentService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +13 -1
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js +18 -0
- package/build/dist/Server/Types/Database/Permissions/OwnerTableRegistry.js.map +1 -1
- package/build/dist/Server/Types/Database/QueryHelper.js +73 -2
- package/build/dist/Server/Types/Database/QueryHelper.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js +13 -2
- package/build/dist/Server/Utils/AI/Toolbox/MetricTools.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js +58 -1
- package/build/dist/Server/Utils/AI/Toolbox/ResourceTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +1 -1
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js +6 -0
- package/build/dist/Server/Utils/Database/AffectedResourceRelations.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +31 -0
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +135 -14
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +1489 -524
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +22 -6
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +196 -6
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +36 -9
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResourceContext.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js +175 -17
- package/build/dist/Server/Utils/Monitor/MonitorStepResourceIdentity.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js +29 -0
- package/build/dist/Server/Utils/Monitor/PerSeriesResolutionRootCause.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +211 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +82 -10
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -1
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +25 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +20 -4
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js +2 -0
- package/build/dist/Server/Utils/TeamMember/ProjectLeaveResourceCleanup.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js +442 -0
- package/build/dist/Server/Utils/Telemetry/DatabaseEndpointDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +5 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js +15 -1
- package/build/dist/Server/Utils/Telemetry/LogExceptionExtractor.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +121 -5
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +12 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +438 -36
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +12 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js +17 -18
- package/build/dist/Server/Utils/Telemetry/TelemetryEntity.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js +16 -10
- package/build/dist/Server/Utils/Telemetry/TelemetryIngestionKeyGuard.js.map +1 -1
- package/build/dist/Types/AI/AIResourceContext.js +7 -0
- package/build/dist/Types/AI/AIResourceContext.js.map +1 -1
- package/build/dist/Types/BaseDatabase/AggregationType.js +7 -0
- package/build/dist/Types/BaseDatabase/AggregationType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardTemplates.js +22 -8
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js +71 -0
- package/build/dist/Types/DatabaseServer/DatabaseConnectionSpan.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js +1462 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerClassifier.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js +691 -0
- package/build/dist/Types/DatabaseServer/DatabaseContainerCommand.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js +1407 -0
- package/build/dist/Types/DatabaseServer/DatabaseEndpoint.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js +60 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerDiscoverySource.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js +804 -0
- package/build/dist/Types/DatabaseServer/DatabaseServerMetricCatalog.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseSystem.js +1860 -0
- package/build/dist/Types/DatabaseServer/DatabaseSystem.js.map +1 -0
- package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js +406 -0
- package/build/dist/Types/DatabaseServer/DatabaseTelemetryResolver.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Monitor/CephAlertTemplates.js +18 -0
- package/build/dist/Types/Monitor/CephAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/DatabaseAlertTemplates.js +1518 -0
- package/build/dist/Types/Monitor/DatabaseAlertTemplates.js.map +1 -0
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js +57 -24
- package/build/dist/Types/Monitor/KubernetesAlertTemplates.js.map +1 -1
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js +104 -18
- package/build/dist/Types/Monitor/KubernetesMetricCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js +80 -3
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationCatalog.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js +10 -3
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationTypes.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js +29 -0
- package/build/dist/Types/Monitor/Recommendation/MonitorRecommendationUtil.js.map +1 -1
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js +21 -6
- package/build/dist/Types/Monitor/Recommendation/RecommendationCriteriaBuilder.js.map +1 -1
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js +25 -0
- package/build/dist/Types/Monitor/SeriesContext/SeriesLabelDisplay.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js +136 -0
- package/build/dist/Types/NetworkDevice/DiscoveredHostNamingStatus.js.map +1 -0
- package/build/dist/Types/Permission.js +234 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +10 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayCustomEvents.js.map +1 -1
- package/build/dist/Types/Rum/WebVitals.js +12 -0
- package/build/dist/Types/Rum/WebVitals.js.map +1 -1
- package/build/dist/Types/Telemetry/EntityType.js +23 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +14 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -1
- package/build/dist/Types/Telemetry/ServiceType.js +8 -0
- package/build/dist/Types/Telemetry/ServiceType.js.map +1 -1
- package/build/dist/UI/Components/Banner/Banner.js +1 -1
- package/build/dist/UI/Components/Banner/Banner.js.map +1 -1
- package/build/dist/UI/Components/Card/Card.js +1 -1
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +2 -2
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js +1 -1
- package/build/dist/UI/Components/CommandPalette/CommandPalette.js.map +1 -1
- package/build/dist/UI/Components/Date/StartAndEndDate.js +5 -5
- package/build/dist/UI/Components/Date/StartAndEndDate.js.map +1 -1
- package/build/dist/UI/Components/Diagram/HorizontalStepChain.js +1 -1
- package/build/dist/UI/Components/Diagram/HorizontalStepChain.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +1 -1
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js +1 -1
- package/build/dist/UI/Components/Header/HeaderIconDropdownButton.js.map +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js +1 -1
- package/build/dist/UI/Components/HeaderAlert/HeaderAlert.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +1 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +2 -2
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +4 -4
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +9 -2
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/Page/Page.js +1 -1
- package/build/dist/UI/Components/Page/Page.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +3 -3
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js +1 -1
- package/build/dist/UI/Components/ProgressButtons/ProgressButtonItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +4 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +3 -1
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceList.js +6 -6
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ComponentsModal.js +2 -2
- package/build/dist/UI/Components/Workflow/ComponentsModal.js.map +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js +1 -1
- package/build/dist/UI/Components/Workflow/StepTraceViewer.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +66 -33
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +7 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -1
- package/build/dist/UI/Utils/TelemetryService.js +3 -2
- package/build/dist/UI/Utils/TelemetryService.js.map +1 -1
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js +110 -0
- package/build/dist/Utils/Dashboard/KubernetesClusterCpuTileRepair.js.map +1 -0
- package/build/dist/Utils/LabelRuleImportExport.js +2 -0
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/MetricUnitUtil.js +77 -13
- package/build/dist/Utils/MetricUnitUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +58 -13
- package/build/dist/Utils/Monitor/MetricValueFormatter.js.map +1 -1
- package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js +161 -0
- package/build/dist/Utils/Monitor/PlatformMetricUnitUtil.js.map +1 -0
- package/build/dist/Utils/Monitor/PlatformResourceIdentity.js +323 -0
- package/build/dist/Utils/Monitor/PlatformResourceIdentity.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js +270 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostNamingDiagnosis.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +23 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js +61 -0
- package/build/dist/Utils/NetworkDiscovery/NetbiosNameUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js +178 -0
- package/build/dist/Utils/Telemetry/DatabaseServerEntityKeys.js.map +1 -0
- package/build/dist/Utils/Telemetry/EntityKey.js +133 -0
- package/build/dist/Utils/Telemetry/EntityKey.js.map +1 -1
- package/build/dist/Utils/ValueFormatter.js +15 -0
- package/build/dist/Utils/ValueFormatter.js.map +1 -1
- package/package.json +1 -1
- package/tsconfig.tests.json +7 -5
|
@@ -0,0 +1,2250 @@
|
|
|
1
|
+
import AllModelTypes, {
|
|
2
|
+
getModelTypeByName,
|
|
3
|
+
} from "../../../Models/DatabaseModels/Index";
|
|
4
|
+
import Alert from "../../../Models/DatabaseModels/Alert";
|
|
5
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
|
+
import RuleBaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/RuleBaseModel";
|
|
7
|
+
import DatabaseServer from "../../../Models/DatabaseModels/DatabaseServer";
|
|
8
|
+
import DatabaseServerEndpoint from "../../../Models/DatabaseModels/DatabaseServerEndpoint";
|
|
9
|
+
import DatabaseServerFeed, {
|
|
10
|
+
DatabaseServerFeedEventType,
|
|
11
|
+
} from "../../../Models/DatabaseModels/DatabaseServerFeed";
|
|
12
|
+
import DatabaseServerLabelRule from "../../../Models/DatabaseModels/DatabaseServerLabelRule";
|
|
13
|
+
import DatabaseServerOwnerRule from "../../../Models/DatabaseModels/DatabaseServerOwnerRule";
|
|
14
|
+
import DatabaseServerOwnerTeam from "../../../Models/DatabaseModels/DatabaseServerOwnerTeam";
|
|
15
|
+
import DatabaseServerOwnerUser from "../../../Models/DatabaseModels/DatabaseServerOwnerUser";
|
|
16
|
+
import DockerHost from "../../../Models/DatabaseModels/DockerHost";
|
|
17
|
+
import Incident from "../../../Models/DatabaseModels/Incident";
|
|
18
|
+
import KubernetesCluster from "../../../Models/DatabaseModels/KubernetesCluster";
|
|
19
|
+
import Label from "../../../Models/DatabaseModels/Label";
|
|
20
|
+
import PodmanHost from "../../../Models/DatabaseModels/PodmanHost";
|
|
21
|
+
import ScheduledMaintenance from "../../../Models/DatabaseModels/ScheduledMaintenance";
|
|
22
|
+
import DatabaseRequestType from "../../../Server/Types/BaseDatabase/DatabaseRequestType";
|
|
23
|
+
import ColumnPermissions from "../../../Server/Types/Database/Permissions/ColumnPermission";
|
|
24
|
+
import ModelPermission from "../../../Server/Types/Database/Permissions/Index";
|
|
25
|
+
import { ColumnAccessControl } from "../../../Types/BaseDatabase/AccessControl";
|
|
26
|
+
import { OwnedThroughMetadata } from "../../../Types/Database/AccessControl/OwnedThrough";
|
|
27
|
+
import DatabaseCommonInteractionProps from "../../../Types/BaseDatabase/DatabaseCommonInteractionProps";
|
|
28
|
+
import ColumnLength from "../../../Types/Database/ColumnLength";
|
|
29
|
+
import { TableColumnMetadata } from "../../../Types/Database/TableColumn";
|
|
30
|
+
import TableColumnType from "../../../Types/Database/TableColumnType";
|
|
31
|
+
import { getUniqueColumnsBy } from "../../../Types/Database/UniqueColumnBy";
|
|
32
|
+
import { UniqueColumnsTogetherMetadata } from "../../../Types/Database/UniqueColumnsTogether";
|
|
33
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
34
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
35
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
36
|
+
import Permission, {
|
|
37
|
+
PermissionHelper,
|
|
38
|
+
PermissionProps,
|
|
39
|
+
UserTenantAccessPermission,
|
|
40
|
+
} from "../../../Types/Permission";
|
|
41
|
+
import ServiceType from "../../../Types/Telemetry/ServiceType";
|
|
42
|
+
import {
|
|
43
|
+
buildDatabaseServerIdentifier,
|
|
44
|
+
buildWorkloadDatabaseServerIdentifier,
|
|
45
|
+
} from "../../../Types/DatabaseServer/DatabaseEndpoint";
|
|
46
|
+
import { getDatabaseSystemFamily } from "../../../Types/DatabaseServer/DatabaseSystem";
|
|
47
|
+
import { describe, expect, test } from "@jest/globals";
|
|
48
|
+
import { getMetadataArgsStorage } from "typeorm";
|
|
49
|
+
import { ColumnMetadataArgs } from "typeorm/metadata-args/ColumnMetadataArgs";
|
|
50
|
+
import { IndexMetadataArgs } from "typeorm/metadata-args/IndexMetadataArgs";
|
|
51
|
+
import { JoinColumnMetadataArgs } from "typeorm/metadata-args/JoinColumnMetadataArgs";
|
|
52
|
+
import { JoinTableMetadataArgs } from "typeorm/metadata-args/JoinTableMetadataArgs";
|
|
53
|
+
import { RelationMetadataArgs } from "typeorm/metadata-args/RelationMetadataArgs";
|
|
54
|
+
import fs from "fs";
|
|
55
|
+
import path from "path";
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The Databases product is persisted as seven TypeORM entities: the
|
|
59
|
+
* DatabaseServer root, its DatabaseServerEndpoint children (the one-owner
|
|
60
|
+
* endpoint table the telemetry key set is derived from), and the feed / owner
|
|
61
|
+
* / rule models cloned from the Ceph product. Plus a `databaseServers`
|
|
62
|
+
* many-to-many on Incident, Alert and ScheduledMaintenance.
|
|
63
|
+
*
|
|
64
|
+
* What is pinned here only fails in production:
|
|
65
|
+
*
|
|
66
|
+
* - registration in Models/Index.ts (no table, no API otherwise)
|
|
67
|
+
* - the table / route / display names other layers key on
|
|
68
|
+
* - the identity indexes the discovery paths rely on for race safety,
|
|
69
|
+
* including the two NAMED partial unique indexes a generated migration
|
|
70
|
+
* would otherwise drop
|
|
71
|
+
* - the column ACL contract: DatabaseService.create runs onBeforeCreate
|
|
72
|
+
* BEFORE the column permission check, so every column the service sets on
|
|
73
|
+
* a manual create must be user-creatable, and every column only discovery
|
|
74
|
+
* writes must be closed to users - checked here against the real
|
|
75
|
+
* ColumnPermission / CreatePermission code, not just the declarations
|
|
76
|
+
* - the endpoint table reusing the parent's permissions
|
|
77
|
+
* - the feed enum contract, the rule criteria fields and the permissions
|
|
78
|
+
* - Postgres's 63-character identifier limit
|
|
79
|
+
*
|
|
80
|
+
* Pure metadata and in-process permission checks - no Postgres connection.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
type ModelType = { new (): BaseModel };
|
|
84
|
+
|
|
85
|
+
const MODEL_TYPES: Array<ModelType> = AllModelTypes as Array<ModelType>;
|
|
86
|
+
|
|
87
|
+
interface DatabaseModelSpec {
|
|
88
|
+
name: string;
|
|
89
|
+
modelType: ModelType;
|
|
90
|
+
tableName: string;
|
|
91
|
+
crudApiPath: string;
|
|
92
|
+
singularName: string;
|
|
93
|
+
pluralName: string;
|
|
94
|
+
icon: IconProp;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DATABASE_MODELS: Array<DatabaseModelSpec> = [
|
|
98
|
+
{
|
|
99
|
+
name: "DatabaseServer",
|
|
100
|
+
modelType: DatabaseServer,
|
|
101
|
+
tableName: "DatabaseServer",
|
|
102
|
+
crudApiPath: "/database-server",
|
|
103
|
+
singularName: "Database",
|
|
104
|
+
pluralName: "Databases",
|
|
105
|
+
icon: IconProp.Database,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "DatabaseServerEndpoint",
|
|
109
|
+
modelType: DatabaseServerEndpoint,
|
|
110
|
+
tableName: "DatabaseServerEndpoint",
|
|
111
|
+
crudApiPath: "/database-server-endpoint",
|
|
112
|
+
singularName: "Database Endpoint",
|
|
113
|
+
pluralName: "Database Endpoints",
|
|
114
|
+
icon: IconProp.Link,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "DatabaseServerFeed",
|
|
118
|
+
modelType: DatabaseServerFeed,
|
|
119
|
+
tableName: "DatabaseServerFeed",
|
|
120
|
+
crudApiPath: "/database-server-feed",
|
|
121
|
+
singularName: "Database Feed",
|
|
122
|
+
pluralName: "Database Feeds",
|
|
123
|
+
icon: IconProp.List,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "DatabaseServerOwnerTeam",
|
|
127
|
+
modelType: DatabaseServerOwnerTeam,
|
|
128
|
+
tableName: "DatabaseServerOwnerTeam",
|
|
129
|
+
crudApiPath: "/database-server-owner-team",
|
|
130
|
+
singularName: "Database Team Owner",
|
|
131
|
+
pluralName: "Database Team Owners",
|
|
132
|
+
icon: IconProp.Cube,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "DatabaseServerOwnerUser",
|
|
136
|
+
modelType: DatabaseServerOwnerUser,
|
|
137
|
+
tableName: "DatabaseServerOwnerUser",
|
|
138
|
+
crudApiPath: "/database-server-owner-user",
|
|
139
|
+
singularName: "Database User Owner",
|
|
140
|
+
pluralName: "Database User Owners",
|
|
141
|
+
icon: IconProp.Cube,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "DatabaseServerLabelRule",
|
|
145
|
+
modelType: DatabaseServerLabelRule,
|
|
146
|
+
tableName: "DatabaseServerLabelRule",
|
|
147
|
+
crudApiPath: "/database-server-label-rule",
|
|
148
|
+
singularName: "Database Label Rule",
|
|
149
|
+
pluralName: "Database Label Rules",
|
|
150
|
+
icon: IconProp.Tag,
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "DatabaseServerOwnerRule",
|
|
154
|
+
modelType: DatabaseServerOwnerRule,
|
|
155
|
+
tableName: "DatabaseServerOwnerRule",
|
|
156
|
+
crudApiPath: "/database-server-owner-rule",
|
|
157
|
+
singularName: "Database Owner Rule",
|
|
158
|
+
pluralName: "Database Owner Rules",
|
|
159
|
+
// Every infrastructure owner rule (Ceph, VMware, Cloud) uses User.
|
|
160
|
+
icon: IconProp.User,
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
// Every child of DatabaseServer and the relation property it reaches it by.
|
|
165
|
+
const DATABASE_SERVER_CHILDREN: Array<[ModelType, string]> = [
|
|
166
|
+
[DatabaseServerEndpoint, "databaseServer"],
|
|
167
|
+
[DatabaseServerFeed, "databaseServer"],
|
|
168
|
+
[DatabaseServerOwnerTeam, "databaseServer"],
|
|
169
|
+
[DatabaseServerOwnerUser, "databaseServer"],
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
// The affected-resource relation on each event model: join table, own FK.
|
|
173
|
+
const AFFECTED: Array<[ModelType, string, string]> = [
|
|
174
|
+
[Alert, "AlertDatabaseServer", "alertId"],
|
|
175
|
+
[Incident, "IncidentDatabaseServer", "incidentId"],
|
|
176
|
+
[
|
|
177
|
+
ScheduledMaintenance,
|
|
178
|
+
"ScheduledMaintenanceDatabaseServer",
|
|
179
|
+
"scheduledMaintenanceId",
|
|
180
|
+
],
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
const DATABASE_SERVER_PERMISSIONS: Array<string> = [
|
|
184
|
+
"CreateDatabaseServer",
|
|
185
|
+
"DeleteDatabaseServer",
|
|
186
|
+
"EditDatabaseServer",
|
|
187
|
+
"ReadDatabaseServer",
|
|
188
|
+
"CreateDatabaseServerOwnerTeam",
|
|
189
|
+
"DeleteDatabaseServerOwnerTeam",
|
|
190
|
+
"EditDatabaseServerOwnerTeam",
|
|
191
|
+
"ReadDatabaseServerOwnerTeam",
|
|
192
|
+
"CreateDatabaseServerOwnerUser",
|
|
193
|
+
"DeleteDatabaseServerOwnerUser",
|
|
194
|
+
"EditDatabaseServerOwnerUser",
|
|
195
|
+
"ReadDatabaseServerOwnerUser",
|
|
196
|
+
"CreateDatabaseServerLabelRule",
|
|
197
|
+
"DeleteDatabaseServerLabelRule",
|
|
198
|
+
"EditDatabaseServerLabelRule",
|
|
199
|
+
"ReadDatabaseServerLabelRule",
|
|
200
|
+
"CreateDatabaseServerOwnerRule",
|
|
201
|
+
"DeleteDatabaseServerOwnerRule",
|
|
202
|
+
"EditDatabaseServerOwnerRule",
|
|
203
|
+
"ReadDatabaseServerOwnerRule",
|
|
204
|
+
"CreateDatabaseServerFeed",
|
|
205
|
+
"EditDatabaseServerFeed",
|
|
206
|
+
"ReadDatabaseServerFeed",
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
// The rule criteria fields the registry, the engines and the forms share.
|
|
210
|
+
const RULE_CRITERIA_FIELDS: Array<string> = [
|
|
211
|
+
"databaseServerLabels",
|
|
212
|
+
"databaseServerNamePattern",
|
|
213
|
+
"databaseServerDescriptionPattern",
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
/*
|
|
217
|
+
* Every column DatabaseServerService.onBeforeCreate may set on a manual
|
|
218
|
+
* (non-root) create, plus what DatabaseService itself stamps
|
|
219
|
+
* (projectId, createdByUserId) and what the create form may send.
|
|
220
|
+
*/
|
|
221
|
+
const DATABASE_SERVER_USER_CREATE_COLUMNS: Array<string> = [
|
|
222
|
+
"project",
|
|
223
|
+
"projectId",
|
|
224
|
+
"name",
|
|
225
|
+
"description",
|
|
226
|
+
"databaseIdentifier",
|
|
227
|
+
"dbSystem",
|
|
228
|
+
"serverAddress",
|
|
229
|
+
"serverPort",
|
|
230
|
+
"discoverySource",
|
|
231
|
+
"labels",
|
|
232
|
+
"retainTelemetryDataForDays",
|
|
233
|
+
"telemetryRetentionConfig",
|
|
234
|
+
"isArchived",
|
|
235
|
+
"createdByUser",
|
|
236
|
+
"createdByUserId",
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// Columns a person may change after creation.
|
|
240
|
+
const DATABASE_SERVER_USER_UPDATE_COLUMNS: Array<string> = [
|
|
241
|
+
"name",
|
|
242
|
+
"description",
|
|
243
|
+
"labels",
|
|
244
|
+
"retainTelemetryDataForDays",
|
|
245
|
+
"telemetryRetentionConfig",
|
|
246
|
+
"isArchived",
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
// Columns only the discovery / heartbeat / archive paths write, as root.
|
|
250
|
+
const DATABASE_SERVER_ROOT_ONLY_COLUMNS: Array<string> = [
|
|
251
|
+
"slug",
|
|
252
|
+
"workloadIdentifier",
|
|
253
|
+
"dbVersion",
|
|
254
|
+
"kubernetesCluster",
|
|
255
|
+
"kubernetesClusterId",
|
|
256
|
+
"kubernetesNamespace",
|
|
257
|
+
"workloadKind",
|
|
258
|
+
"workloadName",
|
|
259
|
+
"dockerHost",
|
|
260
|
+
"dockerHostId",
|
|
261
|
+
"podmanHost",
|
|
262
|
+
"podmanHostId",
|
|
263
|
+
"memberEntityKeys",
|
|
264
|
+
"instanceCount",
|
|
265
|
+
"otelCollectorStatus",
|
|
266
|
+
"collectorLastSeenAt",
|
|
267
|
+
"agentVersion",
|
|
268
|
+
"lastSeenAt",
|
|
269
|
+
"autoArchivedAt",
|
|
270
|
+
"manuallyRestoredAt",
|
|
271
|
+
"dbSystemSource",
|
|
272
|
+
"workloadLastSeenAt",
|
|
273
|
+
"automaticAssignments",
|
|
274
|
+
"archivedAt",
|
|
275
|
+
"archivedByUser",
|
|
276
|
+
"archivedByUserId",
|
|
277
|
+
"deletedByUser",
|
|
278
|
+
"deletedByUserId",
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const PERMISSION_PROPS: Array<PermissionProps> =
|
|
282
|
+
PermissionHelper.getAllPermissionProps();
|
|
283
|
+
|
|
284
|
+
const PERMISSION_PROPS_BY_NAME: Map<string, PermissionProps> = new Map(
|
|
285
|
+
PERMISSION_PROPS.map((props: PermissionProps) => {
|
|
286
|
+
return [props.permission.toString(), props];
|
|
287
|
+
}),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
291
|
+
"9e1b6b0e-0000-4000-8000-00000000d8a1",
|
|
292
|
+
);
|
|
293
|
+
const USER_ID: ObjectID = new ObjectID("9e1b6b0e-0000-4000-8000-00000000d8a2");
|
|
294
|
+
|
|
295
|
+
/*
|
|
296
|
+
* DatabaseCommonInteractionPropsUtil.getUserPermissions only reads the tenant
|
|
297
|
+
* bucket when props.tenantId is set, and drops every permission whose
|
|
298
|
+
* isBlockPermission does not match. Getting either wrong yields an empty
|
|
299
|
+
* permission set, which would make every "does not throw" below pass for the
|
|
300
|
+
* wrong reason - hence the harness guard test further down.
|
|
301
|
+
*/
|
|
302
|
+
function makeProps(
|
|
303
|
+
permissions: Array<Permission>,
|
|
304
|
+
): DatabaseCommonInteractionProps {
|
|
305
|
+
const tenantPermission: UserTenantAccessPermission = {
|
|
306
|
+
projectId: PROJECT_ID,
|
|
307
|
+
_type: "UserTenantAccessPermission",
|
|
308
|
+
permissions: permissions.map((permission: Permission) => {
|
|
309
|
+
return {
|
|
310
|
+
_type: "UserPermission",
|
|
311
|
+
permission: permission,
|
|
312
|
+
labelIds: [],
|
|
313
|
+
isBlockPermission: false,
|
|
314
|
+
};
|
|
315
|
+
}),
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
userId: USER_ID,
|
|
320
|
+
tenantId: PROJECT_ID,
|
|
321
|
+
userTenantAccessPermission: {
|
|
322
|
+
[PROJECT_ID.toString()]: tenantPermission,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// What a manual create posts after DatabaseServerService.onBeforeCreate ran.
|
|
328
|
+
function manualCreateData(): DatabaseServer {
|
|
329
|
+
const data: DatabaseServer = new DatabaseServer();
|
|
330
|
+
data.projectId = PROJECT_ID;
|
|
331
|
+
data.name = "PostgreSQL orders-db.internal:5432";
|
|
332
|
+
data.description = "Orders database";
|
|
333
|
+
data.dbSystem = "postgresql";
|
|
334
|
+
data.serverAddress = "orders-db.internal";
|
|
335
|
+
data.serverPort = 5432;
|
|
336
|
+
data.databaseIdentifier = "postgresql|orders-db.internal:5432";
|
|
337
|
+
data.discoverySource = "manual";
|
|
338
|
+
data.createdByUserId = USER_ID;
|
|
339
|
+
return data;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
type CheckFunction = () => void;
|
|
343
|
+
|
|
344
|
+
function checkCreate(
|
|
345
|
+
modelType: ModelType,
|
|
346
|
+
data: BaseModel,
|
|
347
|
+
permissions: Array<Permission>,
|
|
348
|
+
): CheckFunction {
|
|
349
|
+
return () => {
|
|
350
|
+
ModelPermission.checkCreatePermissions(
|
|
351
|
+
modelType,
|
|
352
|
+
data,
|
|
353
|
+
makeProps(permissions),
|
|
354
|
+
);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function checkColumns(
|
|
359
|
+
modelType: ModelType,
|
|
360
|
+
data: BaseModel,
|
|
361
|
+
permissions: Array<Permission>,
|
|
362
|
+
requestType: DatabaseRequestType,
|
|
363
|
+
): CheckFunction {
|
|
364
|
+
return () => {
|
|
365
|
+
ColumnPermissions.checkDataColumnPermissions(
|
|
366
|
+
modelType,
|
|
367
|
+
data,
|
|
368
|
+
makeProps(permissions),
|
|
369
|
+
requestType,
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function allReferencedPermissions(model: BaseModel): Array<Permission> {
|
|
375
|
+
const permissions: Array<Permission> = [
|
|
376
|
+
...(model.createRecordPermissions || []),
|
|
377
|
+
...(model.readRecordPermissions || []),
|
|
378
|
+
...(model.updateRecordPermissions || []),
|
|
379
|
+
...(model.deleteRecordPermissions || []),
|
|
380
|
+
];
|
|
381
|
+
|
|
382
|
+
for (const column of model.getTableColumns().columns) {
|
|
383
|
+
const accessControl: ColumnAccessControl | undefined =
|
|
384
|
+
model.getColumnAccessControlFor(column) || undefined;
|
|
385
|
+
|
|
386
|
+
if (!accessControl) {
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
permissions.push(
|
|
391
|
+
...(accessControl.create || []),
|
|
392
|
+
...(accessControl.read || []),
|
|
393
|
+
...(accessControl.update || []),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return permissions;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/*
|
|
401
|
+
* DatabaseBaseModel's own columns (_id, timestamps, version). They carry the
|
|
402
|
+
* table ACL by default (or none, for version) and are never written through
|
|
403
|
+
* the column check, so the per-column sweeps below skip them.
|
|
404
|
+
*/
|
|
405
|
+
const BASE_COLUMNS: Array<string> = [
|
|
406
|
+
"_id",
|
|
407
|
+
"createdAt",
|
|
408
|
+
"updatedAt",
|
|
409
|
+
"deletedAt",
|
|
410
|
+
"version",
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
function ownColumns(model: BaseModel): Array<string> {
|
|
414
|
+
return model.getTableColumns().columns.filter((column: string): boolean => {
|
|
415
|
+
return !BASE_COLUMNS.includes(column);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function columnAccess(model: BaseModel, column: string): ColumnAccessControl {
|
|
420
|
+
const accessControl: ColumnAccessControl | null =
|
|
421
|
+
model.getColumnAccessControlFor(column);
|
|
422
|
+
|
|
423
|
+
if (!accessControl) {
|
|
424
|
+
throw new Error(`${column} has no @ColumnAccessControl`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return accessControl;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function columnArgs(
|
|
431
|
+
modelType: ModelType,
|
|
432
|
+
property: string,
|
|
433
|
+
): ColumnMetadataArgs {
|
|
434
|
+
const column: ColumnMetadataArgs | undefined = (
|
|
435
|
+
getMetadataArgsStorage().columns as Array<ColumnMetadataArgs>
|
|
436
|
+
).find((candidate: ColumnMetadataArgs) => {
|
|
437
|
+
return (
|
|
438
|
+
candidate.target === modelType && candidate.propertyName === property
|
|
439
|
+
);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
if (!column) {
|
|
443
|
+
throw new Error(`${modelType.name}.${property} is not a registered column`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return column;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function relationArgs(
|
|
450
|
+
modelType: ModelType,
|
|
451
|
+
property: string,
|
|
452
|
+
): RelationMetadataArgs {
|
|
453
|
+
const relation: RelationMetadataArgs | undefined = (
|
|
454
|
+
getMetadataArgsStorage().relations as Array<RelationMetadataArgs>
|
|
455
|
+
).find((candidate: RelationMetadataArgs) => {
|
|
456
|
+
return (
|
|
457
|
+
candidate.target === modelType && candidate.propertyName === property
|
|
458
|
+
);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (!relation) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
`${modelType.name}.${property} is not a registered relation`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
return relation;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function joinColumnArgs(
|
|
471
|
+
modelType: ModelType,
|
|
472
|
+
property: string,
|
|
473
|
+
): JoinColumnMetadataArgs {
|
|
474
|
+
const joinColumn: JoinColumnMetadataArgs | undefined = (
|
|
475
|
+
getMetadataArgsStorage().joinColumns as Array<JoinColumnMetadataArgs>
|
|
476
|
+
).find((candidate: JoinColumnMetadataArgs) => {
|
|
477
|
+
return (
|
|
478
|
+
candidate.target === modelType && candidate.propertyName === property
|
|
479
|
+
);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (!joinColumn) {
|
|
483
|
+
throw new Error(`${modelType.name}.${property} has no @JoinColumn`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return joinColumn;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function joinTableArgs(
|
|
490
|
+
modelType: ModelType,
|
|
491
|
+
property: string,
|
|
492
|
+
): JoinTableMetadataArgs {
|
|
493
|
+
const joinTable: JoinTableMetadataArgs | undefined = (
|
|
494
|
+
getMetadataArgsStorage().joinTables as Array<JoinTableMetadataArgs>
|
|
495
|
+
).find((candidate: JoinTableMetadataArgs) => {
|
|
496
|
+
return (
|
|
497
|
+
candidate.target === modelType && candidate.propertyName === property
|
|
498
|
+
);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
if (!joinTable) {
|
|
502
|
+
throw new Error(`${modelType.name}.${property} has no @JoinTable`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return joinTable;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function classIndexes(modelType: ModelType): Array<IndexMetadataArgs> {
|
|
509
|
+
return getMetadataArgsStorage().indices.filter(
|
|
510
|
+
(index: IndexMetadataArgs): boolean => {
|
|
511
|
+
return index.target === modelType;
|
|
512
|
+
},
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function indexOnColumns(
|
|
517
|
+
modelType: ModelType,
|
|
518
|
+
columns: Array<string>,
|
|
519
|
+
): IndexMetadataArgs | undefined {
|
|
520
|
+
return classIndexes(modelType).find((index: IndexMetadataArgs): boolean => {
|
|
521
|
+
return (
|
|
522
|
+
Array.isArray(index.columns) &&
|
|
523
|
+
index.columns.length === columns.length &&
|
|
524
|
+
columns.every((column: string): boolean => {
|
|
525
|
+
return (index.columns as Array<string>).includes(column);
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function propertyIndex(
|
|
532
|
+
modelType: ModelType,
|
|
533
|
+
property: string,
|
|
534
|
+
): IndexMetadataArgs | undefined {
|
|
535
|
+
// A property-level @Index() is recorded with [propertyName] as its columns.
|
|
536
|
+
return classIndexes(modelType).find((index: IndexMetadataArgs): boolean => {
|
|
537
|
+
return (
|
|
538
|
+
Array.isArray(index.columns) &&
|
|
539
|
+
index.columns.length === 1 &&
|
|
540
|
+
index.columns[0] === property
|
|
541
|
+
);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function modelSource(modelName: string): string {
|
|
546
|
+
return fs.readFileSync(
|
|
547
|
+
path.join(
|
|
548
|
+
__dirname,
|
|
549
|
+
"..",
|
|
550
|
+
"..",
|
|
551
|
+
"..",
|
|
552
|
+
"Models",
|
|
553
|
+
"DatabaseModels",
|
|
554
|
+
`${modelName}.ts`,
|
|
555
|
+
),
|
|
556
|
+
"utf8",
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/*
|
|
561
|
+
* The source with its comments removed. Comments may name the template a
|
|
562
|
+
* model was cloned from ("like CloudResourceInstance"); code may not. A line
|
|
563
|
+
* comment must start a line or follow whitespace, so the "//" of a URL inside
|
|
564
|
+
* a string is left alone.
|
|
565
|
+
*/
|
|
566
|
+
function codeOf(source: string): string {
|
|
567
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "");
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function sorted(values: Array<Permission | string>): Array<string> {
|
|
571
|
+
return values
|
|
572
|
+
.map((value: Permission | string): string => {
|
|
573
|
+
return value.toString();
|
|
574
|
+
})
|
|
575
|
+
.sort();
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
describe("Databases (DatabaseServer) models", () => {
|
|
579
|
+
test("the inventory is complete", () => {
|
|
580
|
+
expect(DATABASE_MODELS.length).toBe(7);
|
|
581
|
+
expect(DATABASE_SERVER_PERMISSIONS.length).toBe(23);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
describe("harness guard", () => {
|
|
585
|
+
/*
|
|
586
|
+
* Runs first on purpose. Most permission assertions below are "does not
|
|
587
|
+
* throw", which a props object the permission util silently ignored
|
|
588
|
+
* would also satisfy. A caller holding only a READ grant must be refused
|
|
589
|
+
* a create; if it is not, makeProps is broken, not the model.
|
|
590
|
+
*/
|
|
591
|
+
test("a read-only caller is refused a manual create", () => {
|
|
592
|
+
expect(
|
|
593
|
+
checkCreate(DatabaseServer, manualCreateData(), [
|
|
594
|
+
Permission.ReadDatabaseServer,
|
|
595
|
+
]),
|
|
596
|
+
).toThrow();
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("a caller with no permissions at all is refused a column write", () => {
|
|
600
|
+
expect(
|
|
601
|
+
checkColumns(
|
|
602
|
+
DatabaseServer,
|
|
603
|
+
manualCreateData(),
|
|
604
|
+
[],
|
|
605
|
+
DatabaseRequestType.Create,
|
|
606
|
+
),
|
|
607
|
+
).toThrow(BadDataException);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
describe("registration and identity", () => {
|
|
612
|
+
test.each(DATABASE_MODELS)(
|
|
613
|
+
"$name is registered in Models/Index.ts",
|
|
614
|
+
(spec: DatabaseModelSpec) => {
|
|
615
|
+
/*
|
|
616
|
+
* Boot-time createTables() and the migration generator both iterate
|
|
617
|
+
* that array. A model missing from it type-checks, imports fine, and
|
|
618
|
+
* simply has no table in Postgres.
|
|
619
|
+
*/
|
|
620
|
+
expect(MODEL_TYPES).toContain(spec.modelType);
|
|
621
|
+
expect(
|
|
622
|
+
MODEL_TYPES.filter((modelType: ModelType): boolean => {
|
|
623
|
+
return modelType === spec.modelType;
|
|
624
|
+
}),
|
|
625
|
+
).toHaveLength(1);
|
|
626
|
+
},
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
test.each(DATABASE_MODELS)(
|
|
630
|
+
"$name resolves by its table name through getModelTypeByName",
|
|
631
|
+
(spec: DatabaseModelSpec) => {
|
|
632
|
+
// The generic API / workflow layers look models up by table name.
|
|
633
|
+
expect(getModelTypeByName(spec.tableName)).toBe(spec.modelType);
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
test.each(DATABASE_MODELS)(
|
|
638
|
+
"$name carries its own table, route, display names and icon",
|
|
639
|
+
(spec: DatabaseModelSpec) => {
|
|
640
|
+
const model: BaseModel = new spec.modelType();
|
|
641
|
+
|
|
642
|
+
expect(spec.modelType.name).toBe(spec.name);
|
|
643
|
+
expect(model.tableName).toBe(spec.tableName);
|
|
644
|
+
expect(model.getCrudApiPath()?.toString()).toBe(spec.crudApiPath);
|
|
645
|
+
expect(model.singularName).toBe(spec.singularName);
|
|
646
|
+
expect(model.pluralName).toBe(spec.pluralName);
|
|
647
|
+
expect(model.icon).toBe(spec.icon);
|
|
648
|
+
expect(model.getTenantColumn()).toBe("projectId");
|
|
649
|
+
expect(model.enableDocumentation).toBe(true);
|
|
650
|
+
expect((model.tableDescription || "").length).toBeGreaterThan(20);
|
|
651
|
+
},
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
test("no Databases route or table name collides with another model", () => {
|
|
655
|
+
const routes: Map<string, number> = new Map<string, number>();
|
|
656
|
+
const tables: Map<string, number> = new Map<string, number>();
|
|
657
|
+
|
|
658
|
+
for (const modelType of MODEL_TYPES) {
|
|
659
|
+
const model: BaseModel = new modelType();
|
|
660
|
+
const route: string | undefined = model.getCrudApiPath()?.toString();
|
|
661
|
+
|
|
662
|
+
if (route) {
|
|
663
|
+
routes.set(route, (routes.get(route) || 0) + 1);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (model.tableName) {
|
|
667
|
+
tables.set(model.tableName, (tables.get(model.tableName) || 0) + 1);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const spec of DATABASE_MODELS) {
|
|
672
|
+
expect(routes.get(spec.crudApiPath)).toBe(1);
|
|
673
|
+
expect(tables.get(spec.tableName)).toBe(1);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("does not take the names the base service class and the Database monitor already use", () => {
|
|
678
|
+
/*
|
|
679
|
+
* `DatabaseService` is the base class of every service, so a model
|
|
680
|
+
* called `Database` would collide with it at the service layer. No
|
|
681
|
+
* model may claim the bare name either.
|
|
682
|
+
*/
|
|
683
|
+
expect(getModelTypeByName("Database")).toBeNull();
|
|
684
|
+
|
|
685
|
+
for (const spec of DATABASE_MODELS) {
|
|
686
|
+
expect(spec.tableName).not.toBe("Database");
|
|
687
|
+
expect(spec.crudApiPath.startsWith("/database-server")).toBe(true);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test.each(DATABASE_MODELS)(
|
|
692
|
+
"$name source carries no Ceph or CloudResource leftovers",
|
|
693
|
+
(spec: DatabaseModelSpec) => {
|
|
694
|
+
/*
|
|
695
|
+
* The feed / owner / rule models were cloned from the Ceph product
|
|
696
|
+
* and the root from CloudResource. A leftover `/ceph-cluster` route
|
|
697
|
+
* or `cephClusterId` column reads fine in review and silently binds
|
|
698
|
+
* the wrong table. Comments are exempt: pointing at the template a
|
|
699
|
+
* decision was copied from is documentation, not a binding.
|
|
700
|
+
*/
|
|
701
|
+
const source: string = codeOf(modelSource(spec.name));
|
|
702
|
+
|
|
703
|
+
// Guards the stripping itself: the class must still be in there.
|
|
704
|
+
expect(source).toContain(`export default class ${spec.name}`);
|
|
705
|
+
|
|
706
|
+
for (const token of [
|
|
707
|
+
"Ceph",
|
|
708
|
+
"ceph",
|
|
709
|
+
"CloudResource",
|
|
710
|
+
"cloudResource",
|
|
711
|
+
"cloud-resource",
|
|
712
|
+
"fsid",
|
|
713
|
+
"osdCount",
|
|
714
|
+
]) {
|
|
715
|
+
expect({ token, found: source.includes(token) }).toEqual({
|
|
716
|
+
token,
|
|
717
|
+
found: false,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
);
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
describe("DatabaseServer", () => {
|
|
725
|
+
const model: DatabaseServer = new DatabaseServer();
|
|
726
|
+
|
|
727
|
+
test("is unique per project on databaseIdentifier, the discovery join key", () => {
|
|
728
|
+
/*
|
|
729
|
+
* Every discovery path find-or-creates by databaseIdentifier. Several
|
|
730
|
+
* ingest pods and worker runs race on the same database; only a
|
|
731
|
+
* DB-level unique index collapses them into one row.
|
|
732
|
+
*/
|
|
733
|
+
const found: IndexMetadataArgs | undefined = indexOnColumns(
|
|
734
|
+
DatabaseServer,
|
|
735
|
+
["projectId", "databaseIdentifier"],
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
expect(found).toBeDefined();
|
|
739
|
+
expect(found?.unique).toBe(true);
|
|
740
|
+
// Not partial: a soft-deleted row must not let a duplicate identity in.
|
|
741
|
+
expect(found?.where).toBeUndefined();
|
|
742
|
+
expect(found?.name).toBeUndefined();
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
test("the display name is NOT unique - two engines may share a host", () => {
|
|
746
|
+
const nameIndexes: Array<IndexMetadataArgs> = classIndexes(
|
|
747
|
+
DatabaseServer,
|
|
748
|
+
).filter((index: IndexMetadataArgs): boolean => {
|
|
749
|
+
return (
|
|
750
|
+
Array.isArray(index.columns) &&
|
|
751
|
+
(index.columns as Array<string>).includes("name")
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
for (const index of nameIndexes) {
|
|
756
|
+
expect(index.unique).not.toBe(true);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Nor the app-level @UniqueColumnBy check CephCluster.name carries.
|
|
760
|
+
expect(Object.keys(getUniqueColumnsBy(new DatabaseServer()))).toEqual([]);
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
test("declares IDX_database_server_slug by name, UNIQUE and partial", () => {
|
|
764
|
+
/*
|
|
765
|
+
* TypeORM's schema builder matches database indexes to entity
|
|
766
|
+
* metadata by name and drops every one it cannot find, so the partial
|
|
767
|
+
* slug index must be declared natively and named.
|
|
768
|
+
*/
|
|
769
|
+
const found: IndexMetadataArgs | undefined = classIndexes(
|
|
770
|
+
DatabaseServer,
|
|
771
|
+
).find((index: IndexMetadataArgs): boolean => {
|
|
772
|
+
return index.name === "IDX_database_server_slug";
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
expect(found).toBeDefined();
|
|
776
|
+
expect(found?.unique).toBe(true);
|
|
777
|
+
expect(found?.columns).toEqual(["slug"]);
|
|
778
|
+
expect(found?.where).toBe('"deletedAt" IS NULL');
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("declares IDX_database_server_workload by name: one row per workload, NULLs never collide", () => {
|
|
782
|
+
const found: IndexMetadataArgs | undefined = classIndexes(
|
|
783
|
+
DatabaseServer,
|
|
784
|
+
).find((index: IndexMetadataArgs): boolean => {
|
|
785
|
+
return index.name === "IDX_database_server_workload";
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
expect(found).toBeDefined();
|
|
789
|
+
expect(found?.unique).toBe(true);
|
|
790
|
+
expect(found?.columns).toEqual(["projectId", "workloadIdentifier"]);
|
|
791
|
+
/*
|
|
792
|
+
* The IS NOT NULL half is what lets the many endpoint-only rows (no
|
|
793
|
+
* workload) coexist; the deletedAt half lets a deleted workload row be
|
|
794
|
+
* rediscovered.
|
|
795
|
+
*/
|
|
796
|
+
expect(found?.where).toBe(
|
|
797
|
+
'"workloadIdentifier" IS NOT NULL AND "deletedAt" IS NULL',
|
|
798
|
+
);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
test("indexes the list-page filters: (projectId, isArchived) and (projectId, dbSystem)", () => {
|
|
802
|
+
const archived: IndexMetadataArgs | undefined = indexOnColumns(
|
|
803
|
+
DatabaseServer,
|
|
804
|
+
["projectId", "isArchived"],
|
|
805
|
+
);
|
|
806
|
+
const engine: IndexMetadataArgs | undefined = indexOnColumns(
|
|
807
|
+
DatabaseServer,
|
|
808
|
+
["projectId", "dbSystem"],
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
expect(archived).toBeDefined();
|
|
812
|
+
expect(archived?.unique).not.toBe(true);
|
|
813
|
+
expect(engine).toBeDefined();
|
|
814
|
+
expect(engine?.unique).not.toBe(true);
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("indexes projectId, databaseIdentifier and kubernetesClusterId on their own", () => {
|
|
818
|
+
for (const column of [
|
|
819
|
+
"projectId",
|
|
820
|
+
"databaseIdentifier",
|
|
821
|
+
"kubernetesClusterId",
|
|
822
|
+
]) {
|
|
823
|
+
expect({
|
|
824
|
+
column,
|
|
825
|
+
indexed: Boolean(propertyIndex(DatabaseServer, column)),
|
|
826
|
+
}).toEqual({ column, indexed: true });
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("carries every column of the SPEC schema", () => {
|
|
831
|
+
const columns: Array<string> = model.getTableColumns().columns;
|
|
832
|
+
|
|
833
|
+
for (const column of [
|
|
834
|
+
...DATABASE_SERVER_USER_CREATE_COLUMNS,
|
|
835
|
+
...DATABASE_SERVER_ROOT_ONLY_COLUMNS,
|
|
836
|
+
]) {
|
|
837
|
+
expect({ column, present: columns.includes(column) }).toEqual({
|
|
838
|
+
column,
|
|
839
|
+
present: true,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Ceph snapshot counters and liveness shortcuts that do not apply.
|
|
844
|
+
for (const column of [
|
|
845
|
+
"fsid",
|
|
846
|
+
"cephVersion",
|
|
847
|
+
"monCount",
|
|
848
|
+
"osdCount",
|
|
849
|
+
"healthStatus",
|
|
850
|
+
"endpoints",
|
|
851
|
+
"isConnected",
|
|
852
|
+
"primaryEndpoint",
|
|
853
|
+
]) {
|
|
854
|
+
expect(columns).not.toContain(column);
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("every declared column is accounted for as user-creatable or root-only", () => {
|
|
859
|
+
/*
|
|
860
|
+
* A new column must be placed in one of the two lists above on
|
|
861
|
+
* purpose - which is what decides whether a manual create can set it.
|
|
862
|
+
*/
|
|
863
|
+
const accounted: Set<string> = new Set([
|
|
864
|
+
...DATABASE_SERVER_USER_CREATE_COLUMNS,
|
|
865
|
+
...DATABASE_SERVER_ROOT_ONLY_COLUMNS,
|
|
866
|
+
]);
|
|
867
|
+
const unaccounted: Array<string> = ownColumns(model).filter(
|
|
868
|
+
(column: string): boolean => {
|
|
869
|
+
return !accounted.has(column);
|
|
870
|
+
},
|
|
871
|
+
);
|
|
872
|
+
|
|
873
|
+
expect(unaccounted).toEqual([]);
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("column types and widths follow the SPEC", () => {
|
|
877
|
+
const expectations: Array<
|
|
878
|
+
[string, TableColumnType, boolean, number | undefined]
|
|
879
|
+
> = [
|
|
880
|
+
// column, type, required, varchar length
|
|
881
|
+
["name", TableColumnType.ShortText, true, ColumnLength.ShortText],
|
|
882
|
+
[
|
|
883
|
+
"databaseIdentifier",
|
|
884
|
+
TableColumnType.LongText,
|
|
885
|
+
true,
|
|
886
|
+
ColumnLength.LongText,
|
|
887
|
+
],
|
|
888
|
+
[
|
|
889
|
+
"workloadIdentifier",
|
|
890
|
+
TableColumnType.LongText,
|
|
891
|
+
false,
|
|
892
|
+
ColumnLength.LongText,
|
|
893
|
+
],
|
|
894
|
+
["dbSystem", TableColumnType.ShortText, true, ColumnLength.ShortText],
|
|
895
|
+
[
|
|
896
|
+
"serverAddress",
|
|
897
|
+
TableColumnType.LongText,
|
|
898
|
+
false,
|
|
899
|
+
ColumnLength.LongText,
|
|
900
|
+
],
|
|
901
|
+
["serverPort", TableColumnType.Number, false, undefined],
|
|
902
|
+
["dbVersion", TableColumnType.ShortText, false, ColumnLength.ShortText],
|
|
903
|
+
[
|
|
904
|
+
"discoverySource",
|
|
905
|
+
TableColumnType.ShortText,
|
|
906
|
+
false,
|
|
907
|
+
ColumnLength.ShortText,
|
|
908
|
+
],
|
|
909
|
+
[
|
|
910
|
+
"kubernetesNamespace",
|
|
911
|
+
TableColumnType.ShortText,
|
|
912
|
+
false,
|
|
913
|
+
ColumnLength.ShortText,
|
|
914
|
+
],
|
|
915
|
+
[
|
|
916
|
+
"workloadKind",
|
|
917
|
+
TableColumnType.ShortText,
|
|
918
|
+
false,
|
|
919
|
+
ColumnLength.ShortText,
|
|
920
|
+
],
|
|
921
|
+
[
|
|
922
|
+
"workloadName",
|
|
923
|
+
TableColumnType.LongText,
|
|
924
|
+
false,
|
|
925
|
+
ColumnLength.LongText,
|
|
926
|
+
],
|
|
927
|
+
["memberEntityKeys", TableColumnType.JSON, false, undefined],
|
|
928
|
+
["instanceCount", TableColumnType.Number, false, undefined],
|
|
929
|
+
[
|
|
930
|
+
"otelCollectorStatus",
|
|
931
|
+
TableColumnType.ShortText,
|
|
932
|
+
false,
|
|
933
|
+
ColumnLength.ShortText,
|
|
934
|
+
],
|
|
935
|
+
["collectorLastSeenAt", TableColumnType.Date, false, undefined],
|
|
936
|
+
[
|
|
937
|
+
"agentVersion",
|
|
938
|
+
TableColumnType.ShortText,
|
|
939
|
+
false,
|
|
940
|
+
ColumnLength.ShortText,
|
|
941
|
+
],
|
|
942
|
+
["lastSeenAt", TableColumnType.Date, false, undefined],
|
|
943
|
+
["autoArchivedAt", TableColumnType.Date, false, undefined],
|
|
944
|
+
["manuallyRestoredAt", TableColumnType.Date, false, undefined],
|
|
945
|
+
[
|
|
946
|
+
"dbSystemSource",
|
|
947
|
+
TableColumnType.ShortText,
|
|
948
|
+
false,
|
|
949
|
+
ColumnLength.ShortText,
|
|
950
|
+
],
|
|
951
|
+
["workloadLastSeenAt", TableColumnType.Date, false, undefined],
|
|
952
|
+
["automaticAssignments", TableColumnType.JSON, false, undefined],
|
|
953
|
+
["kubernetesClusterId", TableColumnType.ObjectID, false, undefined],
|
|
954
|
+
["dockerHostId", TableColumnType.ObjectID, false, undefined],
|
|
955
|
+
["podmanHostId", TableColumnType.ObjectID, false, undefined],
|
|
956
|
+
[
|
|
957
|
+
"retainTelemetryDataForDays",
|
|
958
|
+
TableColumnType.Number,
|
|
959
|
+
false,
|
|
960
|
+
undefined,
|
|
961
|
+
],
|
|
962
|
+
["telemetryRetentionConfig", TableColumnType.JSON, false, undefined],
|
|
963
|
+
];
|
|
964
|
+
|
|
965
|
+
for (const [column, type, required, length] of expectations) {
|
|
966
|
+
const metadata: TableColumnMetadata =
|
|
967
|
+
model.getTableColumnMetadata(column);
|
|
968
|
+
const options: ColumnMetadataArgs["options"] = columnArgs(
|
|
969
|
+
DatabaseServer,
|
|
970
|
+
column,
|
|
971
|
+
).options;
|
|
972
|
+
|
|
973
|
+
expect({ column, type: metadata.type }).toEqual({ column, type });
|
|
974
|
+
expect({ column, required: Boolean(metadata.required) }).toEqual({
|
|
975
|
+
column,
|
|
976
|
+
required,
|
|
977
|
+
});
|
|
978
|
+
expect({ column, nullable: options.nullable }).toEqual({
|
|
979
|
+
column,
|
|
980
|
+
nullable: !required,
|
|
981
|
+
});
|
|
982
|
+
expect({ column, length: options.length }).toEqual({ column, length });
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
test("serverPort is an integer column; memberEntityKeys and retention overrides are jsonb", () => {
|
|
987
|
+
expect(columnArgs(DatabaseServer, "serverPort").options.type).toBe(
|
|
988
|
+
"integer",
|
|
989
|
+
);
|
|
990
|
+
expect(columnArgs(DatabaseServer, "instanceCount").options.type).toBe(
|
|
991
|
+
"integer",
|
|
992
|
+
);
|
|
993
|
+
expect(columnArgs(DatabaseServer, "memberEntityKeys").options.type).toBe(
|
|
994
|
+
"jsonb",
|
|
995
|
+
);
|
|
996
|
+
expect(
|
|
997
|
+
columnArgs(DatabaseServer, "automaticAssignments").options.type,
|
|
998
|
+
).toBe("jsonb");
|
|
999
|
+
expect(
|
|
1000
|
+
columnArgs(DatabaseServer, "telemetryRetentionConfig").options.type,
|
|
1001
|
+
).toBe("jsonb");
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("new rows render sensibly: 0 instances, no collector status, not archived", () => {
|
|
1005
|
+
expect(columnArgs(DatabaseServer, "instanceCount").options.default).toBe(
|
|
1006
|
+
0,
|
|
1007
|
+
);
|
|
1008
|
+
/*
|
|
1009
|
+
* "disconnected" means a collector reported and stopped. A row found
|
|
1010
|
+
* from traces, containers or by hand never had one, so it must not be
|
|
1011
|
+
* born claiming one "disconnected": no DB default, NULL until a
|
|
1012
|
+
* collector reports.
|
|
1013
|
+
*/
|
|
1014
|
+
expect(
|
|
1015
|
+
columnArgs(DatabaseServer, "otelCollectorStatus").options.default,
|
|
1016
|
+
).toBeUndefined();
|
|
1017
|
+
expect(
|
|
1018
|
+
columnArgs(DatabaseServer, "otelCollectorStatus").options.nullable,
|
|
1019
|
+
).toBe(true);
|
|
1020
|
+
expect(new DatabaseServer().otelCollectorStatus).toBeUndefined();
|
|
1021
|
+
expect(columnArgs(DatabaseServer, "isArchived").options.default).toBe(
|
|
1022
|
+
false,
|
|
1023
|
+
);
|
|
1024
|
+
expect(columnArgs(DatabaseServer, "isArchived").options.nullable).toBe(
|
|
1025
|
+
false,
|
|
1026
|
+
);
|
|
1027
|
+
expect(model.isDefaultValueColumn("isArchived")).toBe(true);
|
|
1028
|
+
|
|
1029
|
+
// No DB default where "unknown" must stay NULL.
|
|
1030
|
+
for (const column of [
|
|
1031
|
+
"otelCollectorStatus",
|
|
1032
|
+
"lastSeenAt",
|
|
1033
|
+
"collectorLastSeenAt",
|
|
1034
|
+
"autoArchivedAt",
|
|
1035
|
+
"manuallyRestoredAt",
|
|
1036
|
+
"dbSystemSource",
|
|
1037
|
+
"workloadLastSeenAt",
|
|
1038
|
+
"automaticAssignments",
|
|
1039
|
+
"workloadIdentifier",
|
|
1040
|
+
"memberEntityKeys",
|
|
1041
|
+
"discoverySource",
|
|
1042
|
+
"serverPort",
|
|
1043
|
+
]) {
|
|
1044
|
+
expect({
|
|
1045
|
+
column,
|
|
1046
|
+
default: columnArgs(DatabaseServer, column).options.default,
|
|
1047
|
+
}).toEqual({ column, default: undefined });
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
test("the identifier columns document the family-keyed prefix, and their examples use it", () => {
|
|
1052
|
+
for (const column of ["databaseIdentifier", "workloadIdentifier"]) {
|
|
1053
|
+
const metadata: TableColumnMetadata =
|
|
1054
|
+
model.getTableColumnMetadata(column);
|
|
1055
|
+
expect(metadata.description).toContain("family");
|
|
1056
|
+
expect(metadata.description).toContain("mysql|");
|
|
1057
|
+
const prefix: string = String(metadata.example).split("|")[0]!;
|
|
1058
|
+
expect(getDatabaseSystemFamily(prefix)).toBe(prefix);
|
|
1059
|
+
}
|
|
1060
|
+
expect(model.getTableColumnMetadata("databaseIdentifier").example).toBe(
|
|
1061
|
+
buildDatabaseServerIdentifier("postgresql", {
|
|
1062
|
+
host: "orders-db.internal",
|
|
1063
|
+
port: 5432,
|
|
1064
|
+
}),
|
|
1065
|
+
);
|
|
1066
|
+
expect(model.getTableColumnMetadata("workloadIdentifier").example).toBe(
|
|
1067
|
+
buildWorkloadDatabaseServerIdentifier({
|
|
1068
|
+
system: "postgresql",
|
|
1069
|
+
platform: "kubernetes",
|
|
1070
|
+
parentName: "prod-cluster",
|
|
1071
|
+
namespace: "data",
|
|
1072
|
+
workloadKind: "StatefulSet",
|
|
1073
|
+
workloadName: "orders-db",
|
|
1074
|
+
}),
|
|
1075
|
+
);
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
test("slug is computed from name and is never user-writable", () => {
|
|
1079
|
+
expect(model.getSlugifyColumn()).toBe("name");
|
|
1080
|
+
expect(model.getSaveSlugToColumn()).toBe("slug");
|
|
1081
|
+
expect(model.getTableColumnMetadata("slug").computed).toBe(true);
|
|
1082
|
+
expect(model.getTableColumnMetadata("slug").type).toBe(
|
|
1083
|
+
TableColumnType.Slug,
|
|
1084
|
+
);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("table access control: Settings / project roles plus the granular DatabaseServer permissions", () => {
|
|
1088
|
+
const writeRoles: Array<Permission> = [
|
|
1089
|
+
Permission.ProjectOwner,
|
|
1090
|
+
Permission.ProjectAdmin,
|
|
1091
|
+
Permission.ProjectMember,
|
|
1092
|
+
Permission.SettingsAdmin,
|
|
1093
|
+
Permission.SettingsMember,
|
|
1094
|
+
];
|
|
1095
|
+
|
|
1096
|
+
expect(sorted(model.getCreatePermissions())).toEqual(
|
|
1097
|
+
sorted([...writeRoles, Permission.CreateDatabaseServer]),
|
|
1098
|
+
);
|
|
1099
|
+
expect(sorted(model.getUpdatePermissions())).toEqual(
|
|
1100
|
+
sorted([...writeRoles, Permission.EditDatabaseServer]),
|
|
1101
|
+
);
|
|
1102
|
+
expect(sorted(model.getDeletePermissions())).toEqual(
|
|
1103
|
+
sorted([...writeRoles, Permission.DeleteDatabaseServer]),
|
|
1104
|
+
);
|
|
1105
|
+
expect(sorted(model.getReadPermissions())).toEqual(
|
|
1106
|
+
sorted([
|
|
1107
|
+
Permission.ProjectOwner,
|
|
1108
|
+
Permission.ProjectAdmin,
|
|
1109
|
+
Permission.ProjectMember,
|
|
1110
|
+
Permission.Viewer,
|
|
1111
|
+
Permission.SettingsAdmin,
|
|
1112
|
+
Permission.SettingsMember,
|
|
1113
|
+
Permission.SettingsViewer,
|
|
1114
|
+
Permission.ReadDatabaseServer,
|
|
1115
|
+
]),
|
|
1116
|
+
);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
test.each(DATABASE_SERVER_USER_CREATE_COLUMNS)(
|
|
1120
|
+
"%s is creatable by exactly the table's create roles (COLUMN ACL RULE)",
|
|
1121
|
+
(column: string) => {
|
|
1122
|
+
/*
|
|
1123
|
+
* DatabaseService.create runs onBeforeCreate BEFORE
|
|
1124
|
+
* ModelPermission.checkCreatePermissions. A column the service sets
|
|
1125
|
+
* during a manual create that the caller may not create turns every
|
|
1126
|
+
* manual create into "User is not allowed to create on <column>".
|
|
1127
|
+
*/
|
|
1128
|
+
expect(sorted(columnAccess(model, column).create || [])).toEqual(
|
|
1129
|
+
sorted(model.getCreatePermissions()),
|
|
1130
|
+
);
|
|
1131
|
+
},
|
|
1132
|
+
);
|
|
1133
|
+
|
|
1134
|
+
test.each(DATABASE_SERVER_ROOT_ONLY_COLUMNS)(
|
|
1135
|
+
"%s is root-only: nobody creates or updates it through the API",
|
|
1136
|
+
(column: string) => {
|
|
1137
|
+
const accessControl: ColumnAccessControl = columnAccess(model, column);
|
|
1138
|
+
|
|
1139
|
+
expect(accessControl.create || []).toEqual([]);
|
|
1140
|
+
expect(accessControl.update || []).toEqual([]);
|
|
1141
|
+
},
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
test("identity columns are creatable but never updatable", () => {
|
|
1145
|
+
/*
|
|
1146
|
+
* Re-pointing a row at a different engine or endpoint by editing it
|
|
1147
|
+
* would silently hand its history to another database. The endpoints
|
|
1148
|
+
* table is where aliases change.
|
|
1149
|
+
*/
|
|
1150
|
+
for (const column of [
|
|
1151
|
+
"projectId",
|
|
1152
|
+
"databaseIdentifier",
|
|
1153
|
+
"dbSystem",
|
|
1154
|
+
"serverAddress",
|
|
1155
|
+
"serverPort",
|
|
1156
|
+
"discoverySource",
|
|
1157
|
+
"createdByUserId",
|
|
1158
|
+
]) {
|
|
1159
|
+
expect({ column, update: columnAccess(model, column).update }).toEqual({
|
|
1160
|
+
column,
|
|
1161
|
+
update: [],
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
test.each(DATABASE_SERVER_USER_UPDATE_COLUMNS)(
|
|
1167
|
+
"%s is updatable by exactly the table's update roles",
|
|
1168
|
+
(column: string) => {
|
|
1169
|
+
expect(sorted(columnAccess(model, column).update || [])).toEqual(
|
|
1170
|
+
sorted(model.getUpdatePermissions()),
|
|
1171
|
+
);
|
|
1172
|
+
},
|
|
1173
|
+
);
|
|
1174
|
+
|
|
1175
|
+
test("no other column is user-updatable", () => {
|
|
1176
|
+
const updatable: Array<string> = ownColumns(model).filter(
|
|
1177
|
+
(column: string): boolean => {
|
|
1178
|
+
return (columnAccess(model, column).update || []).length > 0;
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
|
|
1182
|
+
expect(updatable.sort()).toEqual(
|
|
1183
|
+
[...DATABASE_SERVER_USER_UPDATE_COLUMNS].sort(),
|
|
1184
|
+
);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("every column is readable by exactly the table's read roles", () => {
|
|
1188
|
+
for (const column of ownColumns(model)) {
|
|
1189
|
+
expect({
|
|
1190
|
+
column,
|
|
1191
|
+
read: sorted(columnAccess(model, column).read || []),
|
|
1192
|
+
}).toEqual({ column, read: sorted(model.getReadPermissions()) });
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
describe("against the real permission checks", () => {
|
|
1197
|
+
test.each([
|
|
1198
|
+
["ProjectOwner", [Permission.ProjectOwner]],
|
|
1199
|
+
["ProjectMember", [Permission.ProjectMember]],
|
|
1200
|
+
["SettingsAdmin", [Permission.SettingsAdmin]],
|
|
1201
|
+
["SettingsMember", [Permission.SettingsMember]],
|
|
1202
|
+
["CreateDatabaseServer only", [Permission.CreateDatabaseServer]],
|
|
1203
|
+
])(
|
|
1204
|
+
"a manual create as %s passes table and column checks",
|
|
1205
|
+
(_label: string, permissions: Array<Permission>) => {
|
|
1206
|
+
expect(
|
|
1207
|
+
checkCreate(DatabaseServer, manualCreateData(), permissions),
|
|
1208
|
+
).not.toThrow();
|
|
1209
|
+
},
|
|
1210
|
+
);
|
|
1211
|
+
|
|
1212
|
+
test("a manual create with labels and retention passes too", () => {
|
|
1213
|
+
const data: DatabaseServer = manualCreateData();
|
|
1214
|
+
const label: Label = new Label();
|
|
1215
|
+
label._id = ObjectID.generate().toString();
|
|
1216
|
+
data.labels = [label];
|
|
1217
|
+
data.retainTelemetryDataForDays = 7;
|
|
1218
|
+
data.isArchived = false;
|
|
1219
|
+
|
|
1220
|
+
expect(
|
|
1221
|
+
checkCreate(DatabaseServer, data, [Permission.CreateDatabaseServer]),
|
|
1222
|
+
).not.toThrow();
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
test.each(
|
|
1226
|
+
DATABASE_SERVER_ROOT_ONLY_COLUMNS.filter((column: string): boolean => {
|
|
1227
|
+
// slug is a Slug column, which ColumnPermission always skips.
|
|
1228
|
+
return column !== "slug";
|
|
1229
|
+
}),
|
|
1230
|
+
)("a non-root create carrying %s is refused", (column: string) => {
|
|
1231
|
+
const data: DatabaseServer = manualCreateData();
|
|
1232
|
+
const metadata: TableColumnMetadata =
|
|
1233
|
+
model.getTableColumnMetadata(column);
|
|
1234
|
+
let value: unknown = "value";
|
|
1235
|
+
|
|
1236
|
+
if (metadata.type === TableColumnType.ObjectID) {
|
|
1237
|
+
value = ObjectID.generate();
|
|
1238
|
+
} else if (metadata.type === TableColumnType.Date) {
|
|
1239
|
+
value = new Date();
|
|
1240
|
+
} else if (metadata.type === TableColumnType.Number) {
|
|
1241
|
+
value = 1;
|
|
1242
|
+
} else if (metadata.type === TableColumnType.JSON) {
|
|
1243
|
+
value = { k: "2026-01-01T00:00:00.000Z" };
|
|
1244
|
+
} else if (metadata.type === TableColumnType.Entity) {
|
|
1245
|
+
value = { _id: ObjectID.generate().toString() };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
(data as unknown as Record<string, unknown>)[column] = value;
|
|
1249
|
+
|
|
1250
|
+
expect(
|
|
1251
|
+
checkColumns(
|
|
1252
|
+
DatabaseServer,
|
|
1253
|
+
data,
|
|
1254
|
+
[Permission.ProjectOwner],
|
|
1255
|
+
DatabaseRequestType.Create,
|
|
1256
|
+
),
|
|
1257
|
+
).toThrow(
|
|
1258
|
+
`User is not allowed to create on ${column} column of Database`,
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
test("an update may rename, relabel and archive", () => {
|
|
1263
|
+
const data: DatabaseServer = new DatabaseServer();
|
|
1264
|
+
data.name = "Orders primary";
|
|
1265
|
+
data.description = "Renamed";
|
|
1266
|
+
data.isArchived = true;
|
|
1267
|
+
|
|
1268
|
+
expect(
|
|
1269
|
+
checkColumns(
|
|
1270
|
+
DatabaseServer,
|
|
1271
|
+
data,
|
|
1272
|
+
[Permission.EditDatabaseServer],
|
|
1273
|
+
DatabaseRequestType.Update,
|
|
1274
|
+
),
|
|
1275
|
+
).not.toThrow();
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
test.each([
|
|
1279
|
+
"databaseIdentifier",
|
|
1280
|
+
"dbSystem",
|
|
1281
|
+
"serverAddress",
|
|
1282
|
+
"discoverySource",
|
|
1283
|
+
"workloadIdentifier",
|
|
1284
|
+
"otelCollectorStatus",
|
|
1285
|
+
])(
|
|
1286
|
+
"an update of %s is refused even for a project owner",
|
|
1287
|
+
(column: string) => {
|
|
1288
|
+
const data: DatabaseServer = new DatabaseServer();
|
|
1289
|
+
(data as unknown as Record<string, unknown>)[column] = "x";
|
|
1290
|
+
|
|
1291
|
+
expect(
|
|
1292
|
+
checkColumns(
|
|
1293
|
+
DatabaseServer,
|
|
1294
|
+
data,
|
|
1295
|
+
[Permission.ProjectOwner],
|
|
1296
|
+
DatabaseRequestType.Update,
|
|
1297
|
+
),
|
|
1298
|
+
).toThrow(
|
|
1299
|
+
`User is not allowed to update on ${column} column of Database`,
|
|
1300
|
+
);
|
|
1301
|
+
},
|
|
1302
|
+
);
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
test("labels drive access control through the DatabaseServerLabel join table", () => {
|
|
1306
|
+
expect(model.accessControlColumn).toBe("labels");
|
|
1307
|
+
|
|
1308
|
+
const joinTable: JoinTableMetadataArgs = joinTableArgs(
|
|
1309
|
+
DatabaseServer,
|
|
1310
|
+
"labels",
|
|
1311
|
+
);
|
|
1312
|
+
|
|
1313
|
+
expect(joinTable.name).toBe("DatabaseServerLabel");
|
|
1314
|
+
expect(joinTable.joinColumns?.[0]?.name).toBe("databaseServerId");
|
|
1315
|
+
expect(joinTable.inverseJoinColumns?.[0]?.name).toBe("labelId");
|
|
1316
|
+
expect(model.getTableColumnMetadata("labels").modelType).toBe(Label);
|
|
1317
|
+
expect(relationArgs(DatabaseServer, "labels").relationType).toBe(
|
|
1318
|
+
"many-to-many",
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
test.each([
|
|
1323
|
+
["kubernetesCluster", "kubernetesClusterId", KubernetesCluster],
|
|
1324
|
+
["dockerHost", "dockerHostId", DockerHost],
|
|
1325
|
+
["podmanHost", "podmanHostId", PodmanHost],
|
|
1326
|
+
] as Array<[string, string, ModelType]>)(
|
|
1327
|
+
"%s is an optional 'runs on' link that survives its parent's deletion",
|
|
1328
|
+
(property: string, idColumn: string, parent: ModelType) => {
|
|
1329
|
+
/*
|
|
1330
|
+
* Deleting a monitored cluster / host must not delete the database
|
|
1331
|
+
* row (its history, owners and incident links): SET NULL, not
|
|
1332
|
+
* CASCADE.
|
|
1333
|
+
*/
|
|
1334
|
+
const relation: RelationMetadataArgs = relationArgs(
|
|
1335
|
+
DatabaseServer,
|
|
1336
|
+
property,
|
|
1337
|
+
);
|
|
1338
|
+
|
|
1339
|
+
expect(relation.relationType).toBe("many-to-one");
|
|
1340
|
+
expect(relation.options.onDelete).toBe("SET NULL");
|
|
1341
|
+
expect(relation.options.nullable).toBe(true);
|
|
1342
|
+
expect(joinColumnArgs(DatabaseServer, property).name).toBe(idColumn);
|
|
1343
|
+
|
|
1344
|
+
const metadata: TableColumnMetadata =
|
|
1345
|
+
model.getTableColumnMetadata(property);
|
|
1346
|
+
|
|
1347
|
+
expect(metadata.type).toBe(TableColumnType.Entity);
|
|
1348
|
+
expect(metadata.modelType).toBe(parent);
|
|
1349
|
+
expect(metadata.manyToOneRelationColumn).toBe(idColumn);
|
|
1350
|
+
expect(columnArgs(DatabaseServer, idColumn).options.nullable).toBe(
|
|
1351
|
+
true,
|
|
1352
|
+
);
|
|
1353
|
+
},
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
test("the project relation cascades", () => {
|
|
1357
|
+
expect(relationArgs(DatabaseServer, "project").options.onDelete).toBe(
|
|
1358
|
+
"CASCADE",
|
|
1359
|
+
);
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test("lastSeenAt and the identity columns can be read through a relation query", () => {
|
|
1363
|
+
/*
|
|
1364
|
+
* The affected-resources chips on incidents and the endpoint list read
|
|
1365
|
+
* a database through a relation select.
|
|
1366
|
+
*/
|
|
1367
|
+
for (const column of [
|
|
1368
|
+
"projectId",
|
|
1369
|
+
"name",
|
|
1370
|
+
"databaseIdentifier",
|
|
1371
|
+
"dbSystem",
|
|
1372
|
+
"serverAddress",
|
|
1373
|
+
"lastSeenAt",
|
|
1374
|
+
]) {
|
|
1375
|
+
expect({
|
|
1376
|
+
column,
|
|
1377
|
+
canRead: Boolean(
|
|
1378
|
+
model.getTableColumnMetadata(column).canReadOnRelationQuery,
|
|
1379
|
+
),
|
|
1380
|
+
}).toEqual({ column, canRead: true });
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
describe("DatabaseServerEndpoint", () => {
|
|
1386
|
+
const model: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1387
|
+
const parent: DatabaseServer = new DatabaseServer();
|
|
1388
|
+
|
|
1389
|
+
test("one owner per endpoint per project, enforced by the database", () => {
|
|
1390
|
+
/*
|
|
1391
|
+
* Telemetry keys are derived from the endpoint, not the row. An
|
|
1392
|
+
* endpoint listed on two databases would show the same traffic on
|
|
1393
|
+
* both - this index is the invariant that prevents it.
|
|
1394
|
+
*/
|
|
1395
|
+
const found: IndexMetadataArgs | undefined = indexOnColumns(
|
|
1396
|
+
DatabaseServerEndpoint,
|
|
1397
|
+
["projectId", "endpoint"],
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
expect(found).toBeDefined();
|
|
1401
|
+
expect(found?.unique).toBe(true);
|
|
1402
|
+
expect(found?.where).toBeUndefined();
|
|
1403
|
+
expect(found?.columns).toEqual(["projectId", "endpoint"]);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
test("indexes databaseServerId for the per-database endpoint list", () => {
|
|
1407
|
+
expect(
|
|
1408
|
+
indexOnColumns(DatabaseServerEndpoint, ["databaseServerId"]),
|
|
1409
|
+
).toBeDefined();
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
test("reuses the parent's permissions: add/remove = edit the database, read = read it", () => {
|
|
1413
|
+
expect(sorted(model.getCreatePermissions())).toEqual(
|
|
1414
|
+
sorted(parent.getUpdatePermissions()),
|
|
1415
|
+
);
|
|
1416
|
+
expect(sorted(model.getDeletePermissions())).toEqual(
|
|
1417
|
+
sorted(parent.getUpdatePermissions()),
|
|
1418
|
+
);
|
|
1419
|
+
expect(sorted(model.getReadPermissions())).toEqual(
|
|
1420
|
+
sorted(parent.getReadPermissions()),
|
|
1421
|
+
);
|
|
1422
|
+
|
|
1423
|
+
// Rows are never edited through the API: remove and re-add instead.
|
|
1424
|
+
expect(model.getUpdatePermissions()).toEqual([]);
|
|
1425
|
+
|
|
1426
|
+
// And the catalogue has no endpoint-specific permissions to grant.
|
|
1427
|
+
for (const permission of allReferencedPermissions(model)) {
|
|
1428
|
+
expect(permission.toString()).not.toContain("DatabaseServerEndpoint");
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test("read access and Owned scope follow the parent database", () => {
|
|
1433
|
+
expect(model.canAccessIfCanReadOn).toBe("databaseServer");
|
|
1434
|
+
|
|
1435
|
+
const ownedThrough: OwnedThroughMetadata | null = model.ownedThrough;
|
|
1436
|
+
|
|
1437
|
+
expect(ownedThrough).toBeTruthy();
|
|
1438
|
+
expect(ownedThrough?.fkColumn).toBe("databaseServerId");
|
|
1439
|
+
expect(ownedThrough?.parentModels).toEqual([DatabaseServer]);
|
|
1440
|
+
expect(ownedThrough?.includeProjectScope).toBe(false);
|
|
1441
|
+
});
|
|
1442
|
+
|
|
1443
|
+
test("carries the SPEC columns with the SPEC types", () => {
|
|
1444
|
+
const columns: Array<string> = model.getTableColumns().columns;
|
|
1445
|
+
|
|
1446
|
+
for (const column of [
|
|
1447
|
+
"project",
|
|
1448
|
+
"projectId",
|
|
1449
|
+
"databaseServer",
|
|
1450
|
+
"databaseServerId",
|
|
1451
|
+
"endpoint",
|
|
1452
|
+
"isPrimary",
|
|
1453
|
+
"source",
|
|
1454
|
+
"lastMatchedAt",
|
|
1455
|
+
"createdByUser",
|
|
1456
|
+
"createdByUserId",
|
|
1457
|
+
"deletedByUser",
|
|
1458
|
+
"deletedByUserId",
|
|
1459
|
+
]) {
|
|
1460
|
+
expect(columns).toContain(column);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
expect(model.getTableColumnMetadata("endpoint").type).toBe(
|
|
1464
|
+
TableColumnType.LongText,
|
|
1465
|
+
);
|
|
1466
|
+
expect(model.getTableColumnMetadata("endpoint").required).toBe(true);
|
|
1467
|
+
expect(columnArgs(DatabaseServerEndpoint, "endpoint").options).toEqual(
|
|
1468
|
+
expect.objectContaining({
|
|
1469
|
+
nullable: false,
|
|
1470
|
+
length: ColumnLength.LongText,
|
|
1471
|
+
}),
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
expect(model.getTableColumnMetadata("databaseServerId").required).toBe(
|
|
1475
|
+
true,
|
|
1476
|
+
);
|
|
1477
|
+
expect(
|
|
1478
|
+
columnArgs(DatabaseServerEndpoint, "databaseServerId").options.nullable,
|
|
1479
|
+
).toBe(false);
|
|
1480
|
+
|
|
1481
|
+
expect(model.getTableColumnMetadata("isPrimary").type).toBe(
|
|
1482
|
+
TableColumnType.Boolean,
|
|
1483
|
+
);
|
|
1484
|
+
expect(columnArgs(DatabaseServerEndpoint, "isPrimary").options).toEqual(
|
|
1485
|
+
expect.objectContaining({ nullable: false, default: false }),
|
|
1486
|
+
);
|
|
1487
|
+
expect(model.isDefaultValueColumn("isPrimary")).toBe(true);
|
|
1488
|
+
|
|
1489
|
+
expect(model.getTableColumnMetadata("source").type).toBe(
|
|
1490
|
+
TableColumnType.ShortText,
|
|
1491
|
+
);
|
|
1492
|
+
expect(model.getTableColumnMetadata("lastMatchedAt").type).toBe(
|
|
1493
|
+
TableColumnType.Date,
|
|
1494
|
+
);
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
test("deleting a database takes its endpoints with it", () => {
|
|
1498
|
+
const relation: RelationMetadataArgs = relationArgs(
|
|
1499
|
+
DatabaseServerEndpoint,
|
|
1500
|
+
"databaseServer",
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
expect(relation.relationType).toBe("many-to-one");
|
|
1504
|
+
expect(relation.options.onDelete).toBe("CASCADE");
|
|
1505
|
+
expect(
|
|
1506
|
+
joinColumnArgs(DatabaseServerEndpoint, "databaseServer").name,
|
|
1507
|
+
).toBe("databaseServerId");
|
|
1508
|
+
});
|
|
1509
|
+
|
|
1510
|
+
test("no column is ever updatable through the API", () => {
|
|
1511
|
+
for (const column of ownColumns(model)) {
|
|
1512
|
+
expect({ column, update: columnAccess(model, column).update }).toEqual({
|
|
1513
|
+
column,
|
|
1514
|
+
update: [],
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
test("the columns an alias add sets are creatable by the parent's edit roles", () => {
|
|
1520
|
+
/*
|
|
1521
|
+
* DatabaseServerEndpointService.onBeforeCreate forces source "user"
|
|
1522
|
+
* and isPrimary false on every non-root create; both are therefore in
|
|
1523
|
+
* the data the column check sees and must be creatable (the forced
|
|
1524
|
+
* value, not the caller's, is what survives the hook).
|
|
1525
|
+
*/
|
|
1526
|
+
for (const column of [
|
|
1527
|
+
"project",
|
|
1528
|
+
"projectId",
|
|
1529
|
+
"databaseServer",
|
|
1530
|
+
"databaseServerId",
|
|
1531
|
+
"endpoint",
|
|
1532
|
+
"source",
|
|
1533
|
+
"isPrimary",
|
|
1534
|
+
"createdByUser",
|
|
1535
|
+
"createdByUserId",
|
|
1536
|
+
]) {
|
|
1537
|
+
expect({
|
|
1538
|
+
column,
|
|
1539
|
+
create: sorted(columnAccess(model, column).create || []),
|
|
1540
|
+
}).toEqual({ column, create: sorted(model.getCreatePermissions()) });
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
for (const column of [
|
|
1544
|
+
"lastMatchedAt",
|
|
1545
|
+
"deletedByUser",
|
|
1546
|
+
"deletedByUserId",
|
|
1547
|
+
]) {
|
|
1548
|
+
expect({ column, create: columnAccess(model, column).create }).toEqual({
|
|
1549
|
+
column,
|
|
1550
|
+
create: [],
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
test("an alias add by a database editor passes the real create check", () => {
|
|
1556
|
+
const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1557
|
+
data.projectId = PROJECT_ID;
|
|
1558
|
+
data.databaseServerId = ObjectID.generate();
|
|
1559
|
+
data.endpoint = "orders-db.data.svc.cluster.local:5432@prod";
|
|
1560
|
+
data.source = "user";
|
|
1561
|
+
data.isPrimary = false;
|
|
1562
|
+
data.createdByUserId = USER_ID;
|
|
1563
|
+
|
|
1564
|
+
expect(
|
|
1565
|
+
checkCreate(DatabaseServerEndpoint, data, [
|
|
1566
|
+
Permission.EditDatabaseServer,
|
|
1567
|
+
]),
|
|
1568
|
+
).not.toThrow();
|
|
1569
|
+
expect(
|
|
1570
|
+
checkCreate(DatabaseServerEndpoint, data, [Permission.ProjectMember]),
|
|
1571
|
+
).not.toThrow();
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
test("a database reader may not add an alias", () => {
|
|
1575
|
+
const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1576
|
+
data.projectId = PROJECT_ID;
|
|
1577
|
+
data.databaseServerId = ObjectID.generate();
|
|
1578
|
+
data.endpoint = "orders-db.internal:5432";
|
|
1579
|
+
|
|
1580
|
+
expect(
|
|
1581
|
+
checkCreate(DatabaseServerEndpoint, data, [
|
|
1582
|
+
Permission.ReadDatabaseServer,
|
|
1583
|
+
]),
|
|
1584
|
+
).toThrow();
|
|
1585
|
+
});
|
|
1586
|
+
|
|
1587
|
+
test("lastMatchedAt cannot be forged on create", () => {
|
|
1588
|
+
const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1589
|
+
data.endpoint = "orders-db.internal:5432";
|
|
1590
|
+
data.lastMatchedAt = new Date();
|
|
1591
|
+
|
|
1592
|
+
expect(
|
|
1593
|
+
checkColumns(
|
|
1594
|
+
DatabaseServerEndpoint,
|
|
1595
|
+
data,
|
|
1596
|
+
[Permission.ProjectOwner],
|
|
1597
|
+
DatabaseRequestType.Create,
|
|
1598
|
+
),
|
|
1599
|
+
).toThrow(
|
|
1600
|
+
"User is not allowed to create on lastMatchedAt column of Database Endpoint",
|
|
1601
|
+
);
|
|
1602
|
+
});
|
|
1603
|
+
|
|
1604
|
+
test("an endpoint cannot be rewritten in place, even by a project owner", () => {
|
|
1605
|
+
const data: DatabaseServerEndpoint = new DatabaseServerEndpoint();
|
|
1606
|
+
data.endpoint = "somewhere-else:5432";
|
|
1607
|
+
|
|
1608
|
+
expect(
|
|
1609
|
+
checkColumns(
|
|
1610
|
+
DatabaseServerEndpoint,
|
|
1611
|
+
data,
|
|
1612
|
+
[Permission.ProjectOwner],
|
|
1613
|
+
DatabaseRequestType.Update,
|
|
1614
|
+
),
|
|
1615
|
+
).toThrow(
|
|
1616
|
+
"User is not allowed to update on endpoint column of Database Endpoint",
|
|
1617
|
+
);
|
|
1618
|
+
});
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
describe("DatabaseServerFeed", () => {
|
|
1622
|
+
test("event enum has exactly the ten contract members with value === key", () => {
|
|
1623
|
+
const entries: Array<[string, string]> = Object.entries(
|
|
1624
|
+
DatabaseServerFeedEventType,
|
|
1625
|
+
);
|
|
1626
|
+
|
|
1627
|
+
expect(entries.length).toBe(10);
|
|
1628
|
+
expect(
|
|
1629
|
+
entries
|
|
1630
|
+
.map(([key]: [string, string]) => {
|
|
1631
|
+
return key;
|
|
1632
|
+
})
|
|
1633
|
+
.sort(),
|
|
1634
|
+
).toEqual(
|
|
1635
|
+
[
|
|
1636
|
+
"DatabaseServerCreated",
|
|
1637
|
+
"DatabaseServerUpdated",
|
|
1638
|
+
"DatabaseServerArchived",
|
|
1639
|
+
"DatabaseServerRestored",
|
|
1640
|
+
"OwnerUserAdded",
|
|
1641
|
+
"OwnerUserRemoved",
|
|
1642
|
+
"OwnerTeamAdded",
|
|
1643
|
+
"OwnerTeamRemoved",
|
|
1644
|
+
"OwnerRuleExecuted",
|
|
1645
|
+
"LabelRuleExecuted",
|
|
1646
|
+
].sort(),
|
|
1647
|
+
);
|
|
1648
|
+
|
|
1649
|
+
/*
|
|
1650
|
+
* Values are stored verbatim in a ShortText column, so a member whose
|
|
1651
|
+
* value drifts from its key orphans every row written under the old
|
|
1652
|
+
* value.
|
|
1653
|
+
*/
|
|
1654
|
+
for (const [key, value] of entries) {
|
|
1655
|
+
expect(value).toBe(key);
|
|
1656
|
+
}
|
|
1657
|
+
});
|
|
1658
|
+
|
|
1659
|
+
test("stores the event under databaseServerFeedEventType and reads through the database", () => {
|
|
1660
|
+
const model: DatabaseServerFeed = new DatabaseServerFeed();
|
|
1661
|
+
|
|
1662
|
+
expect(model.getTableColumns().columns).toContain(
|
|
1663
|
+
"databaseServerFeedEventType",
|
|
1664
|
+
);
|
|
1665
|
+
expect(
|
|
1666
|
+
model.getTableColumnMetadata("databaseServerFeedEventType").required,
|
|
1667
|
+
).toBe(true);
|
|
1668
|
+
// Unbounded varchar like every other feed event column.
|
|
1669
|
+
expect(
|
|
1670
|
+
columnArgs(DatabaseServerFeed, "databaseServerFeedEventType").options
|
|
1671
|
+
.length,
|
|
1672
|
+
).toBeUndefined();
|
|
1673
|
+
expect(model.canAccessIfCanReadOn).toBe("databaseServer");
|
|
1674
|
+
|
|
1675
|
+
const ownedThrough: OwnedThroughMetadata | null = model.ownedThrough;
|
|
1676
|
+
|
|
1677
|
+
expect(ownedThrough?.fkColumn).toBe("databaseServerId");
|
|
1678
|
+
expect(ownedThrough?.parentModels).toEqual([DatabaseServer]);
|
|
1679
|
+
});
|
|
1680
|
+
|
|
1681
|
+
test("is append-only and gated on its own feed permissions", () => {
|
|
1682
|
+
const model: DatabaseServerFeed = new DatabaseServerFeed();
|
|
1683
|
+
|
|
1684
|
+
expect(model.getUpdatePermissions()).toEqual([]);
|
|
1685
|
+
expect(model.getDeletePermissions()).toEqual([]);
|
|
1686
|
+
expect(model.getCreatePermissions()).toContain(
|
|
1687
|
+
Permission.CreateDatabaseServerFeed,
|
|
1688
|
+
);
|
|
1689
|
+
expect(model.getReadPermissions()).toContain(
|
|
1690
|
+
Permission.ReadDatabaseServerFeed,
|
|
1691
|
+
);
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("indexes (databaseServerId, postedAt) for the feed page", () => {
|
|
1695
|
+
expect(
|
|
1696
|
+
indexOnColumns(DatabaseServerFeed, ["databaseServerId", "postedAt"]),
|
|
1697
|
+
).toBeDefined();
|
|
1698
|
+
});
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
describe("children of DatabaseServer", () => {
|
|
1702
|
+
test.each(DATABASE_SERVER_CHILDREN)(
|
|
1703
|
+
"%p.%s cascades on database delete and carries a required databaseServerId",
|
|
1704
|
+
(modelType: ModelType, property: string) => {
|
|
1705
|
+
const relation: RelationMetadataArgs = relationArgs(
|
|
1706
|
+
modelType,
|
|
1707
|
+
property,
|
|
1708
|
+
);
|
|
1709
|
+
|
|
1710
|
+
expect(relation.relationType).toBe("many-to-one");
|
|
1711
|
+
expect(relation.options.onDelete).toBe("CASCADE");
|
|
1712
|
+
|
|
1713
|
+
const model: BaseModel = new modelType();
|
|
1714
|
+
|
|
1715
|
+
expect(model.getTableColumns().columns).toContain("databaseServerId");
|
|
1716
|
+
expect(
|
|
1717
|
+
model.getTableColumnMetadata(property).manyToOneRelationColumn,
|
|
1718
|
+
).toBe("databaseServerId");
|
|
1719
|
+
expect(model.getTableColumnMetadata(property).modelType).toBe(
|
|
1720
|
+
DatabaseServer,
|
|
1721
|
+
);
|
|
1722
|
+
expect(model.getTableColumnMetadata("databaseServerId").required).toBe(
|
|
1723
|
+
true,
|
|
1724
|
+
);
|
|
1725
|
+
expect(columnArgs(modelType, "databaseServerId").options.nullable).toBe(
|
|
1726
|
+
false,
|
|
1727
|
+
);
|
|
1728
|
+
},
|
|
1729
|
+
);
|
|
1730
|
+
|
|
1731
|
+
test.each([
|
|
1732
|
+
[
|
|
1733
|
+
DatabaseServerOwnerTeam,
|
|
1734
|
+
"teamId",
|
|
1735
|
+
"This team is already an owner of this database.",
|
|
1736
|
+
],
|
|
1737
|
+
[
|
|
1738
|
+
DatabaseServerOwnerUser,
|
|
1739
|
+
"userId",
|
|
1740
|
+
"This user is already an owner of this database.",
|
|
1741
|
+
],
|
|
1742
|
+
] as Array<[ModelType, string, string]>)(
|
|
1743
|
+
"%p allows one row per (database, owner, project)",
|
|
1744
|
+
(modelType: ModelType, ownerColumn: string, message: string) => {
|
|
1745
|
+
const unique: IndexMetadataArgs | undefined = indexOnColumns(
|
|
1746
|
+
modelType,
|
|
1747
|
+
["databaseServerId", ownerColumn, "projectId"],
|
|
1748
|
+
);
|
|
1749
|
+
|
|
1750
|
+
expect(unique).toBeDefined();
|
|
1751
|
+
expect(unique?.unique).toBe(true);
|
|
1752
|
+
|
|
1753
|
+
const constraints: Array<UniqueColumnsTogetherMetadata> =
|
|
1754
|
+
new modelType().getUniqueColumnsTogether();
|
|
1755
|
+
|
|
1756
|
+
expect(constraints).toHaveLength(1);
|
|
1757
|
+
expect(constraints[0]!.columnNames).toEqual([
|
|
1758
|
+
"databaseServerId",
|
|
1759
|
+
ownerColumn,
|
|
1760
|
+
"projectId",
|
|
1761
|
+
]);
|
|
1762
|
+
expect(constraints[0]!.errorMessage).toBe(message);
|
|
1763
|
+
},
|
|
1764
|
+
);
|
|
1765
|
+
|
|
1766
|
+
test("owner rows carry the isOwnerNotified flag, which only the server sets", () => {
|
|
1767
|
+
// Owner rule assignment stamps it (OwnerRuleAssignment.createOwner).
|
|
1768
|
+
for (const modelType of [
|
|
1769
|
+
DatabaseServerOwnerTeam,
|
|
1770
|
+
DatabaseServerOwnerUser,
|
|
1771
|
+
] as Array<ModelType>) {
|
|
1772
|
+
const model: BaseModel = new modelType();
|
|
1773
|
+
|
|
1774
|
+
expect(model.getTableColumns().columns).toContain("isOwnerNotified");
|
|
1775
|
+
expect(columnAccess(model, "isOwnerNotified").create).toEqual([]);
|
|
1776
|
+
expect(columnArgs(modelType, "isOwnerNotified").options.default).toBe(
|
|
1777
|
+
false,
|
|
1778
|
+
);
|
|
1779
|
+
expect(model.enableWorkflowOn).toBeTruthy();
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
test.each([
|
|
1784
|
+
[DatabaseServerOwnerTeam, "OwnerTeam"],
|
|
1785
|
+
[DatabaseServerOwnerUser, "OwnerUser"],
|
|
1786
|
+
] as Array<[ModelType, string]>)(
|
|
1787
|
+
"%p gates on its own %s permissions",
|
|
1788
|
+
(modelType: ModelType, kind: string) => {
|
|
1789
|
+
const model: BaseModel = new modelType();
|
|
1790
|
+
|
|
1791
|
+
expect(model.getCreatePermissions()).toContain(
|
|
1792
|
+
`CreateDatabaseServer${kind}` as Permission,
|
|
1793
|
+
);
|
|
1794
|
+
expect(model.getReadPermissions()).toContain(
|
|
1795
|
+
`ReadDatabaseServer${kind}` as Permission,
|
|
1796
|
+
);
|
|
1797
|
+
expect(model.getUpdatePermissions()).toContain(
|
|
1798
|
+
`EditDatabaseServer${kind}` as Permission,
|
|
1799
|
+
);
|
|
1800
|
+
expect(model.getDeletePermissions()).toContain(
|
|
1801
|
+
`DeleteDatabaseServer${kind}` as Permission,
|
|
1802
|
+
);
|
|
1803
|
+
},
|
|
1804
|
+
);
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
describe("rule models", () => {
|
|
1808
|
+
test.each([
|
|
1809
|
+
[DatabaseServerLabelRule, "LabelRule"],
|
|
1810
|
+
[DatabaseServerOwnerRule, "OwnerRule"],
|
|
1811
|
+
] as Array<[ModelType, string]>)(
|
|
1812
|
+
"%p is a RuleBaseModel whose criteria column follows the table ACL",
|
|
1813
|
+
(modelType: ModelType, kind: string) => {
|
|
1814
|
+
const model: BaseModel = new modelType();
|
|
1815
|
+
|
|
1816
|
+
expect(model).toBeInstanceOf(RuleBaseModel);
|
|
1817
|
+
expect(model.getTableColumnMetadata("criteria").type).toBe(
|
|
1818
|
+
TableColumnType.JSON,
|
|
1819
|
+
);
|
|
1820
|
+
expect(model.getColumnAccessControlFor("criteria")).toEqual({
|
|
1821
|
+
create: model.getCreatePermissions(),
|
|
1822
|
+
read: model.getReadPermissions(),
|
|
1823
|
+
update: model.getUpdatePermissions(),
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
// Rule tables: owners and admins write, members and viewers read.
|
|
1827
|
+
expect(sorted(model.getCreatePermissions())).toEqual(
|
|
1828
|
+
sorted([
|
|
1829
|
+
Permission.ProjectOwner,
|
|
1830
|
+
Permission.ProjectAdmin,
|
|
1831
|
+
`CreateDatabaseServer${kind}` as Permission,
|
|
1832
|
+
]),
|
|
1833
|
+
);
|
|
1834
|
+
expect(model.getReadPermissions()).toContain(
|
|
1835
|
+
`ReadDatabaseServer${kind}` as Permission,
|
|
1836
|
+
);
|
|
1837
|
+
expect(model.enableWorkflowOn).toBeTruthy();
|
|
1838
|
+
},
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
test.each([
|
|
1842
|
+
DatabaseServerLabelRule,
|
|
1843
|
+
DatabaseServerOwnerRule,
|
|
1844
|
+
] as Array<ModelType>)(
|
|
1845
|
+
"%p matches on exactly the three registered criteria fields",
|
|
1846
|
+
(modelType: ModelType) => {
|
|
1847
|
+
/*
|
|
1848
|
+
* RuleCriteriaFieldRegistry, the rule engines' legacyFields and the
|
|
1849
|
+
* dashboard's match-criteria step all list these three names; a
|
|
1850
|
+
* rename here breaks all three silently.
|
|
1851
|
+
*/
|
|
1852
|
+
const model: BaseModel = new modelType();
|
|
1853
|
+
const columns: Array<string> = model.getTableColumns().columns;
|
|
1854
|
+
|
|
1855
|
+
for (const field of RULE_CRITERIA_FIELDS) {
|
|
1856
|
+
expect(columns).toContain(field);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
const matchColumns: Array<string> = columns.filter(
|
|
1860
|
+
(column: string): boolean => {
|
|
1861
|
+
return column.startsWith("databaseServer");
|
|
1862
|
+
},
|
|
1863
|
+
);
|
|
1864
|
+
|
|
1865
|
+
expect(matchColumns.sort()).toEqual([...RULE_CRITERIA_FIELDS].sort());
|
|
1866
|
+
|
|
1867
|
+
expect(model.getTableColumnMetadata("databaseServerLabels").type).toBe(
|
|
1868
|
+
TableColumnType.EntityArray,
|
|
1869
|
+
);
|
|
1870
|
+
expect(
|
|
1871
|
+
model.getTableColumnMetadata("databaseServerLabels").modelType,
|
|
1872
|
+
).toBe(Label);
|
|
1873
|
+
expect(
|
|
1874
|
+
model.getTableColumnMetadata("databaseServerNamePattern").type,
|
|
1875
|
+
).toBe(TableColumnType.LongText);
|
|
1876
|
+
expect(
|
|
1877
|
+
model.getTableColumnMetadata("databaseServerDescriptionPattern").type,
|
|
1878
|
+
).toBe(TableColumnType.LongText);
|
|
1879
|
+
},
|
|
1880
|
+
);
|
|
1881
|
+
|
|
1882
|
+
test("label rule attaches labels through its own join tables", () => {
|
|
1883
|
+
const model: DatabaseServerLabelRule = new DatabaseServerLabelRule();
|
|
1884
|
+
|
|
1885
|
+
for (const column of [
|
|
1886
|
+
"name",
|
|
1887
|
+
"description",
|
|
1888
|
+
"isEnabled",
|
|
1889
|
+
"labelsToAdd",
|
|
1890
|
+
]) {
|
|
1891
|
+
expect(model.getTableColumns().columns).toContain(column);
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const matchLabels: JoinTableMetadataArgs = joinTableArgs(
|
|
1895
|
+
DatabaseServerLabelRule,
|
|
1896
|
+
"databaseServerLabels",
|
|
1897
|
+
);
|
|
1898
|
+
|
|
1899
|
+
expect(matchLabels.name).toBe(
|
|
1900
|
+
"DatabaseServerLabelRuleDatabaseServerLabel",
|
|
1901
|
+
);
|
|
1902
|
+
expect(matchLabels.joinColumns?.[0]?.name).toBe(
|
|
1903
|
+
"databaseServerLabelRuleId",
|
|
1904
|
+
);
|
|
1905
|
+
expect(matchLabels.inverseJoinColumns?.[0]?.name).toBe("labelId");
|
|
1906
|
+
|
|
1907
|
+
const labelsToAdd: JoinTableMetadataArgs = joinTableArgs(
|
|
1908
|
+
DatabaseServerLabelRule,
|
|
1909
|
+
"labelsToAdd",
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1912
|
+
expect(labelsToAdd.name).toBe("DatabaseServerLabelRuleLabelToAdd");
|
|
1913
|
+
expect(labelsToAdd.joinColumns?.[0]?.name).toBe(
|
|
1914
|
+
"databaseServerLabelRuleId",
|
|
1915
|
+
);
|
|
1916
|
+
});
|
|
1917
|
+
|
|
1918
|
+
test("owner rule assigns users and teams through its own join tables", () => {
|
|
1919
|
+
const model: DatabaseServerOwnerRule = new DatabaseServerOwnerRule();
|
|
1920
|
+
|
|
1921
|
+
for (const column of [
|
|
1922
|
+
"name",
|
|
1923
|
+
"description",
|
|
1924
|
+
"isEnabled",
|
|
1925
|
+
"notifyOwners",
|
|
1926
|
+
"ownerUsers",
|
|
1927
|
+
"ownerTeams",
|
|
1928
|
+
]) {
|
|
1929
|
+
expect(model.getTableColumns().columns).toContain(column);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
expect(
|
|
1933
|
+
joinTableArgs(DatabaseServerOwnerRule, "databaseServerLabels").name,
|
|
1934
|
+
).toBe("DatabaseServerOwnerRuleDatabaseServerLabel");
|
|
1935
|
+
expect(joinTableArgs(DatabaseServerOwnerRule, "ownerUsers").name).toBe(
|
|
1936
|
+
"DatabaseServerOwnerRuleOwnerUser",
|
|
1937
|
+
);
|
|
1938
|
+
expect(joinTableArgs(DatabaseServerOwnerRule, "ownerTeams").name).toBe(
|
|
1939
|
+
"DatabaseServerOwnerRuleOwnerTeam",
|
|
1940
|
+
);
|
|
1941
|
+
|
|
1942
|
+
for (const property of [
|
|
1943
|
+
"databaseServerLabels",
|
|
1944
|
+
"ownerUsers",
|
|
1945
|
+
"ownerTeams",
|
|
1946
|
+
]) {
|
|
1947
|
+
expect(
|
|
1948
|
+
joinTableArgs(DatabaseServerOwnerRule, property).joinColumns?.[0]
|
|
1949
|
+
?.name,
|
|
1950
|
+
).toBe("databaseServerOwnerRuleId");
|
|
1951
|
+
}
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
test("the name pattern tells people the engine is part of the name", () => {
|
|
1955
|
+
for (const modelType of [
|
|
1956
|
+
DatabaseServerLabelRule,
|
|
1957
|
+
DatabaseServerOwnerRule,
|
|
1958
|
+
] as Array<ModelType>) {
|
|
1959
|
+
expect(
|
|
1960
|
+
new modelType().getTableColumnMetadata("databaseServerNamePattern")
|
|
1961
|
+
.description,
|
|
1962
|
+
).toContain("^PostgreSQL");
|
|
1963
|
+
}
|
|
1964
|
+
});
|
|
1965
|
+
});
|
|
1966
|
+
|
|
1967
|
+
describe("Postgres identifier limits", () => {
|
|
1968
|
+
/*
|
|
1969
|
+
* Postgres silently truncates identifiers to 63 bytes, so two long names
|
|
1970
|
+
* that share a prefix become the same table, and a generated migration
|
|
1971
|
+
* never matches its own output again.
|
|
1972
|
+
*/
|
|
1973
|
+
const LIMIT: number = 63;
|
|
1974
|
+
|
|
1975
|
+
test("every table, join table, join column and named index fits", () => {
|
|
1976
|
+
const names: Array<string> = [];
|
|
1977
|
+
const targets: Array<unknown> = DATABASE_MODELS.map(
|
|
1978
|
+
(spec: DatabaseModelSpec): unknown => {
|
|
1979
|
+
return spec.modelType;
|
|
1980
|
+
},
|
|
1981
|
+
);
|
|
1982
|
+
|
|
1983
|
+
for (const spec of DATABASE_MODELS) {
|
|
1984
|
+
names.push(spec.tableName);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
for (const joinTable of getMetadataArgsStorage()
|
|
1988
|
+
.joinTables as Array<JoinTableMetadataArgs>) {
|
|
1989
|
+
if (!targets.includes(joinTable.target)) {
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
names.push(joinTable.name!);
|
|
1994
|
+
|
|
1995
|
+
for (const column of [
|
|
1996
|
+
...(joinTable.joinColumns || []),
|
|
1997
|
+
...(joinTable.inverseJoinColumns || []),
|
|
1998
|
+
]) {
|
|
1999
|
+
names.push(column.name!);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
for (const target of targets) {
|
|
2004
|
+
for (const index of classIndexes(target as ModelType)) {
|
|
2005
|
+
if (index.name) {
|
|
2006
|
+
names.push(index.name);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
// The three affected-resource join tables live on other models.
|
|
2012
|
+
for (const [modelType] of AFFECTED) {
|
|
2013
|
+
names.push(joinTableArgs(modelType, "databaseServers").name!);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
expect(names.length).toBeGreaterThan(15);
|
|
2017
|
+
|
|
2018
|
+
const tooLong: Array<string> = names.filter((name: string): boolean => {
|
|
2019
|
+
return name.length > LIMIT;
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
expect(tooLong).toEqual([]);
|
|
2023
|
+
});
|
|
2024
|
+
|
|
2025
|
+
test("every column name fits", () => {
|
|
2026
|
+
for (const spec of DATABASE_MODELS) {
|
|
2027
|
+
for (const column of new spec.modelType().getTableColumns().columns) {
|
|
2028
|
+
expect(column.length).toBeLessThanOrEqual(LIMIT);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
});
|
|
2032
|
+
});
|
|
2033
|
+
|
|
2034
|
+
describe("permissions", () => {
|
|
2035
|
+
test("all 23 permissions exist in the enum and the catalogue", () => {
|
|
2036
|
+
const enumValues: Set<string> = new Set(Object.values(Permission));
|
|
2037
|
+
|
|
2038
|
+
for (const name of DATABASE_SERVER_PERMISSIONS) {
|
|
2039
|
+
expect(enumValues.has(name)).toBe(true);
|
|
2040
|
+
expect(Permission[name as keyof typeof Permission]).toBe(name);
|
|
2041
|
+
|
|
2042
|
+
const props: PermissionProps | undefined =
|
|
2043
|
+
PERMISSION_PROPS_BY_NAME.get(name);
|
|
2044
|
+
|
|
2045
|
+
expect(props).toBeDefined();
|
|
2046
|
+
expect(props!.isAssignableToTenant).toBe(true);
|
|
2047
|
+
expect(props!.isRolePermission).toBe(false);
|
|
2048
|
+
expect(props!.group).toBe("Telemetry");
|
|
2049
|
+
expect(props!.title).toContain("Database");
|
|
2050
|
+
expect(props!.title).not.toContain("DatabaseServer");
|
|
2051
|
+
expect(props!.title).not.toContain("Ceph");
|
|
2052
|
+
expect(props!.description.length).toBeGreaterThan(0);
|
|
2053
|
+
expect(props!.description).not.toContain("Ceph");
|
|
2054
|
+
|
|
2055
|
+
// The lookups the permission UI and the error messages use.
|
|
2056
|
+
expect(PermissionHelper.getTitle(name as Permission)).toBe(
|
|
2057
|
+
props!.title,
|
|
2058
|
+
);
|
|
2059
|
+
expect(PermissionHelper.getDescription(name as Permission)).toBe(
|
|
2060
|
+
props!.description,
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// No stray members - the catalogue must not sprout a 24th.
|
|
2065
|
+
const members: Array<string> = Array.from(enumValues).filter(
|
|
2066
|
+
(value: string) => {
|
|
2067
|
+
return value.includes("DatabaseServer");
|
|
2068
|
+
},
|
|
2069
|
+
);
|
|
2070
|
+
|
|
2071
|
+
expect(members.sort()).toEqual([...DATABASE_SERVER_PERMISSIONS].sort());
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
test("each permission appears exactly once in the props catalogue", () => {
|
|
2075
|
+
for (const name of DATABASE_SERVER_PERMISSIONS) {
|
|
2076
|
+
expect(
|
|
2077
|
+
PERMISSION_PROPS.filter((props: PermissionProps): boolean => {
|
|
2078
|
+
return props.permission.toString() === name;
|
|
2079
|
+
}),
|
|
2080
|
+
).toHaveLength(1);
|
|
2081
|
+
}
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
test("titles are unique among the catalogue", () => {
|
|
2085
|
+
for (const name of DATABASE_SERVER_PERMISSIONS) {
|
|
2086
|
+
const title: string = PERMISSION_PROPS_BY_NAME.get(name)!.title;
|
|
2087
|
+
|
|
2088
|
+
expect(
|
|
2089
|
+
PERMISSION_PROPS.filter((props: PermissionProps): boolean => {
|
|
2090
|
+
return props.title === title;
|
|
2091
|
+
}),
|
|
2092
|
+
).toHaveLength(1);
|
|
2093
|
+
}
|
|
2094
|
+
});
|
|
2095
|
+
|
|
2096
|
+
test("database Delete/Edit/Read are label-scoped access-control permissions; nothing else is", () => {
|
|
2097
|
+
for (const name of DATABASE_SERVER_PERMISSIONS) {
|
|
2098
|
+
const props: PermissionProps = PERMISSION_PROPS_BY_NAME.get(name)!;
|
|
2099
|
+
const labelScoped: boolean = [
|
|
2100
|
+
"DeleteDatabaseServer",
|
|
2101
|
+
"EditDatabaseServer",
|
|
2102
|
+
"ReadDatabaseServer",
|
|
2103
|
+
].includes(name);
|
|
2104
|
+
|
|
2105
|
+
expect({ name, flag: props.isAccessControlPermission }).toEqual({
|
|
2106
|
+
name,
|
|
2107
|
+
flag: labelScoped,
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
test("feeds have no Delete permission - they are append-only", () => {
|
|
2113
|
+
expect(Object.values(Permission)).not.toContain(
|
|
2114
|
+
"DeleteDatabaseServerFeed",
|
|
2115
|
+
);
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
test.each(DATABASE_MODELS)(
|
|
2119
|
+
"$name references only real, catalogued permissions",
|
|
2120
|
+
(spec: DatabaseModelSpec) => {
|
|
2121
|
+
const enumValues: Set<string> = new Set(Object.values(Permission));
|
|
2122
|
+
const model: BaseModel = new spec.modelType();
|
|
2123
|
+
|
|
2124
|
+
for (const permission of allReferencedPermissions(model)) {
|
|
2125
|
+
const value: string = permission.toString();
|
|
2126
|
+
|
|
2127
|
+
expect(enumValues.has(value)).toBe(true);
|
|
2128
|
+
expect(PERMISSION_PROPS_BY_NAME.has(value)).toBe(true);
|
|
2129
|
+
}
|
|
2130
|
+
},
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
test.each(DATABASE_MODELS)(
|
|
2134
|
+
"$name gates on Database permissions, never on another product's",
|
|
2135
|
+
(spec: DatabaseModelSpec) => {
|
|
2136
|
+
const model: BaseModel = new spec.modelType();
|
|
2137
|
+
|
|
2138
|
+
for (const permission of allReferencedPermissions(model)) {
|
|
2139
|
+
const value: string = permission.toString();
|
|
2140
|
+
|
|
2141
|
+
expect(value).not.toContain("Ceph");
|
|
2142
|
+
expect(value).not.toContain("CloudResource");
|
|
2143
|
+
expect(value).not.toContain("VMware");
|
|
2144
|
+
}
|
|
2145
|
+
},
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
test.each(DATABASE_MODELS)(
|
|
2149
|
+
"$name has no column that requires a permission its table does not accept on create",
|
|
2150
|
+
(spec: DatabaseModelSpec) => {
|
|
2151
|
+
/*
|
|
2152
|
+
* The PermissionCatalogueCoverage sweep, scoped: a granular holder
|
|
2153
|
+
* that passes the table gate must not fail on a column gate.
|
|
2154
|
+
*/
|
|
2155
|
+
const model: BaseModel = new spec.modelType();
|
|
2156
|
+
const roles: Set<string> = new Set(
|
|
2157
|
+
PermissionHelper.getRolePermissionProps().map(
|
|
2158
|
+
(props: PermissionProps): string => {
|
|
2159
|
+
return props.permission.toString();
|
|
2160
|
+
},
|
|
2161
|
+
),
|
|
2162
|
+
);
|
|
2163
|
+
const tableCreate: Set<string> = new Set(
|
|
2164
|
+
sorted(model.getCreatePermissions()),
|
|
2165
|
+
);
|
|
2166
|
+
|
|
2167
|
+
for (const column of ownColumns(model)) {
|
|
2168
|
+
for (const permission of columnAccess(model, column).create || []) {
|
|
2169
|
+
const value: string = permission.toString();
|
|
2170
|
+
|
|
2171
|
+
expect({
|
|
2172
|
+
column,
|
|
2173
|
+
value,
|
|
2174
|
+
ok: roles.has(value) || tableCreate.has(value),
|
|
2175
|
+
}).toEqual({ column, value, ok: true });
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
},
|
|
2179
|
+
);
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
describe("affected-resource relations", () => {
|
|
2183
|
+
test.each(AFFECTED)(
|
|
2184
|
+
"%p.databaseServers is a ManyToMany through %s",
|
|
2185
|
+
(modelType: ModelType, joinTableName: string, joinColumn: string) => {
|
|
2186
|
+
const model: BaseModel = new modelType();
|
|
2187
|
+
const metadata: TableColumnMetadata =
|
|
2188
|
+
model.getTableColumnMetadata("databaseServers");
|
|
2189
|
+
|
|
2190
|
+
expect(metadata).toBeDefined();
|
|
2191
|
+
expect(metadata.type).toBe(TableColumnType.EntityArray);
|
|
2192
|
+
expect(metadata.modelType).toBe(DatabaseServer);
|
|
2193
|
+
expect(metadata.title).toBe("Databases");
|
|
2194
|
+
expect(metadata.required).toBeFalsy();
|
|
2195
|
+
|
|
2196
|
+
const relation: RelationMetadataArgs = relationArgs(
|
|
2197
|
+
modelType,
|
|
2198
|
+
"databaseServers",
|
|
2199
|
+
);
|
|
2200
|
+
|
|
2201
|
+
expect(relation.relationType).toBe("many-to-many");
|
|
2202
|
+
|
|
2203
|
+
const joinTable: JoinTableMetadataArgs = joinTableArgs(
|
|
2204
|
+
modelType,
|
|
2205
|
+
"databaseServers",
|
|
2206
|
+
);
|
|
2207
|
+
|
|
2208
|
+
expect(joinTable.name).toBe(joinTableName);
|
|
2209
|
+
expect(joinTable.joinColumns?.[0]?.name).toBe(joinColumn);
|
|
2210
|
+
expect(joinTable.inverseJoinColumns?.[0]?.name).toBe(
|
|
2211
|
+
"databaseServerId",
|
|
2212
|
+
);
|
|
2213
|
+
|
|
2214
|
+
// The initialiser matters: BaseModel enumerates own properties.
|
|
2215
|
+
expect(
|
|
2216
|
+
Object.prototype.hasOwnProperty.call(model, "databaseServers"),
|
|
2217
|
+
).toBe(true);
|
|
2218
|
+
},
|
|
2219
|
+
);
|
|
2220
|
+
|
|
2221
|
+
test.each(AFFECTED)(
|
|
2222
|
+
"%p.databaseServers shares the cephClusters access-control lists",
|
|
2223
|
+
(modelType: ModelType) => {
|
|
2224
|
+
const model: BaseModel = new modelType();
|
|
2225
|
+
|
|
2226
|
+
expect(model.getColumnAccessControlFor("databaseServers")).toEqual(
|
|
2227
|
+
model.getColumnAccessControlFor("cephClusters"),
|
|
2228
|
+
);
|
|
2229
|
+
},
|
|
2230
|
+
);
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
describe("telemetry discriminator", () => {
|
|
2234
|
+
test("ServiceType.DatabaseServer is the table name, like every infra type", () => {
|
|
2235
|
+
/*
|
|
2236
|
+
* Stored verbatim in ClickHouse's serviceType / primaryEntityType, so
|
|
2237
|
+
* the value is a contract, not a label.
|
|
2238
|
+
*/
|
|
2239
|
+
expect(ServiceType.DatabaseServer).toBe("DatabaseServer");
|
|
2240
|
+
expect(ServiceType.DatabaseServer.toString()).toBe(
|
|
2241
|
+
new DatabaseServer().tableName,
|
|
2242
|
+
);
|
|
2243
|
+
expect(
|
|
2244
|
+
Object.values(ServiceType).filter((value: string): boolean => {
|
|
2245
|
+
return value === "DatabaseServer";
|
|
2246
|
+
}),
|
|
2247
|
+
).toHaveLength(1);
|
|
2248
|
+
});
|
|
2249
|
+
});
|
|
2250
|
+
});
|