@lightdash/cli 3.164.0 → 3.165.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,10 +1,11 @@
1
1
  import { ContentType, type ContentSlugRenameRequest } from '@lightdash/common';
2
+ type SlugUpdateResourceType = ContentType.CHART | ContentType.DASHBOARD | ContentType.DATA_APP;
2
3
  type SlugUpdateOptions = {
3
4
  verbose: boolean;
4
5
  project?: string;
5
6
  path?: string;
6
7
  dryRun: boolean;
7
- type: ContentType.CHART | ContentType.DASHBOARD;
8
+ type: ContentType.CHART | ContentType.DASHBOARD | 'app';
8
9
  from: string;
9
10
  to: string;
10
11
  };
@@ -17,6 +18,10 @@ type LocalSlugUpdatePlan = {
17
18
  source: string;
18
19
  target: string;
19
20
  }>;
21
+ directoryMoves: Array<{
22
+ source: string;
23
+ target: string;
24
+ }>;
20
25
  metadata: {
21
26
  root: string;
22
27
  resourceType: ContentType.CHART | ContentType.DASHBOARD;
@@ -28,11 +33,11 @@ export type LocalSlugUpdateFileChange = {
28
33
  source: string;
29
34
  target?: string;
30
35
  };
31
- export declare const planLocalSlugUpdate: (customPath: string | undefined, from: string, to: string, resourceType?: ContentType.CHART | ContentType.DASHBOARD) => Promise<LocalSlugUpdatePlan>;
36
+ export declare const planLocalSlugUpdate: (customPath: string | undefined, from: string, to: string, resourceType?: SlugUpdateResourceType) => Promise<LocalSlugUpdatePlan>;
32
37
  export declare const getLocalSlugUpdateFileChanges: (plan: LocalSlugUpdatePlan, customPath: string | undefined) => LocalSlugUpdateFileChange[];
33
38
  export declare const applyLocalSlugUpdate: (plan: LocalSlugUpdatePlan) => Promise<() => Promise<void>>;
34
39
  export declare const requestSlugUpdate: (projectUuid: string, request: ContentSlugRenameRequest) => Promise<void>;
35
- export declare const executeSlugUpdate: (projectUuid: string, customPath: string | undefined, from: string, to: string, resourceType?: ContentType.CHART | ContentType.DASHBOARD) => Promise<LocalSlugUpdatePlan>;
40
+ export declare const executeSlugUpdate: (projectUuid: string, customPath: string | undefined, from: string, to: string, resourceType?: SlugUpdateResourceType) => Promise<LocalSlugUpdatePlan>;
36
41
  export declare const slugUpdateHandler: (options: SlugUpdateOptions) => Promise<void>;
37
42
  export {};
38
43
  //# sourceMappingURL=slugUpdate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slugUpdate.d.ts","sourceRoot":"","sources":["../../src/handlers/slugUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EAKX,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAa3B,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACvB,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,SAAS,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrD,QAAQ,EACF;QACI,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,EAAE,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;QACxD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACjC,GACD,SAAS,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;CAC7B,CAAC;AAIF,MAAM,MAAM,yBAAyB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAyFF,eAAO,MAAM,mBAAmB,eAChB,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,iBACI,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,KACxD,OAAO,CAAC,mBAAmB,CA2L7B,CAAC;AAEF,eAAO,MAAM,6BAA6B,SAChC,mBAAmB,cACb,MAAM,GAAG,SAAS,KAC/B,yBAAyB,EAuB3B,CAAC;AAuDF,eAAO,MAAM,oBAAoB,SACvB,mBAAmB,KAC1B,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAuD7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBACb,MAAM,WACV,wBAAwB,KAClC,OAAO,CAAC,IAAI,CAmBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBACb,MAAM,cACP,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,iBACI,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,KACxD,OAAO,CAAC,mBAAmB,CAuB7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YACjB,iBAAiB,KAC3B,OAAO,CAAC,IAAI,CAuDd,CAAC"}
1
+ {"version":3,"file":"slugUpdate.d.ts","sourceRoot":"","sources":["../../src/handlers/slugUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,WAAW,EAMX,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAa3B,KAAK,sBAAsB,GACrB,WAAW,CAAC,KAAK,GACjB,WAAW,CAAC,SAAS,GACrB,WAAW,CAAC,QAAQ,CAAC;AAE3B,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAEhB,IAAI,EAAE,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,GAAG,KAAK,CAAC;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACvB,WAAW,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,SAAS,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAGrD,cAAc,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1D,QAAQ,EACF;QACI,IAAI,EAAE,MAAM,CAAC;QACb,YAAY,EAAE,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,CAAC;QACxD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KACjC,GACD,SAAS,CAAC;IAChB,iBAAiB,EAAE,MAAM,CAAC;CAC7B,CAAC;AAIF,MAAM,MAAM,yBAAyB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AA+FF,eAAO,MAAM,mBAAmB,eAChB,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,iBACI,sBAAsB,KACrC,OAAO,CAAC,mBAAmB,CAwP7B,CAAC;AAEF,eAAO,MAAM,6BAA6B,SAChC,mBAAmB,cACb,MAAM,GAAG,SAAS,KAC/B,yBAAyB,EA2B3B,CAAC;AAuDF,eAAO,MAAM,oBAAoB,SACvB,mBAAmB,KAC1B,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CA8E7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBACb,MAAM,WACV,wBAAwB,KAClC,OAAO,CAAC,IAAI,CAmBd,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBACb,MAAM,cACP,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,iBACI,sBAAsB,KACrC,OAAO,CAAC,mBAAmB,CAuB7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YACjB,iBAAiB,KAC3B,OAAO,CAAC,IAAI,CA2Dd,CAAC"}
@@ -25,7 +25,11 @@ const getYamlFiles = async (root) => {
25
25
  (entry.name.endsWith('.yml') ||
26
26
  entry.name.endsWith('.yaml')) &&
27
27
  !entry.name.endsWith('.language.map.yml') &&
28
- !entry.name.endsWith('.language.map.yaml'))
28
+ !entry.name.endsWith('.language.map.yaml') &&
29
+ // App bundles can hold installed deps and lockfiles;
30
+ // neither carries renameable content references.
31
+ entry.name !== 'pnpm-lock.yaml' &&
32
+ !entry.parentPath.split(path.sep).includes('node_modules'))
29
33
  .map((entry) => path.join(entry.parentPath, entry.name));
30
34
  }
