@lightdash/cli 1.260.1 → 1.261.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.
@@ -33,7 +33,7 @@ const selectProject_1 = require("./selectProject");
33
33
  const spacesAsCode_1 = require("./spacesAsCode");
34
34
  const shouldDownloadAiAgents = ({ includeAll, includeAgents, agents, appsOnly, }) => appsOnly !== true &&
35
35
  (includeAll === true || includeAgents === true || agents.length > 0);
36
- const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, }) => !spacesOnly &&
36
+ const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, googleSheets, scheduledDeliveries, virtualViews, externalConnections, apps, chartTypes, }) => !spacesOnly &&
37
37
  [
38
38
  charts,
39
39
  dashboards,
@@ -44,6 +44,7 @@ const hasContentFilters = ({ spacesOnly, charts, dashboards, agents, alerts, goo
44
44
  virtualViews,
45
45
  externalConnections,
46
46
  apps ?? [],
47
+ chartTypes ?? [],
47
48
  ].some((filters) => filters.length > 0);
48
49
  /*
49
50
  This function is used to parse the content filters.
@@ -937,6 +938,9 @@ const listAppsViaContentApi = async (projectId) => {
937
938
  const downloadHandler = async (options) => {
938
939
  globalState_1.default.setVerbose(options.verbose);
939
940
  const isOrganizationDownload = options.organization === true;
941
+ if (options.appsOnly && options.chartTypesOnly) {
942
+ throw new common_1.ParameterError('--apps-only cannot be combined with --chart-types-only.');
943
+ }
940
944
  // Bare --apps-only means "all apps": imply --include-apps.
941
945
  if (options.appsOnly &&
942
946
  options.apps === undefined &&
@@ -944,13 +948,36 @@ const downloadHandler = async (options) => {
944
948
  options.includeAll !== true) {
945
949
  options.includeApps = true;
946
950
  }
951
+ // Bare --chart-types-only means "all chart types" likewise.
952
+ if (options.chartTypesOnly &&
953
+ options.chartTypes === undefined &&
954
+ options.includeChartTypes !== true &&
955
+ options.includeAll !== true) {
956
+ options.includeChartTypes = true;
957
+ }
947
958
  const includeAll = options.includeAll === true;
948
- const includeApps = !options.spacesOnly && (options.includeApps === true || includeAll);
949
- const includeAllOptionalContent = includeAll && !options.appsOnly && !options.spacesOnly;
959
+ const includeApps = !options.spacesOnly &&
960
+ !options.chartTypesOnly &&
961
+ (options.includeApps === true || includeAll);
962
+ const includeChartTypes = !options.spacesOnly &&
963
+ !options.appsOnly &&
964
+ (options.includeChartTypes === true || includeAll);
965
+ const includeAllOptionalContent = includeAll &&
966
+ !options.appsOnly &&
967
+ !options.chartTypesOnly &&
968
+ !options.spacesOnly;
950
969
  const { limit: appsLimit, noEffectWarning: appsLimitWarning } = (0, appsDownload_1.resolveAppsLimit)(options.appsLimit, includeApps);
951
970
  if (appsLimitWarning) {
952
971
  globalState_1.default.log(styles.warning(appsLimitWarning));
953
972
  }
973
+ const { limit: chartTypesLimit, noEffectWarning: chartTypesLimitWarning } = (0, appsDownload_1.resolveAppsLimit)(options.chartTypesLimit, includeChartTypes, {
974
+ limitFlag: '--chart-types-limit',
975
+ includeFlag: '--include-chart-types',
976
+ refsFlag: '--chart-types',
977
+ });
978
+ if (chartTypesLimitWarning) {
979
+ globalState_1.default.log(styles.warning(chartTypesLimitWarning));
980
+ }
954
981
  if (options.appsOnly) {
955
982
  const appsOnlySelection = (0, appsDownload_1.selectAppsToDownload)({
956
983
  apps: Array.isArray(options.apps) ? options.apps : undefined,
@@ -959,6 +986,28 @@ const downloadHandler = async (options) => {
959
986
  if (appsOnlySelection.mode === 'none') {
960
987
  throw new common_1.ParameterError('Nothing to download: --apps-only requires --apps <appReferences...>, --include-apps, or --include-all.');
961
988
  }
989
+ options.chartTypes = undefined;
990
+ options.skipCharts = true;
991
+ options.skipDashboards = true;
992
+ options.skipSpaces = true;
993
+ options.includeAgents = false;
994
+ options.includeAlerts = false;
995
+ options.includeGoogleSheets = false;
996
+ options.includeScheduledDeliveries = false;
997
+ options.includeVirtualViews = false;
998
+ options.includeExternalConnections = false;
999
+ }
1000
+ if (options.chartTypesOnly) {
1001
+ const chartTypesOnlySelection = (0, appsDownload_1.selectAppsToDownload)({
1002
+ apps: Array.isArray(options.chartTypes)
1003
+ ? options.chartTypes
1004
+ : undefined,
1005
+ includeApps: includeChartTypes,
1006
+ });
1007
+ if (chartTypesOnlySelection.mode === 'none') {
1008
+ throw new common_1.ParameterError('Nothing to download: --chart-types-only requires --chart-types <chartTypeReferences...>, --include-chart-types, or --include-all.');
1009
+ }
1010
+ options.apps = undefined;
962
1011
  options.skipCharts = true;
963
1012
  options.skipDashboards = true;
964
1013
  options.skipSpaces = true;
@@ -978,6 +1027,7 @@ const downloadHandler = async (options) => {
978
1027
  options.agents = [];
979
1028
  options.alerts = [];
980
1029
  options.apps = [];
1030
+ options.chartTypes = [];
981
1031
  options.googleSheets = [];
982
1032
  options.scheduledDeliveries = [];
983
1033
  options.virtualViews = [];
@@ -1054,6 +1104,8 @@ const downloadHandler = async (options) => {
1054
1104
  // Shared across both apps-download steps so two different apps whose
1055
1105
  // names collide under the pre-slug fallback naming don't clobber each other.
1056
1106
  const downloadedAppFolders = new Set();
1107
+ // Chart types live in their own folder, so they track their own names.
1108
+ const downloadedChartTypeFolders = new Set();
1057
1109
  // App slugs referenced by downloaded dashboards' tiles, populated by
1058
1110
  // the Dashboards step and consumed by the Linked data apps step.
1059
1111
  let dashboardAppSlugs = [];
@@ -1314,7 +1366,7 @@ const downloadHandler = async (options) => {
1314
1366
  output.updateActive(`0 of ${appRefsToDownload.length} downloaded`);
1315
1367
  const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
1316
1368
  const appsDir = path.join(baseDir, 'apps');
1317
- const { successCount, skippedNotBuiltCount, failures } = await (0, appsDownload_1.downloadAppsToDir)({
1369
+ const { successCount, skippedNotBuiltCount, skippedWrongKindCount, failures, } = await (0, appsDownload_1.downloadAppsToDir)({
1318
1370
  appRefs: appRefsToDownload,
1319
1371
  projectId,
1320
1372
  appsDir,
@@ -1325,18 +1377,120 @@ const downloadHandler = async (options) => {
1325
1377
  url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
1326
1378
  body: undefined,
1327
1379
  }),
1380
+ skipBundle: (manifest) => manifest.template === common_1.DATA_APP_VIZ_TEMPLATE
1381
+ ? 'this is a custom chart type — download it with --chart-types or --include-chart-types'
1382
+ : null,
1328
1383
  onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
1329
1384
  });
1330
- const summary = (0, appsDownload_1.appsDownloadSummary)(successCount, appRefsToDownload.length, failures, appsDir, skippedNotBuiltCount);
1385
+ const summary = (0, appsDownload_1.appsDownloadSummary)(successCount, appRefsToDownload.length, failures, appsDir, skippedNotBuiltCount + skippedWrongKindCount);
1331
1386
  counts.appsNum = successCount;
1332
- output.completeItem(`${successCount} downloaded${skippedNotBuiltCount > 0
1333
- ? `, ${skippedNotBuiltCount} skipped`
1387
+ output.completeItem(`${successCount} downloaded${skippedNotBuiltCount + skippedWrongKindCount > 0
1388
+ ? `, ${skippedNotBuiltCount + skippedWrongKindCount} skipped`
1334
1389
  : ''}${failures.length > 0 ? `, ${failures.length} failed` : ''}`, summary.ok ? undefined : 'warning');
1335
1390
  if (!summary.ok) {
1336
1391
  summary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
1337
1392
  }
1338
1393
  }
1339
1394
  }
1395
+ // Download custom chart types (enterprise, opt-in via --chart-types /
1396
+ // --include-chart-types / --include-all) into chart-types/, kept
1397
+ // separate from data apps.
1398
+ const chartTypesSelection = (0, appsDownload_1.selectAppsToDownload)({
1399
+ apps: Array.isArray(options.chartTypes)
1400
+ ? options.chartTypes
1401
+ : undefined,
1402
+ includeApps: includeChartTypes,
1403
+ });
1404
+ if (chartTypesSelection.mode !== 'none') {
1405
+ output.startItem('Custom chart types');
1406
+ let chartTypeRefsToDownload;
1407
+ let chartTypeListingError = null;
1408
+ if (chartTypesSelection.mode === 'explicit') {
1409
+ chartTypeRefsToDownload = chartTypesSelection.appRefs;
1410
+ }
1411
+ else {
1412
+ output.updateActive('listing project chart types…');
1413
+ let listedChartTypes = [];
1414
+ try {
1415
+ const projectChartTypes = await (0, apiClient_1.lightdashApi)({
1416
+ method: 'GET',
1417
+ url: `/api/v1/ee/projects/${projectId}/apps/chart-types`,
1418
+ body: undefined,
1419
+ });
1420
+ listedChartTypes = projectChartTypes.map((chartType) => ({
1421
+ appUuid: chartType.appUuid,
1422
+ slug: chartType.slug,
1423
+ }));
1424
+ }
1425
+ catch (listErr) {
1426
+ if ((0, appsDownload_1.shouldFallBackToSpaceScopedListing)(listErr)) {
1427
+ // 404: the server predates the chart-types listing
1428
+ // (or chart types entirely) — nothing to list.
1429
+ globalState_1.default.log(styles.warning('This server does not support listing custom chart types; pass explicit --chart-types references or upgrade the server.'));
1430
+ listedChartTypes = [];
1431
+ }
1432
+ else if (includeAllOptionalContent) {
1433
+ chartTypeListingError = (0, common_1.getErrorMessage)(listErr);
1434
+ listedChartTypes = [];
1435
+ }
1436
+ else {
1437
+ throw listErr;
1438
+ }
1439
+ }
1440
+ const { appUuids: cappedChartTypeUuids, truncatedCount: chartTypesTruncated, } = (0, appsDownload_1.capListedApps)(listedChartTypes.map((chartType) => chartType.appUuid), chartTypesLimit);
1441
+ if (chartTypesTruncated > 0) {
1442
+ globalState_1.default.log(styles.warning(`Found ${listedChartTypes.length} custom chart types, downloading the first ${chartTypesLimit}. Pass --chart-types-limit <n> to raise the cap.`));
1443
+ }
1444
+ chartTypeRefsToDownload = [
1445
+ ...new Set([
1446
+ ...cappedChartTypeUuids,
1447
+ ...chartTypesSelection.extraAppRefs,
1448
+ ]),
1449
+ ];
1450
+ }
1451
+ if (chartTypeRefsToDownload.length === 0) {
1452
+ counts.chartTypesNum = 0;
1453
+ if (chartTypeListingError === null) {
1454
+ output.completeItem('0 found');
1455
+ }
1456
+ else {
1457
+ output.completeItem(`listing failed: ${chartTypeListingError}`, 'warning');
1458
+ }
1459
+ }
1460
+ else {
1461
+ output.updateActive(`0 of ${chartTypeRefsToDownload.length} downloaded`);
1462
+ const chartTypesDir = path.join((0, contentAsCodePaths_1.getDownloadFolder)(options.path), 'chart-types');
1463
+ const chartTypesOutcome = await (0, appsDownload_1.downloadAppsToDir)({
1464
+ appRefs: chartTypeRefsToDownload,
1465
+ projectId,
1466
+ appsDir: chartTypesDir,
1467
+ takenFolders: downloadedChartTypeFolders,
1468
+ cliVersion: env_1.CLI_VERSION,
1469
+ fetchApp: (fetchProjectId, appRef) => (0, apiClient_1.lightdashApi)({
1470
+ method: 'GET',
1471
+ url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
1472
+ body: undefined,
1473
+ }),
1474
+ skipBundle: (manifest) => manifest.template !== common_1.DATA_APP_VIZ_TEMPLATE
1475
+ ? 'this is a data app — download it with --apps or --include-apps'
1476
+ : null,
1477
+ onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
1478
+ });
1479
+ const chartTypesSummary = (0, appsDownload_1.appsDownloadSummary)(chartTypesOutcome.successCount, chartTypeRefsToDownload.length, chartTypesOutcome.failures, chartTypesDir, chartTypesOutcome.skippedNotBuiltCount +
1480
+ chartTypesOutcome.skippedWrongKindCount, 'custom chart type');
1481
+ counts.chartTypesNum = chartTypesOutcome.successCount;
1482
+ const chartTypesSkipped = chartTypesOutcome.skippedNotBuiltCount +
1483
+ chartTypesOutcome.skippedWrongKindCount;
1484
+ output.completeItem(`${chartTypesOutcome.successCount} downloaded${chartTypesSkipped > 0
1485
+ ? `, ${chartTypesSkipped} skipped`
1486
+ : ''}${chartTypesOutcome.failures.length > 0
1487
+ ? `, ${chartTypesOutcome.failures.length} failed`
1488
+ : ''}`, chartTypesSummary.ok ? undefined : 'warning');
1489
+ if (!chartTypesSummary.ok) {
1490
+ chartTypesSummary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
1491
+ }
1492
+ }
1493
+ }
1340
1494
  // Dashboard-referenced apps not already covered above (explicit
1341
1495
  // --apps ref, or a non-truncated slot in the --include-apps cap).
1342
1496
  const linkedAppSlugs = (0, appsDownload_1.computeLinkedAppSlugs)({
@@ -1359,13 +1513,16 @@ const downloadHandler = async (options) => {
1359
1513
  url: `/api/v1/ee/projects/${fetchProjectId}/apps/${encodeURIComponent(appRef)}/download`,
1360
1514
  body: undefined,
1361
1515
  }),
1516
+ // Dashboard data-app tiles reference apps, never chart types.
1517
+ skipBundle: (manifest) => manifest.template === common_1.DATA_APP_VIZ_TEMPLATE
1518
+ ? 'this is a custom chart type — dashboard app tiles cannot reference it'
1519
+ : null,
1362
1520
  onProgress: (processed, total) => output.updateActive(`${processed} of ${total} processed`),
1363
1521
  });
1364
- const linkedSummary = (0, appsDownload_1.appsDownloadSummary)(outcome.successCount, linkedAppSlugs.length, outcome.failures, appsDir, outcome.skippedNotBuiltCount);
1522
+ const linkedSkipped = outcome.skippedNotBuiltCount + outcome.skippedWrongKindCount;
1523
+ const linkedSummary = (0, appsDownload_1.appsDownloadSummary)(outcome.successCount, linkedAppSlugs.length, outcome.failures, appsDir, linkedSkipped);
1365
1524
  counts.appsNum = (counts.appsNum ?? 0) + outcome.successCount;
1366
- output.completeItem(`${outcome.successCount} downloaded${outcome.skippedNotBuiltCount > 0
1367
- ? `, ${outcome.skippedNotBuiltCount} skipped`
1368
- : ''}${outcome.failures.length > 0
1525
+ output.completeItem(`${outcome.successCount} downloaded${linkedSkipped > 0 ? `, ${linkedSkipped} skipped` : ''}${outcome.failures.length > 0
1369
1526
  ? `, ${outcome.failures.length} failed`
1370
1527
  : ''}`, linkedSummary.ok ? undefined : 'warning');
1371
1528
  if (!linkedSummary.ok) {
@@ -1720,20 +1877,47 @@ const uploadHandler = async (options) => {
1720
1877
  if (options.appsOnly && options.spacesOnly) {
1721
1878
  throw new common_1.ParameterError('--apps-only cannot be combined with --spaces-only.');
1722
1879
  }
1880
+ if (options.appsOnly && options.chartTypesOnly) {
1881
+ throw new common_1.ParameterError('--apps-only cannot be combined with --chart-types-only.');
1882
+ }
1883
+ if (options.chartTypesOnly && options.spacesOnly) {
1884
+ throw new common_1.ParameterError('--chart-types-only cannot be combined with --spaces-only.');
1885
+ }
1723
1886
  if (options.appsOnly &&
1724
1887
  (options.charts.length > 0 || options.dashboards.length > 0)) {
1725
1888
  throw new common_1.ParameterError('--apps-only cannot be combined with --charts or --dashboards.');
1726
1889
  }
1890
+ if (options.chartTypesOnly &&
1891
+ (options.charts.length > 0 || options.dashboards.length > 0)) {
1892
+ throw new common_1.ParameterError('--chart-types-only cannot be combined with --charts or --dashboards.');
1893
+ }
1727
1894
  // Bare --apps-only means "all apps": imply --include-apps.
1728
1895
  if (options.appsOnly &&
1729
1896
  options.apps === undefined &&
1730
1897
  options.includeApps !== true) {
1731
1898
  options.includeApps = true;
1732
1899
  }
1900
+ if (options.appsOnly) {
1901
+ options.chartTypes = undefined;
1902
+ options.includeChartTypes = false;
1903
+ }
1904
+ // Bare --chart-types-only means "all chart types" likewise.
1905
+ if (options.chartTypesOnly &&
1906
+ options.chartTypes === undefined &&
1907
+ options.includeChartTypes !== true) {
1908
+ options.includeChartTypes = true;
1909
+ }
1910
+ if (options.chartTypesOnly) {
1911
+ options.apps = undefined;
1912
+ options.includeApps = false;
1913
+ }
1733
1914
  const isOrganizationUpload = options.organization === true;
1734
- // --apps-only rides the existing filter machinery: every non-app phase
1735
- // skips exactly as it does when only app refs are passed.
1736
- const hasFilters = hasContentFilters(options) || options.appsOnly === true;
1915
+ // --apps-only / --chart-types-only ride the existing filter machinery:
1916
+ // every other phase skips exactly as it does when only those refs are
1917
+ // passed.
1918
+ const hasFilters = hasContentFilters(options) ||
1919
+ options.appsOnly === true ||
1920
+ options.chartTypesOnly === true;
1737
1921
  const shouldReconcileSpaces = !isOrganizationUpload && !options.skipSpaces && !hasFilters;
1738
1922
  let preflightSpaceFiles = [];
1739
1923
  if (shouldReconcileSpaces) {
@@ -1915,8 +2099,10 @@ const uploadHandler = async (options) => {
1915
2099
  });
1916
2100
  }
1917
2101
  }
1918
- // Upload data apps (enterprise; explicit --apps/--include-apps, or
1919
- // auto-pushed for a dashboard's apps). Must land before dashboards.
2102
+ // Upload data apps and custom chart types (enterprise). Data apps:
2103
+ // explicit --apps/--include-apps, or auto-pushed for a dashboard's
2104
+ // apps; must land before dashboards. Chart types: explicit
2105
+ // --chart-types/--include-chart-types, from their own folder.
1920
2106
  const explicitAppReferences = Array.isArray(options.apps)
1921
2107
  ? options.apps
1922
2108
  : [];
@@ -1924,26 +2110,70 @@ const uploadHandler = async (options) => {
1924
2110
  const autoPushAppSlugs = isFilteredWithNoDashboards(hasFilters, options.dashboards)
1925
2111
  ? []
1926
2112
  : selectDashboardAppSlugs(await loadDashboardItems(), options.dashboards);
1927
- const shouldUploadApps = isExplicitAppSelection || autoPushAppSlugs.length > 0;
1928
- let appsCreated = 0;
1929
- let appsUpdated = 0;
1930
- let appsUnchanged = 0;
1931
- let appsFailed = 0;
1932
- let appsSkipped = 0;
1933
- let eeAppRoutesUnavailable = false;
1934
- const changesBeforeApps = { ...changes };
1935
- if (shouldUploadApps && !uploadPermissions.dataApps) {
1936
- counts.appsNum = 0;
1937
- output.startItem('Data apps');
1938
- globalState_1.default.log(styles.warning(`Skipping data apps: create:DataApp or manage:DataApp permission is required for this project (the create:DataApp@preview and manage:DataApp@preview scopes only cover preview projects you created). Dashboard tiles will resolve only if their apps already exist in this project.`));
1939
- output.completeItem('permission denied', 'warning');
1940
- }
1941
- else if (shouldUploadApps) {
1942
- output.startItem('Data apps');
1943
- // Explicit refs filter by slug/appUuid; --include-apps uploads all.
1944
- // A pure auto-push run applies no filter — gated per folder below.
1945
- let uploadFilter = isExplicitAppSelection
1946
- ? (0, appsDownload_1.getDataAppUploadFilter)(explicitAppReferences, options.includeApps === true)
2113
+ const explicitChartTypeReferences = Array.isArray(options.chartTypes)
2114
+ ? options.chartTypes
2115
+ : [];
2116
+ const isExplicitChartTypeSelection = options.includeChartTypes === true ||
2117
+ explicitChartTypeReferences.length > 0;
2118
+ const appsPhaseActive = isExplicitAppSelection || autoPushAppSlugs.length > 0;
2119
+ const bundleUploadPhases = [
2120
+ {
2121
+ label: 'Data apps',
2122
+ noun: 'data app',
2123
+ changesPrefix: 'data apps',
2124
+ dirName: 'apps',
2125
+ explicitRefs: explicitAppReferences,
2126
+ includeAllFolders: options.includeApps === true,
2127
+ isExplicitSelection: isExplicitAppSelection,
2128
+ autoPushSlugs: autoPushAppSlugs,
2129
+ useAppSpace: true,
2130
+ isChartTypes: false,
2131
+ },
2132
+ {
2133
+ label: 'Custom chart types',
2134
+ noun: 'custom chart type',
2135
+ changesPrefix: 'chart types',
2136
+ dirName: 'chart-types',
2137
+ explicitRefs: explicitChartTypeReferences,
2138
+ includeAllFolders: options.includeChartTypes === true,
2139
+ isExplicitSelection: isExplicitChartTypeSelection,
2140
+ autoPushSlugs: [],
2141
+ useAppSpace: false,
2142
+ isChartTypes: true,
2143
+ },
2144
+ ];
2145
+ for (const phase of bundleUploadPhases) {
2146
+ const shouldUploadPhase = phase.isExplicitSelection || phase.autoPushSlugs.length > 0;
2147
+ if (!shouldUploadPhase) {
2148
+ // eslint-disable-next-line no-continue
2149
+ continue;
2150
+ }
2151
+ let appsCreated = 0;
2152
+ let appsUpdated = 0;
2153
+ let appsUnchanged = 0;
2154
+ let appsFailed = 0;
2155
+ let appsSkipped = 0;
2156
+ let eeAppRoutesUnavailable = false;
2157
+ const changesBeforeApps = { ...changes };
2158
+ if (!uploadPermissions.dataApps) {
2159
+ if (phase.isChartTypes) {
2160
+ counts.chartTypesNum = 0;
2161
+ }
2162
+ else {
2163
+ counts.appsNum = 0;
2164
+ }
2165
+ output.startItem(phase.label);
2166
+ globalState_1.default.log(styles.warning(`Skipping ${phase.changesPrefix}: create:DataApp or manage:DataApp permission is required for this project (the create:DataApp@preview and manage:DataApp@preview scopes only cover preview projects you created). Dashboard tiles will resolve only if their apps already exist in this project.`));
2167
+ output.completeItem('permission denied', 'warning');
2168
+ // eslint-disable-next-line no-continue
2169
+ continue;
2170
+ }
2171
+ output.startItem(phase.label);
2172
+ // Explicit refs filter by slug/appUuid; include-all uploads every
2173
+ // folder. A pure auto-push run applies no filter — gated per
2174
+ // folder below.
2175
+ let uploadFilter = phase.isExplicitSelection
2176
+ ? (0, appsDownload_1.getDataAppUploadFilter)(phase.explicitRefs, phase.includeAllFolders)
1947
2177
  : null;
1948
2178
  // uuid/URL --apps refs resolve to slugs against the target
1949
2179
  // project's listing so they can match slug-identity local folders.
@@ -1952,7 +2182,9 @@ const uploadHandler = async (options) => {
1952
2182
  try {
1953
2183
  const projectApps = await (0, apiClient_1.lightdashApi)({
1954
2184
  method: 'GET',
1955
- url: `/api/v1/ee/projects/${projectId}/apps`,
2185
+ // Each kind resolves against its own listing — the
2186
+ // apps listing never includes chart types.
2187
+ url: `/api/v1/ee/projects/${projectId}/apps${phase.isChartTypes ? '/chart-types' : ''}`,
1956
2188
  body: undefined,
1957
2189
  });
1958
2190
  uploadFilter = (0, appsDownload_1.resolveUploadFilterUuids)(uploadFilter, projectApps);
@@ -1962,9 +2194,10 @@ const uploadHandler = async (options) => {
1962
2194
  }
1963
2195
  }
1964
2196
  // The server applies the space on creates only; existing apps
1965
- // keep their space.
2197
+ // keep their space. Chart types are spaceless, so --app-space
2198
+ // never applies to them.
1966
2199
  let appSpaceUuid;
1967
- if (options.appSpace !== undefined) {
2200
+ if (phase.useAppSpace && options.appSpace !== undefined) {
1968
2201
  if ((0, uuid_1.validate)(options.appSpace)) {
1969
2202
  appSpaceUuid = options.appSpace;
1970
2203
  }
@@ -1977,8 +2210,13 @@ const uploadHandler = async (options) => {
1977
2210
  appSpaceUuid = (0, appsDownload_1.resolveAppSpaceUuid)(options.appSpace, spaces);
1978
2211
  }
1979
2212
  }
2213
+ else if (phase.isChartTypes &&
2214
+ options.appSpace !== undefined &&
2215
+ !appsPhaseActive) {
2216
+ globalState_1.default.log(styles.warning('--app-space does not apply to custom chart types — they are project-global and spaceless.'));
2217
+ }
1980
2218
  const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
1981
- const appsDir = path.join(baseDir, 'apps');
2219
+ const appsDir = path.join(baseDir, phase.dirName);
1982
2220
  let appFolderEntries;
1983
2221
  try {
1984
2222
  appFolderEntries = await fs_1.promises.readdir(appsDir, {
@@ -1995,9 +2233,14 @@ const uploadHandler = async (options) => {
1995
2233
  }
1996
2234
  const subDirs = appFolderEntries.filter((e) => e.isDirectory());
1997
2235
  if (subDirs.length === 0) {
1998
- globalState_1.default.log(styles.warning(isExplicitAppSelection
1999
- ? `No app folders found in ${appsDir}. Run 'lightdash download --include-apps' first.`
2000
- : `No app folders found in ${appsDir} for the dashboard(s) being uploaded. Re-run 'lightdash download' to fetch their apps.`));
2236
+ if (phase.isChartTypes) {
2237
+ globalState_1.default.log(styles.warning(`No chart type folders found in ${appsDir}. Run 'lightdash download --include-chart-types' first.`));
2238
+ }
2239
+ else {
2240
+ globalState_1.default.log(styles.warning(phase.isExplicitSelection
2241
+ ? `No app folders found in ${appsDir}. Run 'lightdash download --include-apps' first.`
2242
+ : `No app folders found in ${appsDir} for the dashboard(s) being uploaded. Re-run 'lightdash download' to fetch their apps.`));
2243
+ }
2001
2244
  }
2002
2245
  const matchedRefs = new Set();
2003
2246
  const buildWaitState = (0, uploadRetry_1.createBuildLimitWaitState)();
@@ -2008,12 +2251,12 @@ const uploadHandler = async (options) => {
2008
2251
  const code = await (0, appCodeFiles_1.readBundleFromDir)(folderPath);
2009
2252
  if (!(0, appsDownload_1.uploadFilterMatches)(uploadFilter, code.manifest)) {
2010
2253
  const isAutoPushCandidate = code.manifest.slug !== undefined &&
2011
- autoPushAppSlugs.includes(code.manifest.slug);
2254
+ phase.autoPushSlugs.includes(code.manifest.slug);
2012
2255
  if (isAutoPushCandidate) {
2013
2256
  globalState_1.default.log(styles.warning(`Skipping app "${subDir.name}" — excluded by --apps, but a dashboard being uploaded references it. Its tile may fail to resolve unless the app already exists in the target project.`));
2014
2257
  }
2015
2258
  else {
2016
- globalState_1.default.debug(`Skipping app folder "${subDir.name}" (not in filter)`);
2259
+ globalState_1.default.debug(`Skipping ${phase.noun} folder "${subDir.name}" (not in filter)`);
2017
2260
  }
2018
2261
  // eslint-disable-next-line no-continue
2019
2262
  continue;
@@ -2021,9 +2264,22 @@ const uploadHandler = async (options) => {
2021
2264
  if (uploadFilter) {
2022
2265
  (0, appsDownload_1.matchedUploadRefs)(uploadFilter, code.manifest).forEach((ref) => matchedRefs.add(ref));
2023
2266
  }
2024
- if (!isExplicitAppSelection) {
2267
+ // Folder-kind guard: the manifest governs what the server
2268
+ // creates, so a bundle filed under the wrong folder would
2269
+ // silently upload as the wrong kind. Skip and say where
2270
+ // it belongs instead.
2271
+ const isVizBundle = code.manifest.template === common_1.DATA_APP_VIZ_TEMPLATE;
2272
+ if (isVizBundle !== phase.isChartTypes) {
2273
+ globalState_1.default.log(styles.warning(isVizBundle
2274
+ ? `Skipping "${subDir.name}": it is a custom chart type — move the folder to chart-types/ and upload with --chart-types or --include-chart-types.`
2275
+ : `Skipping "${subDir.name}": it is a data app — move the folder to apps/ and upload with --apps or --include-apps.`));
2276
+ appsSkipped += 1;
2277
+ // eslint-disable-next-line no-continue
2278
+ continue;
2279
+ }
2280
+ if (!phase.isExplicitSelection) {
2025
2281
  const isAutoPushCandidate = code.manifest.slug !== undefined &&
2026
- autoPushAppSlugs.includes(code.manifest.slug);
2282
+ phase.autoPushSlugs.includes(code.manifest.slug);
2027
2283
  if (!isAutoPushCandidate) {
2028
2284
  // eslint-disable-next-line no-continue
2029
2285
  continue;
@@ -2156,7 +2412,9 @@ const uploadHandler = async (options) => {
2156
2412
  globalState_1.default.log(styles.warning((0, appsDownload_1.preSlugServerHint)(subDir.name)));
2157
2413
  }
2158
2414
  if (action === 'create') {
2159
- globalState_1.default.log(`New app: ${config.context.serverUrl}/projects/${projectId}/apps/${appUuid}`);
2415
+ globalState_1.default.log(phase.isChartTypes
2416
+ ? `New chart type: ${config.context.serverUrl}/projects/${projectId}/chart-types/${appUuid}`
2417
+ : `New app: ${config.context.serverUrl}/projects/${projectId}/apps/${appUuid}`);
2160
2418
  }
2161
2419
  }
2162
2420
  catch (appErr) {
@@ -2165,7 +2423,7 @@ const uploadHandler = async (options) => {
2165
2423
  : undefined;
2166
2424
  // Auto-push is flag-free, so a server without the EE app
2167
2425
  // routes must not fail an upload the user never asked for.
2168
- if (!isExplicitAppSelection && status === 404) {
2426
+ if (!phase.isExplicitSelection && status === 404) {
2169
2427
  eeAppRoutesUnavailable = true;
2170
2428
  globalState_1.default.log(styles.warning(`Skipping data apps: the enterprise "data apps" feature is not available on this instance. Dashboard tiles will resolve only if their apps already exist in this project.`));
2171
2429
  break;
@@ -2190,21 +2448,26 @@ const uploadHandler = async (options) => {
2190
2448
  }
2191
2449
  }
2192
2450
  if (appsCreated > 0)
2193
- changes['data apps created'] = appsCreated;
2451
+ changes[`${phase.changesPrefix} created`] = appsCreated;
2194
2452
  if (appsUpdated > 0)
2195
- changes['data apps updated'] = appsUpdated;
2453
+ changes[`${phase.changesPrefix} updated`] = appsUpdated;
2196
2454
  if (appsUnchanged > 0)
2197
- changes['data apps unchanged'] = appsUnchanged;
2455
+ changes[`${phase.changesPrefix} unchanged`] = appsUnchanged;
2198
2456
  if (appsFailed > 0)
2199
- changes['data apps failed'] = appsFailed;
2457
+ changes[`${phase.changesPrefix} failed`] = appsFailed;
2200
2458
  if (appsSkipped > 0)
2201
- changes['data apps skipped'] = appsSkipped;
2202
- counts.appsNum =
2203
- appsCreated +
2204
- appsUpdated +
2205
- appsUnchanged +
2206
- appsFailed +
2207
- appsSkipped;
2459
+ changes[`${phase.changesPrefix} skipped`] = appsSkipped;
2460
+ const phaseBundleTotal = appsCreated +
2461
+ appsUpdated +
2462
+ appsUnchanged +
2463
+ appsFailed +
2464
+ appsSkipped;
2465
+ if (phase.isChartTypes) {
2466
+ counts.chartTypesNum = phaseBundleTotal;
2467
+ }
2468
+ else {
2469
+ counts.appsNum = phaseBundleTotal;
2470
+ }
2208
2471
  const appSummary = summarizeUploadChanges(changesBeforeApps, changes);
2209
2472
  if (eeAppRoutesUnavailable) {
2210
2473
  output.completeItem('not available on this server', 'warning');
@@ -2216,7 +2479,7 @@ const uploadHandler = async (options) => {
2216
2479
  // App uploads are fire-and-forget per folder, so failures are
2217
2480
  // logged and tallied rather than thrown — but the process must
2218
2481
  // still exit non-zero or CI pipelines read the run as green.
2219
- globalState_1.default.log(styles.error(`${appsFailed} data app upload(s) failed — see errors above.`));
2482
+ globalState_1.default.log(styles.error(`${appsFailed} ${phase.noun} upload(s) failed — see errors above.`));
2220
2483
  process.exitCode = 1;
2221
2484
  }
2222
2485
  }
package/dist/index.js CHANGED
@@ -389,6 +389,10 @@ const downloadCommand = commander_1.program
389
389
  .option('--include-apps', "Include all of the project's data apps (enterprise), capped at --apps-limit (default: 50)", false)
390
390
  .option('--apps-limit <number>', 'Maximum number of data apps downloaded by --include-apps or --include-all (default: 50)', undefined)
391
391
  .option('--apps-only', "Download only data apps (implies --skip-charts --skip-dashboards --skip-spaces). Bare --apps-only downloads all the project's apps; pass --apps <appReferences...> to select.", false)
392
+ .option('--chart-types <chartTypeReferences...>', 'Download only the specified custom chart types, by slug, URL, or UUID (enterprise).')
393
+ .option('--include-chart-types', "Include all of the project's custom chart types (enterprise), capped at --chart-types-limit (default: 50)", false)
394
+ .option('--chart-types-limit <number>', 'Maximum number of custom chart types downloaded by --include-chart-types or --include-all (default: 50)', undefined)
395
+ .option('--chart-types-only', "Download only custom chart types (implies --skip-charts --skip-dashboards --skip-spaces). Bare --chart-types-only downloads all the project's chart types; pass --chart-types <chartTypeReferences...> to select.", false)
392
396
  .option('--organization', 'download all organization-scoped resources, including Data App themes, without selecting a project', false);
393
397
  downloadCommand.action(withOrganizationMode(download_1.downloadHandler));
394
398
  const uploadCommand = commander_1.program
@@ -425,6 +429,9 @@ const uploadCommand = commander_1.program
425
429
  .option('--apps <appReferences...>', 'Upload only the specified data apps, by slug (the app folder name), app URL, or UUID (enterprise). URL and UUID refs are resolved against the target project.')
426
430
  .option('--include-apps', 'Upload all app folders on disk (enterprise).', false)
427
431
  .option('--apps-only', 'Upload only data apps, skipping charts, dashboards, and space reconciliation. Bare --apps-only uploads every app folder; pass --apps <appReferences...> to select.', false)
432
+ .option('--chart-types <chartTypeReferences...>', 'Upload only the specified custom chart types, by slug (the chart type folder name), URL, or UUID (enterprise). URL and UUID refs are resolved against the target project.')
433
+ .option('--include-chart-types', 'Upload all chart type folders on disk (enterprise).', false)
434
+ .option('--chart-types-only', 'Upload only custom chart types, skipping charts, dashboards, and space reconciliation. Bare --chart-types-only uploads every chart type folder; pass --chart-types <chartTypeReferences...> to select.', false)
428
435
  .option('--allow-custom-dependencies', 'Approve uploading apps that declare custom dependencies without prompting. Required for non-interactive runs of such uploads.', false)
429
436
  .option('--app-space <spaceRef>', 'Space (slug or UUID) for data apps this upload creates. Existing apps keep their space.')
430
437
  .option('--create-new', 'Always create a new app from the uploaded code instead of updating the app referenced by lightdash-app.yml. The new app gets a fresh slug.', false)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "1.260.1",
3
+ "version": "1.261.0",
4
4
  "description": "Lightdash CLI tool",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "unique-names-generator": "4.7.1",
46
46
  "uuid": "11.1.1",
47
47
  "yaml": "2.8.3",
48
- "@lightdash/common": "1.260.1",
49
- "@lightdash/warehouses": "1.260.1"
48
+ "@lightdash/common": "1.261.0",
49
+ "@lightdash/warehouses": "1.261.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/inquirer": "8.2.4",