@lightdash/cli 0.2659.0 → 0.2661.2

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 +1 @@
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,CAsMd,CAAC;AA4ZF,eAAO,MAAM,aAAa,GACtB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAkJd,CAAC"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"AA4CA,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;AAoVF,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,CAsMd,CAAC;AAgbF,eAAO,MAAM,aAAa,GACtB,SAAS,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAiKd,CAAC"}
@@ -119,15 +119,15 @@ const isLightdashContentFile = (folder, entry) => entry.isFile() &&
119
119
  entry.parentPath.endsWith(path.sep + folder) &&
120
120
  entry.name.endsWith('.yml') &&
121
121
  !entry.name.endsWith('.language.map.yml');
122
- const loadYamlFile = async (file, folder, metadata) => {
123
- const filePath = path.join(file.parentPath, file.name);
124
- const [fileContent, stats] = await Promise.all([
125
- fs_1.promises.readFile(filePath, 'utf-8'),
126
- fs_1.promises.stat(filePath),
127
- ]);
128
- const item = yaml.load(fileContent);
122
+ const isLooseContentFile = (entry) => entry.isFile() &&
123
+ entry.parentPath &&
124
+ !entry.parentPath.endsWith(`${path.sep}charts`) &&
125
+ !entry.parentPath.endsWith(`${path.sep}dashboards`) &&
126
+ entry.name.endsWith('.yml') &&
127
+ !entry.name.endsWith('.language.map.yml');
128
+ const processYamlItem = (item, fileName, stats, folder, metadata) => {
129
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.`));
130
+ globalState_1.default.log(styles.warning(`Warning: ${fileName} has unsorted YAML keys. Re-download to fix, or sort keys alphabetically.`));
131
131
  }
132
132
  const metadataSection = folder === 'dashboards' ? metadata.dashboards : metadata.charts;
133
133
  const downloadedAtRaw = metadataSection[item.slug] ?? item.downloadedAt;
@@ -144,6 +144,15 @@ const loadYamlFile = async (file, folder, metadata) => {
144
144
  needsUpdating: needsUpdating ?? true,
145
145
  };
146
146
  };
147
+ const loadYamlFile = async (file, folder, metadata) => {
148
+ const filePath = path.join(file.parentPath, file.name);
149
+ const [fileContent, stats] = await Promise.all([
150
+ fs_1.promises.readFile(filePath, 'utf-8'),
151
+ fs_1.promises.stat(filePath),
152
+ ]);
153
+ const item = yaml.load(fileContent);
154
+ return processYamlItem(item, file.name, stats, folder, metadata);
155
+ };
147
156
  const readCodeFiles = async (folder, customPath) => {
148
157
  const baseDir = getDownloadFolder(customPath);
149
158
  globalState_1.default.log(`Reading ${folder} from ${baseDir}`);
@@ -176,6 +185,55 @@ const readCodeFiles = async (folder, customPath) => {
176
185
  throw error;
177
186
  }
178
187
  };
188
+ /**
189
+ * Reads YAML files outside the standard charts/ and dashboards/ directories
190
+ * and classifies them by their contentType field.
191
+ */
192
+ const readLooseCodeFiles = async (customPath) => {
193
+ const baseDir = getDownloadFolder(customPath);
194
+ const charts = [];
195
+ const dashboards = [];
196
+ try {
197
+ const metadata = await (0, metadataFile_1.readMetadataFile)(baseDir);
198
+ const allEntries = await fs_1.promises.readdir(baseDir, {
199
+ recursive: true,
200
+ withFileTypes: true,
201
+ });
202
+ const looseFiles = allEntries.filter(isLooseContentFile);
203
+ await Promise.all(looseFiles.map(async (file) => {
204
+ try {
205
+ const filePath = path.join(file.parentPath, file.name);
206
+ const [fileContent, stats] = await Promise.all([
207
+ fs_1.promises.readFile(filePath, 'utf-8'),
208
+ fs_1.promises.stat(filePath),
209
+ ]);
210
+ const parsed = yaml.load(fileContent);
211
+ const contentType = parsed?.contentType;
212
+ if (contentType === common_1.ContentAsCodeType.CHART ||
213
+ contentType === common_1.ContentAsCodeType.SQL_CHART) {
214
+ charts.push(processYamlItem(parsed, file.name, stats, 'charts', metadata));
215
+ }
216
+ else if (contentType === common_1.ContentAsCodeType.DASHBOARD) {
217
+ dashboards.push(processYamlItem(parsed, file.name, stats, 'dashboards', metadata));
218
+ }
219
+ else {
220
+ globalState_1.default.debug(`Skipping ${file.name}: no recognized contentType`);
221
+ }
222
+ }
223
+ catch (e) {
224
+ globalState_1.default.debug(`Skipping ${file.name}: failed to parse (${(0, common_1.getErrorMessage)(e)})`);
225
+ }
226
+ }));
227
+ }
228
+ catch (error) {
229
+ if (error.code === 'ENOENT') {
230
+ // Base directory doesn't exist — nothing to discover
231
+ return { charts, dashboards };
232
+ }
233
+ throw error;
234
+ }
235
+ return { charts, dashboards };
236
+ };
179
237
  const groupBySpace = (items) => {
180
238
  const itemsWithIndex = items.map((item, index) => ({ item, index }));
181
239
  return (0, groupBy_1.default)(itemsWithIndex, (entry) => entry.item.spaceSlug);
@@ -521,6 +579,16 @@ const upsertSingleItem = async (item, type, projectId, changes, force, config, s
521
579
  Object.keys(updatedChanges).forEach((key) => {
522
580
  changes[key] = updatedChanges[key];
523
581
  });
582
+ // Warn if contentType contradicts the folder this item came from
583
+ const itemContentType = item.contentType;
584
+ if (itemContentType) {
585
+ const expectedType = itemContentType === common_1.ContentAsCodeType.DASHBOARD
586
+ ? 'dashboards'
587
+ : 'charts';
588
+ if (expectedType !== type) {
589
+ globalState_1.default.log(styles.warning(`Warning: "${item.name}" has contentType "${itemContentType}" but is in the ${type}/ directory. It will be uploaded as a ${type.slice(0, -1)}.`));
590
+ }
591
+ }
524
592
  // Run validation if requested
525
593
  if (validate && !isSqlChartItem) {
526
594
  const contentUuid = type === 'charts'
@@ -581,9 +649,10 @@ const upsertSingleItem = async (item, type, projectId, changes, force, config, s
581
649
  *
582
650
  * @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
583
651
  */
584
- const upsertResources = async (type, projectId, changes, force, slugs, customPath, skipSpaceCreate, publicSpaceCreate, validate, concurrency = 1) => {
652
+ const upsertResources = async (type, projectId, changes, force, slugs, customPath, skipSpaceCreate, publicSpaceCreate, validate, concurrency = 1, extraItems = []) => {
585
653
  const config = await (0, config_1.getConfig)();
586
- const items = await readCodeFiles(type, customPath);
654
+ const folderItems = await readCodeFiles(type, customPath);
655
+ const items = [...folderItems, ...extraItems];
587
656
  globalState_1.default.log(`Found ${items.length} ${type} files`);
588
657
  const hasFilter = slugs.length > 0;
589
658
  const filteredItems = hasFilter
@@ -724,11 +793,19 @@ const uploadHandler = async (options) => {
724
793
  if (parseInt(String(options.concurrency), 10) > 1000) {
725
794
  globalState_1.default.log(styles.warning(`Concurrency limit exceeded. Using maximum of 1000 instead of ${options.concurrency}`));
726
795
  }
796
+ // Discover loose YAML files (outside charts/ and dashboards/) classified by contentType
797
+ const looseFiles = await readLooseCodeFiles(options.path);
798
+ if (looseFiles.charts.length > 0) {
799
+ globalState_1.default.log(`Found ${looseFiles.charts.length} chart(s) outside charts/ directory (classified by contentType)`);
800
+ }
801
+ if (looseFiles.dashboards.length > 0) {
802
+ globalState_1.default.log(`Found ${looseFiles.dashboards.length} dashboard(s) outside dashboards/ directory (classified by contentType)`);
803
+ }
727
804
  if (hasFilters && chartSlugs.length === 0) {
728
805
  globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
729
806
  }
730
807
  else {
731
- const { changes: chartChanges, total } = await upsertResources('charts', projectId, changes, options.force, chartSlugs, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency);
808
+ const { changes: chartChanges, total } = await upsertResources('charts', projectId, changes, options.force, chartSlugs, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.charts);
732
809
  changes = chartChanges;
733
810
  chartTotal = total;
734
811
  }
@@ -736,7 +813,7 @@ const uploadHandler = async (options) => {
736
813
  globalState_1.default.log(styles.warning(`No dashboard filters provided, skipping`));
737
814
  }
738
815
  else {
739
- const { changes: dashboardChanges, total } = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency);
816
+ const { changes: dashboardChanges, total } = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.dashboards);
740
817
  changes = dashboardChanges;
741
818
  dashboardTotal = total;
742
819
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.2659.0",
3
+ "version": "0.2661.2",
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/common": "0.2659.0",
44
- "@lightdash/warehouses": "0.2659.0"
43
+ "@lightdash/common": "0.2661.2",
44
+ "@lightdash/warehouses": "0.2661.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/inquirer": "^8.2.1",