@lightdash/cli 0.3406.0 → 0.3406.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.
Files changed (44) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/handlers/contentAsCode/fileNames.d.ts +24 -0
  3. package/dist/handlers/contentAsCode/fileNames.d.ts.map +1 -0
  4. package/dist/handlers/contentAsCode/fileNames.js +67 -0
  5. package/dist/handlers/contentAsCode/fileNames.test.d.ts +2 -0
  6. package/dist/handlers/contentAsCode/fileNames.test.d.ts.map +1 -0
  7. package/dist/handlers/contentAsCode/fileNames.test.js +79 -0
  8. package/dist/handlers/contentAsCode/projectResources.d.ts +8 -0
  9. package/dist/handlers/contentAsCode/projectResources.d.ts.map +1 -0
  10. package/dist/handlers/contentAsCode/projectResources.js +95 -0
  11. package/dist/handlers/contentAsCode/registry.d.ts +9 -0
  12. package/dist/handlers/contentAsCode/registry.d.ts.map +1 -0
  13. package/dist/handlers/contentAsCode/registry.js +44 -0
  14. package/dist/handlers/contentAsCode/registry.test.d.ts +2 -0
  15. package/dist/handlers/contentAsCode/registry.test.d.ts.map +1 -0
  16. package/dist/handlers/contentAsCode/registry.test.js +18 -0
  17. package/dist/handlers/contentAsCode/resource.d.ts +67 -0
  18. package/dist/handlers/contentAsCode/resource.d.ts.map +1 -0
  19. package/dist/handlers/contentAsCode/resource.js +171 -0
  20. package/dist/handlers/contentAsCode/resource.test.d.ts +2 -0
  21. package/dist/handlers/contentAsCode/resource.test.d.ts.map +1 -0
  22. package/dist/handlers/contentAsCode/resource.test.js +39 -0
  23. package/dist/handlers/download.d.ts.map +1 -1
  24. package/dist/handlers/download.js +59 -109
  25. package/dist/handlers/organizationContent/customRoles.d.ts +4 -3
  26. package/dist/handlers/organizationContent/customRoles.d.ts.map +1 -1
  27. package/dist/handlers/organizationContent/customRoles.js +51 -137
  28. package/dist/handlers/organizationContent/customRoles.test.js +13 -2
  29. package/dist/handlers/organizationContent/fileNames.d.ts.map +1 -1
  30. package/dist/handlers/organizationContent/fileNames.js +8 -29
  31. package/dist/handlers/organizationContent/groups.d.ts +4 -3
  32. package/dist/handlers/organizationContent/groups.d.ts.map +1 -1
  33. package/dist/handlers/organizationContent/groups.js +54 -139
  34. package/dist/handlers/organizationContent/groups.test.js +2 -1
  35. package/dist/handlers/organizationContent/index.d.ts.map +1 -1
  36. package/dist/handlers/organizationContent/index.js +7 -22
  37. package/dist/handlers/organizationContent/users.d.ts +4 -3
  38. package/dist/handlers/organizationContent/users.d.ts.map +1 -1
  39. package/dist/handlers/organizationContent/users.js +75 -146
  40. package/dist/handlers/organizationContent/users.test.js +4 -3
  41. package/dist/handlers/spacesAsCode.d.ts +1 -1
  42. package/dist/handlers/spacesAsCode.d.ts.map +1 -1
  43. package/dist/handlers/spacesAsCode.js +21 -26
  44. package/package.json +3 -3
