@lightdash/cli 1.137.0 → 1.138.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.
@@ -1 +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;AAa3B,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,EAwBrE,CAAC;AAEF,eAAO,MAAM,iCAAiC,cAC/B,SAAS,wBAAwB,EAAE,KAC/C,IAeF,CAAC"}
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;AAc3B,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,EAMtE,CAAC;AAEN,eAAO,MAAM,sBAAsB,EAAE,SAAS,wBAAwB,EAwBrE,CAAC;AAEF,eAAO,MAAM,iCAAiC,cAC/B,SAAS,wBAAwB,EAAE,KAC/C,IAeF,CAAC"}
@@ -4,9 +4,15 @@ exports.assertCodeResourceDependencyOrder = exports.PROJECT_CODE_RESOURCES = exp
4
4
  const common_1 = require("@lightdash/common");
5
5
  const customRoles_1 = require("../organizationContent/customRoles");
6
6
  const groups_1 = require("../organizationContent/groups");
7
+ const themes_1 = require("../organizationContent/themes");
7
8
  const users_1 = require("../organizationContent/users");
8
9
  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.ORGANIZATION_CODE_RESOURCES = [
11
+ customRoles_1.CUSTOM_ROLE_CODE_RESOURCE,
12
+ users_1.USER_CODE_RESOURCE,
13
+ groups_1.GROUP_CODE_RESOURCE,
14
+ themes_1.THEME_CODE_RESOURCE,
15
+ ];
10
16
  exports.PROJECT_CODE_RESOURCES = [
11
17
  { kind: common_1.ContentAsCodeType.SPACE, dependencies: [] },
12
18
  projectResources_1.VIRTUAL_VIEW_CODE_RESOURCE,
@@ -1,11 +1,15 @@
1
1
  import { ApiResponse, ProjectType, type LightdashUserWithAbilityRules } from '@lightdash/common';
2
- import { BodyInit } from 'node-fetch';
2
+ import { BodyInit, type Response } from 'node-fetch';
3
3
  type LightdashApiProps = {
4
4
  method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
5
5
  url: string;
6
6
  body: BodyInit | undefined;
7
7
  };
8
+ type LightdashRawApiProps = LightdashApiProps & {
9
+ headers?: Record<string, string>;
10
+ };
8
11
  export declare const setGzipEnabled: (enabled: boolean) => void;
12
+ export declare const lightdashRawApi: ({ method, url, body, headers: requestHeaders, }: LightdashRawApiProps) => Promise<Response>;
9
13
  export declare const lightdashApi: <T extends ApiResponse['results']>({ method, url, body, }: LightdashApiProps) => Promise<T>;
10
14
  export declare const getUserContext: () => Promise<LightdashUserWithAbilityRules>;
11
15
  export type ContentAsCodeUploadPermissions = {
@@ -1 +1 @@
1
- {"version":3,"file":"apiClient.d.ts","sourceRoot":"","sources":["../../../src/handlers/dbt/apiClient.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,WAAW,EAIX,WAAW,EAEX,KAAK,6BAA6B,EAGrC,MAAM,mBAAmB,CAAC;AAC3B,OAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS7C,KAAK,iBAAiB,GAAG;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B,CAAC;AAKF,eAAO,MAAM,cAAc,YAAa,OAAO,SAE9C,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,CAAC,SAAS,WAAW,CAAC,SAAS,CAAC,0BAIhE,iBAAiB,KAAG,OAAO,CAAC,CAAC,CAqE/B,CAAC;AAEF,eAAO,MAAM,cAAc,QACb,OAAO,CAAC,6BAA6B,CAKzC,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,iCAAiC,gBAC7B,MAAM,KACpB,OAAO,CAAC,8BAA8B,CA0FxC,CAAC;AAEF,eAAO,MAAM,8BAA8B,wBAClB,MAAM,GAAG,SAAS,eAC1B,WAAW,KACzB,OAAO,CAAC,IAAI,CA0Gd,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CA2B1D,CAAC"}
1
+ {"version":3,"file":"apiClient.d.ts","sourceRoot":"","sources":["../../../src/handlers/dbt/apiClient.ts"],"names":[],"mappings":"AACA,OAAO,EAGH,WAAW,EAIX,WAAW,EAEX,KAAK,6BAA6B,EAGrC,MAAM,mBAAmB,CAAC;AAC3B,OAAc,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAS5D,KAAK,iBAAiB,GAAG;IACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B,CAAC;AAEF,KAAK,oBAAoB,GAAG,iBAAiB,GAAG;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAKF,eAAO,MAAM,cAAc,YAAa,OAAO,SAE9C,CAAC;AAEF,eAAO,MAAM,eAAe,oDAKzB,oBAAoB,KAAG,OAAO,CAAC,QAAQ,CA8BzC,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,CAAC,SAAS,WAAW,CAAC,SAAS,CAAC,0BAIhE,iBAAiB,KAAG,OAAO,CAAC,CAAC,CAgD/B,CAAC;AAEF,eAAO,MAAM,cAAc,QACb,OAAO,CAAC,6BAA6B,CAKzC,CAAC;AAEX,MAAM,MAAM,8BAA8B,GAAG;IACzC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,mBAAmB,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,eAAO,MAAM,iCAAiC,gBAC7B,MAAM,KACpB,OAAO,CAAC,8BAA8B,CA0FxC,CAAC;AAEF,eAAO,MAAM,8BAA8B,wBAClB,MAAM,GAAG,SAAS,eAC1B,WAAW,KACzB,OAAO,CAAC,IAAI,CA0Gd,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,IAAI,CA2B1D,CAAC"}
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.checkLightdashVersion = exports.checkProjectCreationPermission = exports.getContentAsCodeUploadPermissions = exports.getUserContext = exports.lightdashApi = exports.setGzipEnabled = void 0;
3
+ exports.checkLightdashVersion = exports.checkProjectCreationPermission = exports.getContentAsCodeUploadPermissions = exports.getUserContext = exports.lightdashApi = exports.lightdashRawApi = exports.setGzipEnabled = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const ability_1 = require("@casl/ability");
6
6
  const common_1 = require("@lightdash/common");
@@ -18,14 +18,33 @@ const setGzipEnabled = (enabled) => {
18
18
  gzipEnabled = enabled;
19
19
  };
20
20
  exports.setGzipEnabled = setGzipEnabled;
21
- const lightdashApi = async ({ method, url, body, }) => {
21
+ const lightdashRawApi = async ({ method, url, body, headers: requestHeaders = {}, }) => {
22
22
  const config = await (0, config_1.getConfig)();
23
23
  if (!(config.context?.apiKey && config.context.serverUrl)) {
24
24
  throw new common_1.AuthorizationError(`Not logged in. Run 'lightdash login --help'`);
25
25
  }
26
- const headers = (0, utils_1.buildRequestHeaders)(config.context.apiKey);
26
+ const headers = {
27
+ ...(0, utils_1.buildRequestHeaders)(config.context.apiKey),
28
+ ...requestHeaders,
29
+ };
27
30
  const fullUrl = new url_1.URL(url, config.context.serverUrl).href;
28
31
  globalState_1.default.debug(`> Making HTTP ${method} request to: ${fullUrl}`);
32
+ const response = await (0, node_fetch_1.default)(fullUrl, { method, headers, body });
33
+ globalState_1.default.debug(`> HTTP request returned status: ${response.status}`);
34
+ if (!response.ok) {
35
+ const contentType = response.headers.get('content-type');
36
+ if (contentType?.includes('application/json')) {
37
+ const data = await response.json();
38
+ throw new common_1.LightdashError(data.error);
39
+ }
40
+ const responseText = await response.text();
41
+ throw new Error(`Received non-JSON response from server (status ${response.status}): ${responseText}`);
42
+ }
43
+ return response;
44
+ };
45
+ exports.lightdashRawApi = lightdashRawApi;
46
+ const lightdashApi = async ({ method, url, body, }) => {
47
+ const headers = {};
29
48
  let requestBody = body;
30
49
  if (gzipEnabled &&
31
50
  body &&
@@ -42,21 +61,11 @@ const lightdashApi = async ({ method, url, body, }) => {
42
61
  globalState_1.default.debug(`> Gzip compression failed, sending uncompressed: ${err}`);
43
62
  }
44
63
  }
45
- return (0, node_fetch_1.default)(fullUrl, { method, headers, body: requestBody })
46
- .then((r) => {
47
- globalState_1.default.debug(`> HTTP request returned status: ${r.status}`);
48
- if (!r.ok) {
49
- const contentType = r.headers.get('content-type');
50
- if (contentType && contentType.includes('application/json')) {
51
- return r.json().then((d) => {
52
- throw new common_1.LightdashError(d.error);
53
- });
54
- }
55
- return r.text().then((text) => {
56
- throw new Error(`Received non-JSON response from server (status ${r.status}): ${text}`);
57
- });
58
- }
59
- return r;
64
+ return (0, exports.lightdashRawApi)({
65
+ method,
66
+ url,
67
+ body: requestBody,
68
+ headers,
60
69
  })
61
70
  .then((r) => r.json())
62
71
  .then((d) => {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AA0B3C,KAAK,0BAA0B,GAAG;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAkBF,eAAO,MAAM,4BAA4B,gBAAiB,MAAM,KAAG,MAClC,CAAC;AAElC,eAAO,MAAM,2BAA2B,4BAGrC,0BAA0B,KAAG,OAAO,CAAC,IAAI,CA2F3C,CAAC;AAEF,eAAO,MAAM,yBAAyB,yCAInC,0BAA0B,KAAG,OAAO,CAAC,IAAI,CAoJ3C,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAgC3C,KAAK,0BAA0B,GAAG;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAqBF,eAAO,MAAM,4BAA4B,gBAAiB,MAAM,KAAG,MAClC,CAAC;AAElC,eAAO,MAAM,2BAA2B,4BAGrC,0BAA0B,KAAG,OAAO,CAAC,IAAI,CAiG3C,CAAC;AAEF,eAAO,MAAM,yBAAyB,yCAInC,0BAA0B,KAAG,OAAO,CAAC,IAAI,CA+K3C,CAAC"}
@@ -12,10 +12,12 @@ const resource_1 = require("../contentAsCode/resource");
12
12
  const contentAsCodePaths_1 = require("../contentAsCodePaths");
13
13
  const customRoles_1 = require("./customRoles");
14
14
  const groups_1 = require("./groups");
15
+ const themes_1 = require("./themes");
15
16
  const users_1 = require("./users");
16
17
  const createCustomRolePartialUploadError = (message) => new resource_1.CodeResourcePhaseError('custom_role', message);
17
18
  const createUserPartialUploadError = (message) => new resource_1.CodeResourcePhaseError('user', message);
18
19
  const createGroupPartialUploadError = (message) => new resource_1.CodeResourcePhaseError('group', message);
20
+ const createThemePartialUploadError = (message) => new resource_1.CodeResourcePhaseError('theme', message);
19
21
  const isPartialUploadError = resource_1.isCodeResourcePhaseError;
20
22
  const isGroupServiceDisabledError = (error) => error instanceof common_1.LightdashError &&
21
23
  error.statusCode === 403 &&
@@ -65,6 +67,11 @@ const downloadOrganizationContent = async ({ customPath, config, }) => {
65
67
  output.completeItem('service unavailable', 'warning');
66
68
  globalState_1.default.debug('> Warning: groups were not downloaded because the group service is not enabled');
67
69
  }
70
+ const themesTotal = await output.runItem({
71
+ label: 'Themes',
72
+ action: () => (0, themes_1.downloadThemes)(organizationContentPath),
73
+ detail: (total) => `${total} downloaded`,
74
+ });
68
75
  const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
69
76
  if (!renderedSummary) {
70
77
  globalState_1.default.log(styles.success(`Downloaded organization content saved to ${organizationContentPath}`));
@@ -78,6 +85,7 @@ const downloadOrganizationContent = async ({ customPath, config, }) => {
78
85
  customRolesNum: customRolesTotal,
79
86
  usersNum: usersTotal,
80
87
  groupsNum: groupsTotal,
88
+ themesNum: themesTotal,
81
89
  timeToCompleted: (Date.now() - start) / 1000,
82
90
  },
83
91
  });
@@ -118,6 +126,7 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
118
126
  scope: 'organization',
119
127
  });
120
128
  try {
129
+ const preparedThemes = await (0, themes_1.prepareThemeUploads)(organizationContentPath);
121
130
  output.startItem('Custom roles');
122
131
  const summary = await (0, customRoles_1.uploadCustomRoles)(organizationUuid, organizationContentPath);
123
132
  const summaryMessage = (0, customRoles_1.formatCustomRoleUploadSummary)(summary);
@@ -152,6 +161,18 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
152
161
  throw createGroupPartialUploadError(`Processed groups: ${groupSummaryMessage}`);
153
162
  }
154
163
  output.completeItem(groupSummaryMessage);
164
+ output.startItem('Themes');
165
+ const themeSummary = await (0, themes_1.uploadThemes)(preparedThemes);
166
+ const themeSummaryMessage = (0, themes_1.formatThemeUploadSummary)(themeSummary);
167
+ if (themeSummary.failed > 0) {
168
+ themeSummary.failures.forEach(({ message }) => globalState_1.default.log(styles.error(message)));
169
+ if (themeSummary.completedSlugs.length > 0) {
170
+ globalState_1.default.log(styles.warning(`Processed themes before failure: ${themeSummary.completedSlugs.join(', ')}`));
171
+ }
172
+ output.prepareForFailureDetails();
173
+ throw createThemePartialUploadError(`Processed themes: ${themeSummaryMessage}`);
174
+ }
175
+ output.completeItem(themeSummaryMessage);
155
176
  const renderedSummary = output.complete(organizationContentPath, (Date.now() - start) / 1000);
156
177
  if (!renderedSummary) {
157
178
  globalState_1.default.log(styles.success(`Uploaded organization content from ${organizationContentPath}`));
@@ -173,6 +194,10 @@ const uploadOrganizationContent = async ({ customPath, config, sendInvites = fal
173
194
  groupsCreated: groupSummary.created,
174
195
  groupsUpdated: groupSummary.updated,
175
196
  groupsUnchanged: groupSummary.unchanged,
197
+ themesCreated: themeSummary.created,
198
+ themesUpdated: themeSummary.updated,
199
+ themesUnchanged: themeSummary.unchanged,
200
+ themesUploaded: themeSummary.created + themeSummary.updated,
176
201
  timeToCompleted: (Date.now() - start) / 1000,
177
202
  },
178
203
  });
@@ -7,6 +7,7 @@ const globalState_1 = tslib_1.__importDefault(require("../../globalState"));
7
7
  const customRoles_1 = require("./customRoles");
8
8
  const groups_1 = require("./groups");
9
9
  const index_1 = require("./index");
10
+ const themes_1 = require("./themes");
10
11
  const users_1 = require("./users");
11
12
  vi.mock('./customRoles', async (importOriginal) => ({
12
13
  ...(await importOriginal()),
@@ -24,6 +25,12 @@ vi.mock('./groups', async (importOriginal) => ({
24
25
  uploadGroups: vi.fn(),
25
26
  countDependencySkippedGroups: vi.fn(),
26
27
  }));
28
+ vi.mock('./themes', async (importOriginal) => ({
29
+ ...(await importOriginal()),
30
+ downloadThemes: vi.fn(),
31
+ prepareThemeUploads: vi.fn(),
32
+ uploadThemes: vi.fn(),
33
+ }));
27
34
  describe('organization content download', () => {
28
35
  const spinner = {
29
36
  start: vi.fn(),
@@ -47,6 +54,7 @@ describe('organization content download', () => {
47
54
  vi.mocked(customRoles_1.downloadCustomRoles).mockResolvedValue(2);
48
55
  vi.mocked(users_1.downloadUsers).mockResolvedValue(3);
49
56
  vi.mocked(groups_1.downloadGroups).mockResolvedValue(4);
57
+ vi.mocked(themes_1.downloadThemes).mockResolvedValue(5);
50
58
  });
51
59
  it('skips groups when the group service is disabled', async () => {
52
60
  vi.mocked(groups_1.downloadGroups).mockRejectedValue(new common_1.LightdashError({
@@ -62,7 +70,7 @@ describe('organization content download', () => {
62
70
  });
63
71
  it('reports a duration for every downloaded resource', async () => {
64
72
  await (0, index_1.downloadOrganizationContent)({ config });
65
- expect(spinner.succeed).toHaveBeenCalledTimes(3);
73
+ expect(spinner.succeed).toHaveBeenCalledTimes(4);
66
74
  spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
67
75
  });
68
76
  it('still fails for other group download errors', async () => {
@@ -112,6 +120,14 @@ describe('organization content upload sequencing', () => {
112
120
  dependencySkipped: 0,
113
121
  failures: failed > 0 ? [{ message: 'group failure' }] : [],
114
122
  });
123
+ const themeSummary = (failed = 0) => ({
124
+ created: failed > 0 ? 1 : 0,
125
+ updated: 0,
126
+ unchanged: 0,
127
+ failed,
128
+ completedSlugs: failed > 0 ? ['processed-theme'] : [],
129
+ failures: failed > 0 ? [{ message: 'theme failure' }] : [],
130
+ });
115
131
  beforeEach(() => {
116
132
  vi.clearAllMocks();
117
133
  vi.spyOn(globalState_1.default, 'startSpinner').mockReturnValue(spinner);
@@ -120,10 +136,16 @@ describe('organization content upload sequencing', () => {
120
136
  vi.mocked(customRoles_1.uploadCustomRoles).mockResolvedValue(customRoleSummary());
121
137
  vi.mocked(users_1.uploadUsers).mockResolvedValue(userSummary());
122
138
  vi.mocked(groups_1.uploadGroups).mockResolvedValue(groupSummary());
139
+ vi.mocked(themes_1.prepareThemeUploads).mockResolvedValue([]);
140
+ vi.mocked(themes_1.uploadThemes).mockResolvedValue(themeSummary());
123
141
  vi.mocked(groups_1.countDependencySkippedGroups).mockResolvedValue(2);
124
142
  });
125
- it('awaits custom roles, then users, then groups', async () => {
143
+ it('preflights themes, then awaits organization resource uploads in order', async () => {
126
144
  const phaseOrder = [];
145
+ vi.mocked(themes_1.prepareThemeUploads).mockImplementation(async () => {
146
+ phaseOrder.push('themes:preflight:start', 'themes:preflight:end');
147
+ return [];
148
+ });
127
149
  vi.mocked(customRoles_1.uploadCustomRoles).mockImplementation(async () => {
128
150
  phaseOrder.push('roles:start', 'roles:end');
129
151
  return customRoleSummary();
@@ -136,19 +158,27 @@ describe('organization content upload sequencing', () => {
136
158
  phaseOrder.push('groups:start', 'groups:end');
137
159
  return groupSummary();
138
160
  });
161
+ vi.mocked(themes_1.uploadThemes).mockImplementation(async () => {
162
+ phaseOrder.push('themes:start', 'themes:end');
163
+ return themeSummary();
164
+ });
139
165
  await (0, index_1.uploadOrganizationContent)({ config });
140
166
  expect(phaseOrder).toStrictEqual([
167
+ 'themes:preflight:start',
168
+ 'themes:preflight:end',
141
169
  'roles:start',
142
170
  'roles:end',
143
171
  'users:start',
144
172
  'users:end',
145
173
  'groups:start',
146
174
  'groups:end',
175
+ 'themes:start',
176
+ 'themes:end',
147
177
  ]);
148
178
  });
149
179
  it('reports a duration for every uploaded resource', async () => {
150
180
  await (0, index_1.uploadOrganizationContent)({ config });
151
- expect(spinner.succeed).toHaveBeenCalledTimes(3);
181
+ expect(spinner.succeed).toHaveBeenCalledTimes(4);
152
182
  spinner.succeed.mock.calls.forEach(([message]) => expect(message).toMatch(/\(\d+ms\)$/));
153
183
  });
154
184
  it('does not start users or groups when custom roles fail', async () => {
@@ -156,12 +186,27 @@ describe('organization content upload sequencing', () => {
156
186
  await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed custom roles');
157
187
  expect(users_1.uploadUsers).not.toHaveBeenCalled();
158
188
  expect(groups_1.uploadGroups).not.toHaveBeenCalled();
189
+ expect(themes_1.uploadThemes).not.toHaveBeenCalled();
159
190
  expect(groups_1.countDependencySkippedGroups).toHaveBeenCalledOnce();
160
191
  });
161
192
  it('does not start groups when users fail', async () => {
162
193
  vi.mocked(users_1.uploadUsers).mockResolvedValue(userSummary(1));
163
194
  await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed users');
164
195
  expect(groups_1.uploadGroups).not.toHaveBeenCalled();
196
+ expect(themes_1.uploadThemes).not.toHaveBeenCalled();
165
197
  expect(groups_1.countDependencySkippedGroups).toHaveBeenCalledOnce();
166
198
  });
199
+ it('does not mutate organization resources when theme preflight fails', async () => {
200
+ vi.mocked(themes_1.prepareThemeUploads).mockRejectedValue(new Error('Invalid theme'));
201
+ await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Invalid theme');
202
+ expect(customRoles_1.uploadCustomRoles).not.toHaveBeenCalled();
203
+ expect(users_1.uploadUsers).not.toHaveBeenCalled();
204
+ expect(groups_1.uploadGroups).not.toHaveBeenCalled();
205
+ expect(themes_1.uploadThemes).not.toHaveBeenCalled();
206
+ });
207
+ it('fails after reporting a partial theme upload', async () => {
208
+ vi.mocked(themes_1.uploadThemes).mockResolvedValue(themeSummary(1));
209
+ await expect((0, index_1.uploadOrganizationContent)({ config })).rejects.toThrow('Processed themes: 1 created, 0 updated, 0 unchanged, 1 failed');
210
+ expect(globalState_1.default.log).toHaveBeenCalledWith(expect.stringContaining('processed-theme'));
211
+ });
167
212
  });
@@ -0,0 +1,22 @@
1
+ import { type OrganizationDesignFileKind, type OrganizationDesignPackageManifest } from '@lightdash/common';
2
+ type ThemePackageFile = {
3
+ kind: OrganizationDesignFileKind;
4
+ filename: string;
5
+ contentType: string;
6
+ body: Buffer;
7
+ };
8
+ export type ParsedThemePackage = {
9
+ manifest: OrganizationDesignPackageManifest;
10
+ files: ThemePackageFile[];
11
+ };
12
+ export type PreparedThemeUpload = {
13
+ directory: string;
14
+ manifest: OrganizationDesignPackageManifest;
15
+ archive: Buffer;
16
+ };
17
+ export declare const getThemesFolder: (organizationContentPath: string) => string;
18
+ export declare const prepareThemeUploads: (organizationContentPath: string) => Promise<PreparedThemeUpload[]>;
19
+ export declare const parseThemeArchive: (archive: Buffer) => Promise<ParsedThemePackage>;
20
+ export declare const writeThemePackage: (organizationContentPath: string, parsed: ParsedThemePackage) => Promise<void>;
21
+ export {};
22
+ //# sourceMappingURL=themePackage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themePackage.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/themePackage.ts"],"names":[],"mappings":"AACA,OAAO,EAkBH,KAAK,0BAA0B,EAC/B,KAAK,iCAAiC,EACzC,MAAM,mBAAmB,CAAC;AAgB3B,KAAK,gBAAgB,GAAG;IACpB,IAAI,EAAE,0BAA0B,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC7B,QAAQ,EAAE,iCAAiC,CAAC;IAC5C,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,iCAAiC,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,eAAe,4BAA6B,MAAM,KAAG,MAClB,CAAC;AAiRjD,eAAO,MAAM,mBAAmB,4BACH,MAAM,KAChC,OAAO,CAAC,mBAAmB,EAAE,CA4D/B,CAAC;AAEF,eAAO,MAAM,iBAAiB,YACjB,MAAM,KAChB,OAAO,CAAC,kBAAkB,CAwJvB,CAAC;AA6BP,eAAO,MAAM,iBAAiB,4BACD,MAAM,UACvB,kBAAkB,KAC3B,OAAO,CAAC,IAAI,CA2Cd,CAAC"}