@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?:
|
|
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?:
|
|
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,
|
|
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 (
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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 () =>
|
|
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 ${
|
|
354
|
-
: `Updating ${
|
|
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,
|
|
357
|
-
console.info(styles.success(`Would update ${
|
|
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,
|
|
363
|
-
console.info(styles.success(`Updated ${
|
|
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
|
|
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.
|
|
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.
|
|
26
|
-
"@lightdash/warehouses": "3.
|
|
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",
|