@lightdash/cli 1.34.0 → 1.36.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,4 +1,4 @@
1
- import { type DataAppCode, type DataAppCodeFile, type DataAppContext, type DataAppDependencies, type ImportAppCodeRequestBody } from '@lightdash/common';
1
+ import { type DataAppCode, type DataAppCodeFile, type DataAppContext, type DataAppDependencies, type DataAppManifest, type ImportAppCodeRequestBody } from '@lightdash/common';
2
2
  /**
3
3
  * Derives a collision-safe folder name for a data app.
4
4
  * Uses the slugified app name. If that slug is already taken by another app,
@@ -14,14 +14,10 @@ export declare const buildImportBody: (code: DataAppCode, targetProjectUuid: str
14
14
  createNew?: boolean;
15
15
  }) => ImportAppCodeRequestBody;
16
16
  /**
17
- * Points a downloaded app folder's manifest at a different app, so future
18
- * uploads update that app instead of the one it was downloaded from.
17
+ * The persistent slug from a slug-aware server is the folder name (stable
18
+ * across app renames). Pre-slug servers fall back to name-derived folders.
19
19
  */
20
- export declare const retargetManifest: (dir: string, target: {
21
- appUuid: string;
22
- projectUuid: string;
23
- version: number;
24
- }) => Promise<void>;
20
+ export declare const resolveAppFolderName: (manifest: DataAppManifest, takenFolders: Set<string>) => string;
25
21
  /**
26
22
  * Writes server-provided dependency files as the app folder's package.json +
27
23
  * pnpm-lock.yaml. Called after the scaffold write so they override the
@@ -1 +1 @@
1
- {"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EAExB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAC5D,wBAgBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QACpB,MAAM,UACH;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,KAClE,OAAO,CAAC,IAAI,CAUd,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,QACL,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC3B,MAAM,KACZ,OAAO,CAAC,oBAAoB,GAAG,IAAI,CA6BrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,wBACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,MAAM,EAOH,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,yBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,eAC/B,MAAM,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAC3B,WAAW,cACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,mBAAmB,KAC1B,WAC4E,CAAC;AAEhF,eAAO,MAAM,iBAAiB,QAAe,MAAM,KAAG,OAAO,CAAC,WAAW,CAuBxE,CAAC"}
1
+ {"version":3,"file":"appCodeFiles.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appCodeFiles.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAO3B;;;;GAIG;AACH,eAAO,MAAM,aAAa,SAChB,MAAM,WACH,MAAM,gBACD,GAAG,CAAC,MAAM,CAAC,KAC1B,MAcF,CAAC;AAEF,eAAO,MAAM,eAAe,QACnB,MAAM,SACJ,eAAe,EAAE,KACzB,OAAO,CAAC,IAAI,CAsBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,QACrB,MAAM,WACF,cAAc,KACxB,OAAO,CAAC,IAAI,CASd,CAAC;AAEF,eAAO,MAAM,gBAAgB,QACpB,MAAM,QACL,WAAW,KAClB,OAAO,CAAC,IAAI,CAWd,CAAC;AAmCF,eAAO,MAAM,eAAe,SAClB,WAAW,qBACE,MAAM,QACnB;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAC5D,wBAoBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,aACnB,eAAe,gBACX,GAAG,CAAC,MAAM,CAAC,KAC1B,MAgBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAC1B,MAAM,QACL,mBAAmB,KAC1B,OAAO,CAAC,IAAI,CAGd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,WAAW,EAAE,MAAM,CAAC;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAC3B,MAAM,KACZ,OAAO,CAAC,oBAAoB,GAAG,IAAI,CA6BrC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,eAClB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,wBACZ,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAC7C,MAAM,EAOH,CAAC;AAEP;;;;;GAKG;AACH,eAAO,MAAM,4BAA4B,yBACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,eAC/B,MAAM,KACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAgBvB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,SAC3B,WAAW,cACL,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,QAC5B,mBAAmB,KAC1B,WAC4E,CAAC;AAEhF,eAAO,MAAM,iBAAiB,QAAe,MAAM,KAAG,OAAO,CAAC,WAAW,CAuBxE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readBundleFromDir = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.retargetManifest = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
3
+ exports.readBundleFromDir = exports.attachDependenciesToCode = exports.applySdkMirrorToTemplateDeps = exports.buildDepsWarningLines = exports.readDependenciesFromDir = exports.writeDependenciesToDir = exports.resolveAppFolderName = exports.buildImportBody = exports.writeBundleToDir = exports.writeContextToDir = exports.writeFilesToDir = exports.appFolderName = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
6
  const fs_1 = require("fs");
@@ -95,25 +95,37 @@ const buildImportBody = (code, targetProjectUuid, opts) => {
95
95
  targetAppUuid = opts.app;
96
96
  }
97
97
  else if (targetProjectUuid === code.manifest.projectUuid) {
98
+ // Pre-slug bundles carry an appUuid (uuid-fallback identity, and
99
+ // same-project append on pre-slug servers). Slug-only bundles yield
100
+ // undefined here — slug-aware servers resolve by slug instead.
98
101
  targetAppUuid = code.manifest.appUuid;
99
102
  }
100
103
  return {
101
104
  code,
102
105
  targetAppUuid,
103
106
  spaceUuid: opts.space,
107
+ ...(opts.createNew ? { createNew: true } : {}),
104
108
  };
105
109
  };
106
110
  exports.buildImportBody = buildImportBody;
107
111
  /**
108
- * Points a downloaded app folder's manifest at a different app, so future
109
- * uploads update that app instead of the one it was downloaded from.
112
+ * The persistent slug from a slug-aware server is the folder name (stable
113
+ * across app renames). Pre-slug servers fall back to name-derived folders.
110
114
  */
