@lightdash/cli 0.2644.1 → 0.2646.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/config.d.ts CHANGED
@@ -24,6 +24,7 @@ export type Config = {
24
24
  };
25
25
  export declare const getConfig: () => Promise<Config>;
26
26
  export declare const setProject: (projectUuid: string, projectName: string) => Promise<void>;
27
+ export declare const unsetProject: () => Promise<void>;
27
28
  export declare const setPreviewProject: (projectUuid: string, name: string) => Promise<void>;
28
29
  export declare const unsetPreviewProject: () => Promise<void>;
29
30
  export declare const setDefaultUser: (userUuid: string, organizationUuid: string) => Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,QAAsC,CAAC;AAElE,MAAM,MAAM,MAAM,GAAG;IACjB,IAAI,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACN,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,OAAO,CAAC,EAAE;QACN,qCAAqC,CAAC,EAAE,OAAO,CAAC;KACnD,CAAC;CACL,CAAC;AAgCF,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAmBhD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,aAAa,MAAM,EAAE,aAAa,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,aAAa,MAAM,EAAE,MAAM,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,mBAAmB,qBAU/B,CAAC;AAEF,eAAO,MAAM,cAAc,GACvB,UAAU,MAAM,EAChB,kBAAkB,MAAM,kBAW3B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,SAAS,MAAM,CAAC,SAAS,CAAC,kBAM1D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAU,QAAQ,MAAM,CAAC,SAAS,CAAC,kBAMxD,CAAC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,cAAc,QAAsC,CAAC;AAElE,MAAM,MAAM,MAAM,GAAG;IACjB,IAAI,CAAC,EAAE;QACH,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;IACF,OAAO,CAAC,EAAE;QACN,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB;;;WAGG;QACH,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,OAAO,CAAC,EAAE;QACN,qCAAqC,CAAC,EAAE,OAAO,CAAC;KACnD,CAAC;CACL,CAAC;AAgCF,eAAO,MAAM,SAAS,QAAa,OAAO,CAAC,MAAM,CAmBhD,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,aAAa,MAAM,EAAE,aAAa,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,YAAY,qBAUxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,aAAa,MAAM,EAAE,MAAM,MAAM,kBAUxE,CAAC;AAEF,eAAO,MAAM,mBAAmB,qBAU/B,CAAC;AAEF,eAAO,MAAM,cAAc,GACvB,UAAU,MAAM,EAChB,kBAAkB,MAAM,kBAW3B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAU,SAAS,MAAM,CAAC,SAAS,CAAC,kBAM1D,CAAC;AAEF,eAAO,MAAM,SAAS,GAAU,QAAQ,MAAM,CAAC,SAAS,CAAC,kBAMxD,CAAC"}
package/dist/config.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setAnswer = exports.setContext = exports.setDefaultUser = exports.unsetPreviewProject = exports.setPreviewProject = exports.setProject = exports.getConfig = exports.configFilePath = void 0;
3
+ exports.setAnswer = exports.setContext = exports.setDefaultUser = exports.unsetPreviewProject = exports.setPreviewProject = exports.unsetProject = exports.setProject = exports.getConfig = exports.configFilePath = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs_1 = require("fs");
6
6
  const yaml = tslib_1.__importStar(require("js-yaml"));
@@ -68,6 +68,18 @@ const setProject = async (projectUuid, projectName) => {
68
68
  });
69
69
  };
70
70
  exports.setProject = setProject;
