@lightdash/cli 0.3482.1 → 0.3484.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.
@@ -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;AA4CF,eAAO,MAAM,gCAAgC,iDAKvC,CAAC;AAEP,eAAO,MAAM,mBAAmB,qCAI9B,CAAC;AAEH,eAAO,MAAM,2BAA2B,gDAKlC,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;AAY3B,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,EAuBrE,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAC1C,WAAW,SAAS,wBAAwB,EAAE,KAC/C,IAeF,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;CACrB,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAC1C,aAAa,MAAM,KACpB,OAAO,CAAC,8BAA8B,CAsFxC,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"}
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":"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"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,WAAW,EAyBX,WAAW,EAGX,eAAe,EACf,wBAAwB,EAexB,KAAK,WAAW,EACnB,MAAM,mBAAmB,CAAC;AAY3B,OAAO,EAIH,KAAK,0BAA0B,EAClC,MAAM,iCAAiC,CAAC;AAgEzC,OAAO,EAmBH,KAAK,aAAa,EACrB,MAAM,gBAAgB,CAAC;AAExB,MAAM,MAAM,sBAAsB,GAAG;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uBAAuB,EAAE,OAAO,CAAC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,uBAAuB,EAAE,OAAO,CAAC;IACjC,aAAa,EAAE,OAAO,CAAC;IACvB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,0BAA0B,EAAE,OAAO,CAAC;IACpC,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAmIF,KAAK,aAAa,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AA6XF,KAAK,mBAAmB,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAkDjE,eAAO,MAAM,eAAe,GACxB,KAAK,MAAM,EAAE,EACb,MAAM,mBAAmB,EACzB,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,aAAa,MAAM,EACnB,cAAa,OAAe,EAC5B,SAAQ,OAAe,EACvB,aAAY,OAAe,EAC3B,mBAAkB,OAAe,EACjC,aAAY,OAAe,EAC3B,aAAa,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,KACtC,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,CAAC,CAsI5D,CAAC;AAoIF,eAAO,MAAM,iCAAiC,GAAI,MAAM,MAAM,KAAG,MAGzC,CAAC;AAwgBzB,eAAO,MAAM,eAAe,GACxB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkpBd,CAAC;AAudF,eAAO,MAAM,aAAa,GACtB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkvBd,CAAC;AAEF,eAAO,MAAM,WAAW;;;;6CAvxBJ,MAAM,EAAE,eACX,MAAM,oBACF,CAAC,eAAe,GAAG;QAAE,aAAa,EAAE,OAAO,CAAA;KAAE,CAAC,EAAE;yJAx4ElE,IAAI,CACH,sBAAsB,EACpB,YAAY,GACZ,QAAQ,GACR,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,cAAc,GACd,qBAAqB,GACrB,cAAc,GACd,qBAAqB,GACrB,MAAM,CACX,KAAG,OAAO;mDAuzB2C,OAAO,KAAG,OAAO;oCA6QtD,MAAM,KACpB,OAAO,CAAC,WAAW,EAAE,CAAC;+CA7LR,MAAM,KACpB,OAAO,CAAC,wBAAwB,EAAE,CAAC;;;sCAtyB3B,WAAW,oBACA,OAAO,KAC1B,WAAW;;+EAnIX,IAAI,CACH,sBAAsB,EACtB,YAAY,GAAG,eAAe,GAAG,QAAQ,GAAG,UAAU,CACzD,KAAG,OAAO;;qCAihEC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC9B;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,0BAA0B,CAAA;KAAE;2CArmC5C,MAAM,SACV,MAAM,EAAE,WACN,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACxB,OAAO,aACH,OAAO,eACL,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;oCAzLnB,MAAM,SACV,MAAM,EAAE,WACN,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,SACxB,OAAO,aACH,OAAO,eACL,MAAM,KACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;;;CAg8EjC,CAAC"}
@@ -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.3482.1",
3
+ "version": "0.3484.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/common": "0.3482.1",
45
- "@lightdash/warehouses": "0.3482.1"
44
+ "@lightdash/warehouses": "0.3484.0",
45
+ "@lightdash/common": "0.3484.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/inquirer": "8.2.4",