@lightdash/cli 0.2234.3 → 0.2235.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.
@@ -11,7 +11,9 @@ export type DownloadHandlerOptions = {
11
11
  includeCharts: boolean;
12
12
  nested: boolean;
13
13
  };
14
- export declare const downloadContent: (ids: string[], type: "charts" | "dashboards", projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean) => Promise<[number, string[]]>;
14
+ type DownloadContentType = 'charts' | 'dashboards' | 'sqlCharts';
15
+ export declare const downloadContent: (ids: string[], type: DownloadContentType, projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean) => Promise<[number, string[]]>;
15
16
  export declare const downloadHandler: (options: DownloadHandlerOptions) => Promise<void>;
16
17
  export declare const uploadHandler: (options: DownloadHandlerOptions) => Promise<void>;
18
+ export {};
17
19
  //# sourceMappingURL=download.d.ts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AA6BA,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;AA2PF,KAAK,mBAAmB,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAkDjE,eAAO,MAAM,eAAe,QACnB,MAAM,EAAE,QACP,mBAAmB,aACd,MAAM,eACJ,MAAM,eACN,MAAM,gBACN,OAAO,WACZ,OAAO,KAChB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA2G5B,CAAC;AAEF,eAAO,MAAM,eAAe,YACf,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkLd,CAAC;AAyNF,eAAO,MAAM,aAAa,YACb,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAuHd,CAAC"}
@@ -51,8 +51,24 @@ const createDirForContent = async (projectName, spaceSlug, folder, customPath, f
51
51
  await fs_1.promises.mkdir(outputDir, { recursive: true });
52
52
  return outputDir;
53
53
  };
