@lightdash/cli 0.2230.0 → 0.2231.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.
- package/dist/.tsbuildinfo +1 -1
- package/dist/handlers/download.d.ts +2 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +117 -60
- package/dist/index.js +1 -0
- package/package.json +3 -3
|
@@ -9,8 +9,9 @@ export type DownloadHandlerOptions = {
|
|
|
9
9
|
skipSpaceCreate: boolean;
|
|
10
10
|
public: boolean;
|
|
11
11
|
includeCharts: boolean;
|
|
12
|
+
nested: boolean;
|
|
12
13
|
};
|
|
13
|
-
export declare const downloadContent: (ids: string[], type: "charts" | "dashboards", projectId: string, customPath?: string, languageMap?: boolean) => Promise<[number, string[]]>;
|
|
14
|
+
export declare const downloadContent: (ids: string[], type: "charts" | "dashboards", projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean) => Promise<[number, string[]]>;
|
|
14
15
|
export declare const downloadHandler: (options: DownloadHandlerOptions) => Promise<void>;
|
|
15
16
|
export declare const uploadHandler: (options: DownloadHandlerOptions) => Promise<void>;
|
|
16
17
|
//# sourceMappingURL=download.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AA2BA,MAAM,MAAM,sBAAsB,GAAG;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,eAAe,EAAE,OAAO,CAAC;IACzB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,OAAO,CAAC;CACnB,CAAC;AA2NF,eAAO,MAAM,eAAe,QACnB,MAAM,EAAE,QACP,QAAQ,GAAG,YAAY,aAClB,MAAM,eACJ,MAAM,eACN,MAAM,gBACN,OAAO,WACZ,OAAO,KAChB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA4F5B,CAAC;AAEF,eAAO,MAAM,eAAe,YACf,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAqJd,CAAC;AA6MF,eAAO,MAAM,aAAa,YACb,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAuHd,CAAC"}
|
|
@@ -7,6 +7,7 @@ const tslib_1 = require("tslib");
|
|
|
7
7
|
const common_1 = require("@lightdash/common");
|
|
8
8
|
const fs_1 = require("fs");
|
|
9
9
|
const yaml = tslib_1.__importStar(require("js-yaml"));
|
|
10
|
+
const groupBy_1 = tslib_1.__importDefault(require("lodash/groupBy"));
|
|
10
11
|
const path = tslib_1.__importStar(require("path"));
|
|
11
12
|
const analytics_1 = require("../analytics/analytics");
|
|
12
13
|
const config_1 = require("../config");
|
|
@@ -35,12 +36,19 @@ const parseContentFilters = (items) => {
|
|
|
35
36
|
});
|
|
36
37
|
return `?${new URLSearchParams(parsedItems.map((item) => ['ids', item])).toString()}`;
|
|
37
38
|
};
|
|
38
|
-
const createDirForContent = async (
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
const createDirForContent = async (projectName, spaceSlug, folder, customPath, folderScheme) => {
|
|
40
|
+
const baseDir = getDownloadFolder(customPath);
|
|
41
|
+
let outputDir;
|
|
42
|
+
if (folderScheme === 'flat') {
|
|
43
|
+
// Flat scheme: baseDir/folder
|
|
44
|
+
outputDir = path.join(baseDir, folder);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// Nested scheme: baseDir/projectName/spaceSlug/folder
|
|
48
|
+
outputDir = path.join(baseDir, projectName, spaceSlug, folder);
|
|
49
|
+
}
|
|
50
|
+
globalState_1.default.debug(`Creating directory: ${outputDir}`);
|
|
51
|
+
await fs_1.promises.mkdir(outputDir, { recursive: true });
|
|
44
52
|
return outputDir;
|
|
45
53
|
};
|
|
46
54
|
const writeContent = async (contentAsCode, outputDir, languageMap) => {
|
|
@@ -54,56 +62,76 @@ const writeContent = async (contentAsCode, outputDir, languageMap) => {
|
|
|
54
62
|
await fs_1.promises.writeFile(translationPath, yaml.dump(contentAsCode.translationMap));
|
|
55
63
|
}
|
|
56
64
|
};
|
|
65
|
+
const isLightdashContentFile = (folder, entry) => entry.isFile() &&
|
|
66
|
+
entry.parentPath.endsWith(path.sep + folder) &&
|
|
67
|
+
entry.name.endsWith('.yml') &&
|
|
68
|
+
!entry.name.endsWith('.language.map.yml');
|
|
69
|
+
const loadYamlFile = async (file) => {
|
|
70
|
+
const filePath = path.join(file.parentPath, file.name);
|
|
71
|
+
const [fileContent, stats] = await Promise.all([
|
|
72
|
+
fs_1.promises.readFile(filePath, 'utf-8'),
|
|
73
|
+
fs_1.promises.stat(filePath),
|
|
74
|
+
]);
|
|
75
|
+
const item = yaml.load(fileContent);
|
|
76
|
+
const downloadedAt = item.downloadedAt
|
|
77
|
+
? new Date(item.downloadedAt)
|
|
78
|
+
: undefined;
|
|
79
|
+
const needsUpdating = downloadedAt &&
|
|
80
|
+
Math.abs(stats.mtime.getTime() - downloadedAt.getTime()) > 30000;
|
|
81
|
+
return {
|
|
82
|
+
...item,
|
|
83
|
+
updatedAt: needsUpdating ? stats.mtime : item.updatedAt,
|
|
84
|
+
needsUpdating: needsUpdating ?? true,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
57
87
|
const readCodeFiles = async (folder, customPath) => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const items = [];
|
|
88
|
+
const baseDir = getDownloadFolder(customPath);
|
|
89
|
+
globalState_1.default.log(`Reading ${folder} from ${baseDir}`);
|
|
61
90
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.filter((
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const fileContent = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
72
|
-
const item = yaml.load(fileContent);
|
|
73
|
-
const fileUpdatedAt = (await fs_1.promises.stat(filePath)).mtime;
|
|
74
|
-
// We override the updatedAt to the file's updatedAt
|
|
75
|
-
// in case there were some changes made locally
|
|
76
|
-
// do not override if the file was just created
|
|
77
|
-
const downloadedAt = item.downloadedAt
|
|
78
|
-
? new Date(item.downloadedAt)
|
|
79
|
-
: undefined;
|
|
80
|
-
const needsUpdating = downloadedAt &&
|
|
81
|
-
Math.abs(fileUpdatedAt.getTime() - downloadedAt.getTime()) >
|
|
82
|
-
30000;
|
|
83
|
-
const locallyUpdatedItem = {
|
|
84
|
-
...item,
|
|
85
|
-
updatedAt: needsUpdating ? fileUpdatedAt : item.updatedAt, // Force the update by changing updatedAt , which is what promotion is going to compare
|
|
86
|
-
needsUpdating: needsUpdating ?? true, // if downloadAt is not set, we force the update
|
|
87
|
-
};
|
|
88
|
-
items.push(locallyUpdatedItem);
|
|
91
|
+
const allEntries = await fs_1.promises.readdir(baseDir, {
|
|
92
|
+
recursive: true,
|
|
93
|
+
withFileTypes: true,
|
|
94
|
+
});
|
|
95
|
+
const items = await Promise.all(allEntries
|
|
96
|
+
.filter((entry) => isLightdashContentFile(folder, entry))
|
|
97
|
+
.map((file) => loadYamlFile(file)));
|
|
98
|
+
if (items.length === 0) {
|
|
99
|
+
console.error(styles.warning(`Unable to upload ${folder}, no files found in "${baseDir}". Run download command first.`));
|
|
89
100
|
}
|
|
101
|
+
return items;
|
|
90
102
|
}
|
|
91
103
|
catch (error) {
|
|
92
|
-
//
|
|
104
|
+
// Handle case where base directory doesn't exist
|
|
93
105
|
if (error.code === 'ENOENT') {
|
|
94
|
-
console.error(styles.warning(`Unable to upload ${folder}, "${
|
|
106
|
+
console.error(styles.warning(`Unable to upload ${folder}, "${baseDir}" folder not found. Run download command first.`));
|
|
95
107
|
return [];
|
|
96
108
|
}
|
|
97
109
|
// Unknown error
|
|
98
|
-
console.error(styles.error(`Error reading ${
|
|
110
|
+
console.error(styles.error(`Error reading ${baseDir}: ${error}`));
|
|
99
111
|
throw error;
|
|
100
112
|
}
|
|
101
|
-
|
|
113
|
+
};
|
|
114
|
+
const groupBySpace = (items) => {
|
|
115
|
+
const itemsWithIndex = items.map((item, index) => ({ item, index }));
|
|
116
|
+
return (0, groupBy_1.default)(itemsWithIndex, (entry) => entry.item.spaceSlug);
|
|
117
|
+
};
|
|
118
|
+
const writeSpaceContent = async ({ projectName, spaceSlug, folder, contentInSpace, contentAsCode, customPath, languageMap, folderScheme, }) => {
|
|
119
|
+
const outputDir = await createDirForContent(projectName, spaceSlug, folder, customPath, folderScheme);
|
|
120
|
+
const contentType = folder === 'charts' ? 'chart' : 'dashboard';
|
|
121
|
+
for (const { item, index } of contentInSpace) {
|
|
122
|
+
await writeContent({
|
|
123
|
+
type: contentType,
|
|
124
|
+
content: item,
|
|
125
|
+
translationMap: contentAsCode.languageMap?.[index],
|
|
126
|
+
}, outputDir, languageMap);
|
|
127
|
+
}
|
|
102
128
|
};
|
|
103
129
|
const downloadContent = async (ids, // slug, uuid or url
|
|
104
|
-
type, projectId, customPath, languageMap = false) => {
|
|
130
|
+
type, projectId, projectName, customPath, languageMap = false, nested = false) => {
|
|
105
131
|
const spinner = globalState_1.default.getActiveSpinner();
|
|
106
132
|
const contentFilters = parseContentFilters(ids);
|
|
133
|
+
// Convert boolean flag to FolderScheme type
|
|
134
|
+
const folderScheme = nested ? 'nested' : 'flat';
|
|
107
135
|
let contentAsCode;
|
|
108
136
|
let offset = 0;
|
|
109
137
|
let chartSlugs = [];
|
|
@@ -123,13 +151,20 @@ type, projectId, customPath, languageMap = false) => {
|
|
|
123
151
|
console.warn(styles.warning(`\nNo ${type} with id "${missingId}"`));
|
|
124
152
|
});
|
|
125
153
|
if ('dashboards' in contentAsCode) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
// Group dashboards by spaceSlug
|
|
155
|
+
const dashboardsBySpace = groupBySpace(contentAsCode.dashboards);
|
|
156
|
+
// Create directory and write content for each space
|
|
157
|
+
for (const [spaceSlug, dashboardsInSpace] of Object.entries(dashboardsBySpace)) {
|
|
158
|
+
await writeSpaceContent({
|
|
159
|
+
projectName,
|
|
160
|
+
spaceSlug,
|
|
161
|
+
folder: 'dashboards',
|
|
162
|
+
contentInSpace: dashboardsInSpace,
|
|
163
|
+
contentAsCode,
|
|
164
|
+
customPath,
|
|
165
|
+
languageMap,
|
|
166
|
+
folderScheme,
|
|
167
|
+
});
|
|
133
168
|
}
|
|
134
169
|
// Extract chart slugs from dashboards
|
|
135
170
|
chartSlugs = contentAsCode.dashboards.reduce((acc, dashboard) => {
|
|
@@ -143,13 +178,18 @@ type, projectId, customPath, languageMap = false) => {
|
|
|
143
178
|
}, []);
|
|
144
179
|
}
|
|
145
180
|
else {
|
|
146
|
-
const
|
|
147
|
-
for (const [
|
|
148
|
-
await
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
const chartsBySpace = groupBySpace(contentAsCode.charts);
|
|
182
|
+
for (const [spaceSlug, chartsInSpace] of Object.entries(chartsBySpace)) {
|
|
183
|
+
await writeSpaceContent({
|
|
184
|
+
projectName,
|
|
185
|
+
spaceSlug,
|
|
186
|
+
folder: 'charts',
|
|
187
|
+
contentInSpace: chartsInSpace,
|
|
188
|
+
contentAsCode,
|
|
189
|
+
customPath,
|
|
190
|
+
languageMap,
|
|
191
|
+
folderScheme,
|
|
192
|
+
});
|
|
153
193
|
}
|
|
154
194
|
}
|
|
155
195
|
offset = contentAsCode.offset;
|
|
@@ -159,6 +199,8 @@ type, projectId, customPath, languageMap = false) => {
|
|
|
159
199
|
exports.downloadContent = downloadContent;
|
|
160
200
|
const downloadHandler = async (options) => {
|
|
161
201
|
globalState_1.default.setVerbose(options.verbose);
|
|
202
|
+
const spinner = globalState_1.default.startSpinner(`Downloading charts`);
|
|
203
|
+
spinner.start(`Downloading content from project`);
|
|
162
204
|
await (0, apiClient_1.checkLightdashVersion)();
|
|
163
205
|
const config = await (0, config_1.getConfig)();
|
|
164
206
|
if (!config.context?.apiKey || !config.context.serverUrl) {
|
|
@@ -168,6 +210,13 @@ const downloadHandler = async (options) => {
|
|
|
168
210
|
if (!projectId) {
|
|
169
211
|
throw new Error('No project selected. Run lightdash config set-project');
|
|
170
212
|
}
|
|
213
|
+
// Fetch project details to get project name for folder structure
|
|
214
|
+
const project = await (0, apiClient_1.lightdashApi)({
|
|
215
|
+
method: 'GET',
|
|
216
|
+
url: `/api/v1/projects/${projectId}`,
|
|
217
|
+
body: undefined,
|
|
218
|
+
});
|
|
219
|
+
const projectName = (0, common_1.generateSlug)(project.name);
|
|
171
220
|
// For analytics
|
|
172
221
|
let chartTotal;
|
|
173
222
|
let dashboardTotal;
|
|
@@ -181,6 +230,16 @@ const downloadHandler = async (options) => {
|
|
|
181
230
|
},
|
|
182
231
|
});
|
|
183
232
|
try {
|
|
233
|
+
const projectDir = path.join(getDownloadFolder(options.path), projectName);
|
|
234
|
+
const dirExists = () => fs_1.promises
|
|
235
|
+
.access(projectDir, fs_1.promises.constants.F_OK)
|
|
236
|
+
.then(() => true)
|
|
237
|
+
.catch(() => false);
|
|
238
|
+
// We clear the output directory first to get the latest state of content
|
|
239
|
+
// regarding projects and spaces if nested.
|
|
240
|
+
if (options.nested && (await dirExists())) {
|
|
241
|
+
await fs_1.promises.rm(projectDir, { recursive: true });
|
|
242
|
+
}
|
|
184
243
|
// If any filter is provided, we skip those items without filters
|
|
185
244
|
// eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
|
|
186
245
|
const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
|
|
@@ -189,8 +248,7 @@ const downloadHandler = async (options) => {
|
|
|
189
248
|
console.info(styles.warning(`No charts filters provided, skipping`));
|
|
190
249
|
}
|
|
191
250
|
else {
|
|
192
|
-
|
|
193
|
-
[chartTotal] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, options.path, options.languageMap);
|
|
251
|
+
[chartTotal] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
|
|
194
252
|
spinner.succeed(`Downloaded ${chartTotal} charts`);
|
|
195
253
|
}
|
|
196
254
|
// Download dashboards
|
|
@@ -198,15 +256,14 @@ const downloadHandler = async (options) => {
|
|
|
198
256
|
console.info(styles.warning(`No dashboards filters provided, skipping`));
|
|
199
257
|
}
|
|
200
258
|
else {
|
|
201
|
-
const spinner = globalState_1.default.startSpinner(`Downloading dashboards`);
|
|
202
259
|
let chartSlugs = [];
|
|
203
|
-
[dashboardTotal, chartSlugs] = await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, options.path, options.languageMap);
|
|
260
|
+
[dashboardTotal, chartSlugs] = await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested);
|
|
204
261
|
spinner.succeed(`Downloaded ${dashboardTotal} dashboards`);
|
|
205
262
|
// If any filter is provided, we download all charts for these dashboard
|
|
206
263
|
// We don't need to do this if we download everything (no filters)
|
|
207
264
|
if (hasFilters && chartSlugs.length > 0) {
|
|
208
265
|
spinner.start(`Downloading ${chartSlugs.length} charts linked to dashboards`);
|
|
209
|
-
const [totalCharts] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, options.path, options.languageMap);
|
|
266
|
+
const [totalCharts] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
|
|
210
267
|
spinner.succeed(`Downloaded ${totalCharts} charts linked to dashboards`);
|
|
211
268
|
}
|
|
212
269
|
}
|
package/dist/index.js
CHANGED
|
@@ -241,6 +241,7 @@ commander_1.program
|
|
|
241
241
|
.option('-d, --dashboards <dashboards...>', 'specify dashboard slugs, uuids or urls to download', [])
|
|
242
242
|
.option('-l, --language-map', 'generate a language maps for the downloaded charts and dashboards', false)
|
|
243
243
|
.option('-p, --path <path>', 'specify a custom path to download charts and dashboards', undefined)
|
|
244
|
+
.option('--nested', 'organize downloads in nested folders by project and space (default: flat structure)', false)
|
|
244
245
|
.option('--project <project uuid>', 'specify a project UUID to download', parseProjectArgument, undefined)
|
|
245
246
|
.action(download_1.downloadHandler);
|
|
246
247
|
commander_1.program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightdash/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2231.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lightdash": "dist/index.js"
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"unique-names-generator": "^4.7.1",
|
|
35
35
|
"uuid": "^11.0.3",
|
|
36
36
|
"yaml": "^2.7.0",
|
|
37
|
-
"@lightdash/common": "0.
|
|
38
|
-
"@lightdash/warehouses": "0.
|
|
37
|
+
"@lightdash/common": "0.2231.0",
|
|
38
|
+
"@lightdash/warehouses": "0.2231.0"
|
|
39
39
|
},
|
|
40
40
|
"description": "Lightdash CLI tool",
|
|
41
41
|
"devDependencies": {
|