@lightdash/cli 0.3482.1 → 0.3483.0
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/dist/.tsbuildinfo +1 -1
- package/dist/handlers/contentAsCode/projectResources.d.ts +2 -1
- package/dist/handlers/contentAsCode/projectResources.d.ts.map +1 -1
- package/dist/handlers/contentAsCode/projectResources.js +24 -1
- package/dist/handlers/contentAsCode/registry.d.ts.map +1 -1
- package/dist/handlers/contentAsCode/registry.js +1 -0
- package/dist/handlers/dbt/apiClient.d.ts +1 -0
- package/dist/handlers/dbt/apiClient.d.ts.map +1 -1
- package/dist/handlers/dbt/apiClient.js +1 -0
- package/dist/handlers/download.d.ts +9 -2
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +159 -2
- package/dist/handlers/download.test.js +123 -1
- package/dist/index.js +4 -0
- package/dist/vendor/authoring/developing-data-apps-locally/SKILL.md +5 -0
- package/package.json +3 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { type AgentAsCode, type AlertAsCode, type GoogleSheetsSyncAsCode, type ScheduledDeliveryAsCode, type VirtualViewAsCode } from '@lightdash/common';
|
|
1
|
+
import { type AgentAsCode, type AlertAsCode, type ExternalConnectionAsCode, type GoogleSheetsSyncAsCode, type ScheduledDeliveryAsCode, type VirtualViewAsCode } from '@lightdash/common';
|
|
2
2
|
import type { CodeResourceDefinition } from './resource';
|
|
3
3
|
export declare const VIRTUAL_VIEW_CODE_RESOURCE: CodeResourceDefinition<VirtualViewAsCode>;
|
|
4
4
|
export declare const AI_AGENT_CODE_RESOURCE: CodeResourceDefinition<AgentAsCode>;
|
|
5
|
+
export declare const EXTERNAL_CONNECTION_CODE_RESOURCE: CodeResourceDefinition<ExternalConnectionAsCode>;
|
|
5
6
|
export declare const SCHEDULED_DELIVERY_CODE_RESOURCE: CodeResourceDefinition<ScheduledDeliveryAsCode>;
|
|
6
7
|
export declare const ALERT_CODE_RESOURCE: CodeResourceDefinition<AlertAsCode>;
|
|
7
8
|
export declare const GOOGLE_SHEETS_CODE_RESOURCE: CodeResourceDefinition<GoogleSheetsSyncAsCode>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projectResources.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/projectResources.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,iBAAiB,CAwB5E,CAAC;AAEN,eAAO,MAAM,sBAAsB,EAAE,sBAAsB,CAAC,WAAW,CAyBtE,CAAC;
|
|
1
|
+
{"version":3,"file":"projectResources.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/projectResources.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC7B,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,iBAAiB,CAwB5E,CAAC;AAEN,eAAO,MAAM,sBAAsB,EAAE,sBAAsB,CAAC,WAAW,CAyBtE,CAAC;AAEF,eAAO,MAAM,iCAAiC,EAAE,sBAAsB,CAAC,wBAAwB,CAwB1F,CAAC;AA4CN,eAAO,MAAM,gCAAgC,iDAKvC,CAAC;AAEP,eAAO,MAAM,mBAAmB,qCAI9B,CAAC;AAEH,eAAO,MAAM,2BAA2B,gDAKlC,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GOOGLE_SHEETS_CODE_RESOURCE = exports.ALERT_CODE_RESOURCE = exports.SCHEDULED_DELIVERY_CODE_RESOURCE = exports.AI_AGENT_CODE_RESOURCE = exports.VIRTUAL_VIEW_CODE_RESOURCE = void 0;
|
|
3
|
+
exports.GOOGLE_SHEETS_CODE_RESOURCE = exports.ALERT_CODE_RESOURCE = exports.SCHEDULED_DELIVERY_CODE_RESOURCE = exports.EXTERNAL_CONNECTION_CODE_RESOURCE = exports.AI_AGENT_CODE_RESOURCE = exports.VIRTUAL_VIEW_CODE_RESOURCE = void 0;
|
|
4
4
|
const common_1 = require("@lightdash/common");
|
|
5
5
|
exports.VIRTUAL_VIEW_CODE_RESOURCE = {
|
|
6
6
|
kind: common_1.ContentAsCodeType.VIRTUAL_VIEW,
|
|
@@ -50,6 +50,29 @@ exports.AI_AGENT_CODE_RESOURCE = {
|
|
|
50
50
|
serialize: ({ updatedAt, downloadedAt, ...agent }) => agent,
|
|
51
51
|
sort: (left, right) => left.slug.localeCompare(right.slug),
|
|
52
52
|
};
|
|
53
|
+
exports.EXTERNAL_CONNECTION_CODE_RESOURCE = {
|
|
54
|
+
kind: common_1.ContentAsCodeType.EXTERNAL_CONNECTION,
|
|
55
|
+
displayLabel: 'external connection',
|
|
56
|
+
identityLabel: 'slug',
|
|
57
|
+
scope: 'project',
|
|
58
|
+
folderName: 'external-connections',
|
|
59
|
+
acceptedExtensions: ['.yml', '.yaml'],
|
|
60
|
+
fileName: {
|
|
61
|
+
strategy: 'identity',
|
|
62
|
+
fallbackPrefix: 'external-connection',
|
|
63
|
+
extension: '.yml',
|
|
64
|
+
},
|
|
65
|
+
dependencies: [],
|
|
66
|
+
identity: ({ slug }) => slug,
|
|
67
|
+
displayName: ({ name }) => name,
|
|
68
|
+
parse: (input, source) => (0, common_1.parseVersionedContentAsCodeDocument)({
|
|
69
|
+
input,
|
|
70
|
+
source,
|
|
71
|
+
resourceLabel: 'external connection',
|
|
72
|
+
contentType: common_1.ContentAsCodeType.EXTERNAL_CONNECTION,
|
|
73
|
+
}),
|
|
74
|
+
sort: (left, right) => left.slug.localeCompare(right.slug),
|
|
75
|
+
};
|
|
53
76
|
const scheduledResource = ({ contentType, folderName, label, }) => ({
|
|
54
77
|
kind: contentType,
|
|
55
78
|
displayLabel: label,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,yBAAyB,EACjC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,yBAAyB,EACjC,MAAM,mBAAmB,CAAC;AAa3B,MAAM,MAAM,wBAAwB,GAAG;IACnC,IAAI,EAAE,yBAAyB,CAAC;IAChC,YAAY,EAAE,SAAS,yBAAyB,EAAE,CAAC;CACtD,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,wBAAwB,EACH,CAAC;AAEzE,eAAO,MAAM,sBAAsB,EAAE,SAAS,wBAAwB,EAwBrE,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAC1C,WAAW,SAAS,wBAAwB,EAAE,KAC/C,IAeF,CAAC"}
|
|
@@ -30,6 +30,7 @@ exports.PROJECT_CODE_RESOURCES = [
|
|
|
30
30
|
projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE,
|
|
31
31
|
projectResources_1.ALERT_CODE_RESOURCE,
|
|
32
32
|
projectResources_1.GOOGLE_SHEETS_CODE_RESOURCE,
|
|
33
|
+
projectResources_1.EXTERNAL_CONNECTION_CODE_RESOURCE,
|
|
33
34
|
];
|
|
34
35
|
const assertCodeResourceDependencyOrder = (resources) => {
|
|
35
36
|
const completed = new Set();
|
|
@@ -16,6 +16,7 @@ export type ContentAsCodeUploadPermissions = {
|
|
|
16
16
|
scheduledDeliveries: boolean;
|
|
17
17
|
googleSheets: boolean;
|
|
18
18
|
dataApps: boolean;
|
|
19
|
+
externalConnections: boolean;
|
|
19
20
|
};
|
|
20
21
|
export declare const getContentAsCodeUploadPermissions: (projectUuid: string) => Promise<ContentAsCodeUploadPermissions>;
|
|
21
22
|
export declare const checkProjectCreationPermission: (upstreamProjectUuid: string | undefined, projectType: ProjectType) => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apiClient.d.ts","sourceRoot":"","sources":["../../../src/handlers/dbt/apiClient.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,WAAW,EAIX,WAAW,EAEX,KAAK,6BAA6B,EAGrC,MAAM,mBAAmB,CAAC;AAC3B,OAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS7C,KAAK,iBAAiB,GAAG;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B,CAAC;AAKF,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,SAE9C,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,CAAC,SAAS,WAAW,CAAC,SAAS,CAAC,EAAE,wBAIlE,iBAAiB,KAAG,OAAO,CAAC,CAAC,CAqE/B,CAAC;AAEF,eAAO,MAAM,cAAc,QACb,OAAO,CAAC,6BAA6B,CAKzC,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"apiClient.d.ts","sourceRoot":"","sources":["../../../src/handlers/dbt/apiClient.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,WAAW,EAIX,WAAW,EAEX,KAAK,6BAA6B,EAGrC,MAAM,mBAAmB,CAAC;AAC3B,OAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS7C,KAAK,iBAAiB,GAAG;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B,CAAC;AAKF,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,SAE9C,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,CAAC,SAAS,WAAW,CAAC,SAAS,CAAC,EAAE,wBAIlE,iBAAiB,KAAG,OAAO,CAAC,CAAC,CAqE/B,CAAC;AAEF,eAAO,MAAM,cAAc,QACb,OAAO,CAAC,6BAA6B,CAKzC,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAC1C,aAAa,MAAM,KACpB,OAAO,CAAC,8BAA8B,CA0FxC,CAAC;AAEF,eAAO,MAAM,8BAA8B,GACvC,qBAAqB,MAAM,GAAG,SAAS,EACvC,aAAa,WAAW,KACzB,OAAO,CAAC,IAAI,CA0Gd,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CA2B1D,CAAC"}
|
|
@@ -135,6 +135,7 @@ const getContentAsCodeUploadPermissions = async (projectUuid) => {
|
|
|
135
135
|
ability.can('manage', (0, ability_1.subject)('GoogleSheets', { ...baseSubject })),
|
|
136
136
|
dataApps: ability.can('create', (0, ability_1.subject)('DataApp', { ...accessibleResourceSubject })) ||
|
|
137
137
|
ability.can('manage', (0, ability_1.subject)('DataApp', { ...accessibleResourceSubject })),
|
|
138
|
+
externalConnections: ability.can('manage', (0, ability_1.subject)('ExternalConnection', { ...baseSubject })),
|
|
138
139
|
};
|
|
139
140
|
};
|
|
140
141
|
exports.getContentAsCodeUploadPermissions = getContentAsCodeUploadPermissions;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AgentAsCode, ChartAsCode, DashboardAsCode, type SpaceAsCode } from '@lightdash/common';
|
|
1
|
+
import { AgentAsCode, ChartAsCode, DashboardAsCode, ExternalConnectionAsCode, type SpaceAsCode } from '@lightdash/common';
|
|
2
2
|
import { type ContentAsCodeOutputVariant } from '../terminal/contentAsCodeOutput';
|
|
3
3
|
import { type SpaceCodeFile } from './spacesAsCode';
|
|
4
4
|
export type DownloadHandlerOptions = {
|
|
@@ -10,6 +10,7 @@ export type DownloadHandlerOptions = {
|
|
|
10
10
|
googleSheets: string[];
|
|
11
11
|
scheduledDeliveries: string[];
|
|
12
12
|
virtualViews: string[];
|
|
13
|
+
externalConnections: string[];
|
|
13
14
|
apps?: string[];
|
|
14
15
|
includeAgents?: boolean;
|
|
15
16
|
includeApps?: boolean;
|
|
@@ -34,10 +35,12 @@ export type DownloadHandlerOptions = {
|
|
|
34
35
|
skipGoogleSheets: boolean;
|
|
35
36
|
skipScheduledDeliveries: boolean;
|
|
36
37
|
skipVirtualViews: boolean;
|
|
38
|
+
skipExternalConnections: boolean;
|
|
37
39
|
includeAlerts: boolean;
|
|
38
40
|
includeGoogleSheets: boolean;
|
|
39
41
|
includeScheduledDeliveries: boolean;
|
|
40
42
|
includeVirtualViews: boolean;
|
|
43
|
+
includeExternalConnections: boolean;
|
|
41
44
|
includeAll: boolean;
|
|
42
45
|
appsOnly?: boolean;
|
|
43
46
|
stripPivotSeries: boolean;
|
|
@@ -54,6 +57,7 @@ type MetadataEntry = {
|
|
|
54
57
|
};
|
|
55
58
|
type DownloadContentType = 'charts' | 'dashboards' | 'sqlCharts';
|
|
56
59
|
export declare const downloadContent: (ids: string[], type: DownloadContentType, projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean, skipSpaces?: boolean, stripPivotSeries?: boolean, rootSpaces?: boolean, onProgress?: (detail: string) => void) => Promise<[number, string[], MetadataEntry[], SpaceAsCode[]]>;
|
|
60
|
+
export declare const getExternalConnectionSecretEnvVar: (slug: string) => string;
|
|
57
61
|
export declare const downloadHandler: (options: DownloadHandlerOptions) => Promise<void>;
|
|
58
62
|
export declare const uploadHandler: (options: DownloadHandlerOptions) => Promise<void>;
|
|
59
63
|
export declare const testHelpers: {
|
|
@@ -63,8 +67,10 @@ export declare const testHelpers: {
|
|
|
63
67
|
getDashboardChartSlugs: (dashboardSlugs: string[], customPath?: string, looseDashboards?: (DashboardAsCode & {
|
|
64
68
|
needsUpdating: boolean;
|
|
65
69
|
})[]) => Promise<string[]>;
|
|
66
|
-
hasContentFilters: ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, apps, }: Pick<DownloadHandlerOptions, "spacesOnly" | "charts" | "dashboards" | "agents" | "alerts" | "googleSheets" | "scheduledDeliveries" | "virtualViews" | "apps">) => boolean;
|
|
70
|
+
hasContentFilters: ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, }: Pick<DownloadHandlerOptions, "spacesOnly" | "charts" | "dashboards" | "agents" | "alerts" | "googleSheets" | "scheduledDeliveries" | "virtualViews" | "externalConnections" | "apps">) => boolean;
|
|
71
|
+
isExternalConnectionsUnavailableError: (error: unknown) => boolean;
|
|
67
72
|
readAiAgentFiles: (customPath?: string) => Promise<AgentAsCode[]>;
|
|
73
|
+
readExternalConnectionFiles: (customPath?: string) => Promise<ExternalConnectionAsCode[]>;
|
|
68
74
|
readSpaceFiles: (customPath?: string) => Promise<SpaceCodeFile[]>;
|
|
69
75
|
readSpaceNames: (customPath?: string) => Promise<Record<string, string>>;
|
|
70
76
|
sanitizeChartForDownload: (chart: ChartAsCode, stripPivotSeries: boolean) => ChartAsCode;
|
|
@@ -75,6 +81,7 @@ export declare const testHelpers: {
|
|
|
75
81
|
detail: string;
|
|
76
82
|
variant?: ContentAsCodeOutputVariant;
|
|
77
83
|
};
|
|
84
|
+
upsertExternalConnections: (projectId: string, slugs: string[], changes: Record<string, number>, force: boolean, canUpload: boolean, customPath?: string) => Promise<Record<string, number>>;
|
|
78
85
|
upsertSpaces: (projectId: string, files: SpaceCodeFile[], changes: Record<string, number>, skipSpaceCreate: boolean, publicSpaceCreate: boolean, skipSpaceAccess?: boolean) => Promise<Record<string, number>>;
|
|
79
86
|
upsertVirtualViews: (projectId: string, slugs: string[], changes: Record<string, number>, force: boolean, canUpload: boolean, customPath?: string) => Promise<Record<string, number>>;
|
|
80
87
|
validateSpaceIdentity: (parsed: unknown, filePath: string) => SpaceAsCode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,WAAW,
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.testHelpers = exports.uploadHandler = exports.downloadHandler = exports.downloadContent = void 0;
|
|
3
|
+
exports.testHelpers = exports.uploadHandler = exports.downloadHandler = exports.getExternalConnectionSecretEnvVar = exports.downloadContent = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
/* eslint-disable no-await-in-loop */
|
|
6
6
|
/* eslint-disable no-param-reassign */
|
|
@@ -30,7 +30,7 @@ const selectProject_1 = require("./selectProject");
|
|
|
30
30
|
const spacesAsCode_1 = require("./spacesAsCode");
|
|
31
31
|
const shouldDownloadAiAgents = ({ includeAll, includeAgents, agents, appsOnly, }) => appsOnly !== true &&
|
|
32
32
|
(includeAll === true || includeAgents === true || agents.length > 0);
|
|
33
|
-
const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, apps, }) => !spacesOnly &&
|
|
33
|
+
const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, }) => !spacesOnly &&
|
|
34
34
|
[
|
|
35
35
|
charts,
|
|
36
36
|
dashboards,
|
|
@@ -39,6 +39,7 @@ const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, goo
|
|
|
39
39
|
googleSheets,
|
|
40
40
|
scheduledDeliveries,
|
|
41
41
|
virtualViews,
|
|
42
|
+
externalConnections,
|
|
42
43
|
apps ?? [],
|
|
43
44
|
].some((filters) => filters.length > 0);
|
|
44
45
|
/*
|
|
@@ -502,6 +503,129 @@ const upsertVirtualViews = async (projectId, slugs, changes, force, canUpload, c
|
|
|
502
503
|
}
|
|
503
504
|
return changes;
|
|
504
505
|
};
|
|
506
|
+
const getExternalConnectionSecretEnvVar = (slug) => `LIGHTDASH_EXTERNAL_CONNECTION_SECRET_${slug
|
|
507
|
+
.replace(/-/g, '_')
|
|
508
|
+
.toUpperCase()}`;
|
|
509
|
+
exports.getExternalConnectionSecretEnvVar = getExternalConnectionSecretEnvVar;
|
|
510
|
+
const EXTERNAL_CONNECTION_SECRET_TYPES = new Set([
|
|
511
|
+
'api_key',
|
|
512
|
+
'bearer_token',
|
|
513
|
+
'google_service_account',
|
|
514
|
+
]);
|
|
515
|
+
/**
|
|
516
|
+
* External connections are enterprise-only and admin-gated; when the download
|
|
517
|
+
* was reached implicitly through --include-all these statuses mean "not
|
|
518
|
+
* available here" rather than a real failure: 403 = missing
|
|
519
|
+
* manage:ExternalConnection, 404 = pre-feature server, 422 = OSS server with
|
|
520
|
+
* no EE coder service provider (MissingConfigError).
|
|
521
|
+
*/
|
|
522
|
+
const isExternalConnectionsUnavailableError = (error) => error instanceof common_1.LightdashError &&
|
|
523
|
+
[403, 404, 422].includes(error.statusCode);
|
|
524
|
+
const downloadExternalConnections = async (projectId, slugs, implicit, customPath) => {
|
|
525
|
+
const slugQuery = slugs.map((slug) => ['slugs', slug]);
|
|
526
|
+
let offset = 0;
|
|
527
|
+
let total = 0;
|
|
528
|
+
const connections = [];
|
|
529
|
+
// Every page repeats the same full list, so warn once after the loop.
|
|
530
|
+
let missingSlugs = [];
|
|
531
|
+
try {
|
|
532
|
+
do {
|
|
533
|
+
const query = new URLSearchParams([
|
|
534
|
+
...slugQuery,
|
|
535
|
+
['offset', String(offset)],
|
|
536
|
+
]).toString();
|
|
537
|
+
const results = await (0, apiClient_1.lightdashApi)({
|
|
538
|
+
method: 'GET',
|
|
539
|
+
url: `/api/v1/projects/${projectId}/code/externalConnections?${query}`,
|
|
540
|
+
body: undefined,
|
|
541
|
+
});
|
|
542
|
+
connections.push(...results.externalConnections);
|
|
543
|
+
missingSlugs = results.missingSlugs;
|
|
544
|
+
offset = results.offset;
|
|
545
|
+
total = results.total;
|
|
546
|
+
} while (offset < total);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
if (implicit && isExternalConnectionsUnavailableError(error)) {
|
|
550
|
+
globalState_1.default.log(styles.warning('Skipping external connections: they require Lightdash Enterprise and the manage:ExternalConnection permission.'));
|
|
551
|
+
globalState_1.default.debug(`Could not download external connections: ${(0, common_1.getErrorMessage)(error)}`);
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
missingSlugs.forEach((slug) => globalState_1.default.log(styles.warning(`External connection "${slug}" was not found`)));
|
|
557
|
+
await (0, resource_1.writeCodeResourceDocuments)({
|
|
558
|
+
definition: projectResources_1.EXTERNAL_CONNECTION_CODE_RESOURCE,
|
|
559
|
+
basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
|
|
560
|
+
documents: connections,
|
|
561
|
+
pruneOtherDocuments: slugs.length === 0,
|
|
562
|
+
});
|
|
563
|
+
const secretEnvVars = connections
|
|
564
|
+
.filter(({ authType }) => EXTERNAL_CONNECTION_SECRET_TYPES.has(authType))
|
|
565
|
+
.map(({ slug }) => (0, exports.getExternalConnectionSecretEnvVar)(slug));
|
|
566
|
+
if (secretEnvVars.length > 0) {
|
|
567
|
+
globalState_1.default.log(styles.warning(`Secrets are never downloaded. To create these connections on another instance, set:\n\t${secretEnvVars.join('\n\t')}`));
|
|
568
|
+
}
|
|
569
|
+
return connections.length;
|
|
570
|
+
};
|
|
571
|
+
const readExternalConnectionFiles = async (customPath) => {
|
|
572
|
+
const result = await (0, resource_1.readCodeResourceFiles)({
|
|
573
|
+
definition: projectResources_1.EXTERNAL_CONNECTION_CODE_RESOURCE,
|
|
574
|
+
basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
|
|
575
|
+
});
|
|
576
|
+
(0, resource_1.assertCodeResourceFilesValid)(result);
|
|
577
|
+
return result.files.map(({ document }) => document);
|
|
578
|
+
};
|
|
579
|
+
const upsertExternalConnections = async (projectId, slugs, changes, force, canUpload, customPath) => {
|
|
580
|
+
const connections = await readExternalConnectionFiles(customPath);
|
|
581
|
+
const selected = slugs.length
|
|
582
|
+
? connections.filter(({ slug }) => slugs.includes(slug))
|
|
583
|
+
: connections;
|
|
584
|
+
const selectedSlugs = new Set(selected.map(({ slug }) => slug));
|
|
585
|
+
slugs
|
|
586
|
+
.filter((slug) => !selectedSlugs.has(slug))
|
|
587
|
+
.forEach((slug) => globalState_1.default.log(styles.warning(`External connection "${slug}" was not found locally`)));
|
|
588
|
+
if (selected.length > 0 && !canUpload) {
|
|
589
|
+
globalState_1.default.log(styles.error(`Error uploading external connections: the manage:ExternalConnection permission is required (enterprise feature)`));
|
|
590
|
+
return changes;
|
|
591
|
+
}
|
|
592
|
+
for (const connection of selected.sort((left, right) => left.slug.localeCompare(right.slug))) {
|
|
593
|
+
const envVar = (0, exports.getExternalConnectionSecretEnvVar)(connection.slug);
|
|
594
|
+
const envValue = process.env[envVar];
|
|
595
|
+
const secret = envValue !== undefined && envValue !== '' ? envValue : undefined;
|
|
596
|
+
if (envValue === '') {
|
|
597
|
+
globalState_1.default.log(styles.warning(`Environment variable ${envVar} is set but empty; treating the secret as not provided.`));
|
|
598
|
+
}
|
|
599
|
+
// The parser keeps unknown keys, so a secret authored into the YAML
|
|
600
|
+
// would otherwise be sent verbatim — strip it and tell the user.
|
|
601
|
+
if ('secret' in connection) {
|
|
602
|
+
delete connection.secret;
|
|
603
|
+
globalState_1.default.log(styles.warning(`Ignoring "secret" in the file for "${connection.slug}" — secrets must never be stored in YAML. Set ${envVar} instead.`));
|
|
604
|
+
}
|
|
605
|
+
try {
|
|
606
|
+
const result = await (0, apiClient_1.lightdashApi)({
|
|
607
|
+
method: 'POST',
|
|
608
|
+
url: `/api/v1/projects/${projectId}/code/externalConnections/${encodeURIComponent(connection.slug)}?force=${force}`,
|
|
609
|
+
body: JSON.stringify({
|
|
610
|
+
connection,
|
|
611
|
+
...(secret !== undefined ? { secret } : {}),
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
const action = `external connections ${getPromoteAction(result.action)}`;
|
|
615
|
+
changes[action] = (changes[action] ?? 0) + 1;
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
const errorKey = 'external connections with errors';
|
|
619
|
+
changes[errorKey] = (changes[errorKey] ?? 0) + 1;
|
|
620
|
+
const secretHint = secret === undefined &&
|
|
621
|
+
EXTERNAL_CONNECTION_SECRET_TYPES.has(connection.authType)
|
|
622
|
+
? `\n\tSet ${envVar} to provide the secret for "${connection.slug}" — secrets are read from the environment at upload time and never stored in YAML.`
|
|
623
|
+
: '';
|
|
624
|
+
globalState_1.default.log(styles.error(`Error upserting external connection:\n\t"${connection.name}" (slug: "${connection.slug}")\n\t${(0, common_1.getErrorMessage)(error)}${secretHint}`));
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return changes;
|
|
628
|
+
};
|
|
505
629
|
const getScheduledContentConfig = (contentType, customPath) => {
|
|
506
630
|
switch (contentType) {
|
|
507
631
|
case common_1.ContentAsCodeType.ALERT:
|
|
@@ -733,6 +857,7 @@ const downloadHandler = async (options) => {
|
|
|
733
857
|
options.includeGoogleSheets = false;
|
|
734
858
|
options.includeScheduledDeliveries = false;
|
|
735
859
|
options.includeVirtualViews = false;
|
|
860
|
+
options.includeExternalConnections = false;
|
|
736
861
|
}
|
|
737
862
|
if (options.spacesOnly) {
|
|
738
863
|
if (options.skipSpaces) {
|
|
@@ -746,12 +871,14 @@ const downloadHandler = async (options) => {
|
|
|
746
871
|
options.googleSheets = [];
|
|
747
872
|
options.scheduledDeliveries = [];
|
|
748
873
|
options.virtualViews = [];
|
|
874
|
+
options.externalConnections = [];
|
|
749
875
|
options.includeAgents = false;
|
|
750
876
|
options.includeApps = false;
|
|
751
877
|
options.includeAlerts = false;
|
|
752
878
|
options.includeGoogleSheets = false;
|
|
753
879
|
options.includeScheduledDeliveries = false;
|
|
754
880
|
options.includeVirtualViews = false;
|
|
881
|
+
options.includeExternalConnections = false;
|
|
755
882
|
}
|
|
756
883
|
if (options.rootSpaces && options.nested) {
|
|
757
884
|
throw new common_1.ParameterError('--root-spaces cannot be combined with --nested');
|
|
@@ -934,6 +1061,20 @@ const downloadHandler = async (options) => {
|
|
|
934
1061
|
detail: (total) => `${total} downloaded`,
|
|
935
1062
|
});
|
|
936
1063
|
}
|
|
1064
|
+
if (includeAllOptionalContent ||
|
|
1065
|
+
options.includeExternalConnections ||
|
|
1066
|
+
options.externalConnections.length > 0) {
|
|
1067
|
+
// Only --include-all is implicit: unavailable (non-EE / no
|
|
1068
|
+
// permission) then warns and skips instead of failing the download
|
|
1069
|
+
const implicit = includeAllOptionalContent &&
|
|
1070
|
+
!options.includeExternalConnections &&
|
|
1071
|
+
options.externalConnections.length === 0;
|
|
1072
|
+
await output.runItem({
|
|
1073
|
+
label: 'External connections',
|
|
1074
|
+
action: () => downloadExternalConnections(projectId, options.externalConnections, implicit, options.path),
|
|
1075
|
+
detail: (total) => `${total} downloaded`,
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
937
1078
|
// Download data apps (enterprise, opt-in via --apps / --include-apps / --include-all)
|
|
938
1079
|
const appsSelection = (0, appsDownload_1.selectAppsToDownload)({
|
|
939
1080
|
apps: Array.isArray(options.apps) ? options.apps : undefined,
|
|
@@ -1596,6 +1737,19 @@ const uploadHandler = async (options) => {
|
|
|
1596
1737
|
});
|
|
1597
1738
|
}
|
|
1598
1739
|
}
|
|
1740
|
+
if (!options.skipExternalConnections) {
|
|
1741
|
+
if (hasFilters && options.externalConnections.length === 0) {
|
|
1742
|
+
globalState_1.default.log(styles.warning(`No external connection filters provided, skipping`));
|
|
1743
|
+
}
|
|
1744
|
+
else {
|
|
1745
|
+
changes = await runUploadChangesPhase({
|
|
1746
|
+
output,
|
|
1747
|
+
label: 'External connections',
|
|
1748
|
+
changes,
|
|
1749
|
+
action: () => upsertExternalConnections(projectId, options.externalConnections, changes, options.force, uploadPermissions.externalConnections, options.path),
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1599
1753
|
// Upload data apps (enterprise, opt-in via --apps <references...> or
|
|
1600
1754
|
// --include-apps, fire-and-forget)
|
|
1601
1755
|
const explicitAppReferences = Array.isArray(options.apps)
|
|
@@ -1854,7 +2008,9 @@ exports.testHelpers = {
|
|
|
1854
2008
|
getFlatSpaceFileNames: spacesAsCode_1.getFlatSpaceFileNames,
|
|
1855
2009
|
getDashboardChartSlugs,
|
|
1856
2010
|
hasContentFilters,
|
|
2011
|
+
isExternalConnectionsUnavailableError,
|
|
1857
2012
|
readAiAgentFiles,
|
|
2013
|
+
readExternalConnectionFiles,
|
|
1858
2014
|
readSpaceFiles: spacesAsCode_1.readSpaceFiles,
|
|
1859
2015
|
readSpaceNames: spacesAsCode_1.readSpaceNames,
|
|
1860
2016
|
sanitizeChartForDownload,
|
|
@@ -1862,6 +2018,7 @@ exports.testHelpers = {
|
|
|
1862
2018
|
shouldDownloadAiAgents,
|
|
1863
2019
|
sortSpaceFilesParentFirst: spacesAsCode_1.sortSpaceFilesParentFirst,
|
|
1864
2020
|
summarizeUploadChanges,
|
|
2021
|
+
upsertExternalConnections,
|
|
1865
2022
|
upsertSpaces: spacesAsCode_1.upsertSpaces,
|
|
1866
2023
|
upsertVirtualViews,
|
|
1867
2024
|
validateSpaceIdentity: spacesAsCode_1.validateSpaceIdentity,
|
|
@@ -13,7 +13,7 @@ vitest_1.vi.mock('./dbt/apiClient', async (importOriginal) => ({
|
|
|
13
13
|
...(await importOriginal()),
|
|
14
14
|
lightdashApi: vitest_1.vi.fn(),
|
|
15
15
|
}));
|
|
16
|
-
const { assertUniqueSpacePaths, downloadSpaces, getDashboardChartSlugs, getFlatSpaceFileNames, hasContentFilters, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, summarizeUploadChanges, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
|
|
16
|
+
const { assertUniqueSpacePaths, downloadSpaces, getDashboardChartSlugs, getFlatSpaceFileNames, hasContentFilters, isExternalConnectionsUnavailableError, readAiAgentFiles, readSpaceFiles, readSpaceNames, sanitizeChartForDownload, shouldFallBackToEmbeddedSpaces, shouldDownloadAiAgents, summarizeUploadChanges, upsertExternalConnections, upsertSpaces, upsertVirtualViews, validateSpaceIdentity, writeSpaceFiles, } = download_1.testHelpers;
|
|
17
17
|
const makeContentFilterOptions = (overrides = {}) => ({
|
|
18
18
|
spacesOnly: false,
|
|
19
19
|
charts: [],
|
|
@@ -23,6 +23,7 @@ const makeContentFilterOptions = (overrides = {}) => ({
|
|
|
23
23
|
googleSheets: [],
|
|
24
24
|
scheduledDeliveries: [],
|
|
25
25
|
virtualViews: [],
|
|
26
|
+
externalConnections: [],
|
|
26
27
|
apps: [],
|
|
27
28
|
...overrides,
|
|
28
29
|
});
|
|
@@ -30,6 +31,9 @@ describe('hasContentFilters', () => {
|
|
|
30
31
|
it('treats explicit apps as a filtered download and upload', () => {
|
|
31
32
|
expect(hasContentFilters(makeContentFilterOptions({ apps: ['app-reference'] }))).toBe(true);
|
|
32
33
|
});
|
|
34
|
+
it('treats explicit external connections as a filtered upload', () => {
|
|
35
|
+
expect(hasContentFilters(makeContentFilterOptions({ externalConnections: ['stripe'] }))).toBe(true);
|
|
36
|
+
});
|
|
33
37
|
it('keeps an unfiltered content operation unfiltered', () => {
|
|
34
38
|
expect(hasContentFilters(makeContentFilterOptions())).toBe(false);
|
|
35
39
|
});
|
|
@@ -290,6 +294,124 @@ version: 1
|
|
|
290
294
|
expect(logSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Error uploading virtual views'));
|
|
291
295
|
});
|
|
292
296
|
});
|
|
297
|
+
describe('upsertExternalConnections', () => {
|
|
298
|
+
let tmpDir;
|
|
299
|
+
const externalConnectionYaml = (slug, extraLines = []) => [
|
|
300
|
+
'allowedContentTypes:',
|
|
301
|
+
' - application/json',
|
|
302
|
+
'allowedMethods:',
|
|
303
|
+
' - GET',
|
|
304
|
+
'allowedPathPrefixes: []',
|
|
305
|
+
'apiKeyLocation: header',
|
|
306
|
+
'apiKeyName: Authorization',
|
|
307
|
+
'authType: api_key',
|
|
308
|
+
'contentType: external_connection',
|
|
309
|
+
'customHeaders: null',
|
|
310
|
+
'instructions: null',
|
|
311
|
+
`name: ${slug}`,
|
|
312
|
+
'oauthScopes: null',
|
|
313
|
+
'origin: https://api.example.com',
|
|
314
|
+
'rateLimitPerMinute: null',
|
|
315
|
+
'requestMaxBytes: 262144',
|
|
316
|
+
'responseMaxBytes: 1048576',
|
|
317
|
+
`slug: ${slug}`,
|
|
318
|
+
'timeoutMs: 10000',
|
|
319
|
+
'version: 1',
|
|
320
|
+
...extraLines,
|
|
321
|
+
'',
|
|
322
|
+
].join('\n');
|
|
323
|
+
beforeEach(async () => {
|
|
324
|
+
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockReset();
|
|
325
|
+
tmpDir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'external-connections-test-'));
|
|
326
|
+
await fs_1.promises.mkdir(path.join(tmpDir, 'external-connections'));
|
|
327
|
+
});
|
|
328
|
+
afterEach(async () => {
|
|
329
|
+
vitest_1.vi.restoreAllMocks();
|
|
330
|
+
vitest_1.vi.unstubAllEnvs();
|
|
331
|
+
await fs_1.promises.rm(tmpDir, { recursive: true, force: true });
|
|
332
|
+
});
|
|
333
|
+
it('omits the secret from the request when the env var is unset', async () => {
|
|
334
|
+
await fs_1.promises.writeFile(path.join(tmpDir, 'external-connections', 'stripe-api.yml'), externalConnectionYaml('stripe-api'));
|
|
335
|
+
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
336
|
+
action: 'no changes',
|
|
337
|
+
});
|
|
338
|
+
const changes = await upsertExternalConnections('project-uuid', [], {}, false, true, tmpDir);
|
|
339
|
+
expect(changes).toEqual({ 'external connections skipped': 1 });
|
|
340
|
+
const request = vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[0][0];
|
|
341
|
+
expect(request.url).toContain('/code/externalConnections/stripe-api?force=false');
|
|
342
|
+
const body = JSON.parse(request.body);
|
|
343
|
+
expect(body).not.toHaveProperty('secret');
|
|
344
|
+
expect(body.connection.slug).toBe('stripe-api');
|
|
345
|
+
});
|
|
346
|
+
it('resolves the secret from LIGHTDASH_EXTERNAL_CONNECTION_SECRET_<SLUG>', async () => {
|
|
347
|
+
await fs_1.promises.writeFile(path.join(tmpDir, 'external-connections', 'stripe-api.yml'), externalConnectionYaml('stripe-api'));
|
|
348
|
+
vitest_1.vi.stubEnv('LIGHTDASH_EXTERNAL_CONNECTION_SECRET_STRIPE_API', 'sk-123');
|
|
349
|
+
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
350
|
+
action: 'create',
|
|
351
|
+
});
|
|
352
|
+
const changes = await upsertExternalConnections('project-uuid', [], {}, false, true, tmpDir);
|
|
353
|
+
expect(changes).toEqual({ 'external connections created': 1 });
|
|
354
|
+
const body = JSON.parse(vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[0][0].body);
|
|
355
|
+
expect(body.secret).toBe('sk-123');
|
|
356
|
+
// The secret rides next to the document, never inside it
|
|
357
|
+
expect(body.connection).not.toHaveProperty('secret');
|
|
358
|
+
});
|
|
359
|
+
it('strips a secret authored into the YAML and warns', async () => {
|
|
360
|
+
const logSpy = vitest_1.vi
|
|
361
|
+
.spyOn(globalState_1.default, 'log')
|
|
362
|
+
.mockImplementation(() => undefined);
|
|
363
|
+
await fs_1.promises.writeFile(path.join(tmpDir, 'external-connections', 'stripe-api.yml'), externalConnectionYaml('stripe-api', ['secret: leaked-secret']));
|
|
364
|
+
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
|
|
365
|
+
action: 'update',
|
|
366
|
+
});
|
|
367
|
+
await upsertExternalConnections('project-uuid', [], {}, false, true, tmpDir);
|
|
368
|
+
const rawBody = vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[0][0].body;
|
|
369
|
+
expect(rawBody).not.toContain('leaked-secret');
|
|
370
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('secrets must never be stored in YAML'));
|
|
371
|
+
});
|
|
372
|
+
it('reports one category error without uploading when the caller lacks permission', async () => {
|
|
373
|
+
const logSpy = vitest_1.vi
|
|
374
|
+
.spyOn(globalState_1.default, 'log')
|
|
375
|
+
.mockImplementation(() => undefined);
|
|
376
|
+
await fs_1.promises.writeFile(path.join(tmpDir, 'external-connections', 'stripe-api.yml'), externalConnectionYaml('stripe-api'));
|
|
377
|
+
const changes = await upsertExternalConnections('project-uuid', [], {}, false, false, tmpDir);
|
|
378
|
+
expect(changes).toEqual({});
|
|
379
|
+
expect(apiClient_1.lightdashApi).not.toHaveBeenCalled();
|
|
380
|
+
expect(logSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining('Error uploading external connections'));
|
|
381
|
+
});
|
|
382
|
+
it('appends the env-var hint when a secret-requiring create fails without the env var', async () => {
|
|
383
|
+
const logSpy = vitest_1.vi
|
|
384
|
+
.spyOn(globalState_1.default, 'log')
|
|
385
|
+
.mockImplementation(() => undefined);
|
|
386
|
+
await fs_1.promises.writeFile(path.join(tmpDir, 'external-connections', 'stripe-api.yml'), externalConnectionYaml('stripe-api'));
|
|
387
|
+
vitest_1.vi.mocked(apiClient_1.lightdashApi).mockRejectedValueOnce(new Error('type "api_key" requires a secret'));
|
|
388
|
+
const changes = await upsertExternalConnections('project-uuid', [], {}, false, true, tmpDir);
|
|
389
|
+
expect(changes).toEqual({ 'external connections with errors': 1 });
|
|
390
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Set LIGHTDASH_EXTERNAL_CONNECTION_SECRET_STRIPE_API'));
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
describe('getExternalConnectionSecretEnvVar', () => {
|
|
394
|
+
it('upper-snakes the slug', () => {
|
|
395
|
+
expect((0, download_1.getExternalConnectionSecretEnvVar)('stripe-api-v2')).toBe('LIGHTDASH_EXTERNAL_CONNECTION_SECRET_STRIPE_API_V2');
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
describe('isExternalConnectionsUnavailableError', () => {
|
|
399
|
+
const lightdashError = (statusCode) => new common_1.LightdashError({
|
|
400
|
+
message: 'nope',
|
|
401
|
+
name: 'TestError',
|
|
402
|
+
statusCode,
|
|
403
|
+
data: {},
|
|
404
|
+
});
|
|
405
|
+
it('classifies permission, missing-route and non-EE errors as unavailable', () => {
|
|
406
|
+
[403, 404, 422].forEach((statusCode) => {
|
|
407
|
+
expect(isExternalConnectionsUnavailableError(lightdashError(statusCode))).toBe(true);
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
it('keeps real failures fatal', () => {
|
|
411
|
+
expect(isExternalConnectionsUnavailableError(lightdashError(500))).toBe(false);
|
|
412
|
+
expect(isExternalConnectionsUnavailableError(new Error('network down'))).toBe(false);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
293
415
|
describe('readAiAgentFiles', () => {
|
|
294
416
|
let tmpDir;
|
|
295
417
|
beforeEach(async () => {
|
package/dist/index.js
CHANGED
|
@@ -357,6 +357,8 @@ const downloadCommand = commander_1.program
|
|
|
357
357
|
.option('--virtual-views <slugs...>', 'specify virtual view slugs to download', [])
|
|
358
358
|
.option('--google-sheets <slugs...>', 'specify Google Sheets sync slugs to download', [])
|
|
359
359
|
.option('--scheduled-deliveries <slugs...>', 'specify scheduled delivery slugs to download', [])
|
|
360
|
+
.option('--external-connections <slugs...>', 'specify external connection slugs to download (enterprise)', [])
|
|
361
|
+
.option('--include-external-connections', "include all of the project's data-app external connections (enterprise)", false)
|
|
360
362
|
.option('-l, --language-map', 'generate a language maps for the downloaded charts and dashboards', false)
|
|
361
363
|
.option('-p, --path <path>', 'specify a custom path to download content', undefined)
|
|
362
364
|
.option('--nested', 'organize downloads in nested folders by project and space (default: flat structure)', false)
|
|
@@ -389,6 +391,7 @@ const uploadCommand = commander_1.program
|
|
|
389
391
|
.option('--virtual-views <slugs...>', 'specify virtual view slugs to upload', [])
|
|
390
392
|
.option('--google-sheets <slugs...>', 'specify Google Sheets sync slugs to upload', [])
|
|
391
393
|
.option('--scheduled-deliveries <slugs...>', 'specify scheduled delivery slugs to upload', [])
|
|
394
|
+
.option('--external-connections <slugs...>', 'specify external connection slugs to upload (enterprise)', [])
|
|
392
395
|
.option('--force', 'Force upload unchanged files and allow destructive virtual-view column changes', false)
|
|
393
396
|
.option('-p, --path <path>', 'specify a custom path to upload content from', undefined)
|
|
394
397
|
.option('--project <project uuid>', 'specify a project UUID to upload', parseProjectArgument, undefined)
|
|
@@ -402,6 +405,7 @@ const uploadCommand = commander_1.program
|
|
|
402
405
|
.option('--skip-virtual-views', 'skip uploading virtual views', false)
|
|
403
406
|
.option('--skip-google-sheets', 'skip uploading Google Sheets syncs', false)
|
|
404
407
|
.option('--skip-scheduled-deliveries', 'skip uploading scheduled deliveries', false)
|
|
408
|
+
.option('--skip-external-connections', 'skip uploading external connections', false)
|
|
405
409
|
.option('--concurrency <number>', 'Number of parallel uploads (default: 1)', '1')
|
|
406
410
|
.option('--include-charts', 'Include charts updates when uploading dashboards', false)
|
|
407
411
|
.option('--validate', 'Validate charts and dashboards after upload', false)
|
|
@@ -12,6 +12,11 @@ You are editing a Lightdash **data app** that was downloaded with the Lightdash
|
|
|
12
12
|
- All data access goes through `@lightdash/query-sdk` (a postMessage bridge to Lightdash). There is **no** `fetch`, no REST calls, no other network access at runtime — anything else is blocked.
|
|
13
13
|
- For the SDK surface (query builder, `useLightdash`, filters, downloads), read the `lightdash-data-app` skill in this folder.
|
|
14
14
|
|
|
15
|
+
## External HTTP APIs go through linked connections
|
|
16
|
+
|
|
17
|
+
- The one sanctioned path to a third-party API is `lightdash.externalFetch('<alias>', ...)` against an **external connection** a project admin configured and linked to this app (see the `lightdash-data-app` skill).
|
|
18
|
+
- If the app needs an API that is not linked, **stop and say so** — do not vendor an HTTP client or try to reach it another way. An admin must create the connection (in project settings, or as code: `lightdash download --include-external-connections`, edit `lightdash/external-connections/<slug>.yml`, `lightdash upload` with the secret in `LIGHTDASH_EXTERNAL_CONNECTION_SECRET_<SLUG>`) and link it to the app in Lightdash. That is project configuration, not part of this folder's source tree.
|
|
19
|
+
|
|
15
20
|
## Library boundaries — build with what's preinstalled
|
|
16
21
|
|
|
17
22
|
- The app builds against a **fixed template dependency set** — see `package.json` (React, Recharts, d3 + d3-cloud/d3-sankey, Radix primitives, Tailwind, lucide-react, date-fns, html-to-image, jspdf, and more). **Design within this set**; it covers almost all data-app needs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3483.0",
|
|
4
4
|
"description": "Lightdash CLI tool",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"unique-names-generator": "4.7.1",
|
|
42
42
|
"uuid": "11.1.1",
|
|
43
43
|
"yaml": "2.8.3",
|
|
44
|
-
"@lightdash/
|
|
45
|
-
"@lightdash/
|
|
44
|
+
"@lightdash/warehouses": "0.3483.0",
|
|
45
|
+
"@lightdash/common": "0.3483.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/inquirer": "8.2.4",
|