54
+ /**
55
+ * Get file extension for content-as-code files.
56
+ * SQL charts use '.sql.yml' extension to avoid filename conflicts with regular charts
57
+ * that may have the same slug, since both chart types share the same output directory.
58
+ */
59
+ const getFileExtension = (contentType) => {
60
+ switch (contentType) {
61
+ case 'sqlChart':
62
+ return '.sql.yml';
63
+ case 'chart':
64
+ case 'dashboard':
65
+ default:
66
+ return '.yml';
67
+ }
68
+ };
54
69
  const writeContent = async (contentAsCode, outputDir, languageMap) => {
55
- const itemPath = path.join(outputDir, `${contentAsCode.content.slug}.yml`);
70
+ const extension = getFileExtension(contentAsCode.type);
71
+ const itemPath = path.join(outputDir, `${contentAsCode.content.slug}${extension}`);
56
72
  const chartYml = yaml.dump(contentAsCode.content, {
57
73
  quotingType: '"',
58
74
  });
@@ -115,86 +131,135 @@ const groupBySpace = (items) => {
115
131
  const itemsWithIndex = items.map((item, index) => ({ item, index }));
116
132
  return (0, groupBy_1.default)(itemsWithIndex, (entry) => entry.item.spaceSlug);
117
133
  };
118
- const writeSpaceContent = async ({ projectName, spaceSlug, folder, contentInSpace, contentAsCode, customPath, languageMap, folderScheme, }) => {
134
+ const writeSpaceContent = async ({ projectName, spaceSlug, folder, contentType, contentInSpace, contentAsCode, customPath, languageMap, folderScheme, }) => {
119
135
  const outputDir = await createDirForContent(projectName, spaceSlug, folder, customPath, folderScheme);
120
- const contentType = folder === 'charts' ? 'chart' : 'dashboard';
121
136
  for (const { item, index } of contentInSpace) {
137
+ const translationMap = 'languageMap' in contentAsCode
138
+ ? contentAsCode.languageMap?.[index]
139
+ : undefined;
122
140
  await writeContent({
123
141
  type: contentType,
124
142
  content: item,
125
- translationMap: contentAsCode.languageMap?.[index],
143
+ translationMap,
126
144
  }, outputDir, languageMap);
127
145
  }
128
146
  };
129
- const downloadContent = async (ids, // slug, uuid or url
130
- type, projectId, projectName, customPath, languageMap = false, nested = false) => {
147
+ const getContentTypeConfig = (type, projectId) => {
148
+ switch (type) {
149
+ case 'charts':
150
+ return {
151
+ endpoint: `/api/v1/projects/${projectId}/charts/code`,
152
+ displayName: 'charts',
153
+ supportsLanguageMap: true,
154
+ };
155
+ case 'dashboards':
156
+ return {
157
+ endpoint: `/api/v1/projects/${projectId}/dashboards/code`,
158
+ displayName: 'dashboards',
159
+ supportsLanguageMap: true,
160
+ };
161
+ case 'sqlCharts':
162
+ return {
163
+ endpoint: `/api/v1/projects/${projectId}/sqlCharts/code`,
164
+ displayName: 'SQL charts',
165
+ supportsLanguageMap: false,
166
+ };
167
+ default:
168
+ return (0, common_1.assertUnreachable)(type, `Unknown content type: ${type}`);
169
+ }
170
+ };
171
+ const extractChartSlugsFromDashboards = (dashboards) => dashboards.reduce((acc, dashboard) => {
172
+ const slugs = dashboard.tiles
173
+ .map((tile) => 'chartSlug' in tile.properties
174
+ ? tile.properties.chartSlug
175
+ : undefined)
176
+ .filter((slug) => slug !== undefined);
177
+ return [...acc, ...slugs];
178
+ }, []);
179
+ const downloadContent = async (ids, type, projectId, projectName, customPath, languageMap = false, nested = false) => {
131
180
  const spinner = globalState_1.default.getActiveSpinner();
132
181
  const contentFilters = parseContentFilters(ids);
133
- // Convert boolean flag to FolderScheme type
134
182
  const folderScheme = nested ? 'nested' : 'flat';
135
- let contentAsCode;
183
+ const config = getContentTypeConfig(type, projectId);
136
184
  let offset = 0;
185
+ let total = 0;
137
186
  let chartSlugs = [];
138
187
  do {
139
- globalState_1.default.debug(`Downloading ${type} with offset "${offset}" and filters "${contentFilters}"`);
140
- const commonParams = `offset=${offset}&languageMap=${languageMap}`;
188
+ globalState_1.default.debug(`Downloading ${config.displayName} with offset "${offset}" and filters "${contentFilters}"`);
189
+ const commonParams = config.supportsLanguageMap
190
+ ? `offset=${offset}&languageMap=${languageMap}`
191
+ : `offset=${offset}`;
141
192
  const queryParams = contentFilters
142
193
  ? `${contentFilters}&${commonParams}`
143
194
  : `?${commonParams}`;
144
- contentAsCode = await (0, apiClient_1.lightdashApi)({
195
+ const results = await (0, apiClient_1.lightdashApi)({
145
196
  method: 'GET',
146
- url: `/api/v1/projects/${projectId}/${type}/code${queryParams}`,
197
+ url: `${config.endpoint}${queryParams}`,
147
198
  body: undefined,
148
199
  });
149
- spinner?.start(`Downloaded ${contentAsCode.offset} of ${contentAsCode.total} ${type}`);
150
- contentAsCode.missingIds.forEach((missingId) => {
151
- console.warn(styles.warning(`\nNo ${type} with id "${missingId}"`));
200
+ spinner?.start(`Downloaded ${results.offset} of ${results.total} ${config.displayName}`);
201
+ // For the same chart slug, we run the code for saved charts and sql chart
202
+ // so we are going to get more false positives here, so we keep it on the debug log
203
+ results.missingIds.forEach((missingId) => {
204
+ globalState_1.default.debug(`\nNo ${config.displayName} with id "${missingId}"`);
152
205
  });
153
- if ('dashboards' in contentAsCode) {
154
- // Group dashboards by spaceSlug
155
- const dashboardsBySpace = groupBySpace(contentAsCode.dashboards);
156
- // Create directory and write content for each space
206
+ // Write content based on type
207
+ if ('sqlCharts' in results) {
208
+ const sqlChartsBySpace = groupBySpace(results.sqlCharts);
209
+ for (const [spaceSlug, sqlChartsInSpace] of Object.entries(sqlChartsBySpace)) {
210
+ await writeSpaceContent({
211
+ projectName,
212
+ spaceSlug,
213
+ folder: 'charts',
214
+ contentType: 'sqlChart',
215
+ contentInSpace: sqlChartsInSpace,
216
+ contentAsCode: results,
217
+ customPath,
218
+ languageMap,
219
+ folderScheme,
220
+ });
221
+ }
222
+ }
223
+ else if ('dashboards' in results) {
224
+ const dashboardsBySpace = groupBySpace(results.dashboards);
157
225
  for (const [spaceSlug, dashboardsInSpace] of Object.entries(dashboardsBySpace)) {
158
226
  await writeSpaceContent({
159
227
  projectName,
160
228
  spaceSlug,
161
229
  folder: 'dashboards',
230
+ contentType: 'dashboard',
162
231
  contentInSpace: dashboardsInSpace,
163
- contentAsCode,
232
+ contentAsCode: results,
164
233
  customPath,
165
234
  languageMap,
166
235
  folderScheme,
167
236
  });
168
237
  }
169
- // Extract chart slugs from dashboards
170
- chartSlugs = contentAsCode.dashboards.reduce((acc, dashboard) => {
171
- const slugs = dashboard.tiles.map((chart) => 'chartSlug' in chart.properties
172
- ? chart.properties.chartSlug
173
- : undefined);
174
- return [
175
- ...acc,
176
- ...slugs.filter((slug) => slug !== undefined),
177
- ];
178
- }, []);
238
+ chartSlugs = [
239
+ ...chartSlugs,
240
+ ...extractChartSlugsFromDashboards(results.dashboards),
241
+ ];
179
242
  }
180
243
  else {
181
- const chartsBySpace = groupBySpace(contentAsCode.charts);
244
+ const chartsBySpace = groupBySpace(results.charts);
182
245
  for (const [spaceSlug, chartsInSpace] of Object.entries(chartsBySpace)) {
183
246
  await writeSpaceContent({
184
247
  projectName,
185
248
  spaceSlug,
186
249
  folder: 'charts',
250
+ contentType: 'chart',
187
251
  contentInSpace: chartsInSpace,
188
- contentAsCode,
252
+ contentAsCode: results,
189
253
  customPath,
190
254
  languageMap,
191
255
  folderScheme,
192
256
  });
193
257
  }
194
258
  }
195
- offset = contentAsCode.offset;
196
- } while (contentAsCode.offset < contentAsCode.total);
197
- return [contentAsCode.total, [...new Set(chartSlugs)]];
259
+ offset = results.offset;
260
+ total = results.total;
261
+ } while (offset < total);
262
+ return [total, [...new Set(chartSlugs)]];
198
263
  };
199
264
  exports.downloadContent = downloadContent;
200
265
  const downloadHandler = async (options) => {
@@ -243,13 +308,18 @@ const downloadHandler = async (options) => {
243
308
  // If any filter is provided, we skip those items without filters
244
309
  // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
245
310
  const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
246
- // Download charts
311
+ // Download regular charts
247
312
  if (hasFilters && options.charts.length === 0) {
248
313
  console.info(styles.warning(`No charts filters provided, skipping`));
249
314
  }
250
315
  else {
251
- [chartTotal] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
252
- spinner.succeed(`Downloaded ${chartTotal} charts`);
316
+ const [regularChartTotal] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
317
+ spinner.succeed(`Downloaded ${regularChartTotal} charts`);
318
+ // Download SQL charts
319
+ spinner.start(`Downloading SQL charts`);
320
+ const [sqlChartTotal] = await (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested);
321
+ spinner.succeed(`Downloaded ${sqlChartTotal} SQL charts`);
322
+ chartTotal = regularChartTotal + sqlChartTotal;
253
323
  }
254
324
  // Download dashboards
255
325
  if (hasFilters && options.dashboards.length === 0) {
@@ -259,12 +329,14 @@ const downloadHandler = async (options) => {
259
329
  let chartSlugs = [];
260
330
  [dashboardTotal, chartSlugs] = await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested);
261
331
  spinner.succeed(`Downloaded ${dashboardTotal} dashboards`);
262
- // If any filter is provided, we download all charts for these dashboard
332
+ // If any filter is provided, we download all charts linked to these dashboards
263
333
  // We don't need to do this if we download everything (no filters)
264
334
  if (hasFilters && chartSlugs.length > 0) {
265
335
  spinner.start(`Downloading ${chartSlugs.length} charts linked to dashboards`);
266
- const [totalCharts] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
267
- spinner.succeed(`Downloaded ${totalCharts} charts linked to dashboards`);
336
+ // Download both regular charts and SQL charts linked to dashboards
337
+ const [regularCharts] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
338
+ const [sqlCharts] = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested);
339
+ spinner.succeed(`Downloaded ${regularCharts + sqlCharts} charts linked to dashboards`);
268
340
  }
269
341
  }
270
342
  const end = Date.now();
@@ -335,6 +407,8 @@ const logUploadChanges = (changes) => {
335
407
  console.info(`Total ${key}: ${value} `);
336
408
  });
337
409
  };
410
+ // SQL charts have 'sql' field instead of 'tableName'/'metricQuery'
411
+ const isSqlChart = (item) => 'sql' in item && !('tableName' in item);
338
412
  /**
339
413
  *
340
414
  * @param slugs if slugs are provided, we only force upsert the charts/dashboards that match the slugs, if slugs are empty, we upload files that were locally updated
@@ -368,9 +442,14 @@ const upsertResources = async (type, projectId, changes, force, slugs, customPat
368
442
  continue;
369
443
  }
370
444
  globalState_1.default.debug(`Upserting ${type} ${item.slug}`);
445
+ // SQL charts use a different endpoint
446
+ const isSqlChartItem = type === 'charts' && isSqlChart(item);
447
+ const endpoint = isSqlChartItem
448
+ ? `/api/v1/projects/${projectId}/sqlCharts/${item.slug}/code`
449
+ : `/api/v1/projects/${projectId}/${type}/${item.slug}/code`;
371
450
  const upsertData = await (0, apiClient_1.lightdashApi)({
372
451
  method: 'POST',
373
- url: `/api/v1/projects/${projectId}/${type}/${item.slug}/code`,
452
+ url: endpoint,
374
453
  body: JSON.stringify({
375
454
  ...item,
376
455
  skipSpaceCreate,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.2234.3",
3
+ "version": "0.2235.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.2234.3",
38
- "@lightdash/warehouses": "0.2234.3"
37
+ "@lightdash/common": "0.2235.0",
38
+ "@lightdash/warehouses": "0.2235.0"
39
39
  },
40
40
  "description": "Lightdash CLI tool",
41
41
  "devDependencies": {