111
- const retargetManifest = async (dir, target) => {
112
- const manifestPath = path.join(dir, MANIFEST_FILENAME);
113
- const manifest = YAML.parse(await fs_1.promises.readFile(manifestPath, 'utf-8'));
114
- await fs_1.promises.writeFile(manifestPath, YAML.stringify({ ...manifest, ...target }), 'utf-8');
115
+ const resolveAppFolderName = (manifest, takenFolders) => {
116
+ // Defense-in-depth: a server of unknown version, or a hand-tampered
117
+ // manifest on disk, must not steer the local write path via an
118
+ // unvalidated slug (e.g. `../../etc`) — only trust it once it passes the
119
+ // same shape check the server enforces on upload.
120
+ if (manifest.slug !== undefined && (0, common_1.isValidDataAppSlug)(manifest.slug)) {
121
+ return manifest.slug;
122
+ }
123
+ // The fallback needs a uuid for its collision/untitled suffixes. Real
124
+ // pre-slug servers always emit appUuid; only a tampered slug-only
125
+ // manifest lands here without one.
126
+ return (0, exports.appFolderName)(manifest.name, manifest.appUuid ?? 'unknown', takenFolders);
115
127
  };
116
- exports.retargetManifest = retargetManifest;
128
+ exports.resolveAppFolderName = resolveAppFolderName;
117
129
  /**
118
130
  * Writes server-provided dependency files as the app folder's package.json +
119
131
  * pnpm-lock.yaml. Called after the scaffold write so they override the
@@ -73,6 +73,17 @@ describe('buildImportBody', () => {
73
73
  });
74
74
  expect(body.targetAppUuid).toBeUndefined();
75
75
  });
76
+ it('includes createNew: true in the body when the flag is set', () => {
77
+ const code = makeCode('app-uuid-1', 'proj-uuid-1');
78
+ const body = (0, appCodeFiles_1.buildImportBody)(code, 'proj-uuid-1', { createNew: true });
79
+ expect(body.createNew).toBe(true);
80
+ expect(body.targetAppUuid).toBeUndefined();
81
+ });
82
+ it('omits the createNew key entirely when the flag is not set', () => {
83
+ const code = makeCode('app-uuid-1', 'proj-uuid-1');
84
+ const body = (0, appCodeFiles_1.buildImportBody)(code, 'proj-uuid-1', {});
85
+ expect('createNew' in body).toBe(false);
86
+ });
76
87
  });
77
88
  const bundle = {
78
89
  manifest: {
@@ -134,23 +145,6 @@ it('upload reads back only src/ files, ignoring scaffolding and context', async
134
145
  expect(read.files.every((f) => f.path.startsWith('src/'))).toBe(true);
135
146
  expect(read.files.map((f) => f.path).sort()).toEqual(bundle.files.map((f) => f.path).sort());
136
147
  });
137
- describe('retargetManifest', () => {
138
- it('rewrites appUuid, projectUuid and version, preserving other fields', async () => {
139
- const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-'));
140
- await (0, appCodeFiles_1.writeBundleToDir)(dir, bundle);
141
- await (0, appCodeFiles_1.retargetManifest)(dir, {
142
- appUuid: 'new-app-uuid',
143
- projectUuid: 'new-proj-uuid',
144
- version: 1,
145
- });
146
- const read = await (0, appCodeFiles_1.readBundleFromDir)(dir);
147
- expect(read.manifest.appUuid).toBe('new-app-uuid');
148
- expect(read.manifest.projectUuid).toBe('new-proj-uuid');
149
- expect(read.manifest.version).toBe(1);
150
- expect(read.manifest.name).toBe('N');
151
- expect(read.manifest.downloadedAt).toBe('2026-06-30T00:00:00.000Z');
152
- });
153
- });
154
148
  it('throws a clear error when the manifest is not valid YAML', async () => {
155
149
  const dir = await fs_1.promises.mkdtemp(path.join(os.tmpdir(), 'ld-app-'));
156
150
  await fs_1.promises.writeFile(path.join(dir, 'lightdash-app.yml'), 'foo: [bar', 'utf-8');
@@ -214,6 +208,32 @@ describe('appFolderName', () => {
214
208
  expect((0, appCodeFiles_1.appFolderName)('!!!', 'abcd1234-ef56-7890-ab12-cdef01234567', new Set())).toBe('untitled-app-abcd1234');
215
209
  });
216
210
  });
211
+ describe('resolveAppFolderName', () => {
212
+ it('uses the manifest slug when present', () => {
213
+ const code = makeCode('app-uuid-1', 'proj-uuid-1');
214
+ expect((0, appCodeFiles_1.resolveAppFolderName)({ ...code.manifest, slug: 'sales-app' }, new Set())).toBe('sales-app');
215
+ });
216
+ it('falls back to appFolderName when the manifest has no slug', () => {
217
+ const uuid = 'abcd1234-ef56-7890-ab12-cdef01234567';
218
+ const code = makeCode(uuid, 'proj-uuid-1');
219
+ expect((0, appCodeFiles_1.resolveAppFolderName)(code.manifest, new Set())).toBe((0, appCodeFiles_1.appFolderName)(code.manifest.name, uuid, new Set()));
220
+ });
221
+ it('uses the slug for uuid-free manifests (slug-aware servers)', () => {
222
+ const code = makeCode('app-uuid-1', 'proj-uuid-1');
223
+ expect((0, appCodeFiles_1.resolveAppFolderName)({ ...code.manifest, appUuid: undefined, slug: 'sales-app' }, new Set())).toBe('sales-app');
224
+ });
225
+ // Defense-in-depth: a tampered manifest (or a server of unknown version)
226
+ // must not be able to steer the write path via an invalid slug.
227
+ it('falls back to appFolderName when the slug is a path-traversal attempt', () => {
228
+ const uuid = 'abcd1234-ef56-7890-ab12-cdef01234567';
229
+ const code = makeCode(uuid, 'proj-uuid-1');
230
+ expect((0, appCodeFiles_1.resolveAppFolderName)({ ...code.manifest, slug: '../../../../tmp/evil' }, new Set())).toBe((0, appCodeFiles_1.appFolderName)(code.manifest.name, uuid, new Set()));
231
+ });
232
+ it('still uses a valid manifest slug (unaffected by the new validation)', () => {
233
+ const code = makeCode('app-uuid-1', 'proj-uuid-1');
234
+ expect((0, appCodeFiles_1.resolveAppFolderName)({ ...code.manifest, slug: 'sales-app-2' }, new Set())).toBe('sales-app-2');
235
+ });
236
+ });
217
237
  // ─── readDependenciesFromDir ──────────────────────────────────────────────────
218
238
  const makePackageJson = (deps = {}) => JSON.stringify({ name: 'test-app', dependencies: deps });
219
239
  const LOCKFILE_CONTENT = 'lockfileVersion: 9.0\n\npackages:\n react@19.0.0:\n';
@@ -1,7 +1,27 @@
1
- import { type DataAppCodeDownload } from '@lightdash/common';
1
+ import { type DataAppCodeDownload, type DataAppManifest } from '@lightdash/common';
2
2
  export declare const DEFAULT_APPS_LIMIT = 50;
3
- export declare const getDataAppUuidFromReference: (reference: string) => string;
3
+ export declare const getDataAppReference: (reference: string) => string;
4
4
  export declare const getDataAppUploadFilter: (references: string[], includeApps: boolean) => Set<string> | null;
5
+ /**
6
+ * --include-apps uploads every folder; explicit --apps references filter by
7
+ * the manifest's slug or appUuid (references may be either, or app URLs).
8
+ */
9
+ export declare const uploadFilterMatches: (filter: Set<string> | null, manifest: DataAppManifest) => boolean;
10
+ /** The filter entries this manifest satisfies (for unmatched-ref reporting). */
11
+ export declare const matchedUploadRefs: (filter: Set<string>, manifest: DataAppManifest) => string[];
12
+ /**
13
+ * Warning for --apps references that matched no local app folder. Uuid-shaped
14
+ * refs (including refs parsed out of app URLs) get the slug-identity
15
+ * explanation: id-free bundles can only be selected by slug.
16
+ */
17
+ export declare const unmatchedUploadRefsWarning: (unmatched: string[]) => string | null;
18
+ /**
19
+ * Shown when the upload response carries no slug even though the bundle sent
20
+ * one — the server predates slug identity, so it ignored the slug and matched
21
+ * (or created) by uuid only. A same-slug upload may have just created a
22
+ * duplicate app instead of appending.
23
+ */
24
+ export declare const preSlugServerHint: (folder: string) => string;
5
25
  /**
6
26
  * Resolves the --apps-limit flag. Commander passes the raw string (or
7
27
  * undefined when the flag was not given, so an explicit flag is
@@ -23,15 +43,15 @@ export type AppsDownloadSelection = {
23
43
  mode: 'none';
24
44
  } | {
25
45
  mode: 'explicit';
26
- appUuids: string[];
46
+ appRefs: string[];
27
47
  } | {
28
48
  mode: 'list-all';
29
- extraAppUuids: string[];
49
+ extraAppRefs: string[];
30
50
  };
31
51
  /**
32
- * Explicit UUIDs (--apps) are fetched directly; --include-apps lists every
33
- * app in the project via the project-wide apps endpoint, falling back to
34
- * the space-scoped content API on servers that predate it.
52
+ * Explicit references (--apps) are fetched directly; --include-apps lists
53
+ * every app in the project via the project-wide apps endpoint, falling back
54
+ * to the space-scoped content API on servers that predate it.
35
55
  */
36
56
  export declare const selectAppsToDownload: (request: {
37
57
  apps?: string[];
@@ -45,8 +65,16 @@ export declare const capListedApps: (listedUuids: string[], limit: number) => {
45
65
  * Pre-context servers return a download payload without `context`.
46
66
  */
47
67
  export declare const ensureDownloadedAppContext: (appUuid: string, code: DataAppCodeDownload) => DataAppCodeDownload;
68
+ /**
69
+ * Shown after uploading a bundle whose manifest predates slug identity —
70
+ * uploads keep working via the uuid fallback, but the user should upgrade.
71
+ */
72
+ export declare const preSlugUploadHint: (args: {
73
+ folder: string;
74
+ slug: string | undefined;
75
+ }) => string;
48
76
  export type AppDownloadFailure = {
49
- appUuid: string;
77
+ appRef: string;
50
78
  message: string;
51
79
  };
52
80
  export type AppDownloadErrorOutcome = {
@@ -62,15 +90,6 @@ export type AppDownloadErrorOutcome = {
62
90
  * message over a canned guess.
63
91
  */
64
92
  export declare const classifyAppDownloadError: (err: unknown) => AppDownloadErrorOutcome;
65
- /**
66
- * Shown when a newly created app's folder manifest was NOT retargeted —
67
- * spells out the consequence and the manual fix.
68
- */
69
- export declare const manifestRetargetHint: (args: {
70
- folder: string;
71
- appUuid: string;
72
- projectUuid: string;
73
- }) => string;
74
93
  /**
75
94
  * Sums changes entries that represent actual upserts — excluding both
76
95
  * 'skipped' and 'failed' keys so that failures don't suppress the
@@ -82,15 +101,6 @@ export declare const computeUpsertedTotal: (changes: Record<string, number>) =>
82
101
  * items — the condition that should display the "all skipped" warning.
83
102
  */
84
103
  export declare const shouldWarnAllSkipped: (changes: Record<string, number>) => boolean;
85
- /**
86
- * Determines how an app upload should proceed given a potential project
87
- * mismatch between the manifest and the upload target.
88
- *
89
- * 'proceed' — upload immediately (same project, or --create-new).
90
- * 'needs-confirmation' — projects differ and --create-new was not passed;
91
- * caller must prompt (TTY) or reject (non-TTY).
92
- */
93
- export declare const classifyAppUpload: (manifestProjectUuid: string, targetProjectUuid: string, createNew: boolean) => 'proceed' | 'needs-confirmation';
94
104
  export declare const appsDownloadSummary: (successCount: number, total: number, failures: AppDownloadFailure[], appsDir: string, skippedNotBuiltCount: number) => {
95
105
  ok: boolean;
96
106
  message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,mBAAmB,EAC3B,MAAM,mBAAmB,CAAC;AAG3B,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC,eAAO,MAAM,2BAA2B,cAAe,MAAM,KAAG,MAoB/D,CAAC;AAEF,eAAO,MAAM,sBAAsB,eACnB,MAAM,EAAE,eACP,OAAO,KACrB,GAAG,CAAC,MAAM,CAAC,GAAG,IAC4D,CAAC;AAE9E;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,aACf,MAAM,GAAG,SAAS,eACf,OAAO,KACrB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;CAejD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kCAAkC,QAAS,OAAO,KAAG,OACP,CAAC;AAE5D,MAAM,MAAM,qBAAqB,GAC3B;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,GACxC;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,YAAa;IAC1C,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,KAAG,qBASH,CAAC;AAEF,eAAO,MAAM,aAAa,gBACT,MAAM,EAAE,SACd,MAAM,KACd;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,CAAA;CAG7C,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,0BAA0B,YAC1B,MAAM,QACT,mBAAmB,KAC1B,mBAOF,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,uBAAuB,GAC7B;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,GAC1B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExC;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,QAC5B,OAAO,KACb,uBAoBF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,SAAU;IACvC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;CACvB,KAAG,MAC0O,CAAC;AAE/O;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,YAAa,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAG,MAGpB,CAAC;AAEpD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,YACpB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAChC,OAKF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,wBACL,MAAM,qBACR,MAAM,aACd,OAAO,KACnB,SAAS,GAAG,oBAKd,CAAC;AAEF,eAAO,MAAM,mBAAmB,iBACd,MAAM,SACb,MAAM,YACH,kBAAkB,EAAE,WACrB,MAAM,wBACO,MAAM,KAC7B;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAiBxD,CAAC"}
1
+ {"version":3,"file":"appsDownload.d.ts","sourceRoot":"","sources":["../../../src/handlers/apps/appsDownload.ts"],"names":[],"mappings":"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"}
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appsDownloadSummary = exports.classifyAppUpload = exports.shouldWarnAllSkipped = exports.computeUpsertedTotal = exports.manifestRetargetHint = exports.classifyAppDownloadError = exports.ensureDownloadedAppContext = exports.capListedApps = exports.selectAppsToDownload = exports.shouldFallBackToSpaceScopedListing = exports.resolveAppsLimit = exports.getDataAppUploadFilter = exports.getDataAppUuidFromReference = exports.DEFAULT_APPS_LIMIT = void 0;
3
+ exports.appsDownloadSummary = exports.shouldWarnAllSkipped = exports.computeUpsertedTotal = exports.classifyAppDownloadError = exports.preSlugUploadHint = exports.ensureDownloadedAppContext = exports.capListedApps = exports.selectAppsToDownload = exports.shouldFallBackToSpaceScopedListing = exports.resolveAppsLimit = exports.preSlugServerHint = exports.unmatchedUploadRefsWarning = exports.matchedUploadRefs = exports.uploadFilterMatches = exports.getDataAppUploadFilter = exports.getDataAppReference = exports.DEFAULT_APPS_LIMIT = void 0;
4
4
  const common_1 = require("@lightdash/common");
5
5
  const uuid_1 = require("uuid");
6
6
  exports.DEFAULT_APPS_LIMIT = 50;
7
- const getDataAppUuidFromReference = (reference) => {
7
+ const getDataAppReference = (reference) => {
8
8
  let url;
9
9
  try {
10
10
  url = new URL(reference);
@@ -20,9 +20,42 @@ const getDataAppUuidFromReference = (reference) => {
20
20
  (0, uuid_1.validate)(pathSegments[index + 1]));
21
21
  return appSegmentIndex >= 0 ? pathSegments[appSegmentIndex + 1] : reference;
22
22
  };
23
- exports.getDataAppUuidFromReference = getDataAppUuidFromReference;
24
- const getDataAppUploadFilter = (references, includeApps) => includeApps ? null : new Set(references.map(exports.getDataAppUuidFromReference));
23
+ exports.getDataAppReference = getDataAppReference;
24
+ const getDataAppUploadFilter = (references, includeApps) => includeApps ? null : new Set(references.map(exports.getDataAppReference));
25
25
  exports.getDataAppUploadFilter = getDataAppUploadFilter;
26
+ /**
27
+ * --include-apps uploads every folder; explicit --apps references filter by
28
+ * the manifest's slug or appUuid (references may be either, or app URLs).
29
+ */
30
+ const uploadFilterMatches = (filter, manifest) => filter === null ||
31
+ (manifest.appUuid !== undefined && filter.has(manifest.appUuid)) ||
32
+ (manifest.slug !== undefined && filter.has(manifest.slug));
33
+ exports.uploadFilterMatches = uploadFilterMatches;
34
+ /** The filter entries this manifest satisfies (for unmatched-ref reporting). */
35
+ const matchedUploadRefs = (filter, manifest) => [manifest.appUuid, manifest.slug].filter((ref) => ref !== undefined && filter.has(ref));
36
+ exports.matchedUploadRefs = matchedUploadRefs;
37
+ /**
38
+ * Warning for --apps references that matched no local app folder. Uuid-shaped
39
+ * refs (including refs parsed out of app URLs) get the slug-identity
40
+ * explanation: id-free bundles can only be selected by slug.
41
+ */
42
+ const unmatchedUploadRefsWarning = (unmatched) => {
43
+ if (unmatched.length === 0)
44
+ return null;
45
+ const base = `No local app folder matched: ${unmatched.join(', ')}.`;
46
+ return unmatched.some((ref) => (0, uuid_1.validate)(ref))
47
+ ? `${base} Bundles downloaded with slug identity carry no uuid — select them by slug (the folder name) instead of a UUID or app URL.`
48
+ : base;
49
+ };
50
+ exports.unmatchedUploadRefsWarning = unmatchedUploadRefsWarning;
51
+ /**
52
+ * Shown when the upload response carries no slug even though the bundle sent
53
+ * one — the server predates slug identity, so it ignored the slug and matched
54
+ * (or created) by uuid only. A same-slug upload may have just created a
55
+ * duplicate app instead of appending.
56
+ */
57
+ const preSlugServerHint = (folder) => `This server predates slug-based app identity, so "${folder}" was matched by uuid only. If you expected to update an existing app, verify no duplicate was created, and upgrade the server (or use a matching CLI version).`;
58
+ exports.preSlugServerHint = preSlugServerHint;
26
59
  /**
27
60
  * Resolves the --apps-limit flag. Commander passes the raw string (or
28
61
  * undefined when the flag was not given, so an explicit flag is
@@ -53,17 +86,17 @@ exports.resolveAppsLimit = resolveAppsLimit;
53
86
  const shouldFallBackToSpaceScopedListing = (err) => err instanceof common_1.LightdashError && err.statusCode === 404;
54
87
  exports.shouldFallBackToSpaceScopedListing = shouldFallBackToSpaceScopedListing;
55
88
  /**
56
- * Explicit UUIDs (--apps) are fetched directly; --include-apps lists every
57
- * app in the project via the project-wide apps endpoint, falling back to
58
- * the space-scoped content API on servers that predate it.
89
+ * Explicit references (--apps) are fetched directly; --include-apps lists
90
+ * every app in the project via the project-wide apps endpoint, falling back
91
+ * to the space-scoped content API on servers that predate it.
59
92
  */
60
93
  const selectAppsToDownload = (request) => {
61
- const explicitUuids = (request.apps ?? []).map(exports.getDataAppUuidFromReference);
94
+ const explicitRefs = (request.apps ?? []).map(exports.getDataAppReference);
62
95
  if (request.includeApps) {
63
- return { mode: 'list-all', extraAppUuids: explicitUuids };
96
+ return { mode: 'list-all', extraAppRefs: explicitRefs };
64
97
  }
65
- if (explicitUuids.length > 0) {
66
- return { mode: 'explicit', appUuids: explicitUuids };
98
+ if (explicitRefs.length > 0) {
99
+ return { mode: 'explicit', appRefs: explicitRefs };
67
100
  }
68
101
  return { mode: 'none' };
69
102
  };
@@ -83,6 +116,14 @@ const ensureDownloadedAppContext = (appUuid, code) => {
83
116
  return code;
84
117
  };
85
118
  exports.ensureDownloadedAppContext = ensureDownloadedAppContext;
119
+ /**
120
+ * Shown after uploading a bundle whose manifest predates slug identity —
121
+ * uploads keep working via the uuid fallback, but the user should upgrade.
122
+ */
123
+ const preSlugUploadHint = (args) => `${args.folder}/lightdash-app.yml predates slug identity. Re-download the app to upgrade${args.slug !== undefined
124
+ ? ` (or add \`slug: ${args.slug}\` to lightdash-app.yml)`
125
+ : ''}. Uploads keep working via uuid matching meanwhile.`;
126
+ exports.preSlugUploadHint = preSlugUploadHint;
86
127
  /**
87
128
  * A 404 saying the app has no ready version means it exists but never
88
129
  * finished a successful build — nothing to download, so it's a skip, not a
@@ -108,12 +149,6 @@ const classifyAppDownloadError = (err) => {
108
149
  return { kind: 'fail', message: (0, common_1.getErrorMessage)(err) };
109
150
  };
110
151
  exports.classifyAppDownloadError = classifyAppDownloadError;
111
- /**
112
- * Shown when a newly created app's folder manifest was NOT retargeted —
113
- * spells out the consequence and the manual fix.
114
- */
115
- const manifestRetargetHint = (args) => `${args.folder}/lightdash-app.yml still targets the original app, so future uploads here will ask to create again. To update the new app instead, set appUuid: ${args.appUuid} and projectUuid: ${args.projectUuid} in lightdash-app.yml.`;
116
- exports.manifestRetargetHint = manifestRetargetHint;
117
152
  /**
118
153
  * Sums changes entries that represent actual upserts — excluding both
119
154
  * 'skipped' and 'failed' keys so that failures don't suppress the
@@ -134,21 +169,6 @@ const shouldWarnAllSkipped = (changes) => {
134
169
  return totalSkipped > 0 && (0, exports.computeUpsertedTotal)(changes) === 0;
135
170
  };
136
171
  exports.shouldWarnAllSkipped = shouldWarnAllSkipped;
137
- /**
138
- * Determines how an app upload should proceed given a potential project
139
- * mismatch between the manifest and the upload target.
140
- *
141
- * 'proceed' — upload immediately (same project, or --create-new).
142
- * 'needs-confirmation' — projects differ and --create-new was not passed;
143
- * caller must prompt (TTY) or reject (non-TTY).
144
- */
145
- const classifyAppUpload = (manifestProjectUuid, targetProjectUuid, createNew) => {
146
- if (createNew || manifestProjectUuid === targetProjectUuid) {
147
- return 'proceed';
148
- }
149
- return 'needs-confirmation';
150
- };
151
- exports.classifyAppUpload = classifyAppUpload;
152
172
  const appsDownloadSummary = (successCount, total, failures, appsDir, skippedNotBuiltCount) => {
153
173
  const attempted = total - skippedNotBuiltCount;
154
174
  const skippedSuffix = skippedNotBuiltCount > 0
@@ -161,7 +181,7 @@ const appsDownloadSummary = (successCount, total, failures, appsDir, skippedNotB
161
181
  return {
162
182
  ok: false,
163
183
  message: `${base} — ${failures.length} failed`,
164
- failureLines: failures.map((failure) => ` ✖ ${failure.appUuid}: ${failure.message}`),
184
+ failureLines: failures.map((failure) => ` ✖ ${failure.appRef}: ${failure.message}`),
165
185
  };
166
186
  };
167
187
  exports.appsDownloadSummary = appsDownloadSummary;