@@ -0,0 +1,24 @@
1
+ export declare const CODE_FILENAME_MAX_STEM_LENGTH = 200;
2
+ export declare const CODE_FILENAME_HASH_LENGTH = 8;
3
+ export declare const getStableCodeHash: (value: string, length?: number) => string;
4
+ export declare const getContentFileNameStem: ({ value, fallbackPrefix, fallbackHashValue, maxLength, }: {
5
+ value: string;
6
+ fallbackPrefix: string;
7
+ fallbackHashValue?: string;
8
+ maxLength?: number;
9
+ }) => string;
10
+ export type ContentFileNameInput = {
11
+ identity: string;
12
+ displayName: string;
13
+ preferredFileName?: string;
14
+ };
15
+ export declare const allocateContentFileNames: ({ items, fallbackPrefix, extension, maxStemLength, existingFileNameByIdentity, reservedFileNames, }: {
16
+ items: ContentFileNameInput[];
17
+ fallbackPrefix: string;
18
+ extension?: string;
19
+ maxStemLength?: number;
20
+ existingFileNameByIdentity?: ReadonlyMap<string, string>;
21
+ reservedFileNames?: Iterable<string>;
22
+ }) => string[];
23
+ export declare const assertSafeContentFilePath: (relativePath: string) => void;
24
+ //# sourceMappingURL=fileNames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileNames.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/fileNames.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,6BAA6B,MAAM,CAAC;AACjD,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C,eAAO,MAAM,iBAAiB,GAC1B,OAAO,MAAM,EACb,SAAQ,MAAkC,KAC3C,MAA2E,CAAC;AAE/E,eAAO,MAAM,sBAAsB,GAAI,0DAKpC;IACC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,KAAG,MASH,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,qGAOtC;IACC,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzD,iBAAiB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACxC,KAAG,MAAM,EAgET,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAI,cAAc,MAAM,KAAG,IAUhE,CAAC"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertSafeContentFilePath = exports.allocateContentFileNames = exports.getContentFileNameStem = exports.getStableCodeHash = exports.CODE_FILENAME_HASH_LENGTH = exports.CODE_FILENAME_MAX_STEM_LENGTH = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@lightdash/common");
6
+ const crypto_1 = require("crypto");
7
+ const path = tslib_1.__importStar(require("path"));
8
+ exports.CODE_FILENAME_MAX_STEM_LENGTH = 200;
9
+ exports.CODE_FILENAME_HASH_LENGTH = 8;
10
+ const getStableCodeHash = (value, length = exports.CODE_FILENAME_HASH_LENGTH) => (0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, length);
11
+ exports.getStableCodeHash = getStableCodeHash;
12
+ const getContentFileNameStem = ({ value, fallbackPrefix, fallbackHashValue = value, maxLength = exports.CODE_FILENAME_MAX_STEM_LENGTH, }) => {
13
+ const normalizedValue = value
14
+ .normalize('NFKD')
15
+ .replace(/[\u0300-\u036f]/g, '');
16
+ const stem = /[a-z0-9]/i.test(normalizedValue)
17
+ ? (0, common_1.generateSlug)(normalizedValue)
18
+ : `${fallbackPrefix}-${(0, exports.getStableCodeHash)(fallbackHashValue)}`;
19
+ return stem.slice(0, maxLength);
20
+ };
21
+ exports.getContentFileNameStem = getContentFileNameStem;
22
+ const allocateContentFileNames = ({ items, fallbackPrefix, extension = '.yml', maxStemLength = exports.CODE_FILENAME_MAX_STEM_LENGTH, existingFileNameByIdentity = new Map(), reservedFileNames = [], }) => {
23
+ const stems = items.map(({ identity, displayName, preferredFileName }) => preferredFileName
24
+ ? preferredFileName.slice(0, Math.max(0, preferredFileName.length - extension.length))
25
+ : (0, exports.getContentFileNameStem)({
26
+ value: displayName,
27
+ fallbackPrefix,
28
+ fallbackHashValue: identity,
29
+ maxLength: maxStemLength,
30
+ }));
31
+ const counts = stems.reduce((result, stem) => result.set(stem.toLowerCase(), (result.get(stem.toLowerCase()) ?? 0) + 1), new Map());
32
+ const usedFileNames = new Set([...reservedFileNames].map((fileName) => fileName.toLowerCase()));
33
+ return items.map(({ identity, preferredFileName }, index) => {
34
+ const existingFileName = existingFileNameByIdentity.get(identity);
35
+ if (existingFileName !== undefined) {
36
+ usedFileNames.add(existingFileName.toLowerCase());
37
+ return existingFileName;
38
+ }
39
+ const stem = stems[index];
40
+ const candidateFileName = preferredFileName ??
41
+ ((counts.get(stem.toLowerCase()) ?? 0) === 1
42
+ ? `${stem}${extension}`
43
+ : `${stem.slice(0, maxStemLength - exports.CODE_FILENAME_HASH_LENGTH - 1)}-${(0, exports.getStableCodeHash)(identity)}${extension}`);
44
+ if (!usedFileNames.has(candidateFileName.toLowerCase())) {
45
+ usedFileNames.add(candidateFileName.toLowerCase());
46
+ return candidateFileName;
47
+ }
48
+ for (const hashLength of [8, 12, 16, 24, 32, 64]) {
49
+ const suffix = `-${(0, exports.getStableCodeHash)(identity, hashLength)}`;
50
+ const candidate = `${stem.slice(0, maxStemLength - suffix.length)}${suffix}${extension}`;
51
+ if (!usedFileNames.has(candidate.toLowerCase())) {
52
+ usedFileNames.add(candidate.toLowerCase());
53
+ return candidate;
54
+ }
55
+ }
56
+ throw new common_1.ParameterError(`Could not allocate a safe filename for content "${identity}"`);
57
+ });
58
+ };
59
+ exports.allocateContentFileNames = allocateContentFileNames;
60
+ const assertSafeContentFilePath = (relativePath) => {
61
+ if (relativePath.length === 0 ||
62
+ path.isAbsolute(relativePath) ||
63
+ relativePath.split(/[\\/]/).some((segment) => segment === '..')) {
64
+ throw new common_1.ParameterError(`Unsafe content-as-code file path "${relativePath}"`);
65
+ }
66
+ };
67
+ exports.assertSafeContentFilePath = assertSafeContentFilePath;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fileNames.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileNames.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/fileNames.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const crypto_1 = require("crypto");
4
+ const vitest_1 = require("vitest");
5
+ const fileNames_1 = require("./fileNames");
6
+ (0, vitest_1.describe)('content-as-code filenames', () => {
7
+ (0, vitest_1.it)('normalizes unicode and preserves compatibility extensions', () => {
8
+ (0, vitest_1.expect)((0, fileNames_1.allocateContentFileNames)({
9
+ items: [{ identity: 'malaga', displayName: 'Málaga Finance' }],
10
+ fallbackPrefix: 'group',
11
+ extension: '.space.yml',
12
+ })).toStrictEqual(['malaga-finance.space.yml']);
13
+ });
14
+ (0, vitest_1.it)('uses stable identities for unicode-only names and collisions', () => {
15
+ const hash = (0, crypto_1.createHash)('sha256')
16
+ .update('party')
17
+ .digest('hex')
18
+ .slice(0, 8);
19
+ (0, vitest_1.expect)((0, fileNames_1.allocateContentFileNames)({
20
+ items: [{ identity: 'party', displayName: '🎉' }],
21
+ fallbackPrefix: 'group',
22
+ })).toStrictEqual([`group-${hash}.yml`]);
23
+ const names = (0, fileNames_1.allocateContentFileNames)({
24
+ items: [
25
+ { identity: 'one', displayName: 'Finance' },
26
+ { identity: 'two', displayName: 'finance' },
27
+ ],
28
+ fallbackPrefix: 'group',
29
+ });
30
+ (0, vitest_1.expect)(new Set(names.map((name) => name.toLowerCase())).size).toBe(2);
31
+ });
32
+ (0, vitest_1.it)('bounds long stems while retaining a stable collision suffix', () => {
33
+ const names = (0, fileNames_1.allocateContentFileNames)({
34
+ items: [
35
+ { identity: 'one', displayName: 'a'.repeat(500) },
36
+ { identity: 'two', displayName: 'a'.repeat(500) },
37
+ ],
38
+ fallbackPrefix: 'chart',
39
+ });
40
+ (0, vitest_1.expect)(names.every((name) => name.length <= 204)).toBe(true);
41
+ (0, vitest_1.expect)(names[0]).not.toBe(names[1]);
42
+ });
43
+ (0, vitest_1.it)('preserves SQL and URI-encoded compatibility profiles', () => {
44
+ (0, vitest_1.expect)((0, fileNames_1.allocateContentFileNames)({
45
+ items: [
46
+ {
47
+ identity: 'orders',
48
+ displayName: 'Orders',
49
+ preferredFileName: 'orders.sql.yml',
50
+ },
51
+ ],
52
+ fallbackPrefix: 'sql-chart',
53
+ extension: '.sql.yml',
54
+ })).toStrictEqual(['orders.sql.yml']);
55
+ (0, vitest_1.expect)((0, fileNames_1.allocateContentFileNames)({
56
+ items: [
57
+ {
58
+ identity: 'finance/view',
59
+ displayName: 'Finance view',
60
+ preferredFileName: 'finance%2Fview.yml',
61
+ },
62
+ ],
63
+ fallbackPrefix: 'virtual-view',
64
+ })).toStrictEqual(['finance%2Fview.yml']);
65
+ });
66
+ (0, vitest_1.it)('reuses an existing file owned by the same identity', () => {
67
+ (0, vitest_1.expect)((0, fileNames_1.allocateContentFileNames)({
68
+ items: [{ identity: 'finance', displayName: 'New name' }],
69
+ fallbackPrefix: 'group',
70
+ existingFileNameByIdentity: new Map([
71
+ ['finance', 'user-owned-name.yaml'],
72
+ ]),
73
+ })).toStrictEqual(['user-owned-name.yaml']);
74
+ });
75
+ (0, vitest_1.it)('rejects absolute and parent-traversal paths', () => {
76
+ (0, vitest_1.expect)(() => (0, fileNames_1.assertSafeContentFilePath)('../secrets.yml')).toThrow('Unsafe');
77
+ (0, vitest_1.expect)(() => (0, fileNames_1.assertSafeContentFilePath)('/tmp/file.yml')).toThrow('Unsafe');
78
+ });
79
+ });
@@ -0,0 +1,8 @@
1
+ import { type AgentAsCode, type AlertAsCode, type GoogleSheetsSyncAsCode, type ScheduledDeliveryAsCode, type VirtualViewAsCode } from '@lightdash/common';
2
+ import type { CodeResourceDefinition } from './resource';
3
+ export declare const VIRTUAL_VIEW_CODE_RESOURCE: CodeResourceDefinition<VirtualViewAsCode>;
4
+ export declare const AI_AGENT_CODE_RESOURCE: CodeResourceDefinition<AgentAsCode>;
5
+ export declare const SCHEDULED_DELIVERY_CODE_RESOURCE: CodeResourceDefinition<ScheduledDeliveryAsCode>;
6
+ export declare const ALERT_CODE_RESOURCE: CodeResourceDefinition<AlertAsCode>;
7
+ export declare const GOOGLE_SHEETS_CODE_RESOURCE: CodeResourceDefinition<GoogleSheetsSyncAsCode>;
8
+ //# sourceMappingURL=projectResources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"projectResources.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/projectResources.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iBAAiB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAEzD,eAAO,MAAM,0BAA0B,EAAE,sBAAsB,CAAC,iBAAiB,CAwB5E,CAAC;AAEN,eAAO,MAAM,sBAAsB,EAAE,sBAAsB,CAAC,WAAW,CAyBtE,CAAC;AA4CF,eAAO,MAAM,gCAAgC,iDAKvC,CAAC;AAEP,eAAO,MAAM,mBAAmB,qCAI9B,CAAC;AAEH,eAAO,MAAM,2BAA2B,gDAKlC,CAAC"}
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GOOGLE_SHEETS_CODE_RESOURCE = exports.ALERT_CODE_RESOURCE = exports.SCHEDULED_DELIVERY_CODE_RESOURCE = exports.AI_AGENT_CODE_RESOURCE = exports.VIRTUAL_VIEW_CODE_RESOURCE = void 0;
4
+ const common_1 = require("@lightdash/common");
5
+ exports.VIRTUAL_VIEW_CODE_RESOURCE = {
6
+ kind: common_1.ContentAsCodeType.VIRTUAL_VIEW,
7
+ displayLabel: 'virtual view',
8
+ identityLabel: 'slug',
9
+ scope: 'project',
10
+ folderName: 'virtual-views',
11
+ acceptedExtensions: ['.yml', '.yaml'],
12
+ fileName: {
13
+ strategy: 'uriEncodedIdentity',
14
+ fallbackPrefix: 'virtual-view',
15
+ extension: '.yml',
16
+ },
17
+ dependencies: [],
18
+ identity: ({ slug }) => slug,
19
+ displayName: ({ name }) => name,
20
+ parse: (input, source) => (0, common_1.parseVersionedContentAsCodeDocument)({
21
+ input,
22
+ source,
23
+ resourceLabel: 'virtual view',
24
+ contentType: common_1.ContentAsCodeType.VIRTUAL_VIEW,
25
+ }),
26
+ sort: (left, right) => left.slug.localeCompare(right.slug),
27
+ };
28
+ exports.AI_AGENT_CODE_RESOURCE = {
29
+ kind: common_1.ContentAsCodeType.AI_AGENT,
30
+ displayLabel: 'AI agent',
31
+ identityLabel: 'slug',
32
+ scope: 'project',
33
+ folderName: 'ai-agents',
34
+ acceptedExtensions: ['.yml', '.yaml'],
35
+ fileName: {
36
+ strategy: 'identity',
37
+ fallbackPrefix: 'ai-agent',
38
+ extension: '.yml',
39
+ },
40
+ dependencies: [],
41
+ recursive: true,
42
+ identity: ({ slug }) => slug,
43
+ displayName: ({ name }) => name,
44
+ parse: (input, source) => (0, common_1.parseVersionedContentAsCodeDocument)({
45
+ input,
46
+ source,
47
+ resourceLabel: 'AI agent',
48
+ contentType: common_1.ContentAsCodeType.AI_AGENT,
49
+ }),
50
+ serialize: ({ updatedAt, downloadedAt, ...agent }) => agent,
51
+ sort: (left, right) => left.slug.localeCompare(right.slug),
52
+ };
53
+ const scheduledResource = ({ contentType, folderName, label, }) => ({
54
+ kind: contentType,
55
+ displayLabel: label,
56
+ identityLabel: 'slug',
57
+ scope: 'project',
58
+ folderName,
59
+ acceptedExtensions: ['.yml', '.yaml'],
60
+ fileName: {
61
+ strategy: 'scheduledResource',
62
+ fallbackPrefix: contentType,
63
+ extension: '.yml',
64
+ },
65
+ dependencies: ['chart', 'dashboard'],
66
+ recursive: true,
67
+ identity: ({ slug }) => slug,
68
+ displayName: ({ name }) => name,
69
+ matches: (input) => typeof input === 'object' &&
70
+ input !== null &&
71
+ 'contentType' in input &&
72
+ input.contentType === contentType,
73
+ parse: (input, source) => (0, common_1.parseVersionedContentAsCodeDocument)({
74
+ input,
75
+ source,
76
+ resourceLabel: label,
77
+ contentType,
78
+ }),
79
+ sort: (left, right) => left.slug.localeCompare(right.slug),
80
+ });
81
+ exports.SCHEDULED_DELIVERY_CODE_RESOURCE = scheduledResource({
82
+ contentType: common_1.ContentAsCodeType.SCHEDULED_DELIVERY,
83
+ folderName: 'scheduled-deliveries',
84
+ label: 'scheduled delivery',
85
+ });
86
+ exports.ALERT_CODE_RESOURCE = scheduledResource({
87
+ contentType: common_1.ContentAsCodeType.ALERT,
88
+ folderName: 'alerts',
89
+ label: 'alert',
90
+ });
91
+ exports.GOOGLE_SHEETS_CODE_RESOURCE = scheduledResource({
92
+ contentType: common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC,
93
+ folderName: 'google-sheets',
94
+ label: 'Google Sheets sync',
95
+ });
@@ -0,0 +1,9 @@
1
+ import { type ContentAsCodeResourceKind } from '@lightdash/common';
2
+ export type CodeResourceRegistration = {
3
+ kind: ContentAsCodeResourceKind;
4
+ dependencies: readonly ContentAsCodeResourceKind[];
5
+ };
6
+ export declare const ORGANIZATION_CODE_RESOURCES: readonly CodeResourceRegistration[];
7
+ export declare const PROJECT_CODE_RESOURCES: readonly CodeResourceRegistration[];
8
+ export declare const assertCodeResourceDependencyOrder: (resources: readonly CodeResourceRegistration[]) => void;
9
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAGH,KAAK,yBAAyB,EACjC,MAAM,mBAAmB,CAAC;AAY3B,MAAM,MAAM,wBAAwB,GAAG;IACnC,IAAI,EAAE,yBAAyB,CAAC;IAChC,YAAY,EAAE,SAAS,yBAAyB,EAAE,CAAC;CACtD,CAAC;AAEF,eAAO,MAAM,2BAA2B,EAAE,SAAS,wBAAwB,EACH,CAAC;AAEzE,eAAO,MAAM,sBAAsB,EAAE,SAAS,wBAAwB,EAuBrE,CAAC;AAEF,eAAO,MAAM,iCAAiC,GAC1C,WAAW,SAAS,wBAAwB,EAAE,KAC/C,IAeF,CAAC"}
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertCodeResourceDependencyOrder = exports.PROJECT_CODE_RESOURCES = exports.ORGANIZATION_CODE_RESOURCES = void 0;
4
+ const common_1 = require("@lightdash/common");
5
+ const customRoles_1 = require("../organizationContent/customRoles");
6
+ const groups_1 = require("../organizationContent/groups");
7
+ const users_1 = require("../organizationContent/users");
8
+ const projectResources_1 = require("./projectResources");
9
+ exports.ORGANIZATION_CODE_RESOURCES = [customRoles_1.CUSTOM_ROLE_CODE_RESOURCE, users_1.USER_CODE_RESOURCE, groups_1.GROUP_CODE_RESOURCE];
10
+ exports.PROJECT_CODE_RESOURCES = [
11
+ { kind: common_1.ContentAsCodeType.SPACE, dependencies: [] },
12
+ projectResources_1.VIRTUAL_VIEW_CODE_RESOURCE,
13
+ {
14
+ kind: common_1.ContentAsCodeType.CHART,
15
+ dependencies: [common_1.ContentAsCodeType.SPACE, common_1.ContentAsCodeType.VIRTUAL_VIEW],
16
+ },
17
+ {
18
+ kind: common_1.ContentAsCodeType.SQL_CHART,
19
+ dependencies: [common_1.ContentAsCodeType.SPACE],
20
+ },
21
+ {
22
+ kind: common_1.ContentAsCodeType.DASHBOARD,
23
+ dependencies: [
24
+ common_1.ContentAsCodeType.SPACE,
25
+ common_1.ContentAsCodeType.CHART,
26
+ common_1.ContentAsCodeType.SQL_CHART,
27
+ ],
28
+ },
29
+ projectResources_1.AI_AGENT_CODE_RESOURCE,
30
+ projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE,
31
+ projectResources_1.ALERT_CODE_RESOURCE,
32
+ projectResources_1.GOOGLE_SHEETS_CODE_RESOURCE,
33
+ ];
34
+ const assertCodeResourceDependencyOrder = (resources) => {
35
+ const completed = new Set();
36
+ for (const resource of resources) {
37
+ const missingDependencies = resource.dependencies.filter((dependency) => !completed.has(dependency));
38
+ if (missingDependencies.length > 0) {
39
+ throw new common_1.ParameterError(`Content-as-code resource "${resource.kind}" must run after ${missingDependencies.join(', ')}`);
40
+ }
41
+ completed.add(resource.kind);
42
+ }
43
+ };
44
+ exports.assertCodeResourceDependencyOrder = assertCodeResourceDependencyOrder;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=registry.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/registry.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const registry_1 = require("./registry");
5
+ (0, vitest_1.describe)('content-as-code resource registry', () => {
6
+ (0, vitest_1.it)('keeps organization dependencies in a valid order', () => {
7
+ (0, vitest_1.expect)(() => (0, registry_1.assertCodeResourceDependencyOrder)(registry_1.ORGANIZATION_CODE_RESOURCES)).not.toThrow();
8
+ });
9
+ (0, vitest_1.it)('keeps project dependencies in a valid order', () => {
10
+ (0, vitest_1.expect)(() => (0, registry_1.assertCodeResourceDependencyOrder)(registry_1.PROJECT_CODE_RESOURCES)).not.toThrow();
11
+ });
12
+ (0, vitest_1.it)('rejects resources registered before their dependencies', () => {
13
+ (0, vitest_1.expect)(() => (0, registry_1.assertCodeResourceDependencyOrder)([
14
+ { kind: 'group', dependencies: ['user'] },
15
+ { kind: 'user', dependencies: [] },
16
+ ])).toThrow('group');
17
+ });
18
+ });
@@ -0,0 +1,67 @@
1
+ import { ParameterError, type ContentAsCodeResourceKind, type ContentAsCodeUpsertAction } from '@lightdash/common';
2
+ export type CodeResourceScope = 'project' | 'organization';
3
+ export type CodeResourceDefinition<Document> = {
4
+ kind: ContentAsCodeResourceKind;
5
+ displayLabel: string;
6
+ identityLabel: string;
7
+ scope: CodeResourceScope;
8
+ folderName: string;
9
+ acceptedExtensions: readonly string[];
10
+ fileName: {
11
+ strategy: 'normalizedDisplayName' | 'identity' | 'uriEncodedIdentity' | 'space' | 'scheduledResource';
12
+ fallbackPrefix: string;
13
+ extension: string;
14
+ };
15
+ dependencies: readonly ContentAsCodeResourceKind[];
16
+ recursive?: boolean;
17
+ identity: (document: Document) => string;
18
+ normalizeIdentity?: (identity: string) => string;
19
+ displayName: (document: Document) => string;
20
+ matches?: (value: unknown) => boolean;
21
+ parse: (value: unknown, source: string) => Document;
22
+ serialize?: (document: Document) => unknown;
23
+ sort?: (left: Document, right: Document) => number;
24
+ };
25
+ export type CodeFileFailure = {
26
+ message: string;
27
+ };
28
+ export type CodeResourceFiles<Document> = {
29
+ files: Array<{
30
+ filePath: string;
31
+ document: Document;
32
+ }>;
33
+ failures: CodeFileFailure[];
34
+ };
35
+ export type CodeResourceUploadSummary = {
36
+ created: number;
37
+ updated: number;
38
+ unchanged: number;
39
+ failed: number;
40
+ failures: CodeFileFailure[];
41
+ };
42
+ export declare class CodeResourcePhaseError extends ParameterError {
43
+ readonly resource: ContentAsCodeResourceKind;
44
+ constructor(resource: ContentAsCodeResourceKind, message: string);
45
+ }
46
+ export declare const isCodeResourcePhaseError: (error: unknown) => error is CodeResourcePhaseError;
47
+ export declare const readCodeResourceFiles: <Document>({ definition, basePath, }: {
48
+ definition: CodeResourceDefinition<Document>;
49
+ basePath: string;
50
+ }) => Promise<CodeResourceFiles<Document>>;
51
+ export declare const writeCodeResourceDocuments: <Document>({ definition, basePath, documents, }: {
52
+ definition: CodeResourceDefinition<Document>;
53
+ basePath: string;
54
+ documents: Document[];
55
+ }) => Promise<void>;
56
+ export declare const downloadCodeResource: <Document>({ definition, basePath, list, }: {
57
+ definition: CodeResourceDefinition<Document>;
58
+ basePath: string;
59
+ list: () => Promise<Document[]>;
60
+ }) => Promise<number>;
61
+ export declare const uploadCodeResource: <Document>({ definition, basePath, upsert, }: {
62
+ definition: CodeResourceDefinition<Document>;
63
+ basePath: string;
64
+ upsert: (document: Document) => Promise<ContentAsCodeUpsertAction>;
65
+ }) => Promise<CodeResourceUploadSummary>;
66
+ export declare const assertCodeResourceFilesValid: <Document>(result: CodeResourceFiles<Document>) => void;
67
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/resource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertCodeResourceFilesValid = exports.uploadCodeResource = exports.downloadCodeResource = exports.writeCodeResourceDocuments = exports.readCodeResourceFiles = exports.isCodeResourcePhaseError = exports.CodeResourcePhaseError = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /* eslint-disable no-await-in-loop */
6
+ const common_1 = require("@lightdash/common");
7
+ const fs_1 = require("fs");
8
+ const yaml = tslib_1.__importStar(require("js-yaml"));
9
+ const path = tslib_1.__importStar(require("path"));
10
+ const fileNames_1 = require("./fileNames");
11
+ class CodeResourcePhaseError extends common_1.ParameterError {
12
+ resource;
13
+ constructor(resource, message) {
14
+ super(message);
15
+ this.name = 'CodeResourcePhaseError';
16
+ this.resource = resource;
17
+ }
18
+ }
19
+ exports.CodeResourcePhaseError = CodeResourcePhaseError;
20
+ const isCodeResourcePhaseError = (error) => error instanceof CodeResourcePhaseError;
21
+ exports.isCodeResourcePhaseError = isCodeResourcePhaseError;
22
+ const isAcceptedFile = (definition, entry) => entry.isFile() &&
23
+ definition.acceptedExtensions.some((extension) => entry.name.endsWith(extension));
24
+ const normalizeIdentity = (definition, identity) => definition.normalizeIdentity?.(identity) ?? identity;
25
+ const readCodeResourceFiles = async ({ definition, basePath, }) => {
26
+ const folder = path.join(basePath, definition.folderName);
27
+ let entries;
28
+ try {
29
+ entries = await fs_1.promises.readdir(folder, {
30
+ withFileTypes: true,
31
+ recursive: definition.recursive,
32
+ });
33
+ }
34
+ catch (error) {
35
+ if (error.code === 'ENOENT') {
36
+ return { files: [], failures: [] };
37
+ }
38
+ throw error;
39
+ }
40
+ const parsed = { files: [], failures: [] };
41
+ for (const entry of entries
42
+ .filter((item) => isAcceptedFile(definition, item))
43
+ .sort((left, right) => left.name.localeCompare(right.name))) {
44
+ const filePath = path.join(entry.parentPath ?? folder, entry.name);
45
+ try {
46
+ const value = yaml.load(await fs_1.promises.readFile(filePath, 'utf8'));
47
+ if (!definition.matches || definition.matches(value)) {
48
+ const document = definition.parse(value, filePath);
49
+ parsed.files.push({ filePath, document });
50
+ }
51
+ }
52
+ catch (error) {
53
+ parsed.failures.push({
54
+ message: `Invalid ${definition.displayLabel} file "${filePath}": ${(0, common_1.getErrorMessage)(error)}`,
55
+ });
56
+ }
57
+ }
58
+ const filesByIdentity = parsed.files.reduce((result, file) => {
59
+ const identity = normalizeIdentity(definition, definition.identity(file.document));
60
+ result.set(identity, [...(result.get(identity) ?? []), file]);
61
+ return result;
62
+ }, new Map());
63
+ const duplicateIdentities = new Set([...filesByIdentity.entries()]
64
+ .filter(([, files]) => files.length > 1)
65
+ .map(([identity]) => identity));
66
+ for (const identity of [...duplicateIdentities].sort()) {
67
+ for (const file of filesByIdentity.get(identity) ?? []) {
68
+ parsed.failures.push({
69
+ message: `Duplicate ${definition.displayLabel} ${definition.identityLabel} "${identity}" in "${file.filePath}"`,
70
+ });
71
+ }
72
+ }
73
+ parsed.files = parsed.files.filter(({ document }) => !duplicateIdentities.has(normalizeIdentity(definition, definition.identity(document))));
74
+ return parsed;
75
+ };
76
+ exports.readCodeResourceFiles = readCodeResourceFiles;
77
+ const writeCodeResourceDocuments = async ({ definition, basePath, documents, }) => {
78
+ const sortedDocuments = definition.sort
79
+ ? [...documents].sort(definition.sort)
80
+ : documents;
81
+ const folder = path.join(basePath, definition.folderName);
82
+ await fs_1.promises.mkdir(folder, { recursive: true });
83
+ const current = await (0, exports.readCodeResourceFiles)({ definition, basePath });
84
+ const existingFileNameByIdentity = new Map(current.files.map(({ filePath, document }) => [
85
+ definition.identity(document),
86
+ path.basename(filePath),
87
+ ]));
88
+ const reservedFileNames = (await fs_1.promises.readdir(folder, { withFileTypes: true }))
89
+ .filter((entry) => entry.isFile())
90
+ .map((entry) => entry.name);
91
+ const preferredFileName = (document) => {
92
+ switch (definition.fileName.strategy) {
93
+ case 'identity':
94
+ return `${definition.identity(document)}${definition.fileName.extension}`;
95
+ case 'uriEncodedIdentity':
96
+ return `${encodeURIComponent(definition.identity(document))}${definition.fileName.extension}`;
97
+ default:
98
+ return undefined;
99
+ }
100
+ };
101
+ const fileNames = (0, fileNames_1.allocateContentFileNames)({
102
+ items: sortedDocuments.map((document) => ({
103
+ identity: definition.identity(document),
104
+ displayName: definition.displayName(document),
105
+ preferredFileName: preferredFileName(document),
106
+ })),
107
+ fallbackPrefix: definition.fileName.fallbackPrefix,
108
+ extension: definition.fileName.extension,
109
+ existingFileNameByIdentity,
110
+ reservedFileNames,
111
+ });
112
+ await Promise.all(sortedDocuments.map((document, index) => fs_1.promises.writeFile(path.join(folder, fileNames[index]), yaml.dump(definition.serialize?.(document) ?? document, {
113
+ quotingType: '"',
114
+ sortKeys: true,
115
+ }))));
116
+ const downloadedIdentities = new Set(sortedDocuments.map((document) => normalizeIdentity(definition, definition.identity(document))));
117
+ await Promise.all(current.files
118
+ .filter(({ document }) => !downloadedIdentities.has(normalizeIdentity(definition, definition.identity(document))))
119
+ .map(({ filePath }) => fs_1.promises.unlink(filePath)));
120
+ };
121
+ exports.writeCodeResourceDocuments = writeCodeResourceDocuments;
122
+ const downloadCodeResource = async ({ definition, basePath, list, }) => {
123
+ const documents = await list();
124
+ await (0, exports.writeCodeResourceDocuments)({ definition, basePath, documents });
125
+ return documents.length;
126
+ };
127
+ exports.downloadCodeResource = downloadCodeResource;
128
+ const getActionCountKey = (action) => {
129
+ switch (action) {
130
+ case common_1.PromotionAction.CREATE:
131
+ return 'created';
132
+ case common_1.PromotionAction.UPDATE:
133
+ return 'updated';
134
+ case common_1.PromotionAction.NO_CHANGES:
135
+ return 'unchanged';
136
+ default:
137
+ return (0, common_1.assertUnreachable)(action, 'Unsupported content-as-code action');
138
+ }
139
+ };
140
+ const uploadCodeResource = async ({ definition, basePath, upsert, }) => {
141
+ const { files, failures } = await (0, exports.readCodeResourceFiles)({
142
+ definition,
143
+ basePath,
144
+ });
145
+ const summary = {
146
+ created: 0,
147
+ updated: 0,
148
+ unchanged: 0,
149
+ failed: 0,
150
+ failures,
151
+ };
152
+ for (const { filePath, document } of files) {
153
+ try {
154
+ summary[getActionCountKey(await upsert(document))] += 1;
155
+ }
156
+ catch (error) {
157
+ summary.failures.push({
158
+ message: `Invalid ${definition.displayLabel} file "${filePath}": ${(0, common_1.getErrorMessage)(error)}`,
159
+ });
160
+ }
161
+ }
162
+ summary.failed = summary.failures.length;
163
+ return summary;
164
+ };
165
+ exports.uploadCodeResource = uploadCodeResource;
166
+ const assertCodeResourceFilesValid = (result) => {
167
+ if (result.failures.length > 0) {
168
+ throw new common_1.ParameterError(result.failures.map(({ message }) => message).join('\n'));
169
+ }
170
+ };
171
+ exports.assertCodeResourceFilesValid = assertCodeResourceFilesValid;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=resource.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.test.d.ts","sourceRoot":"","sources":["../../../src/handlers/contentAsCode/resource.test.ts"],"names":[],"mappings":""}