71
+ const unsetProject = async () => {
72
+ const config = await getRawConfig();
73
+ await setConfig({
74
+ ...config,
75
+ context: {
76
+ ...(config.context || {}),
77
+ project: undefined,
78
+ projectName: undefined,
79
+ },
80
+ });
81
+ };
82
+ exports.unsetProject = unsetProject;
71
83
  const setPreviewProject = async (projectUuid, name) => {
72
84
  const config = await getRawConfig();
73
85
  await setConfig({
@@ -14,8 +14,13 @@ export type DownloadHandlerOptions = {
14
14
  concurrency: number;
15
15
  gzip?: boolean;
16
16
  };
17
+ type MetadataEntry = {
18
+ slug: string;
19
+ type: 'charts' | 'dashboards';
20
+ downloadedAt: string;
21
+ };
17
22
  type DownloadContentType = 'charts' | 'dashboards' | 'sqlCharts';
18
- export declare const downloadContent: (ids: string[], type: DownloadContentType, projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean) => Promise<[number, string[]]>;
23
+ export declare const downloadContent: (ids: string[], type: DownloadContentType, projectId: string, projectName: string, customPath?: string, languageMap?: boolean, nested?: boolean) => Promise<[number, string[], MetadataEntry[]]>;
19
24
  export declare const downloadHandler: (options: DownloadHandlerOptions) => Promise<void>;
20
25
  export declare const uploadHandler: (options: DownloadHandlerOptions) => Promise<void>;
21
26
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AAqCA,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;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAmQF,KAAK,mBAAmB,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAkDjE,eAAO,MAAM,eAAe,GACxB,KAAK,MAAM,EAAE,EACb,MAAM,mBAAmB,EACzB,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,aAAa,MAAM,EACnB,cAAa,OAAe,EAC5B,SAAQ,OAAe,KACxB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA2G5B,CAAC;AAEF,eAAO,MAAM,eAAe,GACxB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAuKd,CAAC;AA4ZF,eAAO,MAAM,aAAa,GACtB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CA+Id,CAAC"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AA2CA,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;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AA2FF,KAAK,aAAa,GAAG;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;CACxB,CAAC;AAyOF,KAAK,mBAAmB,GAAG,QAAQ,GAAG,YAAY,GAAG,WAAW,CAAC;AAkDjE,eAAO,MAAM,eAAe,GACxB,KAAK,MAAM,EAAE,EACb,MAAM,mBAAmB,EACzB,WAAW,MAAM,EACjB,aAAa,MAAM,EACnB,aAAa,MAAM,EACnB,cAAa,OAAe,EAC5B,SAAQ,OAAe,KACxB,OAAO,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,CAAC,CA+G7C,CAAC;AAEF,eAAO,MAAM,eAAe,GACxB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAmMd,CAAC;AA4ZF,eAAO,MAAM,aAAa,GACtB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkJd,CAAC"}
@@ -15,6 +15,7 @@ const config_1 = require("../config");
15
15
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
16
16
  const styles = tslib_1.__importStar(require("../styles"));
17
17
  const apiClient_1 = require("./dbt/apiClient");
18
+ const metadataFile_1 = require("./metadataFile");
18
19
  const selectProject_1 = require("./selectProject");
19
20
  const getDownloadFolder = (customPath) => {
20
21
  if (customPath) {
@@ -71,29 +72,69 @@ const getFileExtension = (contentType) => {
71
72
  const writeContent = async (contentAsCode, outputDir, languageMap) => {
72
73
  const extension = getFileExtension(contentAsCode.type);
73
74
  const itemPath = path.join(outputDir, `${contentAsCode.content.slug}${extension}`);
74
- const chartYml = yaml.dump(contentAsCode.content, {
75
+ // Strip timestamps — they go to .lightdash-metadata.json instead
76
+ const { updatedAt, downloadedAt, ...cleanContent } = contentAsCode.content;
77
+ const chartYml = yaml.dump(cleanContent, {
75
78
  quotingType: '"',
79
+ sortKeys: true,
76
80
  });
77
81
  await fs_1.promises.writeFile(itemPath, chartYml);
78
82
  if (contentAsCode.translationMap && languageMap) {
79
83
  const translationPath = path.join(outputDir, `${contentAsCode.content.slug}.language.map.yml`);
80
- await fs_1.promises.writeFile(translationPath, yaml.dump(contentAsCode.translationMap));
84
+ await fs_1.promises.writeFile(translationPath, yaml.dump(contentAsCode.translationMap, { sortKeys: true }));
81
85
  }
86
+ const metadataType = contentAsCode.type === 'dashboard' ? 'dashboards' : 'charts';
87
+ let downloadedAtString;
88
+ if (downloadedAt instanceof Date) {
89
+ downloadedAtString = downloadedAt.toISOString();
90
+ }
91
+ else if (typeof downloadedAt === 'string') {
92
+ downloadedAtString = downloadedAt;
93
+ }
94
+ else {
95
+ downloadedAtString = new Date().toISOString();
96
+ }
97
+ return {
98
+ slug: contentAsCode.content.slug,
99
+ type: metadataType,
100
+ downloadedAt: downloadedAtString,
101
+ };
102
+ };
103
+ const hasUnsortedKeys = (obj) => {
104
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
105
+ if (Array.isArray(obj)) {
106
+ return obj.some(hasUnsortedKeys);
107
+ }
108
+ return false;
109
+ }
110
+ const keys = Object.keys(obj);
111
+ const sorted = [...keys].sort();
112
+ if (keys.some((key, i) => key !== sorted[i])) {
113
+ return true;
114
+ }
115
+ return Object.values(obj).some(hasUnsortedKeys);
82
116
  };
83
117
  const isLightdashContentFile = (folder, entry) => entry.isFile() &&
84
118
  entry.parentPath &&
85
119
  entry.parentPath.endsWith(path.sep + folder) &&
86
120
  entry.name.endsWith('.yml') &&
87
121
  !entry.name.endsWith('.language.map.yml');
88
- const loadYamlFile = async (file) => {
122
+ const loadYamlFile = async (file, folder, metadata) => {
89
123
  const filePath = path.join(file.parentPath, file.name);
90
124
  const [fileContent, stats] = await Promise.all([
91
125
  fs_1.promises.readFile(filePath, 'utf-8'),
92
126
  fs_1.promises.stat(filePath),
93
127
  ]);
94
128
  const item = yaml.load(fileContent);
95
- const downloadedAt = item.downloadedAt
96
- ? new Date(item.downloadedAt)
129
+ if (hasUnsortedKeys(item)) {
130
+ globalState_1.default.log(styles.warning(`Warning: ${file.name} has unsorted YAML keys. Re-download to fix, or sort keys alphabetically.`));
131
+ }
132
+ const metadataSection = folder === 'dashboards' ? metadata.dashboards : metadata.charts;
133
+ const downloadedAtRaw = metadataSection[item.slug] ?? item.downloadedAt;
134
+ const downloadedAt = downloadedAtRaw
135
+ ? new Date(downloadedAtRaw instanceof Date
136
+ ? downloadedAtRaw.getTime()
137
+ : downloadedAtRaw)
97
138
  : undefined;
98
139
  const needsUpdating = downloadedAt &&
99
140
  Math.abs(stats.mtime.getTime() - downloadedAt.getTime()) > 30000;
@@ -111,13 +152,14 @@ const readCodeFiles = async (folder, customPath) => {
111
152
  throw new Error(`Node.js v20.12.0 or later is required for this command (current: ${process.version}).`);
112
153
  }
113
154
  try {
155
+ const metadata = await (0, metadataFile_1.readMetadataFile)(baseDir);
114
156
  const allEntries = await fs_1.promises.readdir(baseDir, {
115
157
  recursive: true,
116
158
  withFileTypes: true,
117
159
  });
118
160
  const items = await Promise.all(allEntries
119
161
  .filter((entry) => isLightdashContentFile(folder, entry))
120
- .map((file) => loadYamlFile(file)));
162
+ .map((file) => loadYamlFile(file, folder, metadata)));
121
163
  if (items.length === 0) {
122
164
  console.error(styles.warning(`Unable to upload ${folder}, no files found in "${baseDir}". Run download command first.`));
123
165
  }
@@ -140,16 +182,19 @@ const groupBySpace = (items) => {
140
182
  };
141
183
  const writeSpaceContent = async ({ projectName, spaceSlug, folder, contentType, contentInSpace, contentAsCode, customPath, languageMap, folderScheme, }) => {
142
184
  const outputDir = await createDirForContent(projectName, spaceSlug, folder, customPath, folderScheme);
185
+ const entries = [];
143
186
  for (const { item, index } of contentInSpace) {
144
187
  const translationMap = 'languageMap' in contentAsCode
145
188
  ? contentAsCode.languageMap?.[index]
146
189
  : undefined;
147
- await writeContent({
190
+ const entry = await writeContent({
148
191
  type: contentType,
149
192
  content: item,
150
193
  translationMap,
151
194
  }, outputDir, languageMap);
195
+ entries.push(entry);
152
196
  }
197
+ return entries;
153
198
  };
154
199
  const getContentTypeConfig = (type, projectId) => {
155
200
  switch (type) {
@@ -191,6 +236,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
191
236
  let offset = 0;
192
237
  let total = 0;
193
238
  let chartSlugs = [];
239
+ let allMetadataEntries = [];
194
240
  do {
195
241
  globalState_1.default.debug(`Downloading ${config.displayName} with offset "${offset}" and filters "${contentFilters}"`);
196
242
  const commonParams = config.supportsLanguageMap
@@ -214,7 +260,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
214
260
  if ('sqlCharts' in results) {
215
261
  const sqlChartsBySpace = groupBySpace(results.sqlCharts);
216
262
  for (const [spaceSlug, sqlChartsInSpace] of Object.entries(sqlChartsBySpace)) {
217
- await writeSpaceContent({
263
+ const entries = await writeSpaceContent({
218
264
  projectName,
219
265
  spaceSlug,
220
266
  folder: 'charts',
@@ -225,12 +271,13 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
225
271
  languageMap,
226
272
  folderScheme,
227
273
  });
274
+ allMetadataEntries = [...allMetadataEntries, ...entries];
228
275
  }
229
276
  }
230
277
  else if ('dashboards' in results) {
231
278
  const dashboardsBySpace = groupBySpace(results.dashboards);
232
279
  for (const [spaceSlug, dashboardsInSpace] of Object.entries(dashboardsBySpace)) {
233
- await writeSpaceContent({
280
+ const entries = await writeSpaceContent({
234
281
  projectName,
235
282
  spaceSlug,
236
283
  folder: 'dashboards',
@@ -241,6 +288,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
241
288
  languageMap,
242
289
  folderScheme,
243
290
  });
291
+ allMetadataEntries = [...allMetadataEntries, ...entries];
244
292
  }
245
293
  chartSlugs = [
246
294
  ...chartSlugs,
@@ -250,7 +298,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
250
298
  else {
251
299
  const chartsBySpace = groupBySpace(results.charts);
252
300
  for (const [spaceSlug, chartsInSpace] of Object.entries(chartsBySpace)) {
253
- await writeSpaceContent({
301
+ const entries = await writeSpaceContent({
254
302
  projectName,
255
303
  spaceSlug,
256
304
  folder: 'charts',
@@ -261,12 +309,13 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
261
309
  languageMap,
262
310
  folderScheme,
263
311
  });
312
+ allMetadataEntries = [...allMetadataEntries, ...entries];
264
313
  }
265
314
  }
266
315
  offset = results.offset;
267
316
  total = results.total;
268
317
  } while (offset < total);
269
- return [total, [...new Set(chartSlugs)]];
318
+ return [total, [...new Set(chartSlugs)], allMetadataEntries];
270
319
  };
271
320
  exports.downloadContent = downloadContent;
272
321
  const downloadHandler = async (options) => {
@@ -278,7 +327,12 @@ const downloadHandler = async (options) => {
278
327
  }
279
328
  const projectSelection = await (0, selectProject_1.selectProject)(config, options.project);
280
329
  if (!projectSelection) {
281
- throw new Error('No project selected. Run lightdash config set-project');
330
+ throw new common_1.LightdashError({
331
+ message: 'No project selected. Run lightdash config set-project',
332
+ name: 'Not Found',
333
+ statusCode: 404,
334
+ data: {},
335
+ });
282
336
  }
283
337
  const projectId = projectSelection.projectUuid;
284
338
  // Log current project info
@@ -305,20 +359,21 @@ const downloadHandler = async (options) => {
305
359
  },
306
360
  });
307
361
  try {
308
- // If any filter is provided, we skip those items without filters
309
- // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
310
362
  const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
363
+ let allMetadataEntries = [];
311
364
  // Download regular charts
312
365
  if (hasFilters && options.charts.length === 0) {
313
366
  console.info(styles.warning(`No charts filters provided, skipping`));
314
367
  }
315
368
  else {
316
- const [regularChartTotal] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
369
+ const [regularChartTotal, , regularChartMeta] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
317
370
  spinner.succeed(`Downloaded ${regularChartTotal} charts`);
371
+ allMetadataEntries = [...allMetadataEntries, ...regularChartMeta];
318
372
  // Download SQL charts
319
373
  spinner.start(`Downloading SQL charts`);
320
- const [sqlChartTotal] = await (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested);
374
+ const [sqlChartTotal, , sqlChartMeta] = await (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested);
321
375
  spinner.succeed(`Downloaded ${sqlChartTotal} SQL charts`);
376
+ allMetadataEntries = [...allMetadataEntries, ...sqlChartMeta];
322
377
  chartTotal = regularChartTotal + sqlChartTotal;
323
378
  }
324
379
  // Download dashboards
@@ -327,18 +382,34 @@ const downloadHandler = async (options) => {
327
382
  }
328
383
  else {
329
384
  let chartSlugs = [];
330
- [dashboardTotal, chartSlugs] = await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested);
385
+ let dashMeta;
386
+ [dashboardTotal, chartSlugs, dashMeta] = await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested);
387
+ allMetadataEntries = [...allMetadataEntries, ...dashMeta];
331
388
  spinner.succeed(`Downloaded ${dashboardTotal} dashboards`);
332
- // If any filter is provided, we download all charts linked to these dashboards
333
- // We don't need to do this if we download everything (no filters)
334
389
  if (hasFilters && chartSlugs.length > 0) {
335
390
  spinner.start(`Downloading ${chartSlugs.length} 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);
391
+ const [regularCharts, , linkedChartMeta] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested);
392
+ allMetadataEntries = [
393
+ ...allMetadataEntries,
394
+ ...linkedChartMeta,
395
+ ];
396
+ const [sqlCharts, , linkedSqlMeta] = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested);
397
+ allMetadataEntries = [...allMetadataEntries, ...linkedSqlMeta];
339
398
  spinner.succeed(`Downloaded ${regularCharts + sqlCharts} charts linked to dashboards`);
340
399
  }
341
400
  }
401
+ // Write metadata file with all downloadedAt timestamps
402
+ const metadataToWrite = {
403
+ version: 1,
404
+ charts: {},
405
+ dashboards: {},
406
+ };
407
+ for (const entry of allMetadataEntries) {
408
+ metadataToWrite[entry.type][entry.slug] = entry.downloadedAt;
409
+ }
410
+ const baseDir = getDownloadFolder(options.path);
411
+ await (0, metadataFile_1.writeMetadataFile)(baseDir, metadataToWrite);
412
+ globalState_1.default.log(styles.warning(`\nNote: ${metadataFile_1.METADATA_FILENAME} was written to ${baseDir}. Consider adding it to your .gitignore.`));
342
413
  const end = Date.now();
343
414
  await analytics_1.LightdashAnalytics.track({
344
415
  event: 'download.completed',
@@ -348,7 +419,7 @@ const downloadHandler = async (options) => {
348
419
  projectId,
349
420
  chartsNum: chartTotal,
350
421
  dashboardsNum: dashboardTotal,
351
- timeToCompleted: (end - start) / 1000, // in seconds
422
+ timeToCompleted: (end - start) / 1000,
352
423
  },
353
424
  });
354
425
  }
@@ -612,7 +683,12 @@ const uploadHandler = async (options) => {
612
683
  }
613
684
  const projectSelection = await (0, selectProject_1.selectProject)(config, options.project);
614
685
  if (!projectSelection) {
615
- throw new Error('No project selected. Run lightdash config set-project');
686
+ throw new common_1.LightdashError({
687
+ message: 'No project selected. Run lightdash config set-project',
688
+ name: 'Not Found',
689
+ statusCode: 404,
690
+ data: {},
691
+ });
616
692
  }
617
693
  const projectId = projectSelection.projectUuid;
618
694
  // Log current project info
@@ -1 +1 @@
1
- {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/handlers/login.ts"],"names":[],"mappings":"AAkBA,KAAK,YAAY,GAAG;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AA4JF,eAAO,MAAM,KAAK,GACd,UAAU,MAAM,GAAG,SAAS,EAC5B,SAAS,YAAY,kBAmJxB,CAAC"}
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/handlers/login.ts"],"names":[],"mappings":"AAkBA,KAAK,YAAY,GAAG;IAChB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;CACpB,CAAC;AA4JF,eAAO,MAAM,KAAK,GACd,UAAU,MAAM,GAAG,SAAS,EAC5B,SAAS,YAAY,kBA2JxB,CAAC"}
@@ -221,11 +221,14 @@ const login = async (urlInput, options) => {
221
221
  await (0, setProject_1.setFirstProject)();
222
222
  }
223
223
  else {
224
- const project = await (0, setProject_1.setProjectCommand)();
225
- if (project === undefined) {
224
+ const result = await (0, setProject_1.setProjectCommand)();
225
+ if (result === 'empty') {
226
226
  console.error('Now you can add your first project to lightdash by doing: ');
227
227
  console.error(`\n ${styles.bold(`⚡️ lightdash deploy --create`)}\n`);
228
228
  }
229
+ else if (result === 'skipped') {
230
+ console.error(`\n No project selected — use ${styles.bold('lightdash config set-project')} or ${styles.bold('--project <uuid>')} when running commands.\n`);
231
+ }
229
232
  }
230
233
  }
231
234
  catch {
@@ -0,0 +1,9 @@
1
+ export declare const METADATA_FILENAME = ".lightdash-metadata.json";
2
+ export type LightdashMetadata = {
3
+ version: 1;
4
+ charts: Record<string, string>;
5
+ dashboards: Record<string, string>;
6
+ };
7
+ export declare const readMetadataFile: (baseDir: string) => Promise<LightdashMetadata>;
8
+ export declare const writeMetadataFile: (baseDir: string, metadata: LightdashMetadata) => Promise<void>;
9
+ //# sourceMappingURL=metadataFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadataFile.d.ts","sourceRoot":"","sources":["../../src/handlers/metadataFile.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,iBAAiB,6BAA6B,CAAC;AAE5D,MAAM,MAAM,iBAAiB,GAAG;IAC5B,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC,CAAC;AAQF,eAAO,MAAM,gBAAgB,GACzB,SAAS,MAAM,KAChB,OAAO,CAAC,iBAAiB,CAQ3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC1B,SAAS,MAAM,EACf,UAAU,iBAAiB,KAC5B,OAAO,CAAC,IAAI,CASd,CAAC"}
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeMetadataFile = exports.readMetadataFile = exports.METADATA_FILENAME = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs_1 = require("fs");
6
+ const path = tslib_1.__importStar(require("path"));
7
+ exports.METADATA_FILENAME = '.lightdash-metadata.json';
8
+ const emptyMetadata = () => ({
9
+ version: 1,
10
+ charts: {},
11
+ dashboards: {},
12
+ });
13
+ const readMetadataFile = async (baseDir) => {
14
+ const filePath = path.join(baseDir, exports.METADATA_FILENAME);
15
+ try {
16
+ const content = await fs_1.promises.readFile(filePath, 'utf-8');
17
+ return JSON.parse(content);
18
+ }
19
+ catch {
20
+ return emptyMetadata();
21
+ }
22
+ };
23
+ exports.readMetadataFile = readMetadataFile;
24
+ const writeMetadataFile = async (baseDir, metadata) => {
25
+ const existing = await (0, exports.readMetadataFile)(baseDir);
26
+ const merged = {
27
+ version: 1,
28
+ charts: { ...existing.charts, ...metadata.charts },
29
+ dashboards: { ...existing.dashboards, ...metadata.dashboards },
30
+ };
31
+ const filePath = path.join(baseDir, exports.METADATA_FILENAME);
32
+ await fs_1.promises.writeFile(filePath, JSON.stringify(merged, null, 2));
33
+ };
34
+ exports.writeMetadataFile = writeMetadataFile;
@@ -1 +1 @@
1
- {"version":3,"file":"renameHandler.d.ts","sourceRoot":"","sources":["../../src/handlers/renameHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,UAAU,EAEb,MAAM,mBAAmB,CAAC;AAkB3B,KAAK,oBAAoB,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACrB,CAAC;AAsCF,eAAO,MAAM,aAAa,GAAU,SAAS,oBAAoB,kBAgMhE,CAAC"}
1
+ {"version":3,"file":"renameHandler.d.ts","sourceRoot":"","sources":["../../src/handlers/renameHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAMH,UAAU,EAEb,MAAM,mBAAmB,CAAC;AAkB3B,KAAK,oBAAoB,GAAG;IACxB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACrB,CAAC;AAsCF,eAAO,MAAM,aAAa,GAAU,SAAS,oBAAoB,kBAmMhE,CAAC"}
@@ -48,7 +48,12 @@ const renameHandler = async (options) => {
48
48
  config.context.previewProject ||
49
49
  config.context.project;
50
50
  if (!projectUuid) {
51
- throw new Error('No project selected. Run lightdash config set-project');
51
+ throw new common_1.LightdashError({
52
+ message: 'No project selected. Run lightdash config set-project',
53
+ name: 'Not Found',
54
+ statusCode: 404,
55
+ data: {},
56
+ });
52
57
  }
53
58
  // Log current project info
54
59
  if (options.project) {
@@ -3,8 +3,12 @@ type SetProjectOptions = {
3
3
  name: string;
4
4
  uuid: string;
5
5
  };
6
- export declare const setProjectCommand: (name?: string, uuid?: string) => Promise<void>;
6
+ export declare const setProjectCommand: (name?: string, uuid?: string) => Promise<"selected" | "skipped" | "empty">;
7
7
  export declare const setFirstProject: () => Promise<void>;
8
8
  export declare const setProjectHandler: (options: SetProjectOptions) => Promise<void>;
9
+ export declare const unsetProjectCommand: () => Promise<void>;
10
+ export declare const unsetProjectHandler: (options: {
11
+ verbose: boolean;
12
+ }) => Promise<void>;
9
13
  export {};
10
14
  //# sourceMappingURL=setProject.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setProject.d.ts","sourceRoot":"","sources":["../../src/handlers/setProject.ts"],"names":[],"mappings":"AAQA,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,OAAO,MAAM,EAAE,OAAO,MAAM,kBAuDnE,CAAC;AAEF,eAAO,MAAM,eAAe,qBAmB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,SAAS,iBAAiB,kBAmBjE,CAAC"}
1
+ {"version":3,"file":"setProject.d.ts","sourceRoot":"","sources":["../../src/handlers/setProject.ts"],"names":[],"mappings":"AAQA,KAAK,iBAAiB,GAAG;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC1B,OAAO,MAAM,EACb,OAAO,MAAM,KACd,OAAO,CAAC,UAAU,GAAG,SAAS,GAAG,OAAO,CAmE1C,CAAC;AAEF,eAAO,MAAM,eAAe,qBAmB3B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAU,SAAS,iBAAiB,kBAsBjE,CAAC;AAEF,eAAO,MAAM,mBAAmB,qBAG/B,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAU,SAAS;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,kBAmBtE,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setProjectHandler = exports.setFirstProject = exports.setProjectCommand = void 0;
3
+ exports.unsetProjectHandler = exports.unsetProjectCommand = exports.setProjectHandler = exports.setFirstProject = exports.setProjectCommand = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const inquirer_1 = tslib_1.__importDefault(require("inquirer"));
6
6
  const url_1 = require("url");
@@ -16,7 +16,7 @@ const setProjectCommand = async (name, uuid) => {
16
16
  });
17
17
  globalState_1.default.debug(`> Set project returned response: ${JSON.stringify(projects)}`);
18
18
  if (projects.length === 0)
19
- return;
19
+ return 'empty';
20
20
  let selectedProject;
21
21
  // --uuid or --name options
22
22
  if (uuid !== undefined || name !== undefined) {
@@ -27,16 +27,27 @@ const setProjectCommand = async (name, uuid) => {
27
27
  [selectedProject] = projects;
28
28
  }
29
29
  else {
30
+ const SKIP_VALUE = '__skip__';
30
31
  const answers = await inquirer_1.default.prompt([
31
32
  {
32
33
  type: 'list',
33
34
  name: 'project',
34
- choices: projects.map((project) => ({
35
- name: project.name,
36
- value: project.projectUuid,
37
- })),
35
+ choices: [
36
+ {
37
+ name: "Don't select a project",
38
+ value: SKIP_VALUE,
39
+ },
40
+ ...projects.map((project) => ({
41
+ name: project.name,
42
+ value: project.projectUuid,
43
+ })),
44
+ ],
38
45
  },
39
46
  ]);
47
+ if (answers.project === SKIP_VALUE) {
48
+ await (0, config_1.unsetProject)();
49
+ return 'skipped';
50
+ }
40
51
  selectedProject = projects.find((project) => project.projectUuid === answers.project);
41
52
  }
42
53
  if (selectedProject !== undefined) {
@@ -45,10 +56,9 @@ const setProjectCommand = async (name, uuid) => {
45
56
  const projectUrl = config.context?.serverUrl &&
46
57
  new url_1.URL(`/projects/${selectedProject.projectUuid}/home`, config.context.serverUrl);
47
58
  console.error(`\n ✅️ Connected to Lightdash project: ${projectUrl || ''}\n`);
59
+ return 'selected';
48
60
  }
49
- else {
50
- throw new Error(`Project not found.`);
51
- }
61
+ throw new Error(`Project not found.`);
52
62
  };
53
63
  exports.setProjectCommand = setProjectCommand;
54
64
  const setFirstProject = async () => {
@@ -70,7 +80,10 @@ const setProjectHandler = async (options) => {
70
80
  let success = true;
71
81
  globalState_1.default.setVerbose(options.verbose);
72
82
  try {
73
- await (0, exports.setProjectCommand)(options.name, options.uuid);
83
+ const result = await (0, exports.setProjectCommand)(options.name, options.uuid);
84
+ if (result === 'skipped') {
85
+ console.error(`\n Project unset.\n`);
86
+ }
74
87
  }
75
88
  catch (e) {
76
89
  success = false;
@@ -88,3 +101,31 @@ const setProjectHandler = async (options) => {
88
101
  }
89
102
  };
90
103
  exports.setProjectHandler = setProjectHandler;
104
+ const unsetProjectCommand = async () => {
105
+ await (0, config_1.unsetProject)();
106
+ console.error(`\n Project unset.\n`);
107
+ };
108
+ exports.unsetProjectCommand = unsetProjectCommand;
109
+ const unsetProjectHandler = async (options) => {
110
+ const startTime = Date.now();
111
+ let success = true;
112
+ globalState_1.default.setVerbose(options.verbose);
113
+ try {
114
+ await (0, exports.unsetProjectCommand)();
115
+ }
116
+ catch (e) {
117
+ success = false;
118
+ throw e;
119
+ }
120
+ finally {
121
+ await analytics_1.LightdashAnalytics.track({
122
+ event: 'command.executed',
123
+ properties: {
124
+ command: 'unset-project',
125
+ durationMs: Date.now() - startTime,
126
+ success,
127
+ },
128
+ });
129
+ }
130
+ };
131
+ exports.unsetProjectHandler = unsetProjectHandler;
package/dist/index.js CHANGED
@@ -111,7 +111,9 @@ ${styles.bold('Examples:')}
111
111
  ${styles.title('⚡')}️lightdash ${styles.bold('login')} http://localhost:3000 --email demo@lightdash.com ${styles.secondary('-- Local dev only: prompts for password securely')}
112
112
  `)
113
113
  .option('--token <token>', 'Login with an API access token', undefined)
114
- .option('--project <project uuid>', 'Select a project by UUID after login', parseProjectArgument, undefined)
114
+ .addOption(new commander_1.Option('--project <project uuid>', 'Select a project by UUID after login')
115
+ .argParser(parseProjectArgument)
116
+ .conflicts('skipProjectSelection'))
115
117
  .option('--email <email>', 'Login with email and password', undefined)
116
118
  .option('--oauth-port <port>', 'Port for the local OAuth callback server (default: random available port)', (value) => {
117
119
  const port = parseInt(value, 10);
@@ -143,6 +145,11 @@ configProgram
143
145
  .description('Show the currently selected project')
144
146
  .option('--verbose', undefined, false)
145
147
  .action(getProject_1.getProjectHandler);
148
+ configProgram
149
+ .command('unset-project')
150
+ .description('Clear the currently selected project')
151
+ .option('--verbose', undefined, false)
152
+ .action(setProject_1.unsetProjectHandler);
146
153
  const dbtProgram = commander_1.program.command('dbt').description('Runs dbt commands');
147
154
  dbtProgram
148
155
  .command('run')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.2644.1",
3
+ "version": "0.2646.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,8 +40,8 @@
40
40
  "unique-names-generator": "^4.7.1",
41
41
  "uuid": "^11.0.3",
42
42
  "yaml": "^2.7.0",
43
- "@lightdash/warehouses": "0.2644.1",
44
- "@lightdash/common": "0.2644.1"
43
+ "@lightdash/common": "0.2646.0",
44
+ "@lightdash/warehouses": "0.2646.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/inquirer": "^8.2.1",