@lightdash/cli 2.203.0 → 2.205.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,9 +1,10 @@
1
- import { type ContentSlugRenameRequest } from '@lightdash/common';
1
+ import { ContentType, type ContentSlugRenameRequest } from '@lightdash/common';
2
2
  type SlugUpdateOptions = {
3
3
  verbose: boolean;
4
4
  project?: string;
5
5
  path?: string;
6
6
  dryRun: boolean;
7
+ type: ContentType.CHART | ContentType.DASHBOARD;
7
8
  from: string;
8
9
  to: string;
9
10
  };
@@ -18,7 +19,8 @@ type LocalSlugUpdatePlan = {
18
19
  }>;
19
20
  metadata: {
20
21
  root: string;
21
- charts: Record<string, string>;
22
+ resourceType: ContentType.CHART | ContentType.DASHBOARD;
23
+ slugs: Record<string, string>;
22
24
  } | undefined;
23
25
  referencesUpdated: number;
24
26
  };
@@ -26,11 +28,11 @@ export type LocalSlugUpdateFileChange = {
26
28
  source: string;
27
29
  target?: string;
28
30
  };
29
- export declare const planLocalChartSlugUpdate: (customPath: string | undefined, from: string, to: string) => Promise<LocalSlugUpdatePlan>;
31
+ export declare const planLocalSlugUpdate: (customPath: string | undefined, from: string, to: string, resourceType?: ContentType.CHART | ContentType.DASHBOARD) => Promise<LocalSlugUpdatePlan>;
30
32
  export declare const getLocalSlugUpdateFileChanges: (plan: LocalSlugUpdatePlan, customPath: string | undefined) => LocalSlugUpdateFileChange[];
31
- export declare const applyLocalChartSlugUpdate: (plan: LocalSlugUpdatePlan) => Promise<() => Promise<void>>;
33
+ export declare const applyLocalSlugUpdate: (plan: LocalSlugUpdatePlan) => Promise<() => Promise<void>>;
32
34
  export declare const requestSlugUpdate: (projectUuid: string, request: ContentSlugRenameRequest) => Promise<void>;
33
- export declare const executeChartSlugUpdate: (projectUuid: string, customPath: string | undefined, from: string, to: string) => Promise<LocalSlugUpdatePlan>;
35
+ export declare const executeSlugUpdate: (projectUuid: string, customPath: string | undefined, from: string, to: string, resourceType?: ContentType.CHART | ContentType.DASHBOARD) => Promise<LocalSlugUpdatePlan>;
34
36
  export declare const slugUpdateHandler: (options: SlugUpdateOptions) => Promise<void>;
35
37
  export {};
36
38
  //# sourceMappingURL=slugUpdate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"slugUpdate.d.ts","sourceRoot":"","sources":["../../src/handlers/slugUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAY3B,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,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,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;KAClC,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;AAwDF,eAAO,MAAM,wBAAwB,eACrB,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,KACX,OAAO,CAAC,mBAAmB,CAyL7B,CAAC;AAEF,eAAO,MAAM,6BAA6B,SAChC,mBAAmB,cACb,MAAM,GAAG,SAAS,KAC/B,yBAAyB,EAuB3B,CAAC;AAuDF,eAAO,MAAM,yBAAyB,SAC5B,mBAAmB,KAC1B,OAAO,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,CAkD7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,gBACb,MAAM,WACV,wBAAwB,KAClC,OAAO,CAAC,IAAI,CAmBd,CAAC;AAEF,eAAO,MAAM,sBAAsB,gBAClB,MAAM,cACP,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,KACX,OAAO,CAAC,mBAAmB,CAkB7B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YACjB,iBAAiB,KAC3B,OAAO,CAAC,IAAI,CAsDd,CAAC"}
