@claryai/cli 0.1.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.
Files changed (237) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +197 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/ajv.d.ts +3 -0
  5. package/dist/ajv.d.ts.map +1 -0
  6. package/dist/ajv.js +13 -0
  7. package/dist/analytics/analytics.d.ts +370 -0
  8. package/dist/analytics/analytics.d.ts.map +1 -0
  9. package/dist/analytics/analytics.js +143 -0
  10. package/dist/config.d.ts +34 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +134 -0
  13. package/dist/dbt/context.d.ts +14 -0
  14. package/dist/dbt/context.d.ts.map +1 -0
  15. package/dist/dbt/context.js +76 -0
  16. package/dist/dbt/context.test.d.ts +2 -0
  17. package/dist/dbt/context.test.d.ts.map +1 -0
  18. package/dist/dbt/context.test.js +152 -0
  19. package/dist/dbt/manifest.d.ts +7 -0
  20. package/dist/dbt/manifest.d.ts.map +1 -0
  21. package/dist/dbt/manifest.js +23 -0
  22. package/dist/dbt/models.d.ts +43 -0
  23. package/dist/dbt/models.d.ts.map +1 -0
  24. package/dist/dbt/models.js +256 -0
  25. package/dist/dbt/models.test.d.ts +2 -0
  26. package/dist/dbt/models.test.d.ts.map +1 -0
  27. package/dist/dbt/models.test.js +19 -0
  28. package/dist/dbt/profile.d.ts +9 -0
  29. package/dist/dbt/profile.d.ts.map +1 -0
  30. package/dist/dbt/profile.js +86 -0
  31. package/dist/dbt/profiles.test.d.ts +2 -0
  32. package/dist/dbt/profiles.test.d.ts.map +1 -0
  33. package/dist/dbt/profiles.test.js +50 -0
  34. package/dist/dbt/schema.d.ts +31 -0
  35. package/dist/dbt/schema.d.ts.map +1 -0
  36. package/dist/dbt/schema.js +49 -0
  37. package/dist/dbt/targets/Bigquery/index.d.ts +18 -0
  38. package/dist/dbt/targets/Bigquery/index.d.ts.map +1 -0
  39. package/dist/dbt/targets/Bigquery/index.js +105 -0
  40. package/dist/dbt/targets/Bigquery/oauth.d.ts +2 -0
  41. package/dist/dbt/targets/Bigquery/oauth.d.ts.map +1 -0
  42. package/dist/dbt/targets/Bigquery/oauth.js +43 -0
  43. package/dist/dbt/targets/Bigquery/serviceAccount.d.ts +35 -0
  44. package/dist/dbt/targets/Bigquery/serviceAccount.d.ts.map +1 -0
  45. package/dist/dbt/targets/Bigquery/serviceAccount.js +149 -0
  46. package/dist/dbt/targets/Databricks/oauth.d.ts +21 -0
  47. package/dist/dbt/targets/Databricks/oauth.d.ts.map +1 -0
  48. package/dist/dbt/targets/Databricks/oauth.js +184 -0
  49. package/dist/dbt/targets/athena.d.ts +21 -0
  50. package/dist/dbt/targets/athena.d.ts.map +1 -0
  51. package/dist/dbt/targets/athena.js +91 -0
  52. package/dist/dbt/targets/athena.test.d.ts +2 -0
  53. package/dist/dbt/targets/athena.test.d.ts.map +1 -0
  54. package/dist/dbt/targets/athena.test.js +60 -0
  55. package/dist/dbt/targets/clickhouse.d.ts +24 -0
  56. package/dist/dbt/targets/clickhouse.d.ts.map +1 -0
  57. package/dist/dbt/targets/clickhouse.js +90 -0
  58. package/dist/dbt/targets/databricks.d.ts +27 -0
  59. package/dist/dbt/targets/databricks.d.ts.map +1 -0
  60. package/dist/dbt/targets/databricks.js +138 -0
  61. package/dist/dbt/targets/duckdb.d.ts +16 -0
  62. package/dist/dbt/targets/duckdb.d.ts.map +1 -0
  63. package/dist/dbt/targets/duckdb.js +63 -0
  64. package/dist/dbt/targets/duckdb.test.d.ts +2 -0
  65. package/dist/dbt/targets/duckdb.test.d.ts.map +1 -0
  66. package/dist/dbt/targets/duckdb.test.js +37 -0
  67. package/dist/dbt/targets/postgres.d.ts +26 -0
  68. package/dist/dbt/targets/postgres.d.ts.map +1 -0
  69. package/dist/dbt/targets/postgres.js +142 -0
  70. package/dist/dbt/targets/redshift.d.ts +23 -0
  71. package/dist/dbt/targets/redshift.d.ts.map +1 -0
  72. package/dist/dbt/targets/redshift.js +96 -0
  73. package/dist/dbt/targets/snowflake.d.ts +4 -0
  74. package/dist/dbt/targets/snowflake.d.ts.map +1 -0
  75. package/dist/dbt/targets/snowflake.js +134 -0
  76. package/dist/dbt/targets/trino.d.ts +16 -0
  77. package/dist/dbt/targets/trino.d.ts.map +1 -0
  78. package/dist/dbt/targets/trino.js +65 -0
  79. package/dist/dbt/templating.d.ts +15 -0
  80. package/dist/dbt/templating.d.ts.map +1 -0
  81. package/dist/dbt/templating.js +50 -0
  82. package/dist/dbt/templating.test.d.ts +2 -0
  83. package/dist/dbt/templating.test.d.ts.map +1 -0
  84. package/dist/dbt/templating.test.js +51 -0
  85. package/dist/dbt/types.d.ts +17 -0
  86. package/dist/dbt/types.d.ts.map +1 -0
  87. package/dist/dbt/types.js +2 -0
  88. package/dist/dbt/validation.d.ts +9 -0
  89. package/dist/dbt/validation.d.ts.map +1 -0
  90. package/dist/dbt/validation.js +54 -0
  91. package/dist/env.d.ts +12 -0
  92. package/dist/env.d.ts.map +1 -0
  93. package/dist/env.js +40 -0
  94. package/dist/error.d.ts +2 -0
  95. package/dist/error.d.ts.map +1 -0
  96. package/dist/error.js +12 -0
  97. package/dist/globalState.d.ts +29 -0
  98. package/dist/globalState.d.ts.map +1 -0
  99. package/dist/globalState.js +67 -0
  100. package/dist/handlers/asyncQuery.d.ts +7 -0
  101. package/dist/handlers/asyncQuery.d.ts.map +1 -0
  102. package/dist/handlers/asyncQuery.js +50 -0
  103. package/dist/handlers/compile.d.ts +16 -0
  104. package/dist/handlers/compile.d.ts.map +1 -0
  105. package/dist/handlers/compile.js +277 -0
  106. package/dist/handlers/compile.test.d.ts +2 -0
  107. package/dist/handlers/compile.test.d.ts.map +1 -0
  108. package/dist/handlers/compile.test.js +201 -0
  109. package/dist/handlers/createProject.d.ts +37 -0
  110. package/dist/handlers/createProject.d.ts.map +1 -0
  111. package/dist/handlers/createProject.js +272 -0
  112. package/dist/handlers/dbt/apiClient.d.ts +14 -0
  113. package/dist/handlers/dbt/apiClient.d.ts.map +1 -0
  114. package/dist/handlers/dbt/apiClient.js +167 -0
  115. package/dist/handlers/dbt/compile.d.ts +35 -0
  116. package/dist/handlers/dbt/compile.d.ts.map +1 -0
  117. package/dist/handlers/dbt/compile.js +220 -0
  118. package/dist/handlers/dbt/getDbtProfileTargetName.d.ts +9 -0
  119. package/dist/handlers/dbt/getDbtProfileTargetName.d.ts.map +1 -0
  120. package/dist/handlers/dbt/getDbtProfileTargetName.js +44 -0
  121. package/dist/handlers/dbt/getDbtVersion.d.ts +16 -0
  122. package/dist/handlers/dbt/getDbtVersion.d.ts.map +1 -0
  123. package/dist/handlers/dbt/getDbtVersion.js +141 -0
  124. package/dist/handlers/dbt/getDbtVersion.mocks.d.ts +11 -0
  125. package/dist/handlers/dbt/getDbtVersion.mocks.d.ts.map +1 -0
  126. package/dist/handlers/dbt/getDbtVersion.mocks.js +70 -0
  127. package/dist/handlers/dbt/getDbtVersion.test.d.ts +2 -0
  128. package/dist/handlers/dbt/getDbtVersion.test.d.ts.map +1 -0
  129. package/dist/handlers/dbt/getDbtVersion.test.js +97 -0
  130. package/dist/handlers/dbt/getWarehouseClient.d.ts +24 -0
  131. package/dist/handlers/dbt/getWarehouseClient.d.ts.map +1 -0
  132. package/dist/handlers/dbt/getWarehouseClient.js +312 -0
  133. package/dist/handlers/dbt/refresh.d.ts +11 -0
  134. package/dist/handlers/dbt/refresh.d.ts.map +1 -0
  135. package/dist/handlers/dbt/refresh.js +114 -0
  136. package/dist/handlers/dbt/run.d.ts +14 -0
  137. package/dist/handlers/dbt/run.d.ts.map +1 -0
  138. package/dist/handlers/dbt/run.js +67 -0
  139. package/dist/handlers/deploy.d.ts +26 -0
  140. package/dist/handlers/deploy.d.ts.map +1 -0
  141. package/dist/handlers/deploy.js +377 -0
  142. package/dist/handlers/diagnostics.d.ts +11 -0
  143. package/dist/handlers/diagnostics.d.ts.map +1 -0
  144. package/dist/handlers/diagnostics.js +194 -0
  145. package/dist/handlers/download.d.ts +29 -0
  146. package/dist/handlers/download.d.ts.map +1 -0
  147. package/dist/handlers/download.js +955 -0
  148. package/dist/handlers/exportChartImage.d.ts +7 -0
  149. package/dist/handlers/exportChartImage.d.ts.map +1 -0
  150. package/dist/handlers/exportChartImage.js +33 -0
  151. package/dist/handlers/generate.d.ts +13 -0
  152. package/dist/handlers/generate.d.ts.map +1 -0
  153. package/dist/handlers/generate.js +159 -0
  154. package/dist/handlers/generateExposures.d.ts +8 -0
  155. package/dist/handlers/generateExposures.d.ts.map +1 -0
  156. package/dist/handlers/generateExposures.js +100 -0
  157. package/dist/handlers/getProject.d.ts +6 -0
  158. package/dist/handlers/getProject.d.ts.map +1 -0
  159. package/dist/handlers/getProject.js +43 -0
  160. package/dist/handlers/installSkills.d.ts +12 -0
  161. package/dist/handlers/installSkills.d.ts.map +1 -0
  162. package/dist/handlers/installSkills.js +321 -0
  163. package/dist/handlers/lint/ajvToSarif.d.ts +66 -0
  164. package/dist/handlers/lint/ajvToSarif.d.ts.map +1 -0
  165. package/dist/handlers/lint/ajvToSarif.js +222 -0
  166. package/dist/handlers/lint/sarifFormatter.d.ts +14 -0
  167. package/dist/handlers/lint/sarifFormatter.d.ts.map +1 -0
  168. package/dist/handlers/lint/sarifFormatter.js +111 -0
  169. package/dist/handlers/lint.d.ts +8 -0
  170. package/dist/handlers/lint.d.ts.map +1 -0
  171. package/dist/handlers/lint.js +308 -0
  172. package/dist/handlers/listProjects.d.ts +6 -0
  173. package/dist/handlers/listProjects.d.ts.map +1 -0
  174. package/dist/handlers/listProjects.js +53 -0
  175. package/dist/handlers/login/oauth.d.ts +2 -0
  176. package/dist/handlers/login/oauth.d.ts.map +1 -0
  177. package/dist/handlers/login/oauth.js +27 -0
  178. package/dist/handlers/login/pat.d.ts +2 -0
  179. package/dist/handlers/login/pat.d.ts.map +1 -0
  180. package/dist/handlers/login/pat.js +31 -0
  181. package/dist/handlers/login.d.ts +15 -0
  182. package/dist/handlers/login.d.ts.map +1 -0
  183. package/dist/handlers/login.js +239 -0
  184. package/dist/handlers/metadataFile.d.ts +9 -0
  185. package/dist/handlers/metadataFile.d.ts.map +1 -0
  186. package/dist/handlers/metadataFile.js +34 -0
  187. package/dist/handlers/oauthLogin.d.ts +6 -0
  188. package/dist/handlers/oauthLogin.d.ts.map +1 -0
  189. package/dist/handlers/oauthLogin.js +191 -0
  190. package/dist/handlers/preview.d.ts +29 -0
  191. package/dist/handlers/preview.d.ts.map +1 -0
  192. package/dist/handlers/preview.js +415 -0
  193. package/dist/handlers/renameHandler.d.ts +16 -0
  194. package/dist/handlers/renameHandler.d.ts.map +1 -0
  195. package/dist/handlers/renameHandler.js +160 -0
  196. package/dist/handlers/runChart.d.ts +10 -0
  197. package/dist/handlers/runChart.d.ts.map +1 -0
  198. package/dist/handlers/runChart.js +105 -0
  199. package/dist/handlers/selectProject.d.ts +20 -0
  200. package/dist/handlers/selectProject.d.ts.map +1 -0
  201. package/dist/handlers/selectProject.js +91 -0
  202. package/dist/handlers/setProject.d.ts +14 -0
  203. package/dist/handlers/setProject.d.ts.map +1 -0
  204. package/dist/handlers/setProject.js +131 -0
  205. package/dist/handlers/setWarehouse.d.ts +14 -0
  206. package/dist/handlers/setWarehouse.d.ts.map +1 -0
  207. package/dist/handlers/setWarehouse.js +94 -0
  208. package/dist/handlers/sql.d.ts +9 -0
  209. package/dist/handlers/sql.d.ts.map +1 -0
  210. package/dist/handlers/sql.js +89 -0
  211. package/dist/handlers/utils.d.ts +11 -0
  212. package/dist/handlers/utils.d.ts.map +1 -0
  213. package/dist/handlers/utils.js +36 -0
  214. package/dist/handlers/validate.d.ts +22 -0
  215. package/dist/handlers/validate.d.ts.map +1 -0
  216. package/dist/handlers/validate.js +201 -0
  217. package/dist/index.d.ts +3 -0
  218. package/dist/index.d.ts.map +1 -0
  219. package/dist/index.js +581 -0
  220. package/dist/lightdash/loader.d.ts +21 -0
  221. package/dist/lightdash/loader.d.ts.map +1 -0
  222. package/dist/lightdash/loader.js +122 -0
  223. package/dist/lightdash/projectType.d.ts +84 -0
  224. package/dist/lightdash/projectType.d.ts.map +1 -0
  225. package/dist/lightdash/projectType.js +75 -0
  226. package/dist/lightdash-config/index.d.ts +2 -0
  227. package/dist/lightdash-config/index.d.ts.map +1 -0
  228. package/dist/lightdash-config/index.js +41 -0
  229. package/dist/lightdash-config/lightdash-config.test.d.ts +2 -0
  230. package/dist/lightdash-config/lightdash-config.test.d.ts.map +1 -0
  231. package/dist/lightdash-config/lightdash-config.test.js +70 -0
  232. package/dist/styles.d.ts +10 -0
  233. package/dist/styles.d.ts.map +1 -0
  234. package/dist/styles.js +14 -0
  235. package/entitlements.plist +33 -0
  236. package/package.json +71 -0
  237. package/track.sh +116 -0
