@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.
@@ -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":"AAwBA,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;CAC1B,CAAC;AA4IF,eAAO,MAAM,eAAe,QACnB,MAAM,EAAE,QACP,QAAQ,GAAG,YAAY,aAClB,MAAM,eACJ,MAAM,gBACN,OAAO,KACrB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA2F5B,CAAC;AAEF,eAAO,MAAM,eAAe,YACf,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAsHd,CAAC;AA6MF,eAAO,MAAM,aAAa,YACb,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAuHd,CAAC"}
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 (items, folder, customPath) => {
39
- const outputDir = path.join(getDownloadFolder(customPath), folder);
40
- globalState_1.default.debug(`Writing ${items.length} ${folder} into ${outputDir}`);
41
- const created = await fs_1.promises.mkdir(outputDir, { recursive: true });
42
- if (created)
43
- console.info(`\nCreated new folder: ${outputDir} `);
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 inputDir = path.join(getDownloadFolder(customPath), folder);
59
- console.info(`Reading ${folder} from ${inputDir}`);
60
- const items = [];
88
+ const baseDir = getDownloadFolder(customPath);
89
+ globalState_1.default.log(`Reading ${folder} from ${baseDir}`);
61
90
  try {
62
- // Read all files from the lightdash directory
63
- // if folder does not exist, this throws an error
64
- const files = await fs_1.promises.readdir(inputDir);
65
- const yamlFiles = files
66
- .filter((file) => file.endsWith('.yml'))
67
- .filter((file) => !file.endsWith('.language.map.yml'));
68
- // Load each JSON file
69
- for (const file of yamlFiles) {
70
- const filePath = path.join(inputDir, file);
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
- // Folder does not exist
104
+ // Handle case where base directory doesn't exist
93
105
  if (error.code === 'ENOENT') {
94
- console.error(styles.warning(`Unable to upload ${folder}, "${inputDir}" folder not found. Run download command first.`));
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 ${inputDir}: ${error}`));
110
+ console.error(styles.error(`Error reading ${baseDir}: ${error}`));
99
111
  throw error;
100
112
  }
101
- return items;
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
- const outputDir = await createDirForContent(contentAsCode.dashboards, 'dashboards', customPath);
127
- for (const [index, dashboard,] of contentAsCode.dashboards.entries()) {
128
- await writeContent({
129
- type: 'dashboard',
130
- content: dashboard,
131
- translationMap: contentAsCode.languageMap?.[index],
132
- }, outputDir, languageMap);
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 outputDir = await createDirForContent(contentAsCode.charts, 'charts', customPath);
147
- for (const [index, chart] of contentAsCode.charts.entries()) {
148
- await writeContent({
149
- type: 'chart',
150
- content: chart,
151
- translationMap: contentAsCode.languageMap?.[index],
152
- }, outputDir, languageMap);
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
- const spinner = globalState_1.default.startSpinner(`Downloading charts`);
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.2230.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.2230.0",
38
- "@lightdash/warehouses": "0.2230.0"
37
+ "@lightdash/common": "0.2231.0",
38
+ "@lightdash/warehouses": "0.2231.0"
39
39
  },
40
40
  "description": "Lightdash CLI tool",
41
41
  "devDependencies": {