1
+ {"version":3,"file":"slugUpdate.d.ts","sourceRoot":"","sources":["../../src/handlers/slugUpdate.ts"],"names":[],"mappings":"AAAA,OAAO,EAEH,WAAW,EAIX,KAAK,wBAAwB,EAChC,MAAM,mBAAmB,CAAC;AAY3B,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;AAwDF,eAAO,MAAM,mBAAmB,eAChB,MAAM,GAAG,SAAS,QACxB,MAAM,MACR,MAAM,iBACI,WAAW,CAAC,KAAK,GAAG,WAAW,CAAC,SAAS,KACxD,OAAO,CAAC,mBAAmB,CAyM7B,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,CAwDd,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.slugUpdateHandler = exports.executeChartSlugUpdate = exports.requestSlugUpdate = exports.applyLocalChartSlugUpdate = exports.getLocalSlugUpdateFileChanges = exports.planLocalChartSlugUpdate = void 0;
3
+ exports.slugUpdateHandler = exports.executeSlugUpdate = exports.requestSlugUpdate = exports.applyLocalSlugUpdate = exports.getLocalSlugUpdateFileChanges = exports.planLocalSlugUpdate = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const common_1 = require("@lightdash/common");
6
6
  const fs_1 = require("fs");
@@ -56,11 +56,12 @@ const assertMovesAreSafe = async (moves) => {
56
56
  }
57
57
  }
58
58
  };