@@ -0,0 +1,955 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.uploadHandler = exports.downloadHandler = exports.downloadContent = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /* eslint-disable no-await-in-loop */
6
+ /* eslint-disable no-param-reassign */
7
+ const common_1 = require("@lightdash/common");
8
+ const fs_1 = require("fs");
9
+ const yaml = tslib_1.__importStar(require("js-yaml"));
10
+ const groupBy_1 = tslib_1.__importDefault(require("lodash/groupBy"));
11
+ const p_limit_1 = tslib_1.__importDefault(require("p-limit"));
12
+ const path = tslib_1.__importStar(require("path"));
13
+ const analytics_1 = require("../analytics/analytics");
14
+ const config_1 = require("../config");
15
+ const globalState_1 = tslib_1.__importDefault(require("../globalState"));
16
+ const styles = tslib_1.__importStar(require("../styles"));
17
+ const apiClient_1 = require("./dbt/apiClient");
18
+ const metadataFile_1 = require("./metadataFile");
19
+ const selectProject_1 = require("./selectProject");
20
+ const getDownloadFolder = (customPath) => {
21
+ if (customPath) {
22
+ return path.isAbsolute(customPath)
23
+ ? customPath
24
+ : path.join(process.cwd(), customPath);
25
+ }
26
+ return path.join(process.cwd(), 'clary');
27
+ };
28
+ /*
29
+ This function is used to parse the content filters.
30
+ It can be slugs, uuids or urls
31
+ We remove the URL part (if any) and return a list of `slugs or uuids` that can be used in the API call
32
+ */
33
+ const parseContentFilters = (items) => {
34
+ if (items.length === 0)
35
+ return '';
36
+ const parsedItems = items.map((item) => {
37
+ const uuidMatch = item.match(/https?:\/\/.+\/(?:saved|dashboards)\/([a-f0-9-]+)/i);
38
+ return uuidMatch ? uuidMatch[1] : item;
39
+ });
40
+ return `?${new URLSearchParams(parsedItems.map((item) => ['ids', item])).toString()}`;
41
+ };
42
+ const createDirForContent = async (projectName, spaceSlug, folder, customPath, folderScheme) => {
43
+ const baseDir = getDownloadFolder(customPath);
44
+ let outputDir;
45
+ if (folderScheme === 'flat') {
46
+ // Flat scheme: baseDir/folder
47
+ outputDir = path.join(baseDir, folder);
48
+ }
49
+ else {
50
+ // Nested scheme: baseDir/projectName/spaceSlug/folder
51
+ outputDir = path.join(baseDir, projectName, spaceSlug, folder);
52
+ }
53
+ globalState_1.default.debug(`Creating directory: ${outputDir}`);
54
+ await fs_1.promises.mkdir(outputDir, { recursive: true });
55
+ return outputDir;
56
+ };
57
+ /**
58
+ * Get file extension for content-as-code files.
59
+ * SQL charts use '.sql.yml' extension to avoid filename conflicts with regular charts
60
+ * that may have the same slug, since both chart types share the same output directory.
61
+ */
62
+ const getFileExtension = (contentType) => {
63
+ switch (contentType) {
64
+ case 'sqlChart':
65
+ return '.sql.yml';
66
+ case 'chart':
67
+ case 'dashboard':
68
+ default:
69
+ return '.yml';
70
+ }
71
+ };
72
+ const writeContent = async (contentAsCode, outputDir, languageMap) => {
73
+ const extension = getFileExtension(contentAsCode.type);
74
+ const itemPath = path.join(outputDir, `${contentAsCode.content.slug}${extension}`);
75
+ // Strip timestamps — they go to .clary-metadata.json instead
76
+ const { updatedAt, downloadedAt, ...cleanContent } = contentAsCode.content;
77
+ const chartYml = yaml.dump(cleanContent, {
78
+ quotingType: '"',
79
+ sortKeys: true,
80
+ });
81
+ await fs_1.promises.writeFile(itemPath, chartYml);
82
+ if (contentAsCode.translationMap && languageMap) {
83
+ const translationPath = path.join(outputDir, `${contentAsCode.content.slug}.language.map.yml`);
84
+ await fs_1.promises.writeFile(translationPath, yaml.dump(contentAsCode.translationMap, { sortKeys: true }));
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
+ /**
104
+ * Writes space YAML files for each space in the download results.
105
+ * In flat mode, files go in the base download directory.
106
+ * In nested mode, files go in the root of each space's directory.
107
+ */
108
+ const writeSpaceFiles = async (spaces, projectName, customPath, folderScheme = 'flat') => {
109
+ if (spaces.length === 0)
110
+ return;
111
+ const baseDir = getDownloadFolder(customPath);
112
+ const seen = new Set();
113
+ for (const space of spaces) {
114
+ // Deduplicate across paginated API responses
115
+ if (!seen.has(space.slug)) {
116
+ seen.add(space.slug);
117
+ let outputDir;
118
+ if (folderScheme === 'nested') {
119
+ outputDir = path.join(baseDir, projectName, space.slug);
120
+ }
121
+ else {
122
+ outputDir = baseDir;
123
+ }
124
+ await fs_1.promises.mkdir(outputDir, { recursive: true });
125
+ const fileName = `${(0, common_1.generateSlug)(space.spaceName)}.space.yml`;
126
+ const filePath = path.join(outputDir, fileName);
127
+ const content = yaml.dump(space, {
128
+ quotingType: '"',
129
+ sortKeys: true,
130
+ });
131
+ await fs_1.promises.writeFile(filePath, content);
132
+ globalState_1.default.debug(`Wrote space file: ${filePath}`);
133
+ }
134
+ }
135
+ };
136
+ /**
137
+ * Reads all .space.yml files from the download directory and returns
138
+ * a map of space slug → original space name. Used during upload to
139
+ * preserve human-readable space names instead of deriving them from slugs.
140
+ */
141
+ const readSpaceNames = async (customPath) => {
142
+ const baseDir = getDownloadFolder(customPath);
143
+ const spaceNames = {};
144
+ try {
145
+ const allEntries = await fs_1.promises.readdir(baseDir, {
146
+ recursive: true,
147
+ withFileTypes: true,
148
+ });
149
+ await Promise.all(allEntries
150
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.space.yml'))
151
+ .map(async (file) => {
152
+ try {
153
+ const filePath = path.join(file.parentPath, file.name);
154
+ const fileContent = await fs_1.promises.readFile(filePath, 'utf-8');
155
+ const parsed = yaml.load(fileContent);
156
+ if (parsed?.contentType ===
157
+ common_1.ContentAsCodeType.SPACE &&
158
+ typeof parsed.slug === 'string' &&
159
+ typeof parsed.spaceName === 'string') {
160
+ spaceNames[parsed.slug] = parsed.spaceName;
161
+ }
162
+ }
163
+ catch (e) {
164
+ globalState_1.default.debug(`Skipping space file ${file.name}: ${(0, common_1.getErrorMessage)(e)}`);
165
+ }
166
+ }));
167
+ }
168
+ catch {
169
+ // Directory doesn't exist or can't be read — return empty map
170
+ }
171
+ return spaceNames;
172
+ };
173
+ const hasUnsortedKeys = (obj) => {
174
+ if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) {
175
+ if (Array.isArray(obj)) {
176
+ return obj.some(hasUnsortedKeys);
177
+ }
178
+ return false;
179
+ }
180
+ const keys = Object.keys(obj);
181
+ const sorted = [...keys].sort();
182
+ if (keys.some((key, i) => key !== sorted[i])) {
183
+ return true;
184
+ }
185
+ return Object.values(obj).some(hasUnsortedKeys);
186
+ };
187
+ const isLightdashContentFile = (folder, entry) => entry.isFile() &&
188
+ entry.parentPath &&
189
+ entry.parentPath.endsWith(path.sep + folder) &&
190
+ entry.name.endsWith('.yml') &&
191
+ !entry.name.endsWith('.language.map.yml');
192
+ const isLooseContentFile = (entry) => entry.isFile() &&
193
+ entry.parentPath &&
194
+ !entry.parentPath.endsWith(`${path.sep}charts`) &&
195
+ !entry.parentPath.endsWith(`${path.sep}dashboards`) &&
196
+ entry.name.endsWith('.yml') &&
197
+ !entry.name.endsWith('.language.map.yml');
198
+ const processYamlItem = (item, fileName, stats, folder, metadata) => {
199
+ if (hasUnsortedKeys(item)) {
200
+ globalState_1.default.log(styles.warning(`Warning: ${fileName} has unsorted YAML keys. Re-download to fix, or sort keys alphabetically.`));
201
+ }
202
+ const metadataSection = folder === 'dashboards' ? metadata.dashboards : metadata.charts;
203
+ const downloadedAtRaw = metadataSection[item.slug] ?? item.downloadedAt;
204
+ const downloadedAt = downloadedAtRaw
205
+ ? new Date(downloadedAtRaw instanceof Date
206
+ ? downloadedAtRaw.getTime()
207
+ : downloadedAtRaw)
208
+ : undefined;
209
+ const needsUpdating = downloadedAt &&
210
+ Math.abs(stats.mtime.getTime() - downloadedAt.getTime()) > 30000;
211
+ return {
212
+ ...item,
213
+ updatedAt: needsUpdating ? stats.mtime : item.updatedAt,
214
+ needsUpdating: needsUpdating ?? true,
215
+ };
216
+ };
217
+ const loadYamlFile = async (file, folder, metadata) => {
218
+ const filePath = path.join(file.parentPath, file.name);
219
+ const [fileContent, stats] = await Promise.all([
220
+ fs_1.promises.readFile(filePath, 'utf-8'),
221
+ fs_1.promises.stat(filePath),
222
+ ]);
223
+ const item = yaml.load(fileContent);
224
+ return processYamlItem(item, file.name, stats, folder, metadata);
225
+ };
226
+ const readCodeFiles = async (folder, customPath) => {
227
+ const baseDir = getDownloadFolder(customPath);
228
+ globalState_1.default.log(`Reading ${folder} from ${baseDir}`);
229
+ const [major, minor] = process.versions.node.split('.').map(Number);
230
+ if (major < 20 || (major === 20 && minor < 12)) {
231
+ throw new Error(`Node.js v20.12.0 or later is required for this command (current: ${process.version}).`);
232
+ }
233
+ try {
234
+ const metadata = await (0, metadataFile_1.readMetadataFile)(baseDir);
235
+ const allEntries = await fs_1.promises.readdir(baseDir, {
236
+ recursive: true,
237
+ withFileTypes: true,
238
+ });
239
+ const items = await Promise.all(allEntries
240
+ .filter((entry) => isLightdashContentFile(folder, entry))
241
+ .map((file) => loadYamlFile(file, folder, metadata)));
242
+ if (items.length === 0) {
243
+ console.error(styles.warning(`Unable to upload ${folder}, no files found in "${baseDir}". Run download command first.`));
244
+ }
245
+ return items;
246
+ }
247
+ catch (error) {
248
+ // Handle case where base directory doesn't exist
249
+ if (error.code === 'ENOENT') {
250
+ console.error(styles.warning(`Unable to upload ${folder}, "${baseDir}" folder not found. Run download command first.`));
251
+ return [];
252
+ }
253
+ // Unknown error
254
+ console.error(styles.error(`Error reading ${baseDir}: ${error}`));
255
+ throw error;
256
+ }
257
+ };
258
+ /**
259
+ * Reads YAML files outside the standard charts/ and dashboards/ directories
260
+ * and classifies them by their contentType field.
261
+ */
262
+ const readLooseCodeFiles = async (customPath) => {
263
+ const baseDir = getDownloadFolder(customPath);
264
+ const charts = [];
265
+ const dashboards = [];
266
+ try {
267
+ const metadata = await (0, metadataFile_1.readMetadataFile)(baseDir);
268
+ const allEntries = await fs_1.promises.readdir(baseDir, {
269
+ recursive: true,
270
+ withFileTypes: true,
271
+ });
272
+ const looseFiles = allEntries.filter(isLooseContentFile);
273
+ await Promise.all(looseFiles.map(async (file) => {
274
+ try {
275
+ const filePath = path.join(file.parentPath, file.name);
276
+ const [fileContent, stats] = await Promise.all([
277
+ fs_1.promises.readFile(filePath, 'utf-8'),
278
+ fs_1.promises.stat(filePath),
279
+ ]);
280
+ const parsed = yaml.load(fileContent);
281
+ const contentType = parsed?.contentType;
282
+ if (contentType === common_1.ContentAsCodeType.CHART ||
283
+ contentType === common_1.ContentAsCodeType.SQL_CHART) {
284
+ charts.push(processYamlItem(parsed, file.name, stats, 'charts', metadata));
285
+ }
286
+ else if (contentType === common_1.ContentAsCodeType.DASHBOARD) {
287
+ dashboards.push(processYamlItem(parsed, file.name, stats, 'dashboards', metadata));
288
+ }
289
+ else if (contentType === common_1.ContentAsCodeType.SPACE) {
290
+ // Space YAML files are metadata-only until the next PR
291
+ }
292
+ else {
293
+ globalState_1.default.debug(`Skipping ${file.name}: no recognized contentType`);
294
+ }
295
+ }
296
+ catch (e) {
297
+ globalState_1.default.debug(`Skipping ${file.name}: failed to parse (${(0, common_1.getErrorMessage)(e)})`);
298
+ }
299
+ }));
300
+ }
301
+ catch (error) {
302
+ if (error.code === 'ENOENT') {
303
+ // Base directory doesn't exist — nothing to discover
304
+ return { charts, dashboards };
305
+ }
306
+ throw error;
307
+ }
308
+ return { charts, dashboards };
309
+ };
310
+ const groupBySpace = (items) => {
311
+ const itemsWithIndex = items.map((item, index) => ({ item, index }));
312
+ return (0, groupBy_1.default)(itemsWithIndex, (entry) => entry.item.spaceSlug);
313
+ };
314
+ const writeSpaceContent = async ({ projectName, spaceSlug, folder, contentType, contentInSpace, contentAsCode, customPath, languageMap, folderScheme, }) => {
315
+ const outputDir = await createDirForContent(projectName, spaceSlug, folder, customPath, folderScheme);
316
+ const entries = [];
317
+ for (const { item, index } of contentInSpace) {
318
+ const translationMap = 'languageMap' in contentAsCode
319
+ ? contentAsCode.languageMap?.[index]
320
+ : undefined;
321
+ const entry = await writeContent({
322
+ type: contentType,
323
+ content: item,
324
+ translationMap,
325
+ }, outputDir, languageMap);
326
+ entries.push(entry);
327
+ }
328
+ return entries;
329
+ };
330
+ const getContentTypeConfig = (type, projectId) => {
331
+ switch (type) {
332
+ case 'charts':
333
+ return {
334
+ endpoint: `/api/v1/projects/${projectId}/charts/code`,
335
+ displayName: 'charts',
336
+ supportsLanguageMap: true,
337
+ };
338
+ case 'dashboards':
339
+ return {
340
+ endpoint: `/api/v1/projects/${projectId}/dashboards/code`,
341
+ displayName: 'dashboards',
342
+ supportsLanguageMap: true,
343
+ };
344
+ case 'sqlCharts':
345
+ return {
346
+ endpoint: `/api/v1/projects/${projectId}/sqlCharts/code`,
347
+ displayName: 'SQL charts',
348
+ supportsLanguageMap: false,
349
+ };
350
+ default:
351
+ return (0, common_1.assertUnreachable)(type, `Unknown content type: ${type}`);
352
+ }
353
+ };
354
+ const extractChartSlugsFromDashboards = (dashboards) => dashboards.reduce((acc, dashboard) => {
355
+ const slugs = dashboard.tiles
356
+ .map((tile) => 'chartSlug' in tile.properties
357
+ ? tile.properties.chartSlug
358
+ : undefined)
359
+ .filter((slug) => slug !== undefined);
360
+ return [...acc, ...slugs];
361
+ }, []);
362
+ const downloadContent = async (ids, type, projectId, projectName, customPath, languageMap = false, nested = false, skipSpaces = false) => {
363
+ const spinner = globalState_1.default.getActiveSpinner();
364
+ const contentFilters = parseContentFilters(ids);
365
+ const folderScheme = nested ? 'nested' : 'flat';
366
+ const config = getContentTypeConfig(type, projectId);
367
+ let offset = 0;
368
+ let total = 0;
369
+ let chartSlugs = [];
370
+ let allMetadataEntries = [];
371
+ let allSpaces = [];
372
+ do {
373
+ globalState_1.default.debug(`Downloading ${config.displayName} with offset "${offset}" and filters "${contentFilters}"`);
374
+ const commonParams = config.supportsLanguageMap
375
+ ? `offset=${offset}&languageMap=${languageMap}`
376
+ : `offset=${offset}`;
377
+ const queryParams = contentFilters
378
+ ? `${contentFilters}&${commonParams}`
379
+ : `?${commonParams}`;
380
+ const results = await (0, apiClient_1.lightdashApi)({
381
+ method: 'GET',
382
+ url: `${config.endpoint}${queryParams}`,
383
+ body: undefined,
384
+ });
385
+ spinner?.start(`Downloaded ${results.offset} of ${results.total} ${config.displayName}`);
386
+ // For the same chart slug, we run the code for saved charts and sql chart
387
+ // so we are going to get more false positives here, so we keep it on the debug log
388
+ results.missingIds.forEach((missingId) => {
389
+ globalState_1.default.debug(`\nNo ${config.displayName} with id "${missingId}"`);
390
+ });
391
+ // Write content based on type
392
+ if ('sqlCharts' in results) {
393
+ const sqlChartsBySpace = groupBySpace(results.sqlCharts);
394
+ for (const [spaceSlug, sqlChartsInSpace] of Object.entries(sqlChartsBySpace)) {
395
+ const entries = await writeSpaceContent({
396
+ projectName,
397
+ spaceSlug,
398
+ folder: 'charts',
399
+ contentType: 'sqlChart',
400
+ contentInSpace: sqlChartsInSpace,
401
+ contentAsCode: results,
402
+ customPath,
403
+ languageMap,
404
+ folderScheme,
405
+ });
406
+ allMetadataEntries = [...allMetadataEntries, ...entries];
407
+ }
408
+ }
409
+ else if ('dashboards' in results) {
410
+ const dashboardsBySpace = groupBySpace(results.dashboards);
411
+ for (const [spaceSlug, dashboardsInSpace] of Object.entries(dashboardsBySpace)) {
412
+ const entries = await writeSpaceContent({
413
+ projectName,
414
+ spaceSlug,
415
+ folder: 'dashboards',
416
+ contentType: 'dashboard',
417
+ contentInSpace: dashboardsInSpace,
418
+ contentAsCode: results,
419
+ customPath,
420
+ languageMap,
421
+ folderScheme,
422
+ });
423
+ allMetadataEntries = [...allMetadataEntries, ...entries];
424
+ }
425
+ chartSlugs = [
426
+ ...chartSlugs,
427
+ ...extractChartSlugsFromDashboards(results.dashboards),
428
+ ];
429
+ }
430
+ else {
431
+ const chartsBySpace = groupBySpace(results.charts);
432
+ for (const [spaceSlug, chartsInSpace] of Object.entries(chartsBySpace)) {
433
+ const entries = await writeSpaceContent({
434
+ projectName,
435
+ spaceSlug,
436
+ folder: 'charts',
437
+ contentType: 'chart',
438
+ contentInSpace: chartsInSpace,
439
+ contentAsCode: results,
440
+ customPath,
441
+ languageMap,
442
+ folderScheme,
443
+ });
444
+ allMetadataEntries = [...allMetadataEntries, ...entries];
445
+ }
446
+ }
447
+ // Accumulate space metadata from each page
448
+ if ('spaces' in results && results.spaces) {
449
+ allSpaces = [...allSpaces, ...results.spaces];
450
+ }
451
+ offset = results.offset;
452
+ total = results.total;
453
+ } while (offset < total);
454
+ // Write space YAML files
455
+ if (!skipSpaces) {
456
+ await writeSpaceFiles(allSpaces, projectName, customPath, folderScheme);
457
+ }
458
+ return [total, [...new Set(chartSlugs)], allMetadataEntries, allSpaces];
459
+ };
460
+ exports.downloadContent = downloadContent;
461
+ const downloadHandler = async (options) => {
462
+ globalState_1.default.setVerbose(options.verbose);
463
+ await (0, apiClient_1.checkLightdashVersion)();
464
+ const config = await (0, config_1.getConfig)();
465
+ if (!config.context?.apiKey || !config.context.serverUrl) {
466
+ throw new common_1.AuthorizationError(`Not logged in. Run 'clary login --help'`);
467
+ }
468
+ const projectSelection = await (0, selectProject_1.selectProject)(config, options.project);
469
+ if (!projectSelection) {
470
+ throw new common_1.LightdashError({
471
+ message: 'No project selected. Run clary config set-project',
472
+ name: 'Not Found',
473
+ statusCode: 404,
474
+ data: {},
475
+ });
476
+ }
477
+ const projectId = projectSelection.projectUuid;
478
+ // Log current project info
479
+ (0, selectProject_1.logSelectedProject)(projectSelection, config, 'Downloading from');
480
+ const spinner = globalState_1.default.startSpinner(`Downloading charts`);
481
+ spinner.start(`Downloading content from project`);
482
+ // Fetch project details to get project name for folder structure
483
+ const project = await (0, apiClient_1.lightdashApi)({
484
+ method: 'GET',
485
+ url: `/api/v1/projects/${projectId}`,
486
+ body: undefined,
487
+ });
488
+ const projectName = (0, common_1.generateSlug)(project.name);
489
+ // For analytics
490
+ let chartTotal;
491
+ let dashboardTotal;
492
+ const start = Date.now();
493
+ await analytics_1.LightdashAnalytics.track({
494
+ event: 'download.started',
495
+ properties: {
496
+ userId: config.user?.userUuid,
497
+ organizationId: config.user?.organizationUuid,
498
+ projectId,
499
+ },
500
+ });
501
+ try {
502
+ const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
503
+ // When downloading specific charts or dashboards, skip space metadata
504
+ const skipSpaces = options.skipSpaces || hasFilters;
505
+ let allMetadataEntries = [];
506
+ let allSpaces = [];
507
+ // Download regular charts
508
+ if (hasFilters && options.charts.length === 0) {
509
+ console.info(styles.warning(`No charts filters provided, skipping`));
510
+ }
511
+ else {
512
+ const [regularChartTotal, , regularChartMeta, regularChartSpaces] = await (0, exports.downloadContent)(options.charts, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipSpaces);
513
+ spinner.succeed(`Downloaded ${regularChartTotal} charts`);
514
+ allMetadataEntries = [...allMetadataEntries, ...regularChartMeta];
515
+ allSpaces = [...allSpaces, ...regularChartSpaces];
516
+ // Download SQL charts
517
+ spinner.start(`Downloading SQL charts`);
518
+ const [sqlChartTotal, , sqlChartMeta, sqlChartSpaces] = await (0, exports.downloadContent)(options.charts, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipSpaces);
519
+ spinner.succeed(`Downloaded ${sqlChartTotal} SQL charts`);
520
+ allMetadataEntries = [...allMetadataEntries, ...sqlChartMeta];
521
+ allSpaces = [...allSpaces, ...sqlChartSpaces];
522
+ chartTotal = regularChartTotal + sqlChartTotal;
523
+ }
524
+ // Download dashboards
525
+ if (hasFilters && options.dashboards.length === 0) {
526
+ console.info(styles.warning(`No dashboards filters provided, skipping`));
527
+ }
528
+ else {
529
+ let chartSlugs = [];
530
+ let dashMeta;
531
+ let dashSpaces;
532
+ [dashboardTotal, chartSlugs, dashMeta, dashSpaces] =
533
+ await (0, exports.downloadContent)(options.dashboards, 'dashboards', projectId, projectName, options.path, options.languageMap, options.nested, skipSpaces);
534
+ allMetadataEntries = [...allMetadataEntries, ...dashMeta];
535
+ allSpaces = [...allSpaces, ...dashSpaces];
536
+ spinner.succeed(`Downloaded ${dashboardTotal} dashboards`);
537
+ if (hasFilters && chartSlugs.length > 0) {
538
+ spinner.start(`Downloading ${chartSlugs.length} charts linked to dashboards`);
539
+ const [regularCharts, , linkedChartMeta, linkedChartSpaces] = await (0, exports.downloadContent)(chartSlugs, 'charts', projectId, projectName, options.path, options.languageMap, options.nested, skipSpaces);
540
+ allMetadataEntries = [
541
+ ...allMetadataEntries,
542
+ ...linkedChartMeta,
543
+ ];
544
+ allSpaces = [...allSpaces, ...linkedChartSpaces];
545
+ const [sqlCharts, , linkedSqlMeta, linkedSqlSpaces] = await (0, exports.downloadContent)(chartSlugs, 'sqlCharts', projectId, projectName, options.path, options.languageMap, options.nested, skipSpaces);
546
+ allMetadataEntries = [...allMetadataEntries, ...linkedSqlMeta];
547
+ allSpaces = [...allSpaces, ...linkedSqlSpaces];
548
+ spinner.succeed(`Downloaded ${regularCharts + sqlCharts} charts linked to dashboards`);
549
+ }
550
+ }
551
+ // Report space definitions count
552
+ if (!skipSpaces) {
553
+ const uniqueSpaceCount = new Set(allSpaces.map((s) => s.slug)).size;
554
+ spinner.succeed(`Downloaded ${uniqueSpaceCount} space definitions`);
555
+ }
556
+ // Write metadata file with all downloadedAt timestamps
557
+ const metadataToWrite = {
558
+ version: 1,
559
+ charts: {},
560
+ dashboards: {},
561
+ };
562
+ for (const entry of allMetadataEntries) {
563
+ metadataToWrite[entry.type][entry.slug] = entry.downloadedAt;
564
+ }
565
+ const baseDir = getDownloadFolder(options.path);
566
+ const downloadRoot = options.nested
567
+ ? path.join(baseDir, projectName)
568
+ : baseDir;
569
+ await (0, metadataFile_1.writeMetadataFile)(baseDir, metadataToWrite);
570
+ if (!config.answers?.metadataFileGitignoreNoticeShown) {
571
+ globalState_1.default.log(styles.warning(`\nNote: ${metadataFile_1.METADATA_FILENAME} was written to ${baseDir}. Consider adding it to your .gitignore.`));
572
+ await (0, config_1.setAnswer)({ metadataFileGitignoreNoticeShown: true });
573
+ }
574
+ globalState_1.default.log(styles.success(`Downloaded content saved to ${downloadRoot}`));
575
+ const end = Date.now();
576
+ await analytics_1.LightdashAnalytics.track({
577
+ event: 'download.completed',
578
+ properties: {
579
+ userId: config.user?.userUuid,
580
+ organizationId: config.user?.organizationUuid,
581
+ projectId,
582
+ chartsNum: chartTotal,
583
+ dashboardsNum: dashboardTotal,
584
+ timeToCompleted: (end - start) / 1000,
585
+ },
586
+ });
587
+ }
588
+ catch (error) {
589
+ console.error(styles.error(`\nError downloading ${error}`));
590
+ await analytics_1.LightdashAnalytics.track({
591
+ event: 'download.error',
592
+ properties: {
593
+ userId: config.user?.userUuid,
594
+ organizationId: config.user?.organizationUuid,
595
+ projectId,
596
+ error: `${error}`,
597
+ },
598
+ });
599
+ }
600
+ };
601
+ exports.downloadHandler = downloadHandler;
602
+ const getPromoteAction = (action) => {
603
+ switch (action) {
604
+ case common_1.PromotionAction.CREATE:
605
+ return 'created';
606
+ case common_1.PromotionAction.UPDATE:
607
+ return 'updated';
608
+ case common_1.PromotionAction.DELETE:
609
+ return 'deleted';
610
+ case common_1.PromotionAction.NO_CHANGES:
611
+ return 'skipped';
612
+ default:
613
+ (0, common_1.assertUnreachable)(action, `Unknown promotion action: ${action}`);
614
+ }
615
+ return 'skipped';
616
+ };
617
+ const storeUploadChanges = (changes, promoteChanges) => {
618
+ const getPromoteChanges = (resource) => {
619
+ const promotions = promoteChanges[resource];
620
+ return promotions.reduce((acc, promoteChange) => {
621
+ const action = getPromoteAction(promoteChange.action);
622
+ const key = `${resource} ${action}`;
623
+ acc[key] = (acc[key] ?? 0) + 1;
624
+ return acc;
625
+ }, {});
626
+ };
627
+ const updatedChanges = {
628
+ ...changes,
629
+ };
630
+ ['spaces', 'charts', 'dashboards'].forEach((resource) => {
631
+ const resourceChanges = getPromoteChanges(resource);
632
+ Object.entries(resourceChanges).forEach(([key, value]) => {
633
+ updatedChanges[key] = (updatedChanges[key] ?? 0) + value;
634
+ });
635
+ });
636
+ return updatedChanges;
637
+ };
638
+ const logUploadChanges = (changes) => {
639
+ Object.entries(changes).forEach(([key, value]) => {
640
+ console.info(`Total ${key}: ${value} `);
641
+ });
642
+ const totalSkipped = Object.entries(changes)
643
+ .filter(([key]) => key.includes('skipped'))
644
+ .reduce((sum, [, value]) => sum + value, 0);
645
+ const totalUpserted = Object.entries(changes)
646
+ .filter(([key]) => !key.includes('skipped'))
647
+ .reduce((sum, [, value]) => sum + value, 0);
648
+ if (totalSkipped > 0 && totalUpserted === 0) {
649
+ console.warn(styles.warning(`\nAll content was skipped (no local changes detected). Use --force to upload all content, e.g. when uploading to a new project.`));
650
+ }
651
+ };
652
+ // SQL charts have 'sql' field instead of 'tableName'/'metricQuery'
653
+ const isSqlChart = (item) => 'sql' in item && !('tableName' in item);
654
+ const upsertSingleItem = async (item, type, projectId, changes, force, config, skipSpaceCreate, publicSpaceCreate, validate, spaceNames) => {
655
+ try {
656
+ if (!force && !item.needsUpdating) {
657
+ globalState_1.default.debug(`Skipping ${type} "${item.slug}" with no local changes`);
658
+ changes[`${type} skipped`] = (changes[`${type} skipped`] ?? 0) + 1;
659
+ return;
660
+ }
661
+ globalState_1.default.debug(`Upserting ${type} ${item.slug}`);
662
+ // SQL charts use a different endpoint
663
+ const isSqlChartItem = type === 'charts' && isSqlChart(item);
664
+ const endpoint = isSqlChartItem
665
+ ? `/api/v1/projects/${projectId}/sqlCharts/${item.slug}/code`
666
+ : `/api/v1/projects/${projectId}/${type}/${item.slug}/code`;
667
+ const upsertData = await (0, apiClient_1.lightdashApi)({
668
+ method: 'POST',
669
+ url: endpoint,
670
+ body: JSON.stringify({
671
+ ...item,
672
+ skipSpaceCreate,
673
+ publicSpaceCreate,
674
+ force,
675
+ ...(spaceNames &&
676
+ Object.keys(spaceNames).length > 0 && { spaceNames }),
677
+ }),
678
+ });
679
+ globalState_1.default.debug(`${type} "${item.name}": ${upsertData[type]?.[0].action}`);
680
+ // Merge storeUploadChanges result into changes in-place
681
+ const updatedChanges = storeUploadChanges(changes, upsertData);
682
+ Object.keys(updatedChanges).forEach((key) => {
683
+ changes[key] = updatedChanges[key];
684
+ });
685
+ // Warn if contentType contradicts the folder this item came from
686
+ const itemContentType = item.contentType;
687
+ if (itemContentType) {
688
+ const expectedType = itemContentType === common_1.ContentAsCodeType.DASHBOARD
689
+ ? 'dashboards'
690
+ : 'charts';
691
+ if (expectedType !== type) {
692
+ 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)}.`));
693
+ }
694
+ }
695
+ // Run validation if requested
696
+ if (validate && !isSqlChartItem) {
697
+ const contentUuid = type === 'charts'
698
+ ? upsertData.charts?.[0]?.data?.uuid
699
+ : upsertData.dashboards?.[0]?.data?.uuid;
700
+ if (contentUuid) {
701
+ try {
702
+ const validationEndpoint = type === 'charts'
703
+ ? `/api/v1/projects/${projectId}/validate/chart/${contentUuid}`
704
+ : `/api/v1/projects/${projectId}/validate/dashboard/${contentUuid}`;
705
+ const validationResult = await (0, apiClient_1.lightdashApi)({
706
+ method: 'POST',
707
+ url: validationEndpoint,
708
+ body: JSON.stringify({}),
709
+ });
710
+ if (validationResult.errors &&
711
+ validationResult.errors.length > 0) {
712
+ globalState_1.default.log(styles.warning(`Validation found ${validationResult.errors.length} issue(s) in ${type.slice(0, -1)} "${item.name}"`));
713
+ validationResult.errors.forEach((error) => {
714
+ globalState_1.default.log(styles.warning(` - ${error.error}`));
715
+ });
716
+ }
717
+ else {
718
+ globalState_1.default.log(styles.success(`✓ No validation issues in ${type.slice(0, -1)} "${item.name}"`));
719
+ }
720
+ }
721
+ catch (validationError) {
722
+ globalState_1.default.debug(`Validation failed for ${type.slice(0, -1)} "${item.name}": ${(0, common_1.getErrorMessage)(validationError)}`);
723
+ }
724
+ }
725
+ }
726
+ }
727
+ catch (error) {
728
+ if (error instanceof common_1.LightdashError &&
729
+ error.name === 'NotFoundError' &&
730
+ skipSpaceCreate) {
731
+ globalState_1.default.log(styles.warning(`Skipping ${type} "${item.slug}" because space "${item.spaceSlug}" does not exist and --skip-space-create is true`));
732
+ changes[`${type} skipped`] = (changes[`${type} skipped`] ?? 0) + 1;
733
+ }
734
+ else {
735
+ changes[`${type} with errors`] =
736
+ (changes[`${type} with errors`] ?? 0) + 1;
737
+ globalState_1.default.log(styles.error(`Error upserting ${type}:\n\t"${item.name}" (slug: "${item.slug}")\n\t${(0, common_1.getErrorMessage)(error)}`));
738
+ await analytics_1.LightdashAnalytics.track({
739
+ event: 'download.error',
740
+ properties: {
741
+ userId: config.user?.userUuid,
742
+ organizationId: config.user?.organizationUuid,
743
+ projectId,
744
+ type,
745
+ error: (0, common_1.getErrorMessage)(error),
746
+ },
747
+ });
748
+ }
749
+ }
750
+ };
751
+ /**
752
+ *
753
+ * @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
754
+ */
755
+ const upsertResources = async (type, projectId, changes, force, slugs, customPath, skipSpaceCreate, publicSpaceCreate, validate, concurrency = 1, extraItems = [], spaceNames) => {
756
+ const config = await (0, config_1.getConfig)();
757
+ const folderItems = await readCodeFiles(type, customPath);
758
+ const items = [...folderItems, ...extraItems];
759
+ globalState_1.default.log(`Found ${items.length} ${type} files`);
760
+ const hasFilter = slugs.length > 0;
761
+ const filteredItems = hasFilter
762
+ ? items.filter((item) => slugs.includes(item.slug))
763
+ : items;
764
+ if (hasFilter) {
765
+ globalState_1.default.log(`Filtered ${filteredItems.length} ${type} with slugs: ${slugs.join(', ')}`);
766
+ const missingItems = slugs.filter((slug) => !items.find((item) => item.slug === slug));
767
+ missingItems.forEach((slug) => {
768
+ globalState_1.default.log(styles.warning(`No ${type} with slug: "${slug}"`));
769
+ });
770
+ }
771
+ if (concurrency <= 1) {
772
+ // Sequential path — preserves original behavior exactly
773
+ for (const item of filteredItems) {
774
+ // eslint-disable-next-line no-await-in-loop
775
+ await upsertSingleItem(item, type, projectId, changes, force, config, skipSpaceCreate, publicSpaceCreate, validate, spaceNames);
776
+ }
777
+ }
778
+ else {
779
+ const grouped = (0, groupBy_1.default)(filteredItems, (item) => item.spaceSlug);
780
+ const seedItems = new Set();
781
+ const remainingItems = [];
782
+ Object.values(grouped).forEach((spaceItems) => {
783
+ // Pick the first item that will actually trigger an API call
784
+ // (and thus create the space). If force is true, any item works.
785
+ const seedIndex = force
786
+ ? 0
787
+ : spaceItems.findIndex((i) => i.needsUpdating);
788
+ if (seedIndex >= 0) {
789
+ seedItems.add(spaceItems[seedIndex]);
790
+ remainingItems.push(...spaceItems.filter((_, idx) => idx !== seedIndex));
791
+ }
792
+ else {
793
+ // No items need updating — all will be skipped, no space needed
794
+ remainingItems.push(...spaceItems);
795
+ }
796
+ });
797
+ // For charts: also seed one item per unique dashboardSlug to avoid
798
+ // concurrent placeholder dashboard creation (duplicate slug bug)
799
+ if (type === 'charts') {
800
+ const chartsWithDashboard = remainingItems.filter((item) => 'dashboardSlug' in item &&
801
+ item.dashboardSlug);
802
+ const groupedByDashboard = (0, groupBy_1.default)(chartsWithDashboard, (item) => item.dashboardSlug);
803
+ Object.values(groupedByDashboard).forEach((dashboardItems) => {
804
+ // If no item for this dashboardSlug was already picked as a
805
+ // space seed, pick the first one as a dashboard seed
806
+ const alreadySeeded = dashboardItems.some((item) => seedItems.has(item));
807
+ if (!alreadySeeded) {
808
+ const seedIndex = force
809
+ ? 0
810
+ : dashboardItems.findIndex((i) => i.needsUpdating);
811
+ if (seedIndex >= 0) {
812
+ seedItems.add(dashboardItems[seedIndex]);
813
+ // Remove from remainingItems since it's now a seed
814
+ const idx = remainingItems.indexOf(dashboardItems[seedIndex]);
815
+ if (idx >= 0) {
816
+ remainingItems.splice(idx, 1);
817
+ }
818
+ }
819
+ }
820
+ });
821
+ }
822
+ // Phase 1: Sequential seeding (spaces + dashboard placeholders)
823
+ for (const item of seedItems) {
824
+ // eslint-disable-next-line no-await-in-loop
825
+ await upsertSingleItem(item, type, projectId, changes, force, config, skipSpaceCreate, publicSpaceCreate, validate, spaceNames);
826
+ }
827
+ // Phase 2: Parallel bulk upload of remaining items
828
+ const limit = (0, p_limit_1.default)(concurrency);
829
+ await Promise.all(remainingItems.map((item) => limit(async () => {
830
+ await upsertSingleItem(item, type, projectId, changes, force, config, skipSpaceCreate, publicSpaceCreate, validate, spaceNames);
831
+ })));
832
+ }
833
+ return { changes, total: filteredItems.length };
834
+ };
835
+ const getDashboardChartSlugs = async (dashboardSlugs, customPath) => {
836
+ const dashboardItems = await readCodeFiles('dashboards', customPath);
837
+ const filteredDashboardItems = dashboardSlugs.length > 0
838
+ ? dashboardItems.filter((dashboard) => dashboardSlugs.includes(dashboard.slug))
839
+ : dashboardItems;
840
+ return filteredDashboardItems.reduce((acc, dashboard) => {
841
+ const dashboardChartSlugs = dashboard.tiles
842
+ .map((tile) => 'chartSlug' in tile.properties
843
+ ? tile.properties.chartSlug
844
+ : undefined)
845
+ .filter((dashboardChartSlug) => !!dashboardChartSlug);
846
+ return [...acc, ...dashboardChartSlugs];
847
+ }, []);
848
+ };
849
+ const uploadHandler = async (options) => {
850
+ globalState_1.default.setVerbose(options.verbose);
851
+ if (options.gzip) {
852
+ (0, apiClient_1.setGzipEnabled)(true);
853
+ }
854
+ await (0, apiClient_1.checkLightdashVersion)();
855
+ const config = await (0, config_1.getConfig)();
856
+ if (!config.context?.apiKey || !config.context.serverUrl) {
857
+ throw new common_1.AuthorizationError(`Not logged in. Run 'clary login --help'`);
858
+ }
859
+ const projectSelection = await (0, selectProject_1.selectProject)(config, options.project);
860
+ if (!projectSelection) {
861
+ throw new common_1.LightdashError({
862
+ message: 'No project selected. Run clary config set-project',
863
+ name: 'Not Found',
864
+ statusCode: 404,
865
+ data: {},
866
+ });
867
+ }
868
+ const projectId = projectSelection.projectUuid;
869
+ // Log current project info
870
+ (0, selectProject_1.logSelectedProject)(projectSelection, config, 'Uploading to');
871
+ let changes = {};
872
+ // For analytics
873
+ let chartTotal;
874
+ let dashboardTotal;
875
+ const start = Date.now();
876
+ await analytics_1.LightdashAnalytics.track({
877
+ event: 'upload.started',
878
+ properties: {
879
+ userId: config.user?.userUuid,
880
+ organizationId: config.user?.organizationUuid,
881
+ projectId,
882
+ },
883
+ });
884
+ try {
885
+ // If any filter is provided, we skip those items without filters
886
+ // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
887
+ const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
888
+ // Always include the charts from dashboards if includeCharts is true regardless of the charts filters
889
+ const chartSlugs = options.includeCharts
890
+ ? Array.from(new Set([
891
+ ...options.charts,
892
+ ...(await getDashboardChartSlugs(options.dashboards, options.path)),
893
+ ]))
894
+ : options.charts;
895
+ const concurrency = Math.min(Math.max(1, parseInt(String(options.concurrency), 10) || 1), 1000);
896
+ if (parseInt(String(options.concurrency), 10) > 1000) {
897
+ globalState_1.default.log(styles.warning(`Concurrency limit exceeded. Using maximum of 1000 instead of ${options.concurrency}`));
898
+ }
899
+ // Read space definition files to preserve original space names during upload
900
+ const spaceNames = await readSpaceNames(options.path);
901
+ if (Object.keys(spaceNames).length > 0) {
902
+ globalState_1.default.log(`Found ${Object.keys(spaceNames).length} space definition(s)`);
903
+ }
904
+ // Discover loose YAML files (outside charts/ and dashboards/) classified by contentType
905
+ const looseFiles = await readLooseCodeFiles(options.path);
906
+ if (looseFiles.charts.length > 0) {
907
+ globalState_1.default.log(`Found ${looseFiles.charts.length} chart(s) outside charts/ directory (classified by contentType)`);
908
+ }
909
+ if (looseFiles.dashboards.length > 0) {
910
+ globalState_1.default.log(`Found ${looseFiles.dashboards.length} dashboard(s) outside dashboards/ directory (classified by contentType)`);
911
+ }
912
+ if (hasFilters && chartSlugs.length === 0) {
913
+ globalState_1.default.log(styles.warning(`No charts filters provided, skipping`));
914
+ }
915
+ else {
916
+ const { changes: chartChanges, total } = await upsertResources('charts', projectId, changes, options.force, chartSlugs, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.charts, spaceNames);
917
+ changes = chartChanges;
918
+ chartTotal = total;
919
+ }
920
+ if (hasFilters && options.dashboards.length === 0) {
921
+ globalState_1.default.log(styles.warning(`No dashboard filters provided, skipping`));
922
+ }
923
+ else {
924
+ const { changes: dashboardChanges, total } = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards, options.path, options.skipSpaceCreate, options.public, options.validate, concurrency, looseFiles.dashboards, spaceNames);
925
+ changes = dashboardChanges;
926
+ dashboardTotal = total;
927
+ }
928
+ const end = Date.now();
929
+ await analytics_1.LightdashAnalytics.track({
930
+ event: 'upload.completed',
931
+ properties: {
932
+ userId: config.user?.userUuid,
933
+ organizationId: config.user?.organizationUuid,
934
+ projectId,
935
+ chartsNum: chartTotal,
936
+ dashboardsNum: dashboardTotal,
937
+ timeToCompleted: (end - start) / 1000, // in seconds
938
+ },
939
+ });
940
+ logUploadChanges(changes);
941
+ }
942
+ catch (error) {
943
+ globalState_1.default.log(styles.error(`\nError downloading: ${(0, common_1.getErrorMessage)(error)}`));
944
+ await analytics_1.LightdashAnalytics.track({
945
+ event: 'download.error',
946
+ properties: {
947
+ userId: config.user?.userUuid,
948
+ organizationId: config.user?.organizationUuid,
949
+ projectId,
950
+ error: (0, common_1.getErrorMessage)(error),
951
+ },
952
+ });
953
+ }
954
+ };
955
+ exports.uploadHandler = uploadHandler;