31
35
  catch (error) {
@@ -57,6 +61,7 @@ const assertMovesAreSafe = async (moves) => {
57
61
  }
58
62
  }
59
63
  };
64
+ const APP_MANIFEST_FILENAME = 'lightdash-app.yml';
60
65
  // Comment-only or scalar YAML (e.g. chart-type context files) parses to null or a primitive
61
66
  const isYamlMapping = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
62
67
  const readYamlDocuments = async (root) => {
@@ -78,18 +83,29 @@ const planLocalSlugUpdate = async (customPath, from, to, resourceType = common_1
78
83
  const root = (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
79
84
  const fileUpdates = [];
80
85
  const fileMoves = [];
86
+ const directoryMoves = [];
81
87
  let referencesUpdated = 0;
82
88
  const languageMapKey = resourceType === common_1.ContentType.CHART ? 'chart' : 'dashboard';
83
89
  if (!from) {
84
90
  throw new common_1.ParameterError('The source slug must not be empty');
85
91
  }
86
- if (!to || to.length > common_1.MAX_CONTENT_SLUG_LENGTH || (0, common_1.generateSlug)(to) !== to) {
92
+ if (resourceType === common_1.ContentType.DATA_APP) {
93
+ // App slugs follow the server's own shape check, which allows more
94
+ // than generateSlug emits (e.g. consecutive hyphens).
95
+ if (!(0, common_1.isValidDataAppSlug)(to)) {
96
+ throw new common_1.ParameterError(`Invalid data app slug "${to}". Slugs must start with a lowercase letter or digit and contain only lowercase letters, digits, and hyphens, up to 255 characters.`);
97
+ }
98
+ }
99
+ else if (!to ||
100
+ to.length > common_1.MAX_CONTENT_SLUG_LENGTH ||
101
+ (0, common_1.generateSlug)(to) !== to) {
87
102
  throw new common_1.ParameterError(`The target slug must contain between 1 and ${common_1.MAX_CONTENT_SLUG_LENGTH} lowercase letters, numbers, or hyphen-separated words`);
88
103
  }
89
104
  if (from === to) {
90
105
  return {
91
106
  fileUpdates,
92
107
  fileMoves,
108
+ directoryMoves,
93
109
  metadata: undefined,
94
110
  referencesUpdated,
95
111
  };
@@ -102,6 +118,38 @@ const planLocalSlugUpdate = async (customPath, from, to, resourceType = common_1
102
118
  });
103
119
  for (const { filePath, document, parsed } of contentDocuments) {
104
120
  let changed = false;
121
+ if (resourceType === common_1.ContentType.DATA_APP) {
122
+ if (path.basename(filePath) === APP_MANIFEST_FILENAME &&
123
+ parsed.slug === from) {
124
+ document.set('slug', to);
125
+ referencesUpdated += 1;
126
+ changed = true;
127
+ // Bundles download to a folder named after the slug; keep the
128
+ // folder in step so the next download matches it.
129
+ const bundleDir = path.dirname(filePath);
130
+ if (path.basename(bundleDir) === from) {
131
+ directoryMoves.push({
132
+ source: bundleDir,
133
+ target: path.join(path.dirname(bundleDir), to),
134
+ });
135
+ }
136
+ }
137
+ if (Array.isArray(parsed.tiles)) {
138
+ for (const [index, tile] of parsed.tiles.entries()) {
139
+ if (tile.type === 'data_app' &&
140
+ tile.properties?.appSlug === from) {
141
+ document.setIn(['tiles', index, 'properties', 'appSlug'], to);
142
+ referencesUpdated += 1;
143
+ changed = true;
144
+ }
145
+ }
146
+ }
147
+ if (changed) {
148
+ fileUpdates.push({ filePath, content: document.toString() });
149
+ }
150
+ // eslint-disable-next-line no-continue
151
+ continue;
152
+ }
105
153
  const isChart = parsed.contentType === common_1.ContentType.CHART ||
106
154
  parsed.metricQuery !== undefined;
107
155
  const isDashboard = parsed.contentType === common_1.ContentType.DASHBOARD ||
@@ -185,24 +233,28 @@ const planLocalSlugUpdate = async (customPath, from, to, resourceType = common_1
185
233
  fileUpdates.push({ filePath, content: document.toString() });
186
234
  }
187
235
  }
188
- const metadata = await (0, metadataFile_1.readMetadataFile)(root);
189
236
  let updatedMetadata;
190
- const metadataSlugs = resourceType === common_1.ContentType.CHART
191
- ? metadata.charts
192
- : metadata.dashboards;
193
- if (from in metadataSlugs) {
194
- if (to !== from && to in metadataSlugs) {
195
- throw new common_1.ParameterError(`Cannot update local metadata because ${resourceType} slug "${to}" already exists`);
237
+ // The local metadata file only tracks charts and dashboards.
238
+ if (resourceType !== common_1.ContentType.DATA_APP) {
239
+ const metadata = await (0, metadataFile_1.readMetadataFile)(root);
240
+ const metadataSlugs = resourceType === common_1.ContentType.CHART
241
+ ? metadata.charts
242
+ : metadata.dashboards;
243
+ if (from in metadataSlugs) {
244
+ if (to !== from && to in metadataSlugs) {
245
+ throw new common_1.ParameterError(`Cannot update local metadata because ${resourceType} slug "${to}" already exists`);
246
+ }
247
+ const slugs = { ...metadataSlugs };
248
+ slugs[to] = slugs[from];
249
+ delete slugs[from];
250
+ updatedMetadata = { root, resourceType, slugs };
196
251
  }
197
- const slugs = { ...metadataSlugs };
198
- slugs[to] = slugs[from];
199
- delete slugs[from];
200
- updatedMetadata = { root, resourceType, slugs };
201
252
  }
202
- await assertMovesAreSafe(fileMoves);
253
+ await assertMovesAreSafe([...fileMoves, ...directoryMoves]);
203
254
  return {
204
255
  fileUpdates,
205
256
  fileMoves,
257
+ directoryMoves,
206
258
  metadata: updatedMetadata,
207
259
  referencesUpdated,
208
260
  };
@@ -217,6 +269,10 @@ const getLocalSlugUpdateFileChanges = (plan, customPath) => {
217
269
  source: relative(source),
218
270
  target: relative(target),
219
271
  })),
272
+ ...plan.directoryMoves.map(({ source, target }) => ({
273
+ source: `${relative(source)}${path.sep}`,
274
+ target: `${relative(target)}${path.sep}`,
275
+ })),
220
276
  ...plan.fileUpdates
221
277
  .filter(({ filePath }) => !movedSources.has(filePath))
222
278
  .map(({ filePath }) => ({ source: relative(filePath) })),
@@ -273,6 +329,14 @@ const applyLocalSlugUpdate = async (plan) => {
273
329
  ]);
274
330
  const snapshots = await captureFiles(affectedPaths);
275
331
  const stagedMoves = [];
332
+ const appliedDirectoryMoves = [];
333
+ const undoDirectoryMoves = async () => {
334
+ for (const move of [...appliedDirectoryMoves].reverse()) {
335
+ // eslint-disable-next-line no-await-in-loop
336
+ await fs_1.promises.rename(move.target, move.source);
337
+ }
338
+ appliedDirectoryMoves.length = 0;
339
+ };
276
340
  try {
277
341
  await Promise.all(plan.fileUpdates.map(({ filePath, content }) => fs_1.promises.writeFile(filePath, content)));
278
342
  for (const [index, move] of plan.fileMoves.entries()) {
@@ -285,6 +349,13 @@ const applyLocalSlugUpdate = async (plan) => {
285
349
  // eslint-disable-next-line no-await-in-loop
286
350
  await fs_1.promises.rename(stagedMoves[index], move.target);
287
351
  }
352
+ // Directory renames (data-app bundle folders) go last so every file
353
+ // write above targets the original paths.
354
+ for (const move of plan.directoryMoves) {
355
+ // eslint-disable-next-line no-await-in-loop
356
+ await fs_1.promises.rename(move.source, move.target);
357
+ appliedDirectoryMoves.push(move);
358
+ }
288
359
  if (plan.metadata && metadataPath) {
289
360
  const existing = await (0, metadataFile_1.readMetadataFile)(plan.metadata.root);
290
361
  await fs_1.promises.writeFile(metadataPath, JSON.stringify({
@@ -296,11 +367,17 @@ const applyLocalSlugUpdate = async (plan) => {
296
367
  }
297
368
  }
298
369
  catch (error) {
370
+ // Directory moves roll back first so restored files land inside
371
+ // folders at their original paths.
372
+ await undoDirectoryMoves();
299
373
  await Promise.all(stagedMoves.map((temporary) => fs_1.promises.rm(temporary, { force: true })));
300
374
  await restoreFiles(snapshots);
301
375
  throw error;
302
376
  }
303
- return () => restoreFiles(snapshots);
377
+ return async () => {
378
+ await undoDirectoryMoves();
379
+ await restoreFiles(snapshots);
380
+ };
304
381
  };
305
382
  exports.applyLocalSlugUpdate = applyLocalSlugUpdate;
306
383
  const requestSlugUpdate = async (projectUuid, request) => {
@@ -341,6 +418,8 @@ const slugUpdateHandler = async (options) => {
341
418
  (0, credentials_1.refuseAgentConnection)();
342
419
  globalState_1.default.setVerbose(options.verbose);
343
420
  await (0, apiClient_1.checkLightdashVersion)();
421
+ const resourceType = options.type === 'app' ? common_1.ContentType.DATA_APP : options.type;
422
+ const resourceLabel = options.type === 'app' ? 'data app' : options.type;
344
423
  const config = await (0, config_1.getConfig)();
345
424
  await (0, credentials_1.resolveCredential)(config);
346
425
  if (!config.context?.serverUrl)
@@ -350,17 +429,17 @@ const slugUpdateHandler = async (options) => {
350
429
  throw new common_1.ParameterError('No project selected. Run lightdash config set-project');
351
430
  }
352
431
  (0, selectProject_1.logSelectedProject)(selection, config, options.dryRun
353
- ? `Previewing ${options.type} slug update in`
354
- : `Updating ${options.type} slug in`);
432
+ ? `Previewing ${resourceLabel} slug update in`
433
+ : `Updating ${resourceLabel} slug in`);
355
434
  if (options.dryRun) {
356
- const localPlan = await (0, exports.planLocalSlugUpdate)(options.path, options.from, options.to, options.type);
357
- console.info(styles.success(`Would update ${options.type} slug "${options.from}" -> "${options.to}".`));
435
+ const localPlan = await (0, exports.planLocalSlugUpdate)(options.path, options.from, options.to, resourceType);
436
+ console.info(styles.success(`Would update ${resourceLabel} slug "${options.from}" -> "${options.to}".`));
358
437
  logLocalSlugUpdateFileChanges(localPlan, options.path, true);
359
438
  console.info(styles.secondary('No changes were made.'));
360
439
  return;
361
440
  }
362
- const localPlan = await (0, exports.executeSlugUpdate)(selection.projectUuid, options.path, options.from, options.to, options.type);
363
- console.info(styles.success(`Updated ${options.type} slug "${options.from}" -> "${options.to}".`));
441
+ const localPlan = await (0, exports.executeSlugUpdate)(selection.projectUuid, options.path, options.from, options.to, resourceType);
442
+ console.info(styles.success(`Updated ${resourceLabel} slug "${options.from}" -> "${options.to}".`));
364
443
  logLocalSlugUpdateFileChanges(localPlan, options.path, false);
365
444
  };
366
445
  exports.slugUpdateHandler = slugUpdateHandler;
package/dist/index.js CHANGED
@@ -711,9 +711,9 @@ commander_1.program
711
711
  .action(renameHandler_1.renameHandler);
712
712
  commander_1.program
713
713
  .command('slug-update')
714
- .description('Rename a chart or dashboard slug and update its local references')
714
+ .description('Rename a chart, dashboard, or data app slug and update its local references')
715
715
  .addOption(new commander_1.Option('--type <resource-type>', 'content type to rename')
716
- .choices(['chart', 'dashboard'])
716
+ .choices(['chart', 'dashboard', 'app'])
717
717
  .default('chart'))
718
718
  .option('--verbose', 'show verbose output', false)
719
719
  .option('--project <project uuid or slug>', 'specify a project by UUID or slug', parseProjectArgument, undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "3.164.0",
3
+ "version": "3.165.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,8 +22,8 @@
22
22
  "@actions/core": "1.11.1",
23
23
  "@aws-sdk/credential-providers": "3.995.0",
24
24
  "@casl/ability": "6.8.0",
25
- "@lightdash/common": "3.164.0",
26
- "@lightdash/warehouses": "3.164.0",
25
+ "@lightdash/common": "3.165.0",
26
+ "@lightdash/warehouses": "3.165.0",
27
27
  "@sidvind/better-ajv-errors": "7.0.0",
28
28
  "@types/columnify": "1.5.1",
29
29
  "ajv": "8.18.0",