@lightdash/cli 1.137.1 → 1.139.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.
@@ -0,0 +1,408 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeThemePackage = exports.parseThemeArchive = exports.prepareThemeUploads = exports.getThemesFolder = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /* eslint-disable no-await-in-loop */
6
+ const common_1 = require("@lightdash/common");
7
+ const crypto_1 = require("crypto");
8
+ const fs_1 = require("fs");
9
+ const path = tslib_1.__importStar(require("path"));
10
+ const tar_stream_1 = require("tar-stream");
11
+ const yaml_1 = require("yaml");
12
+ const TAR_EPOCH = new Date(0);
13
+ const MACOS_METADATA_FILENAME = '.DS_Store';
14
+ const THEME_BACKUP_DIRECTORY_PATTERN = /\.backup-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
15
+ const getThemesFolder = (organizationContentPath) => path.join(organizationContentPath, 'themes');
16
+ exports.getThemesFolder = getThemesFolder;
17
+ const isEnoent = (error) => error.code === 'ENOENT';
18
+ const toError = (error) => error instanceof Error ? error : new Error(String(error));
19
+ const withoutMacOsMetadata = (entries) => entries.filter((entry) => entry.name !== MACOS_METADATA_FILENAME);
20
+ const isInternalThemeDirectory = (name) => name.startsWith('.lightdash-theme-') ||
21
+ THEME_BACKUP_DIRECTORY_PATTERN.test(name);
22
+ const assertNoCaseInsensitiveDuplicates = (entries, source) => {
23
+ const seen = new Map();
24
+ for (const entry of entries) {
25
+ const key = entry.name.toLowerCase();
26
+ const previous = seen.get(key);
27
+ if (previous !== undefined) {
28
+ throw new common_1.ParameterError(`Theme package contains case-insensitive duplicate paths in "${source}": "${previous}" and "${entry.name}"`);
29
+ }
30
+ seen.set(key, entry.name);
31
+ }
32
+ };
33
+ const parseManifestBody = (body) => {
34
+ if (body.length > common_1.MAX_THEME_MANIFEST_BYTES) {
35
+ throw new common_1.ParameterError(`Theme manifest exceeds ${common_1.MAX_THEME_MANIFEST_BYTES} bytes`);
36
+ }
37
+ let source;
38
+ try {
39
+ source = new TextDecoder('utf-8', { fatal: true }).decode(body);
40
+ }
41
+ catch {
42
+ throw new common_1.ParameterError('Theme manifest must be valid UTF-8 YAML');
43
+ }
44
+ let parsed;
45
+ try {
46
+ parsed = (0, yaml_1.parse)(source, { strict: true, uniqueKeys: true });
47
+ }
48
+ catch {
49
+ throw new common_1.ParameterError('Theme manifest contains invalid YAML');
50
+ }
51
+ return (0, common_1.parseOrganizationDesignPackageManifest)(parsed);
52
+ };
53
+ const addTarEntry = (packer, header, body) => new Promise((resolve, reject) => {
54
+ packer.entry(header, body, (error) => {
55
+ if (error)
56
+ reject(error);
57
+ else
58
+ resolve();
59
+ });
60
+ });
61
+ const buildThemeArchive = (manifest, files) => new Promise((resolve, reject) => {
62
+ const packer = (0, tar_stream_1.pack)();
63
+ const chunks = [];
64
+ packer.on('data', (chunk) => chunks.push(chunk));
65
+ packer.on('end', () => resolve(Buffer.concat(chunks)));
66
+ packer.on('error', reject);
67
+ void (async () => {
68
+ const manifestBody = Buffer.from((0, yaml_1.stringify)({
69
+ codeVersion: manifest.codeVersion,
70
+ slug: manifest.slug,
71
+ name: manifest.name,
72
+ description: manifest.description,
73
+ extraInstructions: manifest.extraInstructions,
74
+ }, { lineWidth: 0 }), 'utf8');
75
+ if (manifestBody.length > common_1.MAX_THEME_MANIFEST_BYTES) {
76
+ throw new common_1.ParameterError(`Theme manifest exceeds ${common_1.MAX_THEME_MANIFEST_BYTES} bytes`);
77
+ }
78
+ await addTarEntry(packer, {
79
+ name: common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST,
80
+ type: 'file',
81
+ mode: 0o644,
82
+ mtime: TAR_EPOCH,
83
+ uid: 0,
84
+ gid: 0,
85
+ }, manifestBody);
86
+ for (const directory of common_1.ORGANIZATION_DESIGN_PACKAGE_DIRECTORIES) {
87
+ await addTarEntry(packer, {
88
+ name: `${directory}/`,
89
+ type: 'directory',
90
+ mode: 0o755,
91
+ mtime: TAR_EPOCH,
92
+ uid: 0,
93
+ gid: 0,
94
+ });
95
+ }
96
+ const sortedFiles = [...files].sort((left, right) => (0, common_1.getOrganizationDesignPackageFilePath)(left.kind, left.filename).localeCompare((0, common_1.getOrganizationDesignPackageFilePath)(right.kind, right.filename)));
97
+ for (const file of sortedFiles) {
98
+ await addTarEntry(packer, {
99
+ name: (0, common_1.getOrganizationDesignPackageFilePath)(file.kind, file.filename),
100
+ type: 'file',
101
+ mode: 0o644,
102
+ mtime: TAR_EPOCH,
103
+ uid: 0,
104
+ gid: 0,
105
+ }, file.body);
106
+ }
107
+ packer.finalize();
108
+ })().catch((error) => packer.destroy(toError(error)));
109
+ });
110
+ const readLocalThemeDirectory = async (directory) => {
111
+ const entries = withoutMacOsMetadata(await fs_1.promises.readdir(directory, { withFileTypes: true }));
112
+ assertNoCaseInsensitiveDuplicates(entries, directory);
113
+ const expectedEntries = new Set([
114
+ common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST,
115
+ ...common_1.ORGANIZATION_DESIGN_PACKAGE_DIRECTORIES,
116
+ ]);
117
+ const unknownEntry = entries.find((entry) => !expectedEntries.has(entry.name));
118
+ if (unknownEntry) {
119
+ throw new common_1.ParameterError(`Unknown theme package path: ${path.join(directory, unknownEntry.name)}`);
120
+ }
121
+ if (!entries.some((entry) => entry.name === common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST)) {
122
+ throw new common_1.ParameterError(`Theme package is missing ${path.join(directory, common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST)}`);
123
+ }
124
+ const manifestEntry = entries.find((entry) => entry.name === common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST);
125
+ if (!manifestEntry?.isFile()) {
126
+ throw new common_1.ParameterError(`Theme manifest must be a regular file: ${path.join(directory, common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST)}`);
127
+ }
128
+ const manifestBody = await fs_1.promises.readFile(path.join(directory, common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST));
129
+ const manifest = parseManifestBody(manifestBody);
130
+ if (path.basename(directory) !== manifest.slug) {
131
+ throw new common_1.ParameterError(`Theme directory "${path.basename(directory)}" must match manifest slug "${manifest.slug}"`);
132
+ }
133
+ const files = [];
134
+ let totalBytes = 0;
135
+ for (const [kind, packageDirectory] of Object.entries(common_1.ORGANIZATION_DESIGN_PACKAGE_DIRECTORY_BY_KIND)) {
136
+ const directoryEntry = entries.find((entry) => entry.name === packageDirectory);
137
+ if (directoryEntry && !directoryEntry.isDirectory()) {
138
+ throw new common_1.ParameterError(`Theme package path must be a regular directory: ${path.join(directory, packageDirectory)}`);
139
+ }
140
+ const assetDirectory = path.join(directory, packageDirectory);
141
+ const assetEntries = directoryEntry
142
+ ? withoutMacOsMetadata(await fs_1.promises.readdir(assetDirectory, {
143
+ withFileTypes: true,
144
+ }))
145
+ : [];
146
+ assertNoCaseInsensitiveDuplicates(assetEntries, assetDirectory);
147
+ for (const assetEntry of assetEntries.sort((left, right) => left.name.localeCompare(right.name))) {
148
+ if (!assetEntry.isFile()) {
149
+ throw new common_1.ParameterError(`Theme package files must be regular files without symlinks or nested directories: ${path.join(assetDirectory, assetEntry.name)}`);
150
+ }
151
+ const filePath = path.join(assetDirectory, assetEntry.name);
152
+ const body = await fs_1.promises.readFile(filePath);
153
+ const metadata = (0, common_1.validateOrganizationDesignFileMetadata)({
154
+ kind,
155
+ filename: assetEntry.name,
156
+ });
157
+ if (metadata.filename !== assetEntry.name) {
158
+ throw new common_1.ParameterError(`Theme package filename may not have surrounding whitespace: ${filePath}`);
159
+ }
160
+ (0, common_1.validateOrganizationDesignFileContent)({
161
+ body,
162
+ filename: metadata.filename,
163
+ });
164
+ totalBytes += body.length;
165
+ if (totalBytes > common_1.MAX_THEME_TOTAL_BYTES) {
166
+ throw new common_1.ParameterError(`Theme package files exceed ${common_1.MAX_THEME_TOTAL_BYTES} bytes`);
167
+ }
168
+ files.push({
169
+ ...metadata,
170
+ contentType: (0, common_1.getOrganizationDesignPackageContentType)(metadata.filename),
171
+ body,
172
+ });
173
+ }
174
+ }
175
+ const archive = await buildThemeArchive(manifest, files);
176
+ if (archive.length > common_1.MAX_THEME_PACKAGE_BYTES) {
177
+ throw new common_1.ParameterError(`Theme package exceeds ${common_1.MAX_THEME_PACKAGE_BYTES} bytes`);
178
+ }
179
+ return { directory, manifest, archive };
180
+ };
181
+ const prepareThemeUploads = async (organizationContentPath) => {
182
+ const themesFolder = (0, exports.getThemesFolder)(organizationContentPath);
183
+ let entries;
184
+ try {
185
+ entries = (await fs_1.promises.readdir(themesFolder, { withFileTypes: true })).filter((entry) => entry.name !== MACOS_METADATA_FILENAME &&
186
+ !isInternalThemeDirectory(entry.name));
187
+ }
188
+ catch (error) {
189
+ if (isEnoent(error))
190
+ return [];
191
+ throw error;
192
+ }
193
+ if (entries.length === 0)
194
+ return [];
195
+ assertNoCaseInsensitiveDuplicates(entries, themesFolder);
196
+ const failures = [];
197
+ const prepared = [];
198
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
199
+ const directory = path.join(themesFolder, entry.name);
200
+ if (!entry.isDirectory()) {
201
+ failures.push({
202
+ message: `Invalid theme path "${directory}": expected a regular directory without symlinks`,
203
+ });
204
+ }
205
+ else {
206
+ try {
207
+ prepared.push(await readLocalThemeDirectory(directory));
208
+ }
209
+ catch (error) {
210
+ failures.push({
211
+ message: `Invalid theme directory "${directory}": ${(0, common_1.getErrorMessage)(error)}`,
212
+ });
213
+ }
214
+ }
215
+ }
216
+ const bySlug = new Map();
217
+ for (const item of prepared) {
218
+ bySlug.set(item.manifest.slug, [
219
+ ...(bySlug.get(item.manifest.slug) ?? []),
220
+ item,
221
+ ]);
222
+ }
223
+ for (const [slug, items] of bySlug) {
224
+ if (items.length > 1) {
225
+ failures.push({
226
+ message: `Duplicate theme slug "${slug}" in ${items.map(({ directory }) => `"${directory}"`).join(', ')}`,
227
+ });
228
+ }
229
+ }
230
+ if (failures.length > 0) {
231
+ throw new common_1.ParameterError(failures.map(({ message }) => message).join('\n'));
232
+ }
233
+ return prepared;
234
+ };
235
+ exports.prepareThemeUploads = prepareThemeUploads;
236
+ const parseThemeArchive = (archive) => new Promise((resolve, reject) => {
237
+ if (archive.length === 0 || archive.length > common_1.MAX_THEME_PACKAGE_BYTES) {
238
+ reject(new common_1.ParameterError(`Theme package must be between 1 and ${common_1.MAX_THEME_PACKAGE_BYTES} bytes`));
239
+ return;
240
+ }
241
+ const extractor = (0, tar_stream_1.extract)();
242
+ const seenPaths = new Set();
243
+ const files = [];
244
+ let manifestBody = null;
245
+ let totalBytes = 0;
246
+ let settled = false;
247
+ const fail = (error) => {
248
+ if (settled)
249
+ return;
250
+ settled = true;
251
+ extractor.destroy();
252
+ reject(error instanceof common_1.ParameterError
253
+ ? error
254
+ : new common_1.ParameterError('Theme package is not a valid tar archive'));
255
+ };
256
+ extractor.on('entry', (header, stream, next) => {
257
+ try {
258
+ const entryName = (0, common_1.validateOrganizationDesignPackageEntryPath)(header.name);
259
+ const pathKey = entryName.toLowerCase();
260
+ if (seenPaths.has(pathKey)) {
261
+ throw new common_1.ParameterError(`Theme package contains a duplicate path: ${entryName}`);
262
+ }
263
+ seenPaths.add(pathKey);
264
+ const entryType = header.type ?? 'file';
265
+ if (entryType === 'directory') {
266
+ if (!(0, common_1.isOrganizationDesignPackageDirectory)(entryName)) {
267
+ throw new common_1.ParameterError(`Unknown theme package directory: ${entryName}`);
268
+ }
269
+ stream.resume();
270
+ stream.on('end', next);
271
+ return;
272
+ }
273
+ if (entryType !== 'file') {
274
+ throw new common_1.ParameterError(`Theme package entry type is not allowed: ${entryType}`);
275
+ }
276
+ const isManifest = entryName === common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST;
277
+ const maxBytes = isManifest
278
+ ? common_1.MAX_THEME_MANIFEST_BYTES
279
+ : common_1.MAX_THEME_FILE_BYTES;
280
+ if ((header.size ?? 0) > maxBytes) {
281
+ throw new common_1.ParameterError(`Theme package entry exceeds ${maxBytes} bytes: ${entryName}`);
282
+ }
283
+ const parsedPath = isManifest
284
+ ? null
285
+ : (0, common_1.parseOrganizationDesignPackageFilePath)(entryName);
286
+ const chunks = [];
287
+ let bytes = 0;
288
+ stream.on('data', (chunk) => {
289
+ const buffer = Buffer.isBuffer(chunk)
290
+ ? chunk
291
+ : Buffer.from(chunk);
292
+ bytes += buffer.length;
293
+ if (bytes > maxBytes) {
294
+ stream.destroy(new common_1.ParameterError(`Theme package entry exceeds ${maxBytes} bytes: ${entryName}`));
295
+ return;
296
+ }
297
+ chunks.push(buffer);
298
+ });
299
+ stream.on('error', fail);
300
+ stream.on('end', () => {
301
+ if (settled)
302
+ return;
303
+ try {
304
+ const body = Buffer.concat(chunks);
305
+ if (isManifest) {
306
+ manifestBody = body;
307
+ }
308
+ else if (parsedPath) {
309
+ const metadata = (0, common_1.validateOrganizationDesignFileMetadata)(parsedPath);
310
+ (0, common_1.validateOrganizationDesignFileContent)({
311
+ body,
312
+ filename: metadata.filename,
313
+ });
314
+ totalBytes += body.length;
315
+ if (totalBytes > common_1.MAX_THEME_TOTAL_BYTES) {
316
+ throw new common_1.ParameterError(`Theme package files exceed ${common_1.MAX_THEME_TOTAL_BYTES} bytes`);
317
+ }
318
+ files.push({
319
+ ...metadata,
320
+ contentType: (0, common_1.getOrganizationDesignPackageContentType)(metadata.filename),
321
+ body,
322
+ });
323
+ }
324
+ next();
325
+ }
326
+ catch (error) {
327
+ fail(error);
328
+ }
329
+ });
330
+ }
331
+ catch (error) {
332
+ stream.resume();
333
+ fail(error);
334
+ }
335
+ });
336
+ extractor.on('error', fail);
337
+ extractor.on('finish', () => {
338
+ if (settled)
339
+ return;
340
+ try {
341
+ if (!manifestBody) {
342
+ throw new common_1.ParameterError(`Theme package is missing ${common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST}`);
343
+ }
344
+ const manifest = parseManifestBody(manifestBody);
345
+ settled = true;
346
+ resolve({ manifest, files });
347
+ }
348
+ catch (error) {
349
+ fail(error);
350
+ }
351
+ });
352
+ try {
353
+ extractor.end(archive);
354
+ }
355
+ catch (error) {
356
+ fail(error);
357
+ }
358
+ });
359
+ exports.parseThemeArchive = parseThemeArchive;
360
+ const replaceThemeDirectory = async (sourceDirectory, targetDirectory) => {
361
+ const backupDirectory = path.join(path.dirname(path.dirname(targetDirectory)), `.lightdash-theme-backup-${(0, crypto_1.randomUUID)()}`);
362
+ let hasBackup = false;
363
+ try {
364
+ await fs_1.promises.rename(targetDirectory, backupDirectory);
365
+ hasBackup = true;
366
+ }
367
+ catch (error) {
368
+ if (!isEnoent(error))
369
+ throw error;
370
+ }
371
+ try {
372
+ await fs_1.promises.rename(sourceDirectory, targetDirectory);
373
+ }
374
+ catch (error) {
375
+ if (hasBackup)
376
+ await fs_1.promises.rename(backupDirectory, targetDirectory);
377
+ throw error;
378
+ }
379
+ if (hasBackup) {
380
+ await fs_1.promises.rm(backupDirectory, { recursive: true, force: true });
381
+ }
382
+ };
383
+ const writeThemePackage = async (organizationContentPath, parsed) => {
384
+ const themesFolder = (0, exports.getThemesFolder)(organizationContentPath);
385
+ await fs_1.promises.mkdir(themesFolder, { recursive: true });
386
+ const temporaryDirectory = await fs_1.promises.mkdtemp(path.join(organizationContentPath, '.lightdash-theme-'));
387
+ try {
388
+ for (const directory of common_1.ORGANIZATION_DESIGN_PACKAGE_DIRECTORIES) {
389
+ await fs_1.promises.mkdir(path.join(temporaryDirectory, directory));
390
+ }
391
+ await fs_1.promises.writeFile(path.join(temporaryDirectory, common_1.ORGANIZATION_DESIGN_PACKAGE_MANIFEST), (0, yaml_1.stringify)({
392
+ codeVersion: parsed.manifest.codeVersion,
393
+ slug: parsed.manifest.slug,
394
+ name: parsed.manifest.name,
395
+ description: parsed.manifest.description,
396
+ extraInstructions: parsed.manifest.extraInstructions,
397
+ }, { lineWidth: 0 }));
398
+ for (const file of parsed.files) {
399
+ await fs_1.promises.writeFile(path.join(temporaryDirectory, (0, common_1.getOrganizationDesignPackageFilePath)(file.kind, file.filename)), file.body);
400
+ }
401
+ await replaceThemeDirectory(temporaryDirectory, path.join(themesFolder, parsed.manifest.slug));
402
+ }
403
+ catch (error) {
404
+ await fs_1.promises.rm(temporaryDirectory, { recursive: true, force: true });
405
+ throw error;
406
+ }
407
+ };
408
+ exports.writeThemePackage = writeThemePackage;
@@ -0,0 +1,20 @@
1
+ import { type PreparedThemeUpload } from './themePackage';
2
+ export { prepareThemeUploads } from './themePackage';
3
+ export declare const THEME_CODE_RESOURCE: {
4
+ readonly kind: 'theme';
5
+ readonly dependencies: readonly [];
6
+ };
7
+ export type ThemeUploadSummary = {
8
+ created: number;
9
+ updated: number;
10
+ unchanged: number;
11
+ failed: number;
12
+ completedSlugs: string[];
13
+ failures: Array<{
14
+ message: string;
15
+ }>;
16
+ };
17
+ export declare const downloadThemes: (organizationContentPath: string) => Promise<number>;
18
+ export declare const uploadThemes: (prepared: PreparedThemeUpload[]) => Promise<ThemeUploadSummary>;
19
+ export declare const formatThemeUploadSummary: (summary: ThemeUploadSummary) => string;
20
+ //# sourceMappingURL=themes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themes.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/themes.ts"],"names":[],"mappings":"AAeA,OAAO,EAIH,KAAK,mBAAmB,EAC3B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAErD,eAAO,MAAM,mBAAmB;aAC5B,IAAI,EAAE,OAAO;aACb,YAAY;CACN,CAAC;AAEX,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxC,CAAC;AAgCF,eAAO,MAAM,cAAc,4BACE,MAAM,KAChC,OAAO,CAAC,MAAM,CAuChB,CAAC;AAEF,eAAO,MAAM,YAAY,aACX,mBAAmB,EAAE,KAChC,OAAO,CAAC,kBAAkB,CAwE5B,CAAC;AAEF,eAAO,MAAM,wBAAwB,YAAa,kBAAkB,KAAG,MAMvD,CAAC"}
@@ -0,0 +1,148 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatThemeUploadSummary = exports.uploadThemes = exports.downloadThemes = exports.THEME_CODE_RESOURCE = exports.prepareThemeUploads = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /* eslint-disable no-await-in-loop */
6
+ const common_1 = require("@lightdash/common");
7
+ const crypto_1 = require("crypto");
8
+ const fs_1 = require("fs");
9
+ const path = tslib_1.__importStar(require("path"));
10
+ const apiClient_1 = require("../dbt/apiClient");
11
+ const themePackage_1 = require("./themePackage");
12
+ var themePackage_2 = require("./themePackage");
13
+ Object.defineProperty(exports, "prepareThemeUploads", { enumerable: true, get: function () { return themePackage_2.prepareThemeUploads; } });
14
+ exports.THEME_CODE_RESOURCE = {
15
+ kind: 'theme',
16
+ dependencies: [],
17
+ };
18
+ const isEnoent = (error) => error.code === 'ENOENT';
19
+ const replaceThemesDirectory = async (sourceDirectory, targetDirectory) => {
20
+ const backupDirectory = path.join(path.dirname(targetDirectory), `.lightdash-themes-backup-${(0, crypto_1.randomUUID)()}`);
21
+ let hasBackup = false;
22
+ try {
23
+ await fs_1.promises.rename(targetDirectory, backupDirectory);
24
+ hasBackup = true;
25
+ }
26
+ catch (error) {
27
+ if (!isEnoent(error))
28
+ throw error;
29
+ }
30
+ try {
31
+ await fs_1.promises.rename(sourceDirectory, targetDirectory);
32
+ }
33
+ catch (error) {
34
+ if (hasBackup)
35
+ await fs_1.promises.rename(backupDirectory, targetDirectory);
36
+ throw error;
37
+ }
38
+ if (hasBackup) {
39
+ await fs_1.promises.rm(backupDirectory, { recursive: true, force: true });
40
+ }
41
+ };
42
+ const downloadThemes = async (organizationContentPath) => {
43
+ const themes = await (0, apiClient_1.lightdashApi)({
44
+ method: 'GET',
45
+ url: '/api/v1/org/designs/',
46
+ body: undefined,
47
+ });
48
+ await fs_1.promises.mkdir(organizationContentPath, { recursive: true });
49
+ const stagingRoot = await fs_1.promises.mkdtemp(path.join(organizationContentPath, '.lightdash-themes-download-'));
50
+ try {
51
+ await fs_1.promises.mkdir((0, themePackage_1.getThemesFolder)(stagingRoot));
52
+ for (const theme of [...themes].sort((left, right) => left.slug.localeCompare(right.slug))) {
53
+ const response = await (0, apiClient_1.lightdashRawApi)({
54
+ method: 'GET',
55
+ url: `/api/v1/org/designs/${encodeURIComponent(theme.slug)}/package`,
56
+ body: undefined,
57
+ });
58
+ const parsed = await (0, themePackage_1.parseThemeArchive)(Buffer.from(await response.arrayBuffer()));
59
+ if (parsed.manifest.slug !== theme.slug) {
60
+ throw new common_1.ParameterError(`Downloaded theme slug "${parsed.manifest.slug}" does not match requested slug "${theme.slug}"`);
61
+ }
62
+ await (0, themePackage_1.writeThemePackage)(stagingRoot, parsed);
63
+ }
64
+ await replaceThemesDirectory((0, themePackage_1.getThemesFolder)(stagingRoot), (0, themePackage_1.getThemesFolder)(organizationContentPath));
65
+ }
66
+ finally {
67
+ await fs_1.promises.rm(stagingRoot, { recursive: true, force: true });
68
+ }
69
+ return themes.length;
70
+ };
71
+ exports.downloadThemes = downloadThemes;
72
+ const uploadThemes = async (prepared) => {
73
+ const summary = {
74
+ created: 0,
75
+ updated: 0,
76
+ unchanged: 0,
77
+ failed: 0,
78
+ completedSlugs: [],
79
+ failures: [],
80
+ };
81
+ if (prepared.length === 0)
82
+ return summary;
83
+ const existingThemes = await (0, apiClient_1.lightdashApi)({
84
+ method: 'GET',
85
+ url: '/api/v1/org/designs/',
86
+ body: undefined,
87
+ });
88
+ const existingSlugs = new Set(existingThemes.map(({ slug }) => slug));
89
+ for (const theme of prepared) {
90
+ try {
91
+ const response = await (0, apiClient_1.lightdashRawApi)({
92
+ method: 'PUT',
93
+ url: '/api/v1/org/designs/package',
94
+ body: theme.archive,
95
+ headers: {
96
+ 'Content-Type': common_1.ORGANIZATION_DESIGN_PACKAGE_CONTENT_TYPE,
97
+ 'Content-Length': String(theme.archive.length),
98
+ },
99
+ });
100
+ const result = (await response.json());
101
+ if (result.status !== 'ok') {
102
+ throw new Error('Theme import returned an invalid response');
103
+ }
104
+ if (result.results.slug !== theme.manifest.slug) {
105
+ throw new Error(`Theme import returned slug "${result.results.slug}" instead of "${theme.manifest.slug}"`);
106
+ }
107
+ let action;
108
+ if ('action' in result.results) {
109
+ action = result.results.action;
110
+ }
111
+ else if (existingSlugs.has(result.results.slug)) {
112
+ action = common_1.PromotionAction.UPDATE;
113
+ }
114
+ else {
115
+ action = common_1.PromotionAction.CREATE;
116
+ }
117
+ switch (action) {
118
+ case common_1.PromotionAction.CREATE:
119
+ summary.created += 1;
120
+ break;
121
+ case common_1.PromotionAction.UPDATE:
122
+ summary.updated += 1;
123
+ break;
124
+ case common_1.PromotionAction.NO_CHANGES:
125
+ summary.unchanged += 1;
126
+ break;
127
+ default:
128
+ (0, common_1.assertUnreachable)(action, 'Unsupported theme import action');
129
+ }
130
+ summary.completedSlugs.push(result.results.slug);
131
+ }
132
+ catch (error) {
133
+ summary.failures.push({
134
+ message: `Failed to upload theme "${theme.manifest.slug}": ${(0, common_1.getErrorMessage)(error)}`,
135
+ });
136
+ }
137
+ }
138
+ summary.failed = summary.failures.length;
139
+ return summary;
140
+ };
141
+ exports.uploadThemes = uploadThemes;
142
+ const formatThemeUploadSummary = (summary) => [
143
+ `${summary.created} created`,
144
+ `${summary.updated} updated`,
145
+ `${summary.unchanged} unchanged`,
146
+ ...(summary.failed > 0 ? [`${summary.failed} failed`] : []),
147
+ ].join(', ');
148
+ exports.formatThemeUploadSummary = formatThemeUploadSummary;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=themes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themes.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/themes.test.ts"],"names":[],"mappings":""}