@lightdash/cli 0.3422.2 → 0.3423.1
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/.tsbuildinfo +1 -1
- package/dist/dbt/validation.test.d.ts +2 -0
- package/dist/dbt/validation.test.d.ts.map +1 -0
- package/dist/dbt/validation.test.js +45 -0
- package/dist/handlers/download.d.ts +6 -1
- package/dist/handlers/download.d.ts.map +1 -1
- package/dist/handlers/download.js +220 -101
- package/dist/handlers/download.test.js +22 -2
- package/dist/handlers/organizationContent/index.d.ts.map +1 -1
- package/dist/handlers/organizationContent/index.js +20 -51
- package/dist/handlers/organizationContent/index.test.js +3 -1
- package/dist/terminal/contentAsCodeOutput.d.ts +15 -5
- package/dist/terminal/contentAsCodeOutput.d.ts.map +1 -1
- package/dist/terminal/contentAsCodeOutput.js +83 -35
- package/dist/terminal/contentAsCodeOutput.test.js +64 -6
- package/package.json +3 -3
|
@@ -16,6 +16,7 @@ const config_1 = require("../config");
|
|
|
16
16
|
const env_1 = require("../env");
|
|
17
17
|
const globalState_1 = tslib_1.__importDefault(require("../globalState"));
|
|
18
18
|
const styles = tslib_1.__importStar(require("../styles"));
|
|
19
|
+
const contentAsCodeOutput_1 = require("../terminal/contentAsCodeOutput");
|
|
19
20
|
const appCodeFiles_1 = require("./apps/appCodeFiles");
|
|
20
21
|
const appsDownload_1 = require("./apps/appsDownload");
|
|
21
22
|
const scaffolding_1 = require("./apps/scaffolding");
|
|
@@ -184,7 +185,7 @@ const loadYamlFile = async (file, folder, metadata) => {
|
|
|
184
185
|
};
|
|
185
186
|
const readCodeFiles = async (folder, customPath) => {
|
|
186
187
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(customPath);
|
|
187
|
-
|
|
188
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Reading ${folder} from ${baseDir}`);
|
|
188
189
|
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
189
190
|
if (major < 20 || (major === 20 && minor < 12)) {
|
|
190
191
|
throw new Error(`Node.js v20.12.0 or later is required for this command (current: ${process.version}).`);
|
|
@@ -318,8 +319,7 @@ const extractChartSlugsFromDashboards = (dashboards) => dashboards.reduce((acc,
|
|
|
318
319
|
.filter((slug) => slug !== undefined);
|
|
319
320
|
return [...acc, ...slugs];
|
|
320
321
|
}, []);
|
|
321
|
-
const downloadContent = async (ids, type, projectId, projectName, customPath, languageMap = false, nested = false, skipSpaces = false, stripPivotSeries = false, rootSpaces = false) => {
|
|
322
|
-
const spinner = globalState_1.default.getActiveSpinner();
|
|
322
|
+
const downloadContent = async (ids, type, projectId, projectName, customPath, languageMap = false, nested = false, skipSpaces = false, stripPivotSeries = false, rootSpaces = false, onProgress) => {
|
|
323
323
|
const contentFilters = parseContentFilters(ids);
|
|
324
324
|
const folderScheme = nested ? 'nested' : 'flat';
|
|
325
325
|
const config = getContentTypeConfig(type, projectId);
|
|
@@ -341,7 +341,7 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
|
|
|
341
341
|
url: `${config.endpoint}${queryParams}`,
|
|
342
342
|
body: undefined,
|
|
343
343
|
});
|
|
344
|
-
|
|
344
|
+
onProgress?.(`${results.offset} of ${results.total} ${config.displayName} downloaded`);
|
|
345
345
|
// For the same chart slug, we run the code for saved charts and sql chart
|
|
346
346
|
// so we are going to get more false positives here, so we keep it on the debug log
|
|
347
347
|
results.missingIds.forEach((missingId) => {
|
|
@@ -577,7 +577,7 @@ const upsertAiAgents = async (projectId, slugs, changes, force, customPath) => {
|
|
|
577
577
|
}
|
|
578
578
|
return changes;
|
|
579
579
|
}
|
|
580
|
-
|
|
580
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${filteredAgents.length} AI agent files`);
|
|
581
581
|
const results = await (0, apiClient_1.lightdashApi)({
|
|
582
582
|
method: 'POST',
|
|
583
583
|
url: `/api/v1/projects/${projectId}/code/aiAgents?force=${force}`,
|
|
@@ -644,7 +644,7 @@ const readScheduledContentFiles = async (contentType, customPath) => {
|
|
|
644
644
|
const upsertScheduledContent = async (projectId, slugs, changes, force, contentType, canUpload, customPath) => {
|
|
645
645
|
const config = getScheduledContentConfig(contentType, customPath);
|
|
646
646
|
const scheduledContent = await readScheduledContentFiles(contentType, customPath);
|
|
647
|
-
|
|
647
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${scheduledContent.length} ${config.singular} files`);
|
|
648
648
|
const filteredContent = slugs.length
|
|
649
649
|
? scheduledContent.filter((item) => slugs.includes(item.slug))
|
|
650
650
|
: scheduledContent;
|
|
@@ -785,9 +785,6 @@ const downloadHandler = async (options) => {
|
|
|
785
785
|
const projectId = projectSelection.projectUuid;
|
|
786
786
|
// Log current project info
|
|
787
787
|
(0, selectProject_1.logSelectedProject)(projectSelection, config, 'Downloading from');
|
|
788
|
-
const spinner = globalState_1.default.startSpinner(options.spacesOnly
|
|
789
|
-
? `Downloading spaces`
|
|
790
|
-
: `Downloading content from project`);
|
|
791
788
|
// Fetch project details to get project name for folder structure
|
|
792
789
|
const project = await (0, apiClient_1.lightdashApi)({
|
|
793
790
|
method: 'GET',
|
|
@@ -807,14 +804,17 @@ const downloadHandler = async (options) => {
|
|
|
807
804
|
projectId,
|
|
808
805
|
},
|
|
809
806
|
});
|
|
807
|
+
const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
|
|
808
|
+
operation: 'download',
|
|
809
|
+
scope: 'project',
|
|
810
|
+
});
|
|
810
811
|
try {
|
|
811
812
|
let allMetadataEntries = [];
|
|
812
813
|
if (shouldDownloadSpaces) {
|
|
813
|
-
|
|
814
|
-
spinner.start(`Downloading spaces`);
|
|
814
|
+
output.startItem('Spaces');
|
|
815
815
|
try {
|
|
816
816
|
const spaceTotal = await (0, spacesAsCode_1.downloadSpaces)(projectId, projectName, options.path, options.nested, options.rootSpaces);
|
|
817
|
-
|
|
817
|
+
output.completeItem(`${spaceTotal} downloaded`);
|
|
818
818
|
}
|
|
819
819
|
catch (error) {
|
|
820
820
|
if (!(0, spacesAsCode_1.shouldFallBackToEmbeddedSpaces)(error, options.spacesOnly)) {
|
|
@@ -823,16 +823,19 @@ const downloadHandler = async (options) => {
|
|
|
823
823
|
: error;
|
|
824
824
|
}
|
|
825
825
|
skipEmbeddedSpaces = false;
|
|
826
|
-
|
|
826
|
+
output.completeItem('access unavailable; using legacy metadata', 'warning');
|
|
827
|
+
globalState_1.default.log(styles.warning('Space access is unavailable; continuing with legacy space metadata where available.'));
|
|
827
828
|
globalState_1.default.debug(`Could not download access-aware spaces: ${(0, common_1.getErrorMessage)(error)}`);
|
|
828
829
|
}
|
|
829
830
|
}
|
|
830
831
|
if (includeAllOptionalContent ||
|
|
831
832
|
options.includeVirtualViews ||
|
|
832
833
|
options.virtualViews.length > 0) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
834
|
+
await output.runItem({
|
|
835
|
+
label: 'Virtual views',
|
|
836
|
+
action: () => downloadVirtualViews(projectId, options.virtualViews, options.path),
|
|
837
|
+
detail: (total) => `${total} downloaded`,
|
|
838
|
+
});
|
|
836
839
|
}
|
|
837
840
|
// Download regular charts and SQL charts
|
|
838
841
|
if (!options.skipCharts) {
|
|
@@ -840,16 +843,20 @@ const downloadHandler = async (options) => {
|
|
|
840
843
|
globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
|
|
841
844
|
}
|
|
842
845
|
else {
|
|
843
|
-
const [regularChartTotal, , regularChartMeta] = await
|
|
844
|
-
|
|
846
|
+
const [regularChartTotal, , regularChartMeta] = await output.runItem({
|
|
847
|
+
label: 'Charts',
|
|
848
|
+
action: () => (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, options.stripPivotSeries, options.rootSpaces, output.updateActive),
|
|
849
|
+
detail: ([total]) => `${total} downloaded`,
|
|
850
|
+
});
|
|
845
851
|
allMetadataEntries = [
|
|
846
852
|
...allMetadataEntries,
|
|
847
853
|
...regularChartMeta,
|
|
848
854
|
];
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
855
|
+
const [sqlChartTotal, , sqlChartMeta] = await output.runItem({
|
|
856
|
+
label: 'SQL charts',
|
|
857
|
+
action: () => (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive),
|
|
858
|
+
detail: ([total]) => `${total} downloaded`,
|
|
859
|
+
});
|
|
853
860
|
allMetadataEntries = [...allMetadataEntries, ...sqlChartMeta];
|
|
854
861
|
chartTotal = regularChartTotal + sqlChartTotal;
|
|
855
862
|
}
|
|
@@ -862,52 +869,64 @@ const downloadHandler = async (options) => {
|
|
|
862
869
|
else {
|
|
863
870
|
let chartSlugs = [];
|
|
864
871
|
let dashMeta;
|
|
865
|
-
[dashboardTotal, chartSlugs, dashMeta] = await
|
|
872
|
+
[dashboardTotal, chartSlugs, dashMeta] = await output.runItem({
|
|
873
|
+
label: 'Dashboards',
|
|
874
|
+
action: () => (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive),
|
|
875
|
+
detail: ([total]) => `${total} downloaded`,
|
|
876
|
+
});
|
|
866
877
|
allMetadataEntries = [...allMetadataEntries, ...dashMeta];
|
|
867
|
-
spinner.succeed(`Downloaded ${dashboardTotal} dashboards`);
|
|
868
878
|
if (hasFilters &&
|
|
869
879
|
chartSlugs.length > 0 &&
|
|
870
880
|
!options.skipCharts) {
|
|
871
|
-
|
|
872
|
-
|
|
881
|
+
output.startItem('Linked charts');
|
|
882
|
+
output.updateActive(`${chartSlugs.length} dashboard dependencies`);
|
|
883
|
+
const [regularCharts, , linkedChartMeta] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, options.stripPivotSeries, options.rootSpaces, output.updateActive);
|
|
873
884
|
allMetadataEntries = [
|
|
874
885
|
...allMetadataEntries,
|
|
875
886
|
...linkedChartMeta,
|
|
876
887
|
];
|
|
877
|
-
const [sqlCharts, , linkedSqlMeta] = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces);
|
|
888
|
+
const [sqlCharts, , linkedSqlMeta] = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipEmbeddedSpaces, false, options.rootSpaces, output.updateActive);
|
|
878
889
|
allMetadataEntries = [
|
|
879
890
|
...allMetadataEntries,
|
|
880
891
|
...linkedSqlMeta,
|
|
881
892
|
];
|
|
882
|
-
|
|
893
|
+
output.completeItem(`${regularCharts + sqlCharts} downloaded`);
|
|
883
894
|
}
|
|
884
895
|
}
|
|
885
896
|
}
|
|
886
897
|
if (!options.spacesOnly && shouldDownloadAiAgents(options)) {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
898
|
+
await output.runItem({
|
|
899
|
+
label: 'AI agents',
|
|
900
|
+
action: () => downloadAiAgents(projectId, options.agents, options.path),
|
|
901
|
+
detail: (total) => `${total} downloaded`,
|
|
902
|
+
});
|
|
890
903
|
}
|
|
891
904
|
if (includeAllOptionalContent ||
|
|
892
905
|
options.includeAlerts ||
|
|
893
906
|
options.alerts.length > 0) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
907
|
+
await output.runItem({
|
|
908
|
+
label: 'Alerts',
|
|
909
|
+
action: () => downloadScheduledContent(projectId, options.alerts, common_1.ContentAsCodeType.ALERT, options.path),
|
|
910
|
+
detail: (total) => `${total} downloaded`,
|
|
911
|
+
});
|
|
897
912
|
}
|
|
898
913
|
if (includeAllOptionalContent ||
|
|
899
914
|
options.includeScheduledDeliveries ||
|
|
900
915
|
options.scheduledDeliveries.length > 0) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
916
|
+
await output.runItem({
|
|
917
|
+
label: 'Scheduled deliveries',
|
|
918
|
+
action: () => downloadScheduledContent(projectId, options.scheduledDeliveries, common_1.ContentAsCodeType.SCHEDULED_DELIVERY, options.path),
|
|
919
|
+
detail: (total) => `${total} downloaded`,
|
|
920
|
+
});
|
|
904
921
|
}
|
|
905
922
|
if (includeAllOptionalContent ||
|
|
906
923
|
options.includeGoogleSheets ||
|
|
907
924
|
options.googleSheets.length > 0) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
925
|
+
await output.runItem({
|
|
926
|
+
label: 'Google Sheets syncs',
|
|
927
|
+
action: () => downloadScheduledContent(projectId, options.googleSheets, common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC, options.path),
|
|
928
|
+
detail: (total) => `${total} downloaded`,
|
|
929
|
+
});
|
|
911
930
|
}
|
|
912
931
|
// Download data apps (enterprise, opt-in via --apps / --include-apps / --include-all)
|
|
913
932
|
const appsSelection = (0, appsDownload_1.selectAppsToDownload)({
|
|
@@ -915,6 +934,7 @@ const downloadHandler = async (options) => {
|
|
|
915
934
|
includeApps,
|
|
916
935
|
});
|
|
917
936
|
if (appsSelection.mode !== 'none') {
|
|
937
|
+
output.startItem('Data apps');
|
|
918
938
|
let appUuidsToDownload;
|
|
919
939
|
let appListingError = null;
|
|
920
940
|
if (appsSelection.mode === 'explicit') {
|
|
@@ -922,7 +942,7 @@ const downloadHandler = async (options) => {
|
|
|
922
942
|
}
|
|
923
943
|
else {
|
|
924
944
|
// List every app in the project (includes apps not in any space)
|
|
925
|
-
|
|
945
|
+
output.updateActive('listing project apps…');
|
|
926
946
|
let listedAppUuids;
|
|
927
947
|
try {
|
|
928
948
|
const projectApps = await (0, apiClient_1.lightdashApi)({
|
|
@@ -938,7 +958,6 @@ const downloadHandler = async (options) => {
|
|
|
938
958
|
throw listErr;
|
|
939
959
|
}
|
|
940
960
|
appListingError = (0, common_1.getErrorMessage)(listErr);
|
|
941
|
-
spinner.fail(`Error downloading data apps: ${appListingError}`);
|
|
942
961
|
listedAppUuids = [];
|
|
943
962
|
}
|
|
944
963
|
else {
|
|
@@ -960,11 +979,14 @@ const downloadHandler = async (options) => {
|
|
|
960
979
|
}
|
|
961
980
|
if (appUuidsToDownload.length === 0) {
|
|
962
981
|
if (appListingError === null) {
|
|
963
|
-
|
|
982
|
+
output.completeItem('0 found');
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
output.completeItem(`listing failed: ${appListingError}`, 'warning');
|
|
964
986
|
}
|
|
965
987
|
}
|
|
966
988
|
else {
|
|
967
|
-
|
|
989
|
+
output.updateActive(`0 of ${appUuidsToDownload.length} downloaded`);
|
|
968
990
|
const baseDir = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
969
991
|
const appsDir = path.join(baseDir, 'apps');
|
|
970
992
|
const takenFolders = new Set();
|
|
@@ -1017,13 +1039,17 @@ const downloadHandler = async (options) => {
|
|
|
1017
1039
|
globalState_1.default.log(styles.error(`Failed to download app ${appUuid}: ${outcome.message}`));
|
|
1018
1040
|
}
|
|
1019
1041
|
}
|
|
1042
|
+
output.updateActive(`${appSuccessCount +
|
|
1043
|
+
appSkippedNotBuiltCount +
|
|
1044
|
+
appFailures.length} of ${appUuidsToDownload.length} processed`);
|
|
1020
1045
|
}
|
|
1021
1046
|
const summary = (0, appsDownload_1.appsDownloadSummary)(appSuccessCount, appUuidsToDownload.length, appFailures, appsDir, appSkippedNotBuiltCount);
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1047
|
+
output.completeItem(`${appSuccessCount} downloaded${appSkippedNotBuiltCount > 0
|
|
1048
|
+
? `, ${appSkippedNotBuiltCount} skipped`
|
|
1049
|
+
: ''}${appFailures.length > 0
|
|
1050
|
+
? `, ${appFailures.length} failed`
|
|
1051
|
+
: ''}`, summary.ok ? undefined : 'warning');
|
|
1052
|
+
if (!summary.ok) {
|
|
1027
1053
|
summary.failureLines.forEach((line) => globalState_1.default.log(styles.warning(line)));
|
|
1028
1054
|
}
|
|
1029
1055
|
}
|
|
@@ -1041,13 +1067,18 @@ const downloadHandler = async (options) => {
|
|
|
1041
1067
|
const downloadRoot = options.nested
|
|
1042
1068
|
? path.join(baseDir, projectName)
|
|
1043
1069
|
: baseDir;
|
|
1070
|
+
output.startItem('Metadata');
|
|
1044
1071
|
await (0, metadataFile_1.writeMetadataFile)(baseDir, metadataToWrite);
|
|
1045
1072
|
if (!config.answers?.metadataFileGitignoreNoticeShown) {
|
|
1046
1073
|
globalState_1.default.log(styles.warning(`\nNote: ${metadataFile_1.METADATA_FILENAME} was written to ${baseDir}. Consider adding it to your .gitignore.`));
|
|
1047
1074
|
await (0, config_1.setAnswer)({ metadataFileGitignoreNoticeShown: true });
|
|
1048
1075
|
}
|
|
1049
|
-
|
|
1076
|
+
output.completeItem('timestamps written');
|
|
1050
1077
|
const end = Date.now();
|
|
1078
|
+
const renderedSummary = output.complete(downloadRoot, (end - start) / 1000);
|
|
1079
|
+
if (!renderedSummary) {
|
|
1080
|
+
globalState_1.default.log(styles.success(`Downloaded content saved to ${downloadRoot}`));
|
|
1081
|
+
}
|
|
1051
1082
|
await analytics_1.LightdashAnalytics.track({
|
|
1052
1083
|
event: 'download.completed',
|
|
1053
1084
|
properties: {
|
|
@@ -1061,7 +1092,7 @@ const downloadHandler = async (options) => {
|
|
|
1061
1092
|
});
|
|
1062
1093
|
}
|
|
1063
1094
|
catch (error) {
|
|
1064
|
-
|
|
1095
|
+
output.fail((0, common_1.getErrorMessage)(error), (Date.now() - start) / 1000, true);
|
|
1065
1096
|
await analytics_1.LightdashAnalytics.track({
|
|
1066
1097
|
event: 'download.error',
|
|
1067
1098
|
properties: {
|
|
@@ -1097,6 +1128,42 @@ const storeUploadChanges = (changes, promoteChanges) => {
|
|
|
1097
1128
|
});
|
|
1098
1129
|
return updatedChanges;
|
|
1099
1130
|
};
|
|
1131
|
+
const UPLOAD_CHANGE_SUFFIXES = [
|
|
1132
|
+
'dependency skipped',
|
|
1133
|
+
'with errors',
|
|
1134
|
+
'unchanged',
|
|
1135
|
+
'created',
|
|
1136
|
+
'updated',
|
|
1137
|
+
'deleted',
|
|
1138
|
+
'skipped',
|
|
1139
|
+
'failed',
|
|
1140
|
+
];
|
|
1141
|
+
const summarizeUploadChanges = (before, after) => {
|
|
1142
|
+
const totals = new Map();
|
|
1143
|
+
Object.entries(after).forEach(([key, value]) => {
|
|
1144
|
+
const difference = value - (before[key] ?? 0);
|
|
1145
|
+
if (difference <= 0)
|
|
1146
|
+
return;
|
|
1147
|
+
const suffix = UPLOAD_CHANGE_SUFFIXES.find((candidate) => key.endsWith(candidate));
|
|
1148
|
+
const label = suffix ?? 'processed';
|
|
1149
|
+
totals.set(label, (totals.get(label) ?? 0) + difference);
|
|
1150
|
+
});
|
|
1151
|
+
if (totals.size === 0)
|
|
1152
|
+
return { detail: 'no changes' };
|
|
1153
|
+
const detail = [...totals]
|
|
1154
|
+
.map(([label, total]) => `${total} ${label}`)
|
|
1155
|
+
.join(', ');
|
|
1156
|
+
const hasFailures = [...totals.keys()].some((label) => label === 'with errors' || label === 'failed');
|
|
1157
|
+
return { detail, variant: hasFailures ? 'warning' : undefined };
|
|
1158
|
+
};
|
|
1159
|
+
const runUploadChangesPhase = async ({ output, label, changes, action, }) => {
|
|
1160
|
+
const before = { ...changes };
|
|
1161
|
+
output.startItem(label);
|
|
1162
|
+
const updatedChanges = await action();
|
|
1163
|
+
const summary = summarizeUploadChanges(before, updatedChanges);
|
|
1164
|
+
output.completeItem(summary.detail, summary.variant);
|
|
1165
|
+
return updatedChanges;
|
|
1166
|
+
};
|
|
1100
1167
|
// SQL charts have 'sql' field instead of 'tableName'/'metricQuery'
|
|
1101
1168
|
const isSqlChart = (item) => 'sql' in item && !('tableName' in item);
|
|
1102
1169
|
const upsertSingleItem = async (item, type, projectId, changes, force, config, skipSpaceCreate, publicSpaceCreate, validate, spaceNames) => {
|
|
@@ -1204,7 +1271,7 @@ const upsertResources = async (type, projectId, changes, force, slugs, canUpload
|
|
|
1204
1271
|
const config = await (0, config_1.getConfig)();
|
|
1205
1272
|
const folderItems = await readCodeFiles(type, customPath);
|
|
1206
1273
|
const items = [...folderItems, ...extraItems];
|
|
1207
|
-
|
|
1274
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${items.length} ${type} files`);
|
|
1208
1275
|
const hasFilter = slugs.length > 0;
|
|
1209
1276
|
const filteredItems = hasFilter
|
|
1210
1277
|
? items.filter((item) => slugs.includes(item.slug))
|
|
@@ -1365,18 +1432,33 @@ const uploadHandler = async (options) => {
|
|
|
1365
1432
|
projectId,
|
|
1366
1433
|
},
|
|
1367
1434
|
});
|
|
1368
|
-
|
|
1435
|
+
const output = (0, contentAsCodeOutput_1.createContentAsCodeOutput)({
|
|
1436
|
+
operation: 'upload',
|
|
1437
|
+
scope: 'project',
|
|
1438
|
+
});
|
|
1439
|
+
const uploadRoot = (0, contentAsCodePaths_1.getDownloadFolder)(options.path);
|
|
1440
|
+
const completeUpload = () => {
|
|
1441
|
+
const renderedSummary = output.complete(uploadRoot, (Date.now() - start) / 1000);
|
|
1442
|
+
if (!renderedSummary) {
|
|
1443
|
+
(0, spacesAsCode_1.logUploadChanges)(changes);
|
|
1444
|
+
globalState_1.default.log(styles.success(`Uploaded content from ${uploadRoot}`));
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1369
1447
|
try {
|
|
1370
|
-
uploadResource = 'space definitions';
|
|
1371
1448
|
const spaceFiles = preflightSpaceFiles;
|
|
1372
1449
|
const spaceNames = shouldReconcileSpaces
|
|
1373
1450
|
? (0, spacesAsCode_1.getSpaceNames)(spaceFiles)
|
|
1374
1451
|
: await (0, spacesAsCode_1.readSpaceNames)(options.path);
|
|
1375
1452
|
if (spaceFiles.length > 0) {
|
|
1376
|
-
|
|
1453
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${spaceFiles.length} space definition(s)`);
|
|
1377
1454
|
}
|
|
1378
1455
|
if (shouldReconcileSpaces) {
|
|
1379
|
-
changes = await (
|
|
1456
|
+
changes = await runUploadChangesPhase({
|
|
1457
|
+
output,
|
|
1458
|
+
label: 'Spaces',
|
|
1459
|
+
changes,
|
|
1460
|
+
action: () => (0, spacesAsCode_1.upsertSpaces)(projectId, spaceFiles, changes, options.skipSpaceCreate, options.public),
|
|
1461
|
+
});
|
|
1380
1462
|
}
|
|
1381
1463
|
else if (hasFilters) {
|
|
1382
1464
|
globalState_1.default.debug('Skipping space access reconciliation for a filtered content upload');
|
|
@@ -1391,27 +1473,22 @@ const uploadHandler = async (options) => {
|
|
|
1391
1473
|
timeToCompleted: (Date.now() - start) / 1000,
|
|
1392
1474
|
},
|
|
1393
1475
|
});
|
|
1394
|
-
(
|
|
1476
|
+
completeUpload();
|
|
1395
1477
|
return;
|
|
1396
1478
|
}
|
|
1397
1479
|
const uploadPermissions = await (0, apiClient_1.getContentAsCodeUploadPermissions)(projectId);
|
|
1398
1480
|
// Discover loose YAML files (outside charts/ and dashboards/) classified by contentType
|
|
1399
|
-
|
|
1400
|
-
|
|
1481
|
+
const looseFiles = await output.runItem({
|
|
1482
|
+
label: 'Content files',
|
|
1483
|
+
action: () => readLooseCodeFiles(options.path),
|
|
1484
|
+
detail: ({ charts, dashboards }) => `${charts.length + dashboards.length} discovered`,
|
|
1485
|
+
});
|
|
1401
1486
|
if (looseFiles.charts.length > 0) {
|
|
1402
|
-
|
|
1487
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${looseFiles.charts.length} chart(s) outside charts/ directory (classified by contentType)`);
|
|
1403
1488
|
}
|
|
1404
1489
|
if (looseFiles.dashboards.length > 0) {
|
|
1405
|
-
|
|
1490
|
+
(0, contentAsCodeOutput_1.logContentAsCodeDiscovery)(`Found ${looseFiles.dashboards.length} dashboard(s) outside dashboards/ directory (classified by contentType)`);
|
|
1406
1491
|
}
|
|
1407
|
-
// Always include the charts from dashboards if includeCharts is true regardless of the charts filters
|
|
1408
|
-
uploadResource = 'dashboard chart dependencies';
|
|
1409
|
-
const chartSlugs = options.includeCharts
|
|
1410
|
-
? Array.from(new Set([
|
|
1411
|
-
...options.charts,
|
|
1412
|
-
...(await getDashboardChartSlugs(options.dashboards, options.path, looseFiles.dashboards)),
|
|
1413
|
-
]))
|
|
1414
|
-
: options.charts;
|
|
1415
1492
|
const concurrency = Math.min(Math.max(1, parseInt(String(options.concurrency), 10) || 1), 1000);
|
|
1416
1493
|
if (parseInt(String(options.concurrency), 10) > 1000) {
|
|
1417
1494
|
globalState_1.default.log(styles.warning(`Concurrency limit exceeded. Using maximum of 1000 instead of ${options.concurrency}`));
|
|
@@ -1421,36 +1498,60 @@ const uploadHandler = async (options) => {
|
|
|
1421
1498
|
globalState_1.default.log(styles.warning(`No virtual view filters provided, skipping`));
|
|
1422
1499
|
}
|
|
1423
1500
|
else {
|
|
1424
|
-
|
|
1425
|
-
|
|
1501
|
+
changes = await runUploadChangesPhase({
|
|
1502
|
+
output,
|
|
1503
|
+
label: 'Virtual views',
|
|
1504
|
+
changes,
|
|
1505
|
+
action: () => upsertVirtualViews(projectId, options.virtualViews, changes, options.force, uploadPermissions.virtualViews, options.path),
|
|
1506
|
+
});
|
|
1426
1507
|
}
|
|
1427
1508
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1509
|
+
changes = await runUploadChangesPhase({
|
|
1510
|
+
output,
|
|
1511
|
+
label: 'Charts',
|
|
1512
|
+
changes,
|
|
1513
|
+
action: async () => {
|
|
1514
|
+
const chartSlugs = options.includeCharts
|
|
1515
|
+
? Array.from(new Set([
|
|
1516
|
+
...options.charts,
|
|
1517
|
+
...(await getDashboardChartSlugs(options.dashboards, options.path, looseFiles.dashboards)),
|
|
1518
|
+
]))
|
|
1519
|
+
: options.charts;
|
|
1520
|
+
if (hasFilters && chartSlugs.length === 0) {
|
|
1521
|
+
globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
|
|
1522
|
+
return changes;
|
|
1523
|
+
}
|
|
1524
|
+
const result = await upsertResources('charts', projectId, changes, options.force, chartSlugs, uploadPermissions.charts, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.charts, spaceNames);
|
|
1525
|
+
chartTotal = result.total;
|
|
1526
|
+
return result.changes;
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
changes = await runUploadChangesPhase({
|
|
1530
|
+
output,
|
|
1531
|
+
label: 'Dashboards',
|
|
1532
|
+
changes,
|
|
1533
|
+
action: async () => {
|
|
1534
|
+
if (hasFilters && options.dashboards.length === 0) {
|
|
1535
|
+
globalState_1.default.log(styles.warning(`No dashboard filters provided, skipping`));
|
|
1536
|
+
return changes;
|
|
1537
|
+
}
|
|
1538
|
+
const result = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, uploadPermissions.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.dashboards, spaceNames);
|
|
1539
|
+
dashboardTotal = result.total;
|
|
1540
|
+
return result.changes;
|
|
1541
|
+
},
|
|
1542
|
+
});
|
|
1446
1543
|
if (!options.skipAgents) {
|
|
1447
1544
|
if (hasFilters && options.agents.length === 0) {
|
|
1448
1545
|
globalState_1.default.log(styles.warning(`No AI agent filters provided, skipping`));
|
|
1449
1546
|
}
|
|
1450
1547
|
else {
|
|
1451
|
-
uploadResource = 'AI agents';
|
|
1452
1548
|
try {
|
|
1453
|
-
changes = await
|
|
1549
|
+
changes = await runUploadChangesPhase({
|
|
1550
|
+
output,
|
|
1551
|
+
label: 'AI agents',
|
|
1552
|
+
changes,
|
|
1553
|
+
action: () => upsertAiAgents(projectId, options.agents, changes, options.force, options.path),
|
|
1554
|
+
});
|
|
1454
1555
|
}
|
|
1455
1556
|
catch (error) {
|
|
1456
1557
|
throw new AiAgentAsCodeUploadError(error);
|
|
@@ -1462,8 +1563,12 @@ const uploadHandler = async (options) => {
|
|
|
1462
1563
|
globalState_1.default.log(styles.warning(`No alert filters provided, skipping`));
|
|
1463
1564
|
}
|
|
1464
1565
|
else {
|
|
1465
|
-
|
|
1466
|
-
|
|
1566
|
+
changes = await runUploadChangesPhase({
|
|
1567
|
+
output,
|
|
1568
|
+
label: 'Alerts',
|
|
1569
|
+
changes,
|
|
1570
|
+
action: () => upsertScheduledContent(projectId, options.alerts, changes, options.force, common_1.ContentAsCodeType.ALERT, uploadPermissions.alerts, options.path),
|
|
1571
|
+
});
|
|
1467
1572
|
}
|
|
1468
1573
|
}
|
|
1469
1574
|
if (!options.skipScheduledDeliveries) {
|
|
@@ -1471,8 +1576,12 @@ const uploadHandler = async (options) => {
|
|
|
1471
1576
|
globalState_1.default.log(styles.warning(`No scheduled delivery filters provided, skipping`));
|
|
1472
1577
|
}
|
|
1473
1578
|
else {
|
|
1474
|
-
|
|
1475
|
-
|
|
1579
|
+
changes = await runUploadChangesPhase({
|
|
1580
|
+
output,
|
|
1581
|
+
label: 'Scheduled deliveries',
|
|
1582
|
+
changes,
|
|
1583
|
+
action: () => upsertScheduledContent(projectId, options.scheduledDeliveries, changes, options.force, common_1.ContentAsCodeType.SCHEDULED_DELIVERY, uploadPermissions.scheduledDeliveries, options.path),
|
|
1584
|
+
});
|
|
1476
1585
|
}
|
|
1477
1586
|
}
|
|
1478
1587
|
if (!options.skipGoogleSheets) {
|
|
@@ -1480,8 +1589,12 @@ const uploadHandler = async (options) => {
|
|
|
1480
1589
|
globalState_1.default.log(styles.warning(`No Google Sheets sync filters provided, skipping`));
|
|
1481
1590
|
}
|
|
1482
1591
|
else {
|
|
1483
|
-
|
|
1484
|
-
|
|
1592
|
+
changes = await runUploadChangesPhase({
|
|
1593
|
+
output,
|
|
1594
|
+
label: 'Google Sheets syncs',
|
|
1595
|
+
changes,
|
|
1596
|
+
action: () => upsertScheduledContent(projectId, options.googleSheets, changes, options.force, common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC, uploadPermissions.googleSheets, options.path),
|
|
1597
|
+
});
|
|
1485
1598
|
}
|
|
1486
1599
|
}
|
|
1487
1600
|
// Upload data apps (enterprise, opt-in via --apps <uuids...> or
|
|
@@ -1494,11 +1607,14 @@ const uploadHandler = async (options) => {
|
|
|
1494
1607
|
let appsUpdated = 0;
|
|
1495
1608
|
let appsFailed = 0;
|
|
1496
1609
|
let appsSkipped = 0;
|
|
1610
|
+
const changesBeforeApps = { ...changes };
|
|
1497
1611
|
if (shouldUploadApps && !uploadPermissions.dataApps) {
|
|
1612
|
+
output.startItem('Data apps');
|
|
1498
1613
|
globalState_1.default.log(styles.error(`Error uploading data apps: create:DataApp or manage:DataApp permission is required`));
|
|
1614
|
+
output.completeItem('permission denied', 'warning');
|
|
1499
1615
|
}
|
|
1500
1616
|
else if (shouldUploadApps) {
|
|
1501
|
-
|
|
1617
|
+
output.startItem('Data apps');
|
|
1502
1618
|
// --include-apps uploads every folder on disk; explicit UUIDs
|
|
1503
1619
|
// filter folders by their manifest appUuid
|
|
1504
1620
|
const filterUuids = options.includeApps === true ? null : new Set(explicitAppUuids);
|
|
@@ -1698,6 +1814,8 @@ const uploadHandler = async (options) => {
|
|
|
1698
1814
|
changes['data apps failed'] = appsFailed;
|
|
1699
1815
|
if (appsSkipped > 0)
|
|
1700
1816
|
changes['data apps skipped'] = appsSkipped;
|
|
1817
|
+
const appSummary = summarizeUploadChanges(changesBeforeApps, changes);
|
|
1818
|
+
output.completeItem(appSummary.detail, appSummary.variant);
|
|
1701
1819
|
}
|
|
1702
1820
|
const end = Date.now();
|
|
1703
1821
|
await analytics_1.LightdashAnalytics.track({
|
|
@@ -1711,12 +1829,12 @@ const uploadHandler = async (options) => {
|
|
|
1711
1829
|
timeToCompleted: (end - start) / 1000, // in seconds
|
|
1712
1830
|
},
|
|
1713
1831
|
});
|
|
1714
|
-
(
|
|
1832
|
+
completeUpload();
|
|
1715
1833
|
}
|
|
1716
1834
|
catch (error) {
|
|
1717
|
-
|
|
1835
|
+
output.fail((0, common_1.getErrorMessage)(error), (Date.now() - start) / 1000, true);
|
|
1718
1836
|
await analytics_1.LightdashAnalytics.track({
|
|
1719
|
-
event: '
|
|
1837
|
+
event: 'upload.error',
|
|
1720
1838
|
properties: {
|
|
1721
1839
|
userId: config.user?.userUuid,
|
|
1722
1840
|
organizationId: config.user?.organizationUuid,
|
|
@@ -1743,6 +1861,7 @@ exports.testHelpers = {
|
|
|
1743
1861
|
shouldFallBackToEmbeddedSpaces: spacesAsCode_1.shouldFallBackToEmbeddedSpaces,
|
|
1744
1862
|
shouldDownloadAiAgents,
|
|
1745
1863
|
sortSpaceFilesParentFirst: spacesAsCode_1.sortSpaceFilesParentFirst,
|
|
1864
|
+
summarizeUploadChanges,
|
|
1746
1865
|
upsertSpaces: spacesAsCode_1.upsertSpaces,
|
|
1747
1866
|
upsertVirtualViews,
|
|
1748
1867
|
validateSpaceIdentity: spacesAsCode_1.validateSpaceIdentity,
|