@oneuptime/common 12.0.5 → 12.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/AIRun.ts +33 -0
- package/Models/DatabaseModels/DataSource.ts +744 -0
- package/Models/DatabaseModels/Index.ts +3 -0
- package/Models/DatabaseModels/ProjectCallSMSConfig.ts +10 -2
- package/Models/DatabaseModels/User.ts +2 -0
- package/Models/DatabaseModels/UserOnCallLog.ts +2 -0
- package/Models/DatabaseModels/UserOnCallLogTimeline.ts +2 -0
- package/Models/DatabaseModels/UserSession.ts +2 -0
- package/Models/DatabaseModels/UserTotpAuth.ts +6 -4
- package/Models/DatabaseModels/UserWebAuthn.ts +5 -3
- package/Server/API/AIInsightAPI.ts +47 -4
- package/Server/API/AIInvestigationAPI.ts +25 -0
- package/Server/API/DashboardAPI.ts +176 -492
- package/Server/API/DataSourceAPI.ts +268 -0
- package/Server/API/StatusPageAPI.ts +11 -1
- package/Server/API/UserAPI.ts +119 -0
- package/Server/Infrastructure/ClickhouseConfig.ts +59 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.ts +39 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.ts +24 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.ts +76 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Infrastructure/Queue.ts +26 -0
- package/Server/Infrastructure/QueueWorker.ts +17 -0
- package/Server/Middleware/TelemetryIngest.ts +8 -3
- package/Server/Services/AIInsightService.ts +132 -12
- package/Server/Services/AIRunService.ts +34 -0
- package/Server/Services/AIService.ts +17 -2
- package/Server/Services/AlertService.ts +38 -32
- package/Server/Services/AnalyticsDatabaseService.ts +8 -4
- package/Server/Services/DataSourceService.ts +200 -0
- package/Server/Services/HostService.ts +92 -0
- package/Server/Services/IncidentService.ts +45 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/MonitorService.ts +49 -0
- package/Server/Services/MonitorStatusService.ts +166 -0
- package/Server/Services/OpenTelemetryIngestService.ts +255 -184
- package/Server/Services/RumSessionPinService.ts +7 -21
- package/Server/Services/StatusPageSubscriberService.ts +13 -0
- package/Server/Services/UserService.ts +280 -9
- package/Server/Static/Vendor/README.md +62 -0
- package/Server/Static/Vendor/fonts/InterVariable.woff2 +0 -0
- package/Server/Static/Vendor/highlight/highlight.min.js +1244 -0
- package/Server/Static/Vendor/highlight/languages/apache.min.js +15 -0
- package/Server/Static/Vendor/highlight/languages/bash.min.js +21 -0
- package/Server/Static/Vendor/highlight/languages/c.min.js +41 -0
- package/Server/Static/Vendor/highlight/languages/cpp.min.js +47 -0
- package/Server/Static/Vendor/highlight/languages/csharp.min.js +49 -0
- package/Server/Static/Vendor/highlight/languages/css.min.js +31 -0
- package/Server/Static/Vendor/highlight/languages/diff.min.js +9 -0
- package/Server/Static/Vendor/highlight/languages/dns.min.js +11 -0
- package/Server/Static/Vendor/highlight/languages/dockerfile.min.js +8 -0
- package/Server/Static/Vendor/highlight/languages/go.min.js +20 -0
- package/Server/Static/Vendor/highlight/languages/graphql.min.js +12 -0
- package/Server/Static/Vendor/highlight/languages/http.min.js +14 -0
- package/Server/Static/Vendor/highlight/languages/ini.min.js +15 -0
- package/Server/Static/Vendor/highlight/languages/java.min.js +38 -0
- package/Server/Static/Vendor/highlight/languages/javascript.min.js +81 -0
- package/Server/Static/Vendor/highlight/languages/json.min.js +8 -0
- package/Server/Static/Vendor/highlight/languages/kotlin.min.js +46 -0
- package/Server/Static/Vendor/highlight/languages/lua.min.js +15 -0
- package/Server/Static/Vendor/highlight/languages/makefile.min.js +14 -0
- package/Server/Static/Vendor/highlight/languages/nginx.min.js +21 -0
- package/Server/Static/Vendor/highlight/languages/perl.min.js +41 -0
- package/Server/Static/Vendor/highlight/languages/php.min.js +59 -0
- package/Server/Static/Vendor/highlight/languages/powershell.min.js +39 -0
- package/Server/Static/Vendor/highlight/languages/python.min.js +42 -0
- package/Server/Static/Vendor/highlight/languages/r.min.js +26 -0
- package/Server/Static/Vendor/highlight/languages/ruby.min.js +54 -0
- package/Server/Static/Vendor/highlight/languages/rust.min.js +27 -0
- package/Server/Static/Vendor/highlight/languages/scala.min.js +28 -0
- package/Server/Static/Vendor/highlight/languages/sql.min.js +21 -0
- package/Server/Static/Vendor/highlight/languages/swift.min.js +68 -0
- package/Server/Static/Vendor/highlight/languages/typescript.min.js +99 -0
- package/Server/Static/Vendor/highlight/languages/xml.min.js +29 -0
- package/Server/Static/Vendor/highlight/languages/yaml.min.js +28 -0
- package/Server/Static/Vendor/highlight/styles/github-dark.min.css +10 -0
- package/Server/Static/Vendor/highlight/styles/vs2015.min.css +1 -0
- package/Server/Static/Vendor/tailwind/tailwind-3.4.5.js +21028 -0
- package/Server/Types/Database/Permissions/BasePermission.ts +7 -0
- package/Server/Types/Database/Permissions/DeletePermission.ts +2 -0
- package/Server/Types/Database/Permissions/ReadPermission.ts +3 -0
- package/Server/Types/Database/Permissions/TenantPermission.ts +101 -16
- package/Server/Utils/AI/CodeFix/FixRunBudget.ts +2 -2
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +90 -11
- package/Server/Utils/AI/SRE/AlertInvestigationRunner.ts +1 -0
- package/Server/Utils/AI/SRE/IncidentInvestigationRunner.ts +1 -0
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.ts +125 -0
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +25 -15
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +256 -57
- package/Server/Utils/AI/SRE/Insights/InsightStore.ts +76 -18
- package/Server/Utils/AI/SRE/InvestigationTldr.ts +352 -0
- package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +7 -2
- package/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.ts +1793 -0
- package/Server/Utils/Dashboard/PublicDashboardViewConfig.ts +65 -0
- package/Server/Utils/DataSource/ConnectorRegistry.ts +53 -0
- package/Server/Utils/DataSource/Connectors/ClickHouseConnector.ts +338 -0
- package/Server/Utils/DataSource/Connectors/ElasticsearchConnector.ts +717 -0
- package/Server/Utils/DataSource/Connectors/LokiConnector.ts +606 -0
- package/Server/Utils/DataSource/Connectors/MySqlConnector.ts +307 -0
- package/Server/Utils/DataSource/Connectors/PostgresConnector.ts +260 -0
- package/Server/Utils/DataSource/Connectors/PrometheusConnector.ts +555 -0
- package/Server/Utils/DataSource/Connectors/RestApiConnector.ts +597 -0
- package/Server/Utils/DataSource/Connectors/SqlServerConnector.ts +318 -0
- package/Server/Utils/DataSource/EgressGuard.ts +446 -0
- package/Server/Utils/DataSource/HttpFetch.ts +221 -0
- package/Server/Utils/DataSource/SqlQueryGuard.ts +383 -0
- package/Server/Utils/DataSource/SqlResultShaper.ts +372 -0
- package/Server/Utils/DataSource/Types.ts +93 -0
- package/Server/Utils/Database/PostgresErrorTranslator.ts +172 -17
- package/Server/Utils/InProcessMemo.ts +141 -0
- package/Server/Utils/InstanceHealth/TelemetryIngestion.ts +628 -0
- package/Server/Utils/Monitor/MonitorMetricUtil.ts +58 -0
- package/Server/Utils/Monitor/MonitorResource.ts +14 -0
- package/Server/Utils/Response.ts +12 -1
- package/Server/Utils/SSRFProtection.ts +237 -0
- package/Server/Utils/StartServer.ts +8 -0
- package/Server/Utils/StatusPageSubscriberWebhook.ts +30 -0
- package/Server/Utils/Telemetry/CaptureSpan.ts +30 -18
- package/Server/Utils/Telemetry/IngestionTimestamp.ts +75 -0
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +99 -7
- package/Server/Utils/Telemetry/Telemetry.ts +112 -1
- package/Server/Utils/Telemetry.ts +104 -3
- package/Server/Utils/VendorAssets.ts +165 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +28 -5
- package/Server/Utils/Workspace/Slack/Slack.ts +5 -0
- package/Server/Views/Partials/Head.ejs +21 -7
- package/Tests/App/AdminDashboard/AdminProjectsIdFilter.test.tsx +207 -0
- package/Tests/App/AdminDashboard/AdminUsersIdFilter.test.tsx +216 -0
- package/Tests/App/Dashboard/AIInsightViewActions.test.tsx +422 -0
- package/Tests/App/Dashboard/AddWidgetModal.test.tsx +10 -3
- package/Tests/App/Dashboard/AlertsSideMenu.test.tsx +387 -0
- package/Tests/App/Dashboard/ChatActivityFeed.test.tsx +199 -0
- package/Tests/App/Dashboard/DataSourceChartSyntheticConfig.test.tsx +736 -0
- package/Tests/App/Dashboard/DataSourceQueryEditor.test.tsx +868 -0
- package/Tests/App/Dashboard/DataSourceWidgetCatalog.test.ts +440 -0
- package/Tests/App/Dashboard/DataSourceWidgetFetching.test.tsx +2109 -0
- package/Tests/App/Dashboard/DataSourceWidgetViews.test.tsx +726 -0
- package/Tests/App/Dashboard/IncidentsSideMenu.test.tsx +447 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +514 -3
- package/Tests/App/Dashboard/ProductMenuBreadcrumbs.test.tsx +391 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceSideMenu.test.tsx +326 -0
- package/Tests/App/Dashboard/SideMenuHarness.tsx +187 -0
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +6 -1
- package/Tests/App/StatusPage/StatusPageResourceExplorer.test.tsx +1858 -0
- package/Tests/Models/DatabaseModels/DataSourceModelSecrets.test.ts +70 -0
- package/Tests/Server/API/AIInvestigationAPI.test.ts +187 -0
- package/Tests/Server/API/DashboardPublicDataSourceStrip.test.ts +686 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +585 -823
- package/Tests/Server/API/DataSourceAPIRoutes.test.ts +486 -0
- package/Tests/Server/API/StatusPageSubscriberOwnership.test.ts +291 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +641 -0
- package/Tests/Server/Infrastructure/ClickhouseKeepAliveOptions.test.ts +441 -0
- package/Tests/Server/Middleware/TelemetryIngestTokenLog.test.ts +140 -0
- package/Tests/Server/Services/AIInsightService.test.ts +266 -11
- package/Tests/Server/Services/AIRunAnalysisTldrPersistence.test.ts +160 -0
- package/Tests/Server/Services/AIServiceDailyBudget.test.ts +4 -0
- package/Tests/Server/Services/AlertMetricResourceAttributes.test.ts +352 -0
- package/Tests/Server/Services/DataSourceServiceSettings.test.ts +244 -0
- package/Tests/Server/Services/HostServiceFindOrCreateMemo.test.ts +397 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +41 -2
- package/Tests/Server/Services/IncidentMetricResourceAttributes.test.ts +408 -0
- package/Tests/Server/Services/MonitorStatusServiceDeleteReferenceCleanup.test.ts +351 -0
- package/Tests/Server/Services/MonitorStatusServiceValidation.test.ts +171 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +398 -0
- package/Tests/Server/Services/OpenTelemetryServiceResolutionCache.test.ts +19 -0
- package/Tests/Server/Services/ProjectCallSMSConfigCredentialUniqueness.test.ts +305 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +36 -1
- package/Tests/Server/Services/RumSessionPinUniqueViolationRecovery.test.ts +163 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +973 -0
- package/Tests/Server/Types/Database/ObjectIdColumnQuery.test.ts +262 -0
- package/Tests/Server/Types/Database/Permissions/TenantPermission.test.ts +358 -0
- package/Tests/Server/Utils/AI/FixRunBudget.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/ExceptionSpikeDetector.test.ts +6 -1
- package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +121 -4
- package/Tests/Server/Utils/AI/Insights/NewExceptionDetector.test.ts +361 -10
- package/Tests/Server/Utils/AI/InvestigationSettlement.test.ts +292 -0
- package/Tests/Server/Utils/AI/InvestigationTldr.test.ts +623 -0
- package/Tests/Server/Utils/AI/InvestigationTldrWiring.test.ts +98 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.test.ts +857 -0
- package/Tests/Server/Utils/Dashboard/PublicDashboardViewConfig.test.ts +480 -0
- package/Tests/Server/Utils/DataSource/ClickHouseConnector.test.ts +759 -0
- package/Tests/Server/Utils/DataSource/ConnectorRegistry.test.ts +37 -0
- package/Tests/Server/Utils/DataSource/EgressGuard.test.ts +645 -0
- package/Tests/Server/Utils/DataSource/ElasticsearchConnector.test.ts +1206 -0
- package/Tests/Server/Utils/DataSource/HttpFetch.test.ts +616 -0
- package/Tests/Server/Utils/DataSource/LokiConnector.test.ts +970 -0
- package/Tests/Server/Utils/DataSource/MySqlConnector.test.ts +935 -0
- package/Tests/Server/Utils/DataSource/PostgresConnector.test.ts +854 -0
- package/Tests/Server/Utils/DataSource/PrometheusConnector.test.ts +881 -0
- package/Tests/Server/Utils/DataSource/RestApiConnector.test.ts +1020 -0
- package/Tests/Server/Utils/DataSource/SqlQueryGuard.test.ts +425 -0
- package/Tests/Server/Utils/DataSource/SqlResultShaper.test.ts +555 -0
- package/Tests/Server/Utils/DataSource/SqlServerConnector.test.ts +934 -0
- package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +228 -7
- package/Tests/Server/Utils/InProcessMemo.test.ts +292 -0
- package/Tests/Server/Utils/InstanceHealth/TelemetryIngestion.test.ts +960 -0
- package/Tests/Server/Utils/MicrosoftTeamsWebhookUrlValidation.test.ts +103 -0
- package/Tests/Server/Utils/Monitor/MonitorMetricResourceAttributes.test.ts +336 -0
- package/Tests/Server/Utils/OfflineAssetHygiene.test.ts +521 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +143 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +193 -0
- package/Tests/Server/Utils/StatusPageSubscriberWebhook.test.ts +101 -0
- package/Tests/Server/Utils/TailwindBuildCopy.test.ts +225 -0
- package/Tests/Server/Utils/Telemetry/ResourceHeartbeatL1Memo.test.ts +355 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +128 -0
- package/Tests/Server/Utils/Telemetry/TelemetryUtilAttributeKeys.test.ts +381 -0
- package/Tests/Server/Utils/Telemetry.test.ts +87 -0
- package/Tests/Server/Utils/VendorAssetIntegrity.test.ts +343 -0
- package/Tests/Server/Utils/VendorAssets.test.ts +671 -0
- package/Tests/Server/Views/OfflineRender.test.ts +297 -0
- package/Tests/Types/Dashboard/DataSourceComponentTypes.test.ts +618 -0
- package/Tests/Types/DateClickhouseFormat.test.ts +192 -0
- package/Tests/Types/Password.test.ts +363 -0
- package/Tests/UI/Components/ConfirmModal.test.tsx +7 -0
- package/Tests/UI/Components/ErrorBoundary.test.tsx +53 -0
- package/Tests/UI/Components/Modal.test.tsx +281 -1
- package/Tests/UI/Components/ObjectIdFilterPipeline.test.tsx +396 -0
- package/Tests/UI/Components/StatusPage/ResourceGroupNavigator.test.tsx +1147 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +116 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
- package/Tests/Utils/Analytics.test.ts +23 -0
- package/Tests/Utils/Dashboard/Components/DashboardDataSourceComponents.test.ts +640 -0
- package/Tests/Utils/DataSource/DataSourceQueryText.test.ts +138 -0
- package/Tests/Utils/DataSource/DataSourceValueReducer.test.ts +526 -0
- package/Tests/Utils/Metrics/MetricResourceAttributeUtil.test.ts +691 -0
- package/Tests/Utils/StatusPage/GroupHierarchyView.test.ts +1452 -0
- package/Tests/Utils/StatusPage/ResourceExplorer.test.ts +931 -0
- package/Tests/Utils/Telemetry/CaptureSpanExportGating.test.ts +288 -0
- package/Tests/Utils/Telemetry/IngestionTimestamp.test.ts +170 -0
- package/Types/AI/AIInsightEvidence.ts +6 -0
- package/Types/Analytics/RevenueEvent.ts +31 -0
- package/Types/Dashboard/DashboardComponentType.ts +31 -0
- package/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.ts +27 -0
- package/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.ts +32 -0
- package/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.ts +30 -0
- package/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.ts +55 -0
- package/Types/DataSource/DataSourceLimits.ts +62 -0
- package/Types/DataSource/DataSourceQueryConfig.ts +41 -0
- package/Types/DataSource/DataSourceTableResult.ts +38 -0
- package/Types/DataSource/DataSourceType.ts +213 -0
- package/Types/Date.ts +43 -3
- package/Types/Password.ts +64 -0
- package/Types/Permission.ts +46 -0
- package/Types/UserAuthenticationStatus.ts +34 -0
- package/UI/Components/Button/Button.tsx +1 -1
- package/UI/Components/ErrorBoundary.tsx +459 -108
- package/UI/Components/Icon/Icon.tsx +10 -9
- package/UI/Components/Modal/ConfirmModal.tsx +6 -1
- package/UI/Components/Modal/Modal.tsx +267 -35
- package/UI/Components/Modal/ModalFooter.tsx +6 -1
- package/UI/Components/MoreMenu/MoreMenu.tsx +19 -4
- package/UI/Components/MoreMenu/MoreMenuItem.tsx +6 -1
- package/UI/Components/StatusPage/ResourceGrid.tsx +314 -0
- package/UI/Components/StatusPage/ResourceGroupNavigator.tsx +808 -0
- package/UI/Components/StatusPage/ResourceList.tsx +570 -0
- package/UI/Styles/Theme.css +18 -0
- package/UI/Utils/API/RequestOptions.ts +7 -0
- package/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.ts +1 -0
- package/UI/Utils/ModelAPI/ModelAPI.ts +1 -0
- package/UI/Utils/TestDataSource.ts +61 -0
- package/UI/esbuild-config.js +45 -0
- package/Utils/Analytics.ts +15 -0
- package/Utils/Dashboard/Components/DashboardDataSourceChartComponent.ts +88 -0
- package/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.ts +123 -0
- package/Utils/Dashboard/Components/DashboardDataSourceShared.ts +16 -0
- package/Utils/Dashboard/Components/DashboardDataSourceTableComponent.ts +72 -0
- package/Utils/Dashboard/Components/DashboardDataSourceValueComponent.ts +126 -0
- package/Utils/Dashboard/Components/Index.ts +28 -0
- package/Utils/DataSource/DataSourceQueryText.ts +94 -0
- package/Utils/DataSource/DataSourceValueReducer.ts +156 -0
- package/Utils/Metrics/MetricResourceAttributeUtil.ts +380 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +8 -6
- package/Utils/StatusPage/GroupHierarchyView.ts +931 -0
- package/Utils/StatusPage/ResourceExplorer.ts +853 -0
- package/build/dist/Models/DatabaseModels/AIRun.js +34 -0
- package/build/dist/Models/DatabaseModels/AIRun.js.map +1 -1
- package/build/dist/Models/DatabaseModels/DataSource.js +774 -0
- package/build/dist/Models/DatabaseModels/DataSource.js.map +1 -0
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js +14 -4
- package/build/dist/Models/DatabaseModels/ProjectCallSMSConfig.js.map +1 -1
- package/build/dist/Models/DatabaseModels/User.js +2 -0
- package/build/dist/Models/DatabaseModels/User.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLog.js +2 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js +2 -0
- package/build/dist/Models/DatabaseModels/UserOnCallLogTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserSession.js +2 -0
- package/build/dist/Models/DatabaseModels/UserSession.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTotpAuth.js +6 -4
- package/build/dist/Models/DatabaseModels/UserTotpAuth.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserWebAuthn.js +5 -3
- package/build/dist/Models/DatabaseModels/UserWebAuthn.js.map +1 -1
- package/build/dist/Server/API/AIInsightAPI.js +34 -4
- package/build/dist/Server/API/AIInsightAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +23 -0
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +119 -433
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/API/DataSourceAPI.js +173 -0
- package/build/dist/Server/API/DataSourceAPI.js.map +1 -0
- package/build/dist/Server/API/StatusPageAPI.js +11 -1
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/UserAPI.js +94 -2
- package/build/dist/Server/API/UserAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js +49 -0
- package/build/dist/Server/Infrastructure/ClickhouseConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.js +20 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786303472848-AddDataSourceTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786600000000-AddInvestigationAnalysisTldr.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.js +64 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786700000000-DropProjectCallSMSConfigCredentialUniques.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Infrastructure/Queue.js +27 -0
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Infrastructure/QueueWorker.js +13 -0
- package/build/dist/Server/Infrastructure/QueueWorker.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +8 -3
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/AIInsightService.js +110 -10
- package/build/dist/Server/Services/AIInsightService.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +34 -0
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/AIService.js +16 -2
- package/build/dist/Server/Services/AIService.js.map +1 -1
- package/build/dist/Server/Services/AlertService.js +28 -35
- package/build/dist/Server/Services/AlertService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +8 -4
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DataSourceService.js +159 -0
- package/build/dist/Server/Services/DataSourceService.js.map +1 -0
- package/build/dist/Server/Services/HostService.js +74 -0
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentService.js +35 -24
- package/build/dist/Server/Services/IncidentService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +45 -0
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusService.js +129 -0
- package/build/dist/Server/Services/MonitorStatusService.js.map +1 -1
- package/build/dist/Server/Services/OpenTelemetryIngestService.js +222 -159
- package/build/dist/Server/Services/OpenTelemetryIngestService.js.map +1 -1
- package/build/dist/Server/Services/RumSessionPinService.js +7 -15
- package/build/dist/Server/Services/RumSessionPinService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageSubscriberService.js +10 -0
- package/build/dist/Server/Services/StatusPageSubscriberService.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +241 -10
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js +6 -0
- package/build/dist/Server/Types/Database/Permissions/BasePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/DeletePermission.js +1 -0
- package/build/dist/Server/Types/Database/Permissions/DeletePermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js +2 -0
- package/build/dist/Server/Types/Database/Permissions/ReadPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +68 -12
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js +2 -2
- package/build/dist/Server/Utils/AI/CodeFix/FixRunBudget.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +68 -8
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js +1 -0
- package/build/dist/Server/Utils/AI/SRE/AlertInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js +1 -0
- package/build/dist/Server/Utils/AI/SRE/IncidentInvestigationRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.js +97 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +17 -15
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +178 -54
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightStore.js +65 -16
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightStore.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationTldr.js +303 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationTldr.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -1
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js +1261 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardResourceListPolicy.js.map +1 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardViewConfig.js +47 -0
- package/build/dist/Server/Utils/Dashboard/PublicDashboardViewConfig.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/ConnectorRegistry.js +42 -0
- package/build/dist/Server/Utils/DataSource/ConnectorRegistry.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/ClickHouseConnector.js +187 -0
- package/build/dist/Server/Utils/DataSource/Connectors/ClickHouseConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/ElasticsearchConnector.js +493 -0
- package/build/dist/Server/Utils/DataSource/Connectors/ElasticsearchConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/LokiConnector.js +399 -0
- package/build/dist/Server/Utils/DataSource/Connectors/LokiConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/MySqlConnector.js +195 -0
- package/build/dist/Server/Utils/DataSource/Connectors/MySqlConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/PostgresConnector.js +149 -0
- package/build/dist/Server/Utils/DataSource/Connectors/PostgresConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/PrometheusConnector.js +362 -0
- package/build/dist/Server/Utils/DataSource/Connectors/PrometheusConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/RestApiConnector.js +342 -0
- package/build/dist/Server/Utils/DataSource/Connectors/RestApiConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Connectors/SqlServerConnector.js +186 -0
- package/build/dist/Server/Utils/DataSource/Connectors/SqlServerConnector.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js +305 -0
- package/build/dist/Server/Utils/DataSource/EgressGuard.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/HttpFetch.js +134 -0
- package/build/dist/Server/Utils/DataSource/HttpFetch.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/SqlQueryGuard.js +331 -0
- package/build/dist/Server/Utils/DataSource/SqlQueryGuard.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/SqlResultShaper.js +280 -0
- package/build/dist/Server/Utils/DataSource/SqlResultShaper.js.map +1 -0
- package/build/dist/Server/Utils/DataSource/Types.js +2 -0
- package/build/dist/Server/Utils/DataSource/Types.js.map +1 -0
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +103 -9
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -1
- package/build/dist/Server/Utils/InProcessMemo.js +113 -0
- package/build/dist/Server/Utils/InProcessMemo.js.map +1 -0
- package/build/dist/Server/Utils/InstanceHealth/TelemetryIngestion.js +406 -0
- package/build/dist/Server/Utils/InstanceHealth/TelemetryIngestion.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +34 -0
- package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +14 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +9 -1
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/SSRFProtection.js +192 -0
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -0
- package/build/dist/Server/Utils/StartServer.js +7 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js +24 -0
- package/build/dist/Server/Utils/StatusPageSubscriberWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/CaptureSpan.js +25 -11
- package/build/dist/Server/Utils/Telemetry/CaptureSpan.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/IngestionTimestamp.js +32 -0
- package/build/dist/Server/Utils/Telemetry/IngestionTimestamp.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +92 -5
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +92 -6
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry.js +93 -6
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Server/Utils/VendorAssets.js +121 -0
- package/build/dist/Server/Utils/VendorAssets.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -4
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js +5 -0
- package/build/dist/Server/Utils/Workspace/Slack/Slack.js.map +1 -1
- package/build/dist/Types/Analytics/RevenueEvent.js +26 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js +27 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceChartComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceGaugeComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceTableComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.js +16 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardDataSourceValueComponent.js.map +1 -0
- package/build/dist/Types/DataSource/DataSourceLimits.js +47 -0
- package/build/dist/Types/DataSource/DataSourceLimits.js.map +1 -0
- package/build/dist/Types/DataSource/DataSourceQueryConfig.js +10 -0
- package/build/dist/Types/DataSource/DataSourceQueryConfig.js.map +1 -0
- package/build/dist/Types/DataSource/DataSourceTableResult.js +14 -0
- package/build/dist/Types/DataSource/DataSourceTableResult.js.map +1 -0
- package/build/dist/Types/DataSource/DataSourceType.js +183 -0
- package/build/dist/Types/DataSource/DataSourceType.js.map +1 -0
- package/build/dist/Types/Date.js +38 -3
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Password.js +56 -0
- package/build/dist/Types/Password.js.map +1 -0
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/UserAuthenticationStatus.js +2 -0
- package/build/dist/Types/UserAuthenticationStatus.js.map +1 -0
- package/build/dist/UI/Components/ErrorBoundary.js +388 -72
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +10 -4
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js +1 -1
- package/build/dist/UI/Components/Modal/ConfirmModal.js.map +1 -1
- package/build/dist/UI/Components/Modal/Modal.js +178 -19
- package/build/dist/UI/Components/Modal/Modal.js.map +1 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js +3 -1
- package/build/dist/UI/Components/Modal/ModalFooter.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js +9 -6
- package/build/dist/UI/Components/MoreMenu/MoreMenu.js.map +1 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js +7 -1
- package/build/dist/UI/Components/MoreMenu/MoreMenuItem.js.map +1 -1
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js +119 -0
- package/build/dist/UI/Components/StatusPage/ResourceGrid.js.map +1 -0
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js +354 -0
- package/build/dist/UI/Components/StatusPage/ResourceGroupNavigator.js.map +1 -0
- package/build/dist/UI/Components/StatusPage/ResourceList.js +203 -0
- package/build/dist/UI/Components/StatusPage/ResourceList.js.map +1 -0
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js +1 -6
- package/build/dist/UI/Utils/AnalyticsModelAPI/AnalyticsModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js +3 -8
- package/build/dist/UI/Utils/ModelAPI/ModelAPI.js.map +1 -1
- package/build/dist/UI/Utils/TestDataSource.js +39 -0
- package/build/dist/UI/Utils/TestDataSource.js.map +1 -0
- package/build/dist/Utils/Analytics.js +4 -0
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceChartComponent.js +79 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceChartComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.js +111 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceGaugeComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceShared.js +15 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceShared.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceTableComponent.js +62 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceTableComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceValueComponent.js +112 -0
- package/build/dist/Utils/Dashboard/Components/DashboardDataSourceValueComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +16 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/build/dist/Utils/DataSource/DataSourceQueryText.js +64 -0
- package/build/dist/Utils/DataSource/DataSourceQueryText.js.map +1 -0
- package/build/dist/Utils/DataSource/DataSourceValueReducer.js +124 -0
- package/build/dist/Utils/DataSource/DataSourceValueReducer.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricResourceAttributeUtil.js +281 -0
- package/build/dist/Utils/Metrics/MetricResourceAttributeUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +8 -6
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/build/dist/Utils/StatusPage/GroupHierarchyView.js +560 -0
- package/build/dist/Utils/StatusPage/GroupHierarchyView.js.map +1 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js +519 -0
- package/build/dist/Utils/StatusPage/ResourceExplorer.js.map +1 -0
- package/jest.config.json +1 -0
- package/package.json +5 -1
- package/tsconfig.json +12 -10
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
import RestApiConnector, {
|
|
2
|
+
RestApiFetchFunction,
|
|
3
|
+
RestApiRequestDescriptor,
|
|
4
|
+
applyRestTimeMacros,
|
|
5
|
+
extractRowsByPath,
|
|
6
|
+
parseRequestDescriptor,
|
|
7
|
+
} from "../../../../Server/Utils/DataSource/Connectors/RestApiConnector";
|
|
8
|
+
import DataSourceEgressGuard from "../../../../Server/Utils/DataSource/EgressGuard";
|
|
9
|
+
import DataSourceHttpFetch, {
|
|
10
|
+
DataSourceHttpRequest,
|
|
11
|
+
DataSourceHttpResponse,
|
|
12
|
+
} from "../../../../Server/Utils/DataSource/HttpFetch";
|
|
13
|
+
import {
|
|
14
|
+
DataSourceConnectionSettings,
|
|
15
|
+
DataSourceQueryWindow,
|
|
16
|
+
} from "../../../../Server/Utils/DataSource/Types";
|
|
17
|
+
import AggregatedResult from "../../../../Types/BaseDatabase/AggregatedResult";
|
|
18
|
+
import {
|
|
19
|
+
DATA_SOURCE_CONNECT_TIMEOUT_IN_MS,
|
|
20
|
+
DATA_SOURCE_MAX_TABLE_ROWS,
|
|
21
|
+
DATA_SOURCE_MAX_TIME_SERIES_ROWS,
|
|
22
|
+
DATA_SOURCE_QUERY_TIMEOUT_IN_MS,
|
|
23
|
+
} from "../../../../Types/DataSource/DataSourceLimits";
|
|
24
|
+
import DataSourceTableResult, {
|
|
25
|
+
DataSourceTableColumn,
|
|
26
|
+
DataSourceTableColumnType,
|
|
27
|
+
} from "../../../../Types/DataSource/DataSourceTableResult";
|
|
28
|
+
import DataSourceType from "../../../../Types/DataSource/DataSourceType";
|
|
29
|
+
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
30
|
+
import { afterEach, describe, expect, test } from "@jest/globals";
|
|
31
|
+
|
|
32
|
+
const START_DATE: Date = new Date("2024-01-01T00:00:00.000Z");
|
|
33
|
+
const END_DATE: Date = new Date("2024-01-02T00:00:00.000Z");
|
|
34
|
+
|
|
35
|
+
function makeWindow(): DataSourceQueryWindow {
|
|
36
|
+
return { startDate: START_DATE, endDate: END_DATE };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeSettings(
|
|
40
|
+
overrides?: Partial<DataSourceConnectionSettings>,
|
|
41
|
+
): DataSourceConnectionSettings {
|
|
42
|
+
return {
|
|
43
|
+
dataSourceType: DataSourceType.RestApi,
|
|
44
|
+
url: "https://api.example.com",
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function jsonResponse(
|
|
50
|
+
payload: unknown,
|
|
51
|
+
statusCode: number = 200,
|
|
52
|
+
): DataSourceHttpResponse {
|
|
53
|
+
return {
|
|
54
|
+
statusCode: statusCode,
|
|
55
|
+
bodyText: JSON.stringify(payload),
|
|
56
|
+
bodyJson: payload,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mockGuard(): jest.SpyInstance {
|
|
61
|
+
return jest
|
|
62
|
+
.spyOn(DataSourceEgressGuard, "assertHostnameAllowed")
|
|
63
|
+
.mockResolvedValue([{ address: "203.0.113.10", family: 4 }]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mockFetch(response: DataSourceHttpResponse): jest.SpyInstance {
|
|
67
|
+
return jest.spyOn(DataSourceHttpFetch, "fetch").mockResolvedValue(response);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getRequest(
|
|
71
|
+
spy: jest.SpyInstance,
|
|
72
|
+
callIndex: number = 0,
|
|
73
|
+
): DataSourceHttpRequest {
|
|
74
|
+
return spy.mock.calls[callIndex]![0] as DataSourceHttpRequest;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
afterEach(() => {
|
|
78
|
+
jest.restoreAllMocks();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("parseRequestDescriptor", () => {
|
|
82
|
+
test("applies defaults for an empty object descriptor", () => {
|
|
83
|
+
const descriptor: RestApiRequestDescriptor = parseRequestDescriptor("{}");
|
|
84
|
+
expect(descriptor.path).toBe("/");
|
|
85
|
+
expect(descriptor.method).toBe("GET");
|
|
86
|
+
expect(descriptor.body).toBeUndefined();
|
|
87
|
+
expect(descriptor.rowsPath).toBeUndefined();
|
|
88
|
+
expect(descriptor.timestampField).toBeUndefined();
|
|
89
|
+
expect(descriptor.valueField).toBeUndefined();
|
|
90
|
+
expect(descriptor.seriesField).toBeUndefined();
|
|
91
|
+
expect(descriptor.labelFields).toBeUndefined();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("parses a full descriptor and JSON-encodes an object body", () => {
|
|
95
|
+
const queryText: string = JSON.stringify({
|
|
96
|
+
path: "/api/stats",
|
|
97
|
+
method: "POST",
|
|
98
|
+
body: { filter: "errors" },
|
|
99
|
+
rowsPath: "data.items",
|
|
100
|
+
timestampField: "time",
|
|
101
|
+
valueField: "count",
|
|
102
|
+
seriesField: "service",
|
|
103
|
+
labelFields: ["region", "env"],
|
|
104
|
+
});
|
|
105
|
+
const descriptor: RestApiRequestDescriptor =
|
|
106
|
+
parseRequestDescriptor(queryText);
|
|
107
|
+
expect(descriptor.path).toBe("/api/stats");
|
|
108
|
+
expect(descriptor.method).toBe("POST");
|
|
109
|
+
expect(descriptor.body).toBe('{"filter":"errors"}');
|
|
110
|
+
expect(descriptor.rowsPath).toBe("data.items");
|
|
111
|
+
expect(descriptor.timestampField).toBe("time");
|
|
112
|
+
expect(descriptor.valueField).toBe("count");
|
|
113
|
+
expect(descriptor.seriesField).toBe("service");
|
|
114
|
+
expect(descriptor.labelFields).toEqual(["region", "env"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("keeps a string body as-is", () => {
|
|
118
|
+
const descriptor: RestApiRequestDescriptor = parseRequestDescriptor(
|
|
119
|
+
'{ "method": "POST", "body": "raw-payload" }',
|
|
120
|
+
);
|
|
121
|
+
expect(descriptor.body).toBe("raw-payload");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("normalizes a lowercase method", () => {
|
|
125
|
+
const descriptor: RestApiRequestDescriptor = parseRequestDescriptor(
|
|
126
|
+
'{ "method": "post" }',
|
|
127
|
+
);
|
|
128
|
+
expect(descriptor.method).toBe("POST");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("rejects an empty query", () => {
|
|
132
|
+
expect(() => {
|
|
133
|
+
return parseRequestDescriptor("");
|
|
134
|
+
}).toThrow(BadDataException);
|
|
135
|
+
expect(() => {
|
|
136
|
+
return parseRequestDescriptor(" ");
|
|
137
|
+
}).toThrow(/Query is required/);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("rejects invalid JSON", () => {
|
|
141
|
+
expect(() => {
|
|
142
|
+
return parseRequestDescriptor("{ not json");
|
|
143
|
+
}).toThrow(BadDataException);
|
|
144
|
+
expect(() => {
|
|
145
|
+
return parseRequestDescriptor("{ not json");
|
|
146
|
+
}).toThrow(/must be valid JSON/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("rejects JSON that is not an object", () => {
|
|
150
|
+
expect(() => {
|
|
151
|
+
return parseRequestDescriptor("[]");
|
|
152
|
+
}).toThrow(/must be a JSON object/);
|
|
153
|
+
expect(() => {
|
|
154
|
+
return parseRequestDescriptor('"hello"');
|
|
155
|
+
}).toThrow(/must be a JSON object/);
|
|
156
|
+
expect(() => {
|
|
157
|
+
return parseRequestDescriptor("42");
|
|
158
|
+
}).toThrow(/must be a JSON object/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("rejects unknown HTTP methods", () => {
|
|
162
|
+
expect(() => {
|
|
163
|
+
return parseRequestDescriptor('{ "method": "DELETE" }');
|
|
164
|
+
}).toThrow(/"method" must be "GET" or "POST"/);
|
|
165
|
+
expect(() => {
|
|
166
|
+
return parseRequestDescriptor('{ "method": "PUT" }');
|
|
167
|
+
}).toThrow(BadDataException);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("rejects a non-string method", () => {
|
|
171
|
+
expect(() => {
|
|
172
|
+
return parseRequestDescriptor('{ "method": 5 }');
|
|
173
|
+
}).toThrow(/"method"/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("rejects an absolute URL smuggled into path", () => {
|
|
177
|
+
expect(() => {
|
|
178
|
+
return parseRequestDescriptor('{ "path": "https://evil.example.com/x" }');
|
|
179
|
+
}).toThrow(/absolute http\(s\):\/\/ URLs are not allowed/);
|
|
180
|
+
expect(() => {
|
|
181
|
+
return parseRequestDescriptor('{ "path": "HTTP://evil.example.com/x" }');
|
|
182
|
+
}).toThrow(/absolute/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("rejects a path that does not start with a slash", () => {
|
|
186
|
+
expect(() => {
|
|
187
|
+
return parseRequestDescriptor('{ "path": "api/stats" }');
|
|
188
|
+
}).toThrow(/must start with "\/"/);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("rejects a protocol-relative path", () => {
|
|
192
|
+
expect(() => {
|
|
193
|
+
return parseRequestDescriptor('{ "path": "//evil.example.com/x" }');
|
|
194
|
+
}).toThrow(/protocol-relative/);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("rejects path traversal, including percent-encoded dots", () => {
|
|
198
|
+
expect(() => {
|
|
199
|
+
return parseRequestDescriptor('{ "path": "/api/../admin" }');
|
|
200
|
+
}).toThrow(/".." segments/);
|
|
201
|
+
expect(() => {
|
|
202
|
+
return parseRequestDescriptor('{ "path": "/api/%2e%2e/admin" }');
|
|
203
|
+
}).toThrow(/".." segments/);
|
|
204
|
+
expect(() => {
|
|
205
|
+
return parseRequestDescriptor('{ "path": "/api/.%2E/admin" }');
|
|
206
|
+
}).toThrow(/".." segments/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("rejects backslash-separated traversal, raw and percent-encoded", () => {
|
|
210
|
+
/*
|
|
211
|
+
* WHATWG URL parsing treats "\" as "/" in http(s) URLs, so a ".."
|
|
212
|
+
* between backslashes climbs the path just like one between slashes.
|
|
213
|
+
*/
|
|
214
|
+
expect(() => {
|
|
215
|
+
return parseRequestDescriptor(JSON.stringify({ path: "/a\\..\\secret" }));
|
|
216
|
+
}).toThrow(BadDataException);
|
|
217
|
+
expect(() => {
|
|
218
|
+
return parseRequestDescriptor(JSON.stringify({ path: "/a\\..\\secret" }));
|
|
219
|
+
}).toThrow(/".." segments/);
|
|
220
|
+
expect(() => {
|
|
221
|
+
return parseRequestDescriptor('{ "path": "/%5c..%5csecret" }');
|
|
222
|
+
}).toThrow(/".." segments/);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("accepts a normal multi-segment path", () => {
|
|
226
|
+
const descriptor: RestApiRequestDescriptor = parseRequestDescriptor(
|
|
227
|
+
'{ "path": "/api/v1/data" }',
|
|
228
|
+
);
|
|
229
|
+
expect(descriptor.path).toBe("/api/v1/data");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("rejects a non-string path", () => {
|
|
233
|
+
expect(() => {
|
|
234
|
+
return parseRequestDescriptor('{ "path": 12 }');
|
|
235
|
+
}).toThrow(/"path" must be a string/);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("rejects a body that is neither object nor string", () => {
|
|
239
|
+
expect(() => {
|
|
240
|
+
return parseRequestDescriptor('{ "body": 42 }');
|
|
241
|
+
}).toThrow(/"body" must be a JSON object or a string/);
|
|
242
|
+
expect(() => {
|
|
243
|
+
return parseRequestDescriptor('{ "body": true }');
|
|
244
|
+
}).toThrow(BadDataException);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("rejects wrong types for string fields", () => {
|
|
248
|
+
expect(() => {
|
|
249
|
+
return parseRequestDescriptor('{ "rowsPath": 5 }');
|
|
250
|
+
}).toThrow(/"rowsPath"/);
|
|
251
|
+
expect(() => {
|
|
252
|
+
return parseRequestDescriptor('{ "timestampField": [] }');
|
|
253
|
+
}).toThrow(/"timestampField"/);
|
|
254
|
+
expect(() => {
|
|
255
|
+
return parseRequestDescriptor('{ "valueField": {} }');
|
|
256
|
+
}).toThrow(/"valueField"/);
|
|
257
|
+
expect(() => {
|
|
258
|
+
return parseRequestDescriptor('{ "seriesField": "" }');
|
|
259
|
+
}).toThrow(/"seriesField"/);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("rejects invalid labelFields", () => {
|
|
263
|
+
expect(() => {
|
|
264
|
+
return parseRequestDescriptor('{ "labelFields": "region" }');
|
|
265
|
+
}).toThrow(/"labelFields" must be an array/);
|
|
266
|
+
expect(() => {
|
|
267
|
+
return parseRequestDescriptor('{ "labelFields": ["region", 5] }');
|
|
268
|
+
}).toThrow(/only non-empty strings/);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("applyRestTimeMacros", () => {
|
|
273
|
+
test("replaces millisecond and unquoted ISO macros", () => {
|
|
274
|
+
const text: string =
|
|
275
|
+
"/api?from=$__startTimeMs&to=$__endTimeMs&f=$__startTime&t=$__endTime";
|
|
276
|
+
const replaced: string = applyRestTimeMacros(text, START_DATE, END_DATE);
|
|
277
|
+
expect(replaced).toBe(
|
|
278
|
+
`/api?from=${START_DATE.getTime()}&to=${END_DATE.getTime()}&f=2024-01-01T00:00:00.000Z&t=2024-01-02T00:00:00.000Z`,
|
|
279
|
+
);
|
|
280
|
+
expect(replaced).not.toContain("'");
|
|
281
|
+
expect(replaced).not.toContain("$__");
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("does not corrupt $__startTimeMs with the $__startTime replacement", () => {
|
|
285
|
+
const replaced: string = applyRestTimeMacros(
|
|
286
|
+
"$__startTimeMs",
|
|
287
|
+
START_DATE,
|
|
288
|
+
END_DATE,
|
|
289
|
+
);
|
|
290
|
+
expect(replaced).toBe(START_DATE.getTime().toString());
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("replaces every occurrence of a macro", () => {
|
|
294
|
+
const replaced: string = applyRestTimeMacros(
|
|
295
|
+
"$__endTimeMs and again $__endTimeMs",
|
|
296
|
+
START_DATE,
|
|
297
|
+
END_DATE,
|
|
298
|
+
);
|
|
299
|
+
expect(replaced).toBe(
|
|
300
|
+
`${END_DATE.getTime()} and again ${END_DATE.getTime()}`,
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("leaves text without macros untouched", () => {
|
|
305
|
+
expect(applyRestTimeMacros("/api/stats", START_DATE, END_DATE)).toBe(
|
|
306
|
+
"/api/stats",
|
|
307
|
+
);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("extractRowsByPath", () => {
|
|
312
|
+
test("returns the payload itself when rowsPath is absent and it is an array", () => {
|
|
313
|
+
const rows: Array<Record<string, unknown>> = extractRowsByPath(
|
|
314
|
+
[{ a: 1 }, { a: 2 }],
|
|
315
|
+
undefined,
|
|
316
|
+
);
|
|
317
|
+
expect(rows).toEqual([{ a: 1 }, { a: 2 }]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("navigates a nested dot path", () => {
|
|
321
|
+
const payload: unknown = { data: { items: [{ a: 1 }] } };
|
|
322
|
+
expect(extractRowsByPath(payload, "data.items")).toEqual([{ a: 1 }]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("supports numeric segments to index into arrays", () => {
|
|
326
|
+
const payload: unknown = { results: [{ rows: [{ a: 1 }] }] };
|
|
327
|
+
expect(extractRowsByPath(payload, "results.0.rows")).toEqual([{ a: 1 }]);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("returns an empty array untouched", () => {
|
|
331
|
+
expect(extractRowsByPath({ data: [] }, "data")).toEqual([]);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("names what was found when the root is not an array", () => {
|
|
335
|
+
expect(() => {
|
|
336
|
+
return extractRowsByPath({ hello: "world" }, undefined);
|
|
337
|
+
}).toThrow(/array of rows, but found an object/);
|
|
338
|
+
expect(() => {
|
|
339
|
+
return extractRowsByPath("just text", undefined);
|
|
340
|
+
}).toThrow(/array of rows, but found a string/);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("names what was found when the path resolves to a non-array", () => {
|
|
344
|
+
expect(() => {
|
|
345
|
+
return extractRowsByPath({ data: { items: "nope" } }, "data.items");
|
|
346
|
+
}).toThrow(/found a string/);
|
|
347
|
+
expect(() => {
|
|
348
|
+
return extractRowsByPath({ data: { items: 42 } }, "data.items");
|
|
349
|
+
}).toThrow(/found a number/);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("names the missing field when the path does not exist", () => {
|
|
353
|
+
expect(() => {
|
|
354
|
+
return extractRowsByPath({}, "data.items");
|
|
355
|
+
}).toThrow(/undefined/);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("fails when navigating through a non-object segment", () => {
|
|
359
|
+
expect(() => {
|
|
360
|
+
return extractRowsByPath({ data: 5 }, "data.items");
|
|
361
|
+
}).toThrow(/Could not read "items"/);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("rejects rows that are not objects, naming the index and type", () => {
|
|
365
|
+
expect(() => {
|
|
366
|
+
return extractRowsByPath({ data: [{ a: 1 }, 7] }, "data");
|
|
367
|
+
}).toThrow(/index 1 is a number/);
|
|
368
|
+
expect(() => {
|
|
369
|
+
return extractRowsByPath([[1, 2]], undefined);
|
|
370
|
+
}).toThrow(/index 0 is an array/);
|
|
371
|
+
expect(() => {
|
|
372
|
+
return extractRowsByPath([null], undefined);
|
|
373
|
+
}).toThrow(/index 0 is null/);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("rejects a rowsPath with empty segments", () => {
|
|
377
|
+
expect(() => {
|
|
378
|
+
return extractRowsByPath({ data: [] }, "data..items");
|
|
379
|
+
}).toThrow(/empty segment/);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe("RestApiConnector", () => {
|
|
384
|
+
test("declares the REST API data source type", () => {
|
|
385
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
386
|
+
expect(connector.dataSourceType).toBe(DataSourceType.RestApi);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("uses an injected fetch function instead of the shared client", async () => {
|
|
390
|
+
mockGuard();
|
|
391
|
+
const sharedFetchSpy: jest.SpyInstance = jest.spyOn(
|
|
392
|
+
DataSourceHttpFetch,
|
|
393
|
+
"fetch",
|
|
394
|
+
);
|
|
395
|
+
const injected: jest.Mock = jest.fn();
|
|
396
|
+
injected.mockResolvedValue(jsonResponse([{ a: 1 }]));
|
|
397
|
+
|
|
398
|
+
const connector: RestApiConnector = new RestApiConnector(
|
|
399
|
+
injected as unknown as RestApiFetchFunction,
|
|
400
|
+
);
|
|
401
|
+
const result: DataSourceTableResult = await connector.queryTable(
|
|
402
|
+
makeSettings(),
|
|
403
|
+
"{}",
|
|
404
|
+
makeWindow(),
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
expect(injected).toHaveBeenCalledTimes(1);
|
|
408
|
+
expect(sharedFetchSpy).not.toHaveBeenCalled();
|
|
409
|
+
expect(result.rows).toEqual([{ a: 1 }]);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("RestApiConnector.testConnection", () => {
|
|
414
|
+
test("passes on a 2xx response and sends auth + connect timeout", async () => {
|
|
415
|
+
mockGuard();
|
|
416
|
+
const fetchSpy: jest.SpyInstance = mockFetch({
|
|
417
|
+
statusCode: 204,
|
|
418
|
+
bodyText: "",
|
|
419
|
+
bodyJson: undefined,
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
423
|
+
await connector.testConnection(makeSettings({ apiToken: "token-abc-123" }));
|
|
424
|
+
|
|
425
|
+
const request: DataSourceHttpRequest = getRequest(fetchSpy);
|
|
426
|
+
expect(request.method).toBe("GET");
|
|
427
|
+
expect(request.url).toBe("https://api.example.com");
|
|
428
|
+
expect(request.timeoutInMs).toBe(DATA_SOURCE_CONNECT_TIMEOUT_IN_MS);
|
|
429
|
+
expect(request.headers?.["Authorization"]).toBe("Bearer token-abc-123");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("builds basic auth headers from username and password", async () => {
|
|
433
|
+
mockGuard();
|
|
434
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({}));
|
|
435
|
+
|
|
436
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
437
|
+
await connector.testConnection(
|
|
438
|
+
makeSettings({ username: "user1", password: "pass1234" }),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const expected: string = `Basic ${Buffer.from("user1:pass1234").toString(
|
|
442
|
+
"base64",
|
|
443
|
+
)}`;
|
|
444
|
+
expect(getRequest(fetchSpy).headers?.["Authorization"]).toBe(expected);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("invokes the egress guard before fetching", async () => {
|
|
448
|
+
const guardSpy: jest.SpyInstance = mockGuard();
|
|
449
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({}));
|
|
450
|
+
|
|
451
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
452
|
+
await connector.testConnection(makeSettings());
|
|
453
|
+
|
|
454
|
+
expect(guardSpy).toHaveBeenCalledWith("api.example.com", undefined);
|
|
455
|
+
expect(guardSpy.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
456
|
+
fetchSpy.mock.invocationCallOrder[0]!,
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("does not fetch when the egress guard rejects the host", async () => {
|
|
461
|
+
jest
|
|
462
|
+
.spyOn(DataSourceEgressGuard, "assertHostnameAllowed")
|
|
463
|
+
.mockRejectedValue(
|
|
464
|
+
new BadDataException(
|
|
465
|
+
"Data source host api.example.com resolves to 127.0.0.1, which is not allowed: loopback address.",
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({}));
|
|
469
|
+
|
|
470
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
471
|
+
await expect(connector.testConnection(makeSettings())).rejects.toThrow(
|
|
472
|
+
/not allowed/,
|
|
473
|
+
);
|
|
474
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
test("rejects a non-http(s) base URL", async () => {
|
|
478
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({}));
|
|
479
|
+
|
|
480
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
481
|
+
await expect(
|
|
482
|
+
connector.testConnection(
|
|
483
|
+
makeSettings({ url: "ftp://files.example.com" }),
|
|
484
|
+
),
|
|
485
|
+
).rejects.toThrow(/http or https/);
|
|
486
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("fails on a non-2xx status", async () => {
|
|
490
|
+
mockGuard();
|
|
491
|
+
mockFetch({ statusCode: 500, bodyText: "boom", bodyJson: undefined });
|
|
492
|
+
|
|
493
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
494
|
+
await expect(connector.testConnection(makeSettings())).rejects.toThrow(
|
|
495
|
+
/HTTP 500/,
|
|
496
|
+
);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("fails without a configured URL and never dials", async () => {
|
|
500
|
+
const guardSpy: jest.SpyInstance = mockGuard();
|
|
501
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse({}));
|
|
502
|
+
|
|
503
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
504
|
+
await expect(
|
|
505
|
+
connector.testConnection(makeSettings({ url: undefined })),
|
|
506
|
+
).rejects.toThrow(/no URL configured/);
|
|
507
|
+
expect(guardSpy).not.toHaveBeenCalled();
|
|
508
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test("redacts the password from connection errors", async () => {
|
|
512
|
+
mockGuard();
|
|
513
|
+
jest
|
|
514
|
+
.spyOn(DataSourceHttpFetch, "fetch")
|
|
515
|
+
.mockRejectedValue(
|
|
516
|
+
new Error("basic auth failed for user1 with password hunter2secret"),
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
520
|
+
try {
|
|
521
|
+
await connector.testConnection(
|
|
522
|
+
makeSettings({ username: "user1", password: "hunter2secret" }),
|
|
523
|
+
);
|
|
524
|
+
throw new Error("expected testConnection to throw");
|
|
525
|
+
} catch (error) {
|
|
526
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
527
|
+
expect((error as Error).message).not.toContain("hunter2secret");
|
|
528
|
+
expect((error as Error).message).toContain("***");
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("redacts custom header values from connection errors", async () => {
|
|
533
|
+
mockGuard();
|
|
534
|
+
jest
|
|
535
|
+
.spyOn(DataSourceHttpFetch, "fetch")
|
|
536
|
+
.mockRejectedValue(
|
|
537
|
+
new Error("proxy rejected header X-Auth: supersecrethdrvalue"),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
541
|
+
try {
|
|
542
|
+
await connector.testConnection(
|
|
543
|
+
makeSettings({ customHeaders: { "X-Auth": "supersecrethdrvalue" } }),
|
|
544
|
+
);
|
|
545
|
+
throw new Error("expected testConnection to throw");
|
|
546
|
+
} catch (error) {
|
|
547
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
548
|
+
expect((error as Error).message).not.toContain("supersecrethdrvalue");
|
|
549
|
+
expect((error as Error).message).toContain("***");
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
describe("RestApiConnector.queryTimeSeries", () => {
|
|
555
|
+
const timeSeriesQuery: string = JSON.stringify({
|
|
556
|
+
path: "/api/stats",
|
|
557
|
+
rowsPath: "data.items",
|
|
558
|
+
timestampField: "time",
|
|
559
|
+
valueField: "count",
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("maps rows to timestamped points without attributes", async () => {
|
|
563
|
+
mockGuard();
|
|
564
|
+
const fetchSpy: jest.SpyInstance = mockFetch(
|
|
565
|
+
jsonResponse({
|
|
566
|
+
data: {
|
|
567
|
+
items: [
|
|
568
|
+
{ time: "2024-01-01T01:00:00.000Z", count: 5 },
|
|
569
|
+
{ time: "2024-01-01T02:00:00.000Z", count: 7 },
|
|
570
|
+
],
|
|
571
|
+
},
|
|
572
|
+
}),
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
576
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
577
|
+
makeSettings(),
|
|
578
|
+
timeSeriesQuery,
|
|
579
|
+
makeWindow(),
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
expect(result.truncated).toBe(false);
|
|
583
|
+
expect(result.data).toHaveLength(2);
|
|
584
|
+
expect(result.data[0]!.timestamp).toBeInstanceOf(Date);
|
|
585
|
+
expect(result.data[0]!.timestamp.toISOString()).toBe(
|
|
586
|
+
"2024-01-01T01:00:00.000Z",
|
|
587
|
+
);
|
|
588
|
+
expect(result.data[0]!.value).toBe(5);
|
|
589
|
+
expect(result.data[1]!.value).toBe(7);
|
|
590
|
+
expect("attributes" in result.data[0]!).toBe(false);
|
|
591
|
+
expect(getRequest(fetchSpy).url).toBe("https://api.example.com/api/stats");
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test("parses epoch seconds and epoch milliseconds timestamps", async () => {
|
|
595
|
+
mockGuard();
|
|
596
|
+
mockFetch(
|
|
597
|
+
jsonResponse([
|
|
598
|
+
{ time: 1704070800, count: 1 },
|
|
599
|
+
{ time: 1704074400000, count: 2 },
|
|
600
|
+
]),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
const query: string = JSON.stringify({
|
|
604
|
+
timestampField: "time",
|
|
605
|
+
valueField: "count",
|
|
606
|
+
});
|
|
607
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
608
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
609
|
+
makeSettings(),
|
|
610
|
+
query,
|
|
611
|
+
makeWindow(),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
expect(result.data).toHaveLength(2);
|
|
615
|
+
expect(result.data[0]!.timestamp.toISOString()).toBe(
|
|
616
|
+
"2024-01-01T01:00:00.000Z",
|
|
617
|
+
);
|
|
618
|
+
expect(result.data[1]!.timestamp.toISOString()).toBe(
|
|
619
|
+
"2024-01-01T02:00:00.000Z",
|
|
620
|
+
);
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("builds attributes from seriesField and labelFields", async () => {
|
|
624
|
+
mockGuard();
|
|
625
|
+
mockFetch(
|
|
626
|
+
jsonResponse([
|
|
627
|
+
{
|
|
628
|
+
time: "2024-01-01T01:00:00.000Z",
|
|
629
|
+
count: 5,
|
|
630
|
+
service: "api",
|
|
631
|
+
region: "us",
|
|
632
|
+
},
|
|
633
|
+
]),
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
const query: string = JSON.stringify({
|
|
637
|
+
timestampField: "time",
|
|
638
|
+
valueField: "count",
|
|
639
|
+
seriesField: "service",
|
|
640
|
+
labelFields: ["region"],
|
|
641
|
+
});
|
|
642
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
643
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
644
|
+
makeSettings(),
|
|
645
|
+
query,
|
|
646
|
+
makeWindow(),
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
expect(result.data[0]!["attributes"]).toEqual({
|
|
650
|
+
series: "api",
|
|
651
|
+
region: "us",
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("stringifies label values and marks missing ones as (unset)", async () => {
|
|
656
|
+
mockGuard();
|
|
657
|
+
mockFetch(
|
|
658
|
+
jsonResponse([
|
|
659
|
+
{ time: "2024-01-01T01:00:00.000Z", count: 5, shard: 3, region: null },
|
|
660
|
+
]),
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const query: string = JSON.stringify({
|
|
664
|
+
timestampField: "time",
|
|
665
|
+
valueField: "count",
|
|
666
|
+
labelFields: ["shard", "region"],
|
|
667
|
+
});
|
|
668
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
669
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
670
|
+
makeSettings(),
|
|
671
|
+
query,
|
|
672
|
+
makeWindow(),
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
expect(result.data[0]!["attributes"]).toEqual({
|
|
676
|
+
shard: "3",
|
|
677
|
+
region: "(unset)",
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("skips rows with unparseable timestamps or non-finite values", async () => {
|
|
682
|
+
mockGuard();
|
|
683
|
+
mockFetch(
|
|
684
|
+
jsonResponse([
|
|
685
|
+
{ time: "not-a-date", count: 1 },
|
|
686
|
+
{ time: "2024-01-01T01:00:00.000Z", count: "not-a-number" },
|
|
687
|
+
{ time: "2024-01-01T02:00:00.000Z", count: null },
|
|
688
|
+
{ time: "2024-01-01T03:00:00.000Z" },
|
|
689
|
+
{ time: "2024-01-01T04:00:00.000Z", count: "42.5" },
|
|
690
|
+
]),
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
const query: string = JSON.stringify({
|
|
694
|
+
timestampField: "time",
|
|
695
|
+
valueField: "count",
|
|
696
|
+
});
|
|
697
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
698
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
699
|
+
makeSettings(),
|
|
700
|
+
query,
|
|
701
|
+
makeWindow(),
|
|
702
|
+
);
|
|
703
|
+
|
|
704
|
+
expect(result.data).toHaveLength(1);
|
|
705
|
+
expect(result.data[0]!.value).toBe(42.5);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("requires timestampField and valueField before any request", async () => {
|
|
709
|
+
const guardSpy: jest.SpyInstance = mockGuard();
|
|
710
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
711
|
+
|
|
712
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
713
|
+
await expect(
|
|
714
|
+
connector.queryTimeSeries(makeSettings(), "{}", makeWindow()),
|
|
715
|
+
).rejects.toThrow(/"timestampField" and "valueField"/);
|
|
716
|
+
expect(guardSpy).not.toHaveBeenCalled();
|
|
717
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("replaces time macros in the path and the body before the request", async () => {
|
|
721
|
+
mockGuard();
|
|
722
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
723
|
+
|
|
724
|
+
const query: string = JSON.stringify({
|
|
725
|
+
path: "/metrics?from=$__startTimeMs&to=$__endTimeMs",
|
|
726
|
+
method: "POST",
|
|
727
|
+
body: { range: { from: "$__startTime", to: "$__endTime" } },
|
|
728
|
+
timestampField: "time",
|
|
729
|
+
valueField: "value",
|
|
730
|
+
});
|
|
731
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
732
|
+
await connector.queryTimeSeries(makeSettings(), query, makeWindow());
|
|
733
|
+
|
|
734
|
+
const request: DataSourceHttpRequest = getRequest(fetchSpy);
|
|
735
|
+
expect(request.url).toBe(
|
|
736
|
+
`https://api.example.com/metrics?from=${START_DATE.getTime()}&to=${END_DATE.getTime()}`,
|
|
737
|
+
);
|
|
738
|
+
expect(request.method).toBe("POST");
|
|
739
|
+
expect(request.body).toContain("2024-01-01T00:00:00.000Z");
|
|
740
|
+
expect(request.body).toContain("2024-01-02T00:00:00.000Z");
|
|
741
|
+
expect(request.body).not.toContain("$__");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("joins a base URL with a trailing slash without doubling it", async () => {
|
|
745
|
+
mockGuard();
|
|
746
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
747
|
+
|
|
748
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
749
|
+
await connector.queryTimeSeries(
|
|
750
|
+
makeSettings({ url: "https://api.example.com/v1/" }),
|
|
751
|
+
JSON.stringify({
|
|
752
|
+
path: "/stats",
|
|
753
|
+
timestampField: "time",
|
|
754
|
+
valueField: "value",
|
|
755
|
+
}),
|
|
756
|
+
makeWindow(),
|
|
757
|
+
);
|
|
758
|
+
|
|
759
|
+
expect(getRequest(fetchSpy).url).toBe("https://api.example.com/v1/stats");
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("passes the query timeout and bearer auth to the HTTP client", async () => {
|
|
763
|
+
mockGuard();
|
|
764
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
765
|
+
|
|
766
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
767
|
+
await connector.queryTimeSeries(
|
|
768
|
+
makeSettings({ apiToken: "sekret-token-99" }),
|
|
769
|
+
JSON.stringify({ timestampField: "time", valueField: "value" }),
|
|
770
|
+
makeWindow(),
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
const request: DataSourceHttpRequest = getRequest(fetchSpy);
|
|
774
|
+
expect(request.timeoutInMs).toBe(DATA_SOURCE_QUERY_TIMEOUT_IN_MS);
|
|
775
|
+
expect(request.headers?.["Authorization"]).toBe("Bearer sekret-token-99");
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
test("invokes the egress guard before fetching for queries", async () => {
|
|
779
|
+
const guardSpy: jest.SpyInstance = mockGuard();
|
|
780
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
781
|
+
|
|
782
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
783
|
+
await connector.queryTimeSeries(
|
|
784
|
+
makeSettings(),
|
|
785
|
+
JSON.stringify({ timestampField: "time", valueField: "value" }),
|
|
786
|
+
makeWindow(),
|
|
787
|
+
);
|
|
788
|
+
|
|
789
|
+
expect(guardSpy).toHaveBeenCalledWith("api.example.com", undefined);
|
|
790
|
+
expect(guardSpy.mock.invocationCallOrder[0]!).toBeLessThan(
|
|
791
|
+
fetchSpy.mock.invocationCallOrder[0]!,
|
|
792
|
+
);
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
test("clamps to the time-series row cap and reports truncation", async () => {
|
|
796
|
+
mockGuard();
|
|
797
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
798
|
+
for (
|
|
799
|
+
let index: number = 0;
|
|
800
|
+
index < DATA_SOURCE_MAX_TIME_SERIES_ROWS + 5;
|
|
801
|
+
index++
|
|
802
|
+
) {
|
|
803
|
+
rows.push({ t: START_DATE.getTime() + index * 1000, v: index });
|
|
804
|
+
}
|
|
805
|
+
mockFetch(jsonResponse(rows));
|
|
806
|
+
|
|
807
|
+
const query: string = JSON.stringify({
|
|
808
|
+
timestampField: "t",
|
|
809
|
+
valueField: "v",
|
|
810
|
+
});
|
|
811
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
812
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
813
|
+
makeSettings(),
|
|
814
|
+
query,
|
|
815
|
+
makeWindow(),
|
|
816
|
+
);
|
|
817
|
+
|
|
818
|
+
expect(result.data).toHaveLength(DATA_SOURCE_MAX_TIME_SERIES_ROWS);
|
|
819
|
+
expect(result.truncated).toBe(true);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("returns an empty result for an empty rows array", async () => {
|
|
823
|
+
mockGuard();
|
|
824
|
+
mockFetch(jsonResponse({ data: { items: [] } }));
|
|
825
|
+
|
|
826
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
827
|
+
const result: AggregatedResult = await connector.queryTimeSeries(
|
|
828
|
+
makeSettings(),
|
|
829
|
+
timeSeriesQuery,
|
|
830
|
+
makeWindow(),
|
|
831
|
+
);
|
|
832
|
+
|
|
833
|
+
expect(result.data).toEqual([]);
|
|
834
|
+
expect(result.truncated).toBe(false);
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
test("fails with a useful message when the response is not JSON", async () => {
|
|
838
|
+
mockGuard();
|
|
839
|
+
mockFetch({
|
|
840
|
+
statusCode: 200,
|
|
841
|
+
bodyText: "<html>login page</html>",
|
|
842
|
+
bodyJson: undefined,
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
846
|
+
await expect(
|
|
847
|
+
connector.queryTimeSeries(makeSettings(), timeSeriesQuery, makeWindow()),
|
|
848
|
+
).rejects.toThrow(/did not return JSON/);
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
test("surfaces rowsPath resolution failures", async () => {
|
|
852
|
+
mockGuard();
|
|
853
|
+
mockFetch(jsonResponse({ data: { items: "nope" } }));
|
|
854
|
+
|
|
855
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
856
|
+
await expect(
|
|
857
|
+
connector.queryTimeSeries(makeSettings(), timeSeriesQuery, makeWindow()),
|
|
858
|
+
).rejects.toThrow(/found a string/);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
test("redacts the api token from transport errors", async () => {
|
|
862
|
+
mockGuard();
|
|
863
|
+
jest
|
|
864
|
+
.spyOn(DataSourceHttpFetch, "fetch")
|
|
865
|
+
.mockRejectedValue(
|
|
866
|
+
new BadDataException(
|
|
867
|
+
"Data source responded with HTTP 500: Authorization Bearer very-secret-token was rejected",
|
|
868
|
+
),
|
|
869
|
+
);
|
|
870
|
+
|
|
871
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
872
|
+
try {
|
|
873
|
+
await connector.queryTimeSeries(
|
|
874
|
+
makeSettings({ apiToken: "very-secret-token" }),
|
|
875
|
+
timeSeriesQuery,
|
|
876
|
+
makeWindow(),
|
|
877
|
+
);
|
|
878
|
+
throw new Error("expected queryTimeSeries to throw");
|
|
879
|
+
} catch (error) {
|
|
880
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
881
|
+
expect((error as Error).message).not.toContain("very-secret-token");
|
|
882
|
+
expect((error as Error).message).toContain("***");
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
describe("RestApiConnector.queryTable", () => {
|
|
888
|
+
test("shapes rows into columns, keeping nested objects as Json cells", async () => {
|
|
889
|
+
mockGuard();
|
|
890
|
+
const fetchSpy: jest.SpyInstance = mockFetch(
|
|
891
|
+
jsonResponse({
|
|
892
|
+
data: [
|
|
893
|
+
{ name: "a", count: 1, meta: { x: 1 } },
|
|
894
|
+
{ name: "b", count: 2, meta: null },
|
|
895
|
+
],
|
|
896
|
+
}),
|
|
897
|
+
);
|
|
898
|
+
|
|
899
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
900
|
+
const result: DataSourceTableResult = await connector.queryTable(
|
|
901
|
+
makeSettings(),
|
|
902
|
+
'{ "rowsPath": "data" }',
|
|
903
|
+
makeWindow(),
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
expect(result.truncated).toBe(false);
|
|
907
|
+
expect(
|
|
908
|
+
result.columns.map((column: DataSourceTableColumn) => {
|
|
909
|
+
return column.key;
|
|
910
|
+
}),
|
|
911
|
+
).toEqual(["name", "count", "meta"]);
|
|
912
|
+
|
|
913
|
+
const countColumn: DataSourceTableColumn | undefined = result.columns.find(
|
|
914
|
+
(column: DataSourceTableColumn) => {
|
|
915
|
+
return column.key === "count";
|
|
916
|
+
},
|
|
917
|
+
);
|
|
918
|
+
expect(countColumn?.type).toBe(DataSourceTableColumnType.Number);
|
|
919
|
+
|
|
920
|
+
const metaColumn: DataSourceTableColumn | undefined = result.columns.find(
|
|
921
|
+
(column: DataSourceTableColumn) => {
|
|
922
|
+
return column.key === "meta";
|
|
923
|
+
},
|
|
924
|
+
);
|
|
925
|
+
expect(metaColumn?.type).toBe(DataSourceTableColumnType.Json);
|
|
926
|
+
|
|
927
|
+
expect(result.rows[0]!["meta"]).toBe('{"x":1}');
|
|
928
|
+
expect(result.rows[1]!["meta"]).toBeNull();
|
|
929
|
+
|
|
930
|
+
/* Default request shape: GET on the base URL root with no body. */
|
|
931
|
+
const request: DataSourceHttpRequest = getRequest(fetchSpy);
|
|
932
|
+
expect(request.method).toBe("GET");
|
|
933
|
+
expect(request.url).toBe("https://api.example.com/");
|
|
934
|
+
expect(request.body).toBeUndefined();
|
|
935
|
+
expect(request.timeoutInMs).toBe(DATA_SOURCE_QUERY_TIMEOUT_IN_MS);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
test("reads rows from the response root when rowsPath is omitted", async () => {
|
|
939
|
+
mockGuard();
|
|
940
|
+
mockFetch(jsonResponse([{ a: 1 }]));
|
|
941
|
+
|
|
942
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
943
|
+
const result: DataSourceTableResult = await connector.queryTable(
|
|
944
|
+
makeSettings(),
|
|
945
|
+
"{}",
|
|
946
|
+
makeWindow(),
|
|
947
|
+
);
|
|
948
|
+
|
|
949
|
+
expect(result.rows).toEqual([{ a: 1 }]);
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("passes a string body through with macros applied", async () => {
|
|
953
|
+
mockGuard();
|
|
954
|
+
const fetchSpy: jest.SpyInstance = mockFetch(jsonResponse([]));
|
|
955
|
+
|
|
956
|
+
const query: string = JSON.stringify({
|
|
957
|
+
method: "POST",
|
|
958
|
+
body: "raw-payload $__startTimeMs",
|
|
959
|
+
});
|
|
960
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
961
|
+
await connector.queryTable(makeSettings(), query, makeWindow());
|
|
962
|
+
|
|
963
|
+
expect(getRequest(fetchSpy).body).toBe(
|
|
964
|
+
`raw-payload ${START_DATE.getTime()}`,
|
|
965
|
+
);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
test("clamps to the table row cap and reports truncation", async () => {
|
|
969
|
+
mockGuard();
|
|
970
|
+
const rows: Array<Record<string, unknown>> = [];
|
|
971
|
+
for (
|
|
972
|
+
let index: number = 0;
|
|
973
|
+
index < DATA_SOURCE_MAX_TABLE_ROWS + 3;
|
|
974
|
+
index++
|
|
975
|
+
) {
|
|
976
|
+
rows.push({ index: index });
|
|
977
|
+
}
|
|
978
|
+
mockFetch(jsonResponse(rows));
|
|
979
|
+
|
|
980
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
981
|
+
const result: DataSourceTableResult = await connector.queryTable(
|
|
982
|
+
makeSettings(),
|
|
983
|
+
"{}",
|
|
984
|
+
makeWindow(),
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
expect(result.rows).toHaveLength(DATA_SOURCE_MAX_TABLE_ROWS);
|
|
988
|
+
expect(result.truncated).toBe(true);
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
test("returns empty columns and rows for an empty array", async () => {
|
|
992
|
+
mockGuard();
|
|
993
|
+
mockFetch(jsonResponse([]));
|
|
994
|
+
|
|
995
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
996
|
+
const result: DataSourceTableResult = await connector.queryTable(
|
|
997
|
+
makeSettings(),
|
|
998
|
+
"{}",
|
|
999
|
+
makeWindow(),
|
|
1000
|
+
);
|
|
1001
|
+
|
|
1002
|
+
expect(result.columns).toEqual([]);
|
|
1003
|
+
expect(result.rows).toEqual([]);
|
|
1004
|
+
expect(result.truncated).toBe(false);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test("rejects rows that are not objects", async () => {
|
|
1008
|
+
mockGuard();
|
|
1009
|
+
mockFetch(jsonResponse({ data: [1, 2, 3] }));
|
|
1010
|
+
|
|
1011
|
+
const connector: RestApiConnector = new RestApiConnector();
|
|
1012
|
+
await expect(
|
|
1013
|
+
connector.queryTable(
|
|
1014
|
+
makeSettings(),
|
|
1015
|
+
'{ "rowsPath": "data" }',
|
|
1016
|
+
makeWindow(),
|
|
1017
|
+
),
|
|
1018
|
+
).rejects.toThrow(/index 0 is a number/);
|
|
1019
|
+
});
|
|
1020
|
+
});
|