59
- const planLocalChartSlugUpdate = async (customPath, from, to) => {
59
+ const planLocalSlugUpdate = async (customPath, from, to, resourceType = common_1.ContentType.CHART) => {
60
60
  const root = (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
61
61
  const fileUpdates = [];
62
62
  const fileMoves = [];
63
63
  let referencesUpdated = 0;
64
+ const languageMapKey = resourceType === common_1.ContentType.CHART ? 'chart' : 'dashboard';
64
65
  if (!from || from.length > 255) {
65
66
  throw new common_1.ParameterError('The source slug must contain between 1 and 255 characters');
66
67
  }
@@ -88,7 +89,10 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
88
89
  const parsed = document.toJS();
89
90
  const isChart = parsed.contentType === common_1.ContentType.CHART ||
90
91
  parsed.metricQuery !== undefined;
91
- if (isChart && parsed.slug === from) {
92
+ const isDashboard = parsed.contentType === common_1.ContentType.DASHBOARD ||
93
+ (parsed.contentType === undefined && Array.isArray(parsed.tiles));
94
+ const isTargetContent = resourceType === common_1.ContentType.CHART ? isChart : isDashboard;
95
+ if (isTargetContent && parsed.slug === from) {
92
96
  document.set('slug', to);
93
97
  referencesUpdated += 1;
94
98
  changed = true;
@@ -112,16 +116,21 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
112
116
  .join('; ')}`);
113
117
  }
114
118
  const languageMapEntry = languageMapDocument.getIn([
115
- 'chart',
119
+ languageMapKey,
116
120
  from,
117
121
  ]);
118
122
  if (languageMapEntry !== undefined) {
119
- if (languageMapDocument.getIn(['chart', to]) !==
120
- undefined) {
121
- throw new common_1.ParameterError(`Cannot update language map because chart slug "${to}" already exists in "${sourceLanguageMap}"`);
123
+ if (languageMapDocument.getIn([
124
+ languageMapKey,
125
+ to,
126
+ ]) !== undefined) {
127
+ throw new common_1.ParameterError(`Cannot update language map because ${resourceType} slug "${to}" already exists in "${sourceLanguageMap}"`);
122
128
  }
123
- languageMapDocument.setIn(['chart', to], languageMapEntry);
124
- languageMapDocument.deleteIn(['chart', from]);
129
+ languageMapDocument.setIn([languageMapKey, to], languageMapEntry);
130
+ languageMapDocument.deleteIn([
131
+ languageMapKey,
132
+ from,
133
+ ]);
125
134
  fileUpdates.push({
126
135
  filePath: sourceLanguageMap,
127
136
  content: languageMapDocument.toString(),
@@ -141,7 +150,7 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
141
150
  }
142
151
  }
143
152
  }
144
- if (Array.isArray(parsed.tiles)) {
153
+ if (resourceType === common_1.ContentType.CHART && Array.isArray(parsed.tiles)) {
145
154
  for (const [index, tile] of parsed.tiles.entries()) {
146
155
  if (tile.type === 'saved_chart' &&
147
156
  tile.properties?.chartSlug === from) {
@@ -151,7 +160,7 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
151
160
  }
152
161
  }
153
162
  }
154
- if (parsed.resource?.type === common_1.ContentType.CHART &&
163
+ if (parsed.resource?.type === resourceType &&
155
164
  parsed.resource.slug === from) {
156
165
  document.setIn(['resource', 'slug'], to);
157
166
  referencesUpdated += 1;
@@ -163,14 +172,17 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
163
172
  }
164
173
  const metadata = await (0, metadataFile_1.readMetadataFile)(root);
165
174
  let updatedMetadata;
166
- if (from in metadata.charts) {
167
- if (to !== from && to in metadata.charts) {
168
- throw new common_1.ParameterError(`Cannot update local metadata because chart slug "${to}" already exists`);
175
+ const metadataSlugs = resourceType === common_1.ContentType.CHART
176
+ ? metadata.charts
177
+ : metadata.dashboards;
178
+ if (from in metadataSlugs) {
179
+ if (to !== from && to in metadataSlugs) {
180
+ throw new common_1.ParameterError(`Cannot update local metadata because ${resourceType} slug "${to}" already exists`);
169
181
  }
170
- const charts = { ...metadata.charts };
171
- charts[to] = charts[from];
172
- delete charts[from];
173
- updatedMetadata = { root, charts };
182
+ const slugs = { ...metadataSlugs };
183
+ slugs[to] = slugs[from];
184
+ delete slugs[from];
185
+ updatedMetadata = { root, resourceType, slugs };
174
186
  }
175
187
  await assertMovesAreSafe(fileMoves);
176
188
  return {
@@ -180,7 +192,7 @@ const planLocalChartSlugUpdate = async (customPath, from, to) => {
180
192
  referencesUpdated,
181
193
  };
182
194
  };
183
- exports.planLocalChartSlugUpdate = planLocalChartSlugUpdate;
195
+ exports.planLocalSlugUpdate = planLocalSlugUpdate;
184
196
  const getLocalSlugUpdateFileChanges = (plan, customPath) => {
185
197
  const root = (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
186
198
  const movedSources = new Set(plan.fileMoves.map(({ source }) => source));
@@ -235,7 +247,7 @@ const restoreFiles = async (snapshots) => {
235
247
  ? fs_1.promises.rm(filePath, { force: true })
236
248
  : fs_1.promises.writeFile(filePath, content)));
237
249
  };
238
- const applyLocalChartSlugUpdate = async (plan) => {
250
+ const applyLocalSlugUpdate = async (plan) => {
239
251
  const metadataPath = plan.metadata
240
252
  ? path.join(plan.metadata.root, metadataFile_1.METADATA_FILENAME)
241
253
  : undefined;
@@ -260,7 +272,12 @@ const applyLocalChartSlugUpdate = async (plan) => {
260
272
  }
261
273
  if (plan.metadata && metadataPath) {
262
274
  const existing = await (0, metadataFile_1.readMetadataFile)(plan.metadata.root);
263
- await fs_1.promises.writeFile(metadataPath, JSON.stringify({ ...existing, charts: plan.metadata.charts }, null, 2));
275
+ await fs_1.promises.writeFile(metadataPath, JSON.stringify({
276
+ ...existing,
277
+ [plan.metadata.resourceType === common_1.ContentType.CHART
278
+ ? 'charts'
279
+ : 'dashboards']: plan.metadata.slugs,
280
+ }, null, 2));
264
281
  }
265
282
  }
266
283
  catch (error) {
@@ -270,7 +287,7 @@ const applyLocalChartSlugUpdate = async (plan) => {
270
287
  }
271
288
  return () => restoreFiles(snapshots);
272
289
  };
273
- exports.applyLocalChartSlugUpdate = applyLocalChartSlugUpdate;
290
+ exports.applyLocalSlugUpdate = applyLocalSlugUpdate;
274
291
  const requestSlugUpdate = async (projectUuid, request) => {
275
292
  try {
276
293
  await (0, apiClient_1.lightdashApi)({
@@ -289,22 +306,22 @@ const requestSlugUpdate = async (projectUuid, request) => {
289
306
  }
290
307
  };
291
308
  exports.requestSlugUpdate = requestSlugUpdate;
292
- const executeChartSlugUpdate = async (projectUuid, customPath, from, to) => {
293
- const localPlan = await (0, exports.planLocalChartSlugUpdate)(customPath, from, to);
309
+ const executeSlugUpdate = async (projectUuid, customPath, from, to, resourceType = common_1.ContentType.CHART) => {
310
+ const localPlan = await (0, exports.planLocalSlugUpdate)(customPath, from, to, resourceType);
294
311
  await (0, exports.requestSlugUpdate)(projectUuid, {
295
- resourceType: common_1.ContentType.CHART,
312
+ resourceType,
296
313
  from,
297
314
  to,
298
315
  });
299
316
  try {
300
- await (0, exports.applyLocalChartSlugUpdate)(localPlan);
317
+ await (0, exports.applyLocalSlugUpdate)(localPlan);
301
318
  }
302
319
  catch (error) {
303
- throw new Error('The chart slug was renamed in Lightdash, but the local files could not be updated. Fix the local file error and rerun the same command; the rename is idempotent.', { cause: error });
320
+ throw new Error(`The ${resourceType} slug was renamed in Lightdash, but the local files could not be updated. Fix the local file error and rerun the same command; the rename is idempotent.`, { cause: error });
304
321
  }
305
322
  return localPlan;
306
323
  };
307
- exports.executeChartSlugUpdate = executeChartSlugUpdate;
324
+ exports.executeSlugUpdate = executeSlugUpdate;
308
325
  const slugUpdateHandler = async (options) => {
309
326
  globalState_1.default.setVerbose(options.verbose);
310
327
  await (0, apiClient_1.checkLightdashVersion)();
@@ -317,17 +334,17 @@ const slugUpdateHandler = async (options) => {
317
334
  throw new common_1.ParameterError('No project selected. Run lightdash config set-project');
318
335
  }
319
336
  (0, selectProject_1.logSelectedProject)(selection, config, options.dryRun
320
- ? 'Previewing chart slug update in'
321
- : 'Updating chart slug in');
337
+ ? `Previewing ${options.type} slug update in`
338
+ : `Updating ${options.type} slug in`);
322
339
  if (options.dryRun) {
323
- const localPlan = await (0, exports.planLocalChartSlugUpdate)(options.path, options.from, options.to);
324
- console.info(styles.success(`Would update chart slug "${options.from}" -> "${options.to}".`));
340
+ const localPlan = await (0, exports.planLocalSlugUpdate)(options.path, options.from, options.to, options.type);
341
+ console.info(styles.success(`Would update ${options.type} slug "${options.from}" -> "${options.to}".`));
325
342
  logLocalSlugUpdateFileChanges(localPlan, options.path, true);
326
343
  console.info(styles.secondary('No changes were made.'));
327
344
  return;
328
345
  }
329
- const localPlan = await (0, exports.executeChartSlugUpdate)(selection.projectUuid, options.path, options.from, options.to);
330
- console.info(styles.success(`Updated chart slug "${options.from}" -> "${options.to}".`));
346
+ const localPlan = await (0, exports.executeSlugUpdate)(selection.projectUuid, options.path, options.from, options.to, options.type);
347
+ console.info(styles.success(`Updated ${options.type} slug "${options.from}" -> "${options.to}".`));
331
348
  logLocalSlugUpdateFileChanges(localPlan, options.path, false);
332
349
  };
333
350
  exports.slugUpdateHandler = slugUpdateHandler;
@@ -22,6 +22,63 @@ const createTemporaryContent = async () => {
22
22
  .splice(0)
23
23
  .map((directory) => fs_1.promises.rm(directory, { recursive: true, force: true })));
24
24
  });
25
+ (0, vitest_1.describe)('local dashboard slug updates', () => {
26
+ (0, vitest_1.test)('updates only dashboard identity, language maps, deliveries, and dashboard metadata', async () => {
27
+ const root = await createTemporaryContent();
28
+ const dashboardFile = path.join(root, 'dashboards', 'old-overview.yml');
29
+ const mapFile = path.join(root, 'dashboards', 'old-overview.language.map.yml');
30
+ const chartFile = path.join(root, 'charts', 'old-overview.yml');
31
+ const chartSource = 'contentType: chart\nslug: old-overview\nmetricQuery: {}\n';
32
+ const alertFile = path.join(root, 'alerts', 'delivery.yml');
33
+ await Promise.all([
34
+ fs_1.promises.writeFile(dashboardFile, 'contentType: dashboard\nslug: old-overview\ntiles:\n - type: saved_chart\n properties:\n chartSlug: old-overview\n'),
35
+ fs_1.promises.writeFile(mapFile, 'dashboard:\n old-overview:\n name: Overview\n'),
36
+ fs_1.promises.writeFile(chartFile, chartSource),
37
+ fs_1.promises.writeFile(alertFile, 'resource:\n type: dashboard\n slug: old-overview\n'),
38
+ fs_1.promises.writeFile(path.join(root, '.lightdash-metadata.json'), JSON.stringify({
39
+ version: 1,
40
+ charts: { 'old-overview': 'chart-timestamp' },
41
+ dashboards: { 'old-overview': 'dashboard-timestamp' },
42
+ })),
43
+ ]);
44
+ const plan = await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'old-overview', 'overview', common_1.ContentType.DASHBOARD);
45
+ (0, vitest_1.expect)(await fs_1.promises.readFile(dashboardFile, 'utf8')).toContain('slug: old-overview');
46
+ await (0, slugUpdate_1.applyLocalSlugUpdate)(plan);
47
+ const dashboard = (0, yaml_1.parse)(await fs_1.promises.readFile(path.join(root, 'dashboards', 'overview.yml'), 'utf8'));
48
+ (0, vitest_1.expect)(dashboard.slug).toBe('overview');
49
+ (0, vitest_1.expect)(dashboard.tiles[0].properties.chartSlug).toBe('old-overview');
50
+ (0, vitest_1.expect)((0, yaml_1.parse)(await fs_1.promises.readFile(path.join(root, 'dashboards', 'overview.language.map.yml'), 'utf8'))).toEqual({
51
+ dashboard: { overview: { name: 'Overview' } },
52
+ });
53
+ (0, vitest_1.expect)((0, yaml_1.parse)(await fs_1.promises.readFile(alertFile, 'utf8')).resource.slug).toBe('overview');
54
+ (0, vitest_1.expect)(await fs_1.promises.readFile(chartFile, 'utf8')).toBe(chartSource);
55
+ (0, vitest_1.expect)(JSON.parse(await fs_1.promises.readFile(path.join(root, '.lightdash-metadata.json'), 'utf8'))).toEqual({
56
+ version: 1,
57
+ charts: { 'old-overview': 'chart-timestamp' },
58
+ dashboards: { overview: 'dashboard-timestamp' },
59
+ });
60
+ const replay = await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'old-overview', 'overview', common_1.ContentType.DASHBOARD);
61
+ (0, vitest_1.expect)((0, slugUpdate_1.getLocalSlugUpdateFileChanges)(replay, root)).toEqual([]);
62
+ });
63
+ (0, vitest_1.test)('sends the dashboard type before changing local files', async () => {
64
+ const root = await createTemporaryContent();
65
+ const dashboardFile = path.join(root, 'dashboards', 'old-overview.yml');
66
+ const source = 'slug: old-overview\ntiles: []\n';
67
+ await fs_1.promises.writeFile(dashboardFile, source);
68
+ const api = vitest_1.vi
69
+ .spyOn(apiClient, 'lightdashApi')
70
+ .mockImplementation(async () => {
71
+ (0, vitest_1.expect)(await fs_1.promises.readFile(dashboardFile, 'utf8')).toBe(source);
72
+ return undefined;
73
+ });
74
+ await (0, slugUpdate_1.executeSlugUpdate)('project-uuid', root, 'old-overview', 'overview', common_1.ContentType.DASHBOARD);
75
+ (0, vitest_1.expect)(JSON.parse(api.mock.calls[0][0].body)).toEqual({
76
+ resourceType: common_1.ContentType.DASHBOARD,
77
+ from: 'old-overview',
78
+ to: 'overview',
79
+ });
80
+ });
81
+ });
25
82
  (0, vitest_1.describe)('local chart slug updates', () => {
26
83
  (0, vitest_1.test)('updates chart files, language maps, references, filenames, and metadata', async () => {
27
84
  const root = await createTemporaryContent();
@@ -55,7 +112,7 @@ const createTemporaryContent = async () => {
55
112
  dashboards: { overview: 'dashboard-timestamp' },
56
113
  })),
57
114
  ]);
58
- const plan = await (0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', 'orders');
115
+ const plan = await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', 'orders');
59
116
  (0, vitest_1.expect)(plan.referencesUpdated).toBe(4);
60
117
  (0, vitest_1.expect)(plan.fileMoves).toHaveLength(2);
61
118
  (0, vitest_1.expect)((0, slugUpdate_1.getLocalSlugUpdateFileChanges)(plan, root)).toEqual([
@@ -71,7 +128,7 @@ const createTemporaryContent = async () => {
71
128
  },
72
129
  { source: path.join('dashboards', 'overview.yml') },
73
130
  ]);
74
- await (0, slugUpdate_1.applyLocalChartSlugUpdate)(plan);
131
+ await (0, slugUpdate_1.applyLocalSlugUpdate)(plan);
75
132
  await (0, vitest_1.expect)(fs_1.promises.access(chartFile)).rejects.toThrow();
76
133
  await (0, vitest_1.expect)(fs_1.promises.access(languageMapFile)).rejects.toThrow();
77
134
  const renamedChart = (0, yaml_1.parse)(await fs_1.promises.readFile(path.join(root, 'charts', 'orders.yml'), 'utf8'));
@@ -98,8 +155,8 @@ const createTemporaryContent = async () => {
98
155
  (0, vitest_1.test)('is idempotent after local files have already been updated', async () => {
99
156
  const root = await createTemporaryContent();
100
157
  await fs_1.promises.writeFile(path.join(root, 'charts', 'copy-of-orders.yml'), 'contentType: chart\nslug: copy-of-orders\nname: Orders\nmetricQuery: {}\n');
101
- await (0, slugUpdate_1.applyLocalChartSlugUpdate)(await (0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', 'orders'));
102
- const secondPlan = await (0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', 'orders');
158
+ await (0, slugUpdate_1.applyLocalSlugUpdate)(await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', 'orders'));
159
+ const secondPlan = await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', 'orders');
103
160
  (0, vitest_1.expect)(secondPlan).toEqual({
104
161
  fileUpdates: [],
105
162
  fileMoves: [],
@@ -113,7 +170,7 @@ const createTemporaryContent = async () => {
113
170
  const source = 'contentType: chart\nslug: copy-of-orders\nname: Orders\nmetricQuery: {}\n';
114
171
  await fs_1.promises.writeFile(chartFile, source);
115
172
  const api = vitest_1.vi.spyOn(apiClient, 'lightdashApi');
116
- const plan = await (0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', 'orders');
173
+ const plan = await (0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', 'orders');
117
174
  (0, vitest_1.expect)((0, slugUpdate_1.getLocalSlugUpdateFileChanges)(plan, root)).toEqual([
118
175
  {
119
176
  source: path.join('charts', 'copy-of-orders.yml'),
@@ -131,7 +188,7 @@ const createTemporaryContent = async () => {
131
188
  fs_1.promises.writeFile(sourceFile, 'contentType: chart\nslug: copy-of-orders\nname: Orders\nmetricQuery: {}\n'),
132
189
  fs_1.promises.writeFile(path.join(root, 'charts', 'orders.yml'), 'contentType: chart\nslug: another-chart\nname: Another\nmetricQuery: {}\n'),
133
190
  ]);
134
- await (0, vitest_1.expect)((0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', 'orders')).rejects.toThrow('already exists');
191
+ await (0, vitest_1.expect)((0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', 'orders')).rejects.toThrow('already exists');
135
192
  (0, vitest_1.expect)(await fs_1.promises.readFile(sourceFile, 'utf8')).toContain('slug: copy-of-orders');
136
193
  });
137
194
  (0, vitest_1.test)('rejects malformed target slugs before constructing file paths', async () => {
@@ -139,7 +196,7 @@ const createTemporaryContent = async () => {
139
196
  const chartFile = path.join(root, 'charts', 'copy-of-orders.yml');
140
197
  const source = 'contentType: chart\nslug: copy-of-orders\nname: Orders\nmetricQuery: {}\n';
141
198
  await fs_1.promises.writeFile(chartFile, source);
142
- await (0, vitest_1.expect)((0, slugUpdate_1.planLocalChartSlugUpdate)(root, 'copy-of-orders', '../../orders')).rejects.toThrow('target slug must contain');
199
+ await (0, vitest_1.expect)((0, slugUpdate_1.planLocalSlugUpdate)(root, 'copy-of-orders', '../../orders')).rejects.toThrow('target slug must contain');
143
200
  (0, vitest_1.expect)(await fs_1.promises.readFile(chartFile, 'utf8')).toBe(source);
144
201
  });
145
202
  (0, vitest_1.test)('does not change local files when the server rejects the rename', async () => {
@@ -153,7 +210,7 @@ const createTemporaryContent = async () => {
153
210
  statusCode: 409,
154
211
  data: {},
155
212
  }));
156
- await (0, vitest_1.expect)((0, slugUpdate_1.executeChartSlugUpdate)('project-uuid', root, 'copy-of-orders', 'orders')).rejects.toThrow('Slug already in use');
213
+ await (0, vitest_1.expect)((0, slugUpdate_1.executeSlugUpdate)('project-uuid', root, 'copy-of-orders', 'orders')).rejects.toThrow('Slug already in use');
157
214
  (0, vitest_1.expect)(await fs_1.promises.readFile(chartFile, 'utf8')).toBe(source);
158
215
  await (0, vitest_1.expect)(fs_1.promises.access(path.join(root, 'charts', 'orders.yml'))).rejects.toThrow();
159
216
  (0, vitest_1.expect)((await fs_1.promises.readdir(path.join(root, 'charts'))).some((file) => file.includes('.slug-update-'))).toBe(false);
@@ -167,7 +224,7 @@ const createTemporaryContent = async () => {
167
224
  await (0, vitest_1.expect)(fs_1.promises.access(path.join(root, 'charts', 'orders.yml'))).rejects.toThrow();
168
225
  return undefined;
169
226
  });
170
- await (0, slugUpdate_1.executeChartSlugUpdate)('project-uuid', root, 'copy-of-orders', 'orders');
227
+ await (0, slugUpdate_1.executeSlugUpdate)('project-uuid', root, 'copy-of-orders', 'orders');
171
228
  await (0, vitest_1.expect)(fs_1.promises.access(chartFile)).rejects.toThrow();
172
229
  (0, vitest_1.expect)(await fs_1.promises.readFile(path.join(root, 'charts', 'orders.yml'), 'utf8')).toContain('slug: orders');
173
230
  });
package/dist/index.js CHANGED
@@ -608,11 +608,14 @@ commander_1.program
608
608
  .action(renameHandler_1.renameHandler);
609
609
  commander_1.program
610
610
  .command('slug-update')
611
- .description('Rename a chart slug and update its local references')
611
+ .description('Rename a chart or dashboard slug and update its local references')
612
+ .addOption(new commander_1.Option('--type <resource-type>', 'content type to rename')
613
+ .choices(['chart', 'dashboard'])
614
+ .default('chart'))
612
615
  .option('--verbose', 'show verbose output', false)
613
616
  .option('--project <project uuid or slug>', 'specify a project by UUID or slug', parseProjectArgument, undefined)
614
617
  .option('-p, --path <path>', 'specify the local content-as-code path to update', undefined)
615
- .option('--dry-run', 'preview the chart rename and local file changes without making changes', false)
618
+ .option('--dry-run', 'preview the rename and local file changes without making changes', false)
616
619
  .requiredOption('--from <slug>', 'current content slug')
617
620
  .requiredOption('--to <slug>', 'new content slug')
618
621
  .action(slugUpdate_1.slugUpdateHandler);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "2.203.0",
3
+ "version": "2.205.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": "2.203.0",
26
- "@lightdash/warehouses": "2.203.0",
25
+ "@lightdash/common": "2.205.0",
26
+ "@lightdash/warehouses": "2.205.0",
27
27
  "@sidvind/better-ajv-errors": "7.0.0",
28
28
  "@types/columnify": "1.5.1",
29
29
  "ajv": "8.18.0",