@lightdash/cli 0.3387.0 → 0.3389.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 (32) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/analytics/analytics.d.ts +16 -1
  3. package/dist/analytics/analytics.d.ts.map +1 -1
  4. package/dist/handlers/connectSnowflake.d.ts +43 -0
  5. package/dist/handlers/connectSnowflake.d.ts.map +1 -0
  6. package/dist/handlers/connectSnowflake.js +310 -0
  7. package/dist/handlers/connectSnowflake.test.d.ts +2 -0
  8. package/dist/handlers/connectSnowflake.test.d.ts.map +1 -0
  9. package/dist/handlers/connectSnowflake.test.js +347 -0
  10. package/dist/handlers/organizationContent/customRoles.d.ts.map +1 -1
  11. package/dist/handlers/organizationContent/customRoles.js +6 -30
  12. package/dist/handlers/organizationContent/fileNames.d.ts +5 -0
  13. package/dist/handlers/organizationContent/fileNames.d.ts.map +1 -0
  14. package/dist/handlers/organizationContent/fileNames.js +33 -0
  15. package/dist/handlers/organizationContent/fileNames.test.d.ts +2 -0
  16. package/dist/handlers/organizationContent/fileNames.test.d.ts.map +1 -0
  17. package/dist/handlers/organizationContent/fileNames.test.js +36 -0
  18. package/dist/handlers/organizationContent/groups.d.ts +18 -0
  19. package/dist/handlers/organizationContent/groups.d.ts.map +1 -0
  20. package/dist/handlers/organizationContent/groups.js +171 -0
  21. package/dist/handlers/organizationContent/groups.test.d.ts +2 -0
  22. package/dist/handlers/organizationContent/groups.test.d.ts.map +1 -0
  23. package/dist/handlers/organizationContent/groups.test.js +134 -0
  24. package/dist/handlers/organizationContent/index.d.ts.map +1 -1
  25. package/dist/handlers/organizationContent/index.js +41 -0
  26. package/dist/handlers/organizationContent/index.test.d.ts +2 -0
  27. package/dist/handlers/organizationContent/index.test.d.ts.map +1 -0
  28. package/dist/handlers/organizationContent/index.test.js +155 -0
  29. package/dist/handlers/organizationContent/users.d.ts.map +1 -1
  30. package/dist/handlers/organizationContent/users.js +6 -29
  31. package/dist/index.js +13 -0
  32. package/package.json +3 -3
@@ -0,0 +1,347 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const common_1 = require("@lightdash/common");
37
+ const node_fetch_1 = require("node-fetch");
38
+ const analytics_1 = require("../analytics/analytics");
39
+ const connectSnowflake_1 = require("./connectSnowflake");
40
+ vi.mock('../analytics/analytics');
41
+ const options = {
42
+ code: '11111111_random-code',
43
+ url: 'https://lightdash.example.com/setup',
44
+ account: 'acme.eu-west-1',
45
+ user: 'lightdash_user',
46
+ };
47
+ const discovery = {
48
+ user: 'lightdash_user',
49
+ defaults: {
50
+ database: 'analytics',
51
+ warehouse: 'lightdash_wh',
52
+ role: 'lightdash_role',
53
+ schema: 'public',
54
+ },
55
+ inventory: {
56
+ databases: [
57
+ {
58
+ name: 'analytics',
59
+ comment: 'Analytics data',
60
+ kind: 'STANDARD',
61
+ },
62
+ ],
63
+ warehouses: [
64
+ {
65
+ name: 'lightdash_wh',
66
+ size: 'X-Small',
67
+ state: 'STARTED',
68
+ autoSuspendSeconds: 300,
69
+ },
70
+ ],
71
+ roles: [
72
+ { name: 'lightdash_role', isDefault: true },
73
+ { name: 'PUBLIC', isDefault: false },
74
+ ],
75
+ schemas: [{ databaseName: 'analytics', name: 'public' }],
76
+ },
77
+ };
78
+ const privateKey = '-----BEGIN PRIVATE KEY-----\ntest-private-key\n-----END PRIVATE KEY-----\n';
79
+ const publicKey = 'dGVzdC1wdWJsaWMta2V5';
80
+ const getSessionDiscovery = vi.fn();
81
+ const getUserPublicKeySlots = vi.fn();
82
+ const setUserPublicKey = vi.fn();
83
+ const unsetUserPublicKey = vi.fn();
84
+ const createProgrammaticAccessToken = vi.fn();
85
+ const openDiagnosticConnection = vi.fn();
86
+ const selectOneDiagnosticConnection = vi.fn();
87
+ const closeDiagnosticConnection = vi.fn();
88
+ const warehouseClientFactory = vi.fn(() => ({
89
+ closeDiagnosticConnection,
90
+ createProgrammaticAccessToken,
91
+ getSessionDiscovery,
92
+ getUserPublicKeySlots,
93
+ openDiagnosticConnection,
94
+ selectOneDiagnosticConnection,
95
+ setUserPublicKey,
96
+ unsetUserPublicKey,
97
+ }));
98
+ const fetchMock = vi.fn();
99
+ const write = vi.fn();
100
+ const sleep = vi.fn(async () => undefined);
101
+ const responseForDeposit = () => new node_fetch_1.Response(JSON.stringify({ status: 'ok' }), {
102
+ status: 200,
103
+ headers: { 'Content-Type': 'application/json' },
104
+ });
105
+ const getDepositRequest = () => JSON.parse(String(vi.mocked(fetchMock).mock.calls[0][1]?.body));
106
+ const getDependencies = () => ({
107
+ warehouseClientFactory,
108
+ generateKeyPair: () => ({ privateKey, publicKey }),
109
+ sleep,
110
+ now: () => new Date('2026-07-14T12:00:00.000Z'),
111
+ fetch: fetchMock,
112
+ write,
113
+ });
114
+ const depositInventory = {
115
+ databases: [{ name: 'analytics', comment: 'Analytics data' }],
116
+ warehouses: [{ name: 'lightdash_wh', size: 'X-Small', state: 'STARTED' }],
117
+ roles: [
118
+ { name: 'lightdash_role', isDefault: true },
119
+ { name: 'PUBLIC', isDefault: false },
120
+ ],
121
+ schemas: [{ database: 'analytics', name: 'public' }],
122
+ };
123
+ const privateKeyCredentials = (values = {}) => ({
124
+ type: common_1.WarehouseTypes.SNOWFLAKE,
125
+ account: options.account,
126
+ user: discovery.user,
127
+ authenticationType: common_1.SnowflakeAuthenticationType.PRIVATE_KEY,
128
+ role: discovery.defaults.role ?? undefined,
129
+ database: discovery.defaults.database ?? '',
130
+ warehouse: discovery.defaults.warehouse ?? '',
131
+ schema: discovery.defaults.schema ?? '',
132
+ privateKey,
133
+ ...values,
134
+ });
135
+ describe('connectSnowflakeHandler', () => {
136
+ beforeEach(() => {
137
+ vi.clearAllMocks();
138
+ vi.mocked(analytics_1.categorizeError).mockReturnValue('warehouse_connection');
139
+ vi.mocked(getSessionDiscovery).mockResolvedValue(discovery);
140
+ vi.mocked(getUserPublicKeySlots).mockResolvedValue({
141
+ RSA_PUBLIC_KEY: null,
142
+ RSA_PUBLIC_KEY_2: null,
143
+ });
144
+ vi.mocked(setUserPublicKey).mockResolvedValue(undefined);
145
+ vi.mocked(unsetUserPublicKey).mockResolvedValue(undefined);
146
+ vi.mocked(createProgrammaticAccessToken).mockResolvedValue({
147
+ tokenName: 'LIGHTDASH_ONBOARDING',
148
+ tokenSecret: 'pat-secret',
149
+ });
150
+ vi.mocked(openDiagnosticConnection).mockResolvedValue({
151
+ connection: {},
152
+ });
153
+ vi.mocked(selectOneDiagnosticConnection).mockResolvedValue(undefined);
154
+ vi.mocked(closeDiagnosticConnection).mockResolvedValue(undefined);
155
+ vi.mocked(fetchMock).mockResolvedValue(responseForDeposit());
156
+ });
157
+ it('deposits private-key credentials using session defaults', async () => {
158
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
159
+ expect(warehouseClientFactory).toHaveBeenNthCalledWith(1, {
160
+ type: common_1.WarehouseTypes.SNOWFLAKE,
161
+ account: options.account,
162
+ user: options.user,
163
+ authenticationType: common_1.SnowflakeAuthenticationType.OAUTH_AUTHORIZATION_CODE,
164
+ role: undefined,
165
+ database: '',
166
+ warehouse: '',
167
+ schema: '',
168
+ });
169
+ expect(getSessionDiscovery).toHaveBeenCalledWith(options.user);
170
+ expect(setUserPublicKey).toHaveBeenCalledWith(discovery.user, 'RSA_PUBLIC_KEY', publicKey);
171
+ expect(warehouseClientFactory).toHaveBeenNthCalledWith(2, privateKeyCredentials({
172
+ database: '',
173
+ warehouse: '',
174
+ schema: '',
175
+ role: undefined,
176
+ }));
177
+ const request = {
178
+ code: options.code,
179
+ credentials: privateKeyCredentials(),
180
+ inventory: depositInventory,
181
+ };
182
+ expect(getDepositRequest()).toEqual(request);
183
+ expect(write).toHaveBeenCalledWith('Secured with a key pair');
184
+ expect(write).toHaveBeenCalledWith('Connection values resolved: database (session default), warehouse (session default), schema (session default), role (session default)');
185
+ expect(write).not.toHaveBeenCalledWith(expect.stringContaining(options.code));
186
+ expect(write).not.toHaveBeenCalledWith(expect.stringContaining(privateKey));
187
+ const { LightdashAnalytics } = await Promise.resolve().then(() => __importStar(require('../analytics/analytics')));
188
+ expect(vi.mocked(LightdashAnalytics.track)).toHaveBeenCalledWith({
189
+ event: 'connect_snowflake.started',
190
+ properties: {},
191
+ });
192
+ expect(vi.mocked(LightdashAnalytics.track)).toHaveBeenCalledWith({
193
+ event: 'connect_snowflake.completed',
194
+ properties: { method: 'key_pair' },
195
+ });
196
+ });
197
+ it('prefers explicit flags over session defaults', async () => {
198
+ const overrideOptions = {
199
+ ...options,
200
+ database: 'flag_db',
201
+ warehouse: 'flag_wh',
202
+ role: 'flag_role',
203
+ schema: 'flag_schema',
204
+ };
205
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(overrideOptions, getDependencies())).resolves.toBeUndefined();
206
+ const request = {
207
+ code: options.code,
208
+ credentials: privateKeyCredentials({
209
+ database: 'flag_db',
210
+ warehouse: 'flag_wh',
211
+ role: 'flag_role',
212
+ schema: 'flag_schema',
213
+ }),
214
+ inventory: depositInventory,
215
+ };
216
+ expect(getDepositRequest()).toEqual(request);
217
+ expect(write).toHaveBeenCalledWith('Connection values resolved: database (flag), warehouse (flag), schema (flag), role (flag)');
218
+ });
219
+ it('omits missing values and deposits capped inventory names', async () => {
220
+ const inventoryNames = Array.from({ length: 105 }, (_, index) => `item_${index}`);
221
+ vi.mocked(getSessionDiscovery).mockResolvedValue({
222
+ ...discovery,
223
+ defaults: {
224
+ database: null,
225
+ warehouse: null,
226
+ role: null,
227
+ schema: null,
228
+ },
229
+ inventory: {
230
+ databases: inventoryNames.map((name) => ({
231
+ name,
232
+ comment: null,
233
+ kind: null,
234
+ })),
235
+ warehouses: inventoryNames.map((name) => ({
236
+ name,
237
+ size: null,
238
+ state: null,
239
+ autoSuspendSeconds: null,
240
+ })),
241
+ roles: inventoryNames.map((name) => ({
242
+ name,
243
+ isDefault: false,
244
+ })),
245
+ schemas: inventoryNames.map((name) => ({
246
+ databaseName: 'analytics',
247
+ name,
248
+ })),
249
+ },
250
+ });
251
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
252
+ const request = getDepositRequest();
253
+ expect(request.credentials).not.toHaveProperty('database');
254
+ expect(request.credentials).not.toHaveProperty('warehouse');
255
+ expect(request.credentials).not.toHaveProperty('schema');
256
+ expect(request.credentials).not.toHaveProperty('role');
257
+ expect(request.inventory).toEqual({
258
+ databases: inventoryNames
259
+ .slice(0, 100)
260
+ .map((name) => ({ name, comment: null })),
261
+ warehouses: inventoryNames
262
+ .slice(0, 100)
263
+ .map((name) => ({ name, size: null, state: null })),
264
+ roles: inventoryNames
265
+ .slice(0, 100)
266
+ .map((name) => ({ name, isDefault: false })),
267
+ schemas: inventoryNames
268
+ .slice(0, 1000)
269
+ .map((name) => ({ database: 'analytics', name })),
270
+ });
271
+ expect(write).toHaveBeenCalledWith('Connection values resolved: none');
272
+ expect(write).toHaveBeenCalledWith('Finish choosing database, warehouse, schema, role in the browser.');
273
+ });
274
+ it('selects the first free Snowflake public-key slot', async () => {
275
+ vi.mocked(getUserPublicKeySlots).mockResolvedValue({
276
+ RSA_PUBLIC_KEY: 'SHA256:existing-key',
277
+ RSA_PUBLIC_KEY_2: null,
278
+ });
279
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
280
+ expect(setUserPublicKey).toHaveBeenCalledWith(discovery.user, 'RSA_PUBLIC_KEY_2', publicKey);
281
+ expect(createProgrammaticAccessToken).not.toHaveBeenCalled();
282
+ });
283
+ it('uses the stable PAT name for collision replacement when both key slots are occupied', async () => {
284
+ vi.mocked(getUserPublicKeySlots).mockResolvedValue({
285
+ RSA_PUBLIC_KEY: 'SHA256:existing-key-1',
286
+ RSA_PUBLIC_KEY_2: 'SHA256:existing-key-2',
287
+ });
288
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
289
+ expect(setUserPublicKey).not.toHaveBeenCalled();
290
+ expect(createProgrammaticAccessToken).toHaveBeenCalledWith('LIGHTDASH_ONBOARDING', 365, 1440);
291
+ const request = {
292
+ code: options.code,
293
+ credentials: {
294
+ type: common_1.WarehouseTypes.SNOWFLAKE,
295
+ account: options.account,
296
+ user: discovery.user,
297
+ authenticationType: common_1.SnowflakeAuthenticationType.PASSWORD,
298
+ role: discovery.defaults.role ?? undefined,
299
+ database: discovery.defaults.database ?? '',
300
+ warehouse: discovery.defaults.warehouse ?? '',
301
+ schema: discovery.defaults.schema ?? '',
302
+ password: 'pat-secret',
303
+ },
304
+ inventory: depositInventory,
305
+ };
306
+ expect(getDepositRequest()).toEqual(request);
307
+ });
308
+ it('unsets an unverified key before falling back to a PAT', async () => {
309
+ vi.mocked(openDiagnosticConnection).mockRejectedValue(new Error('Authentication policy excludes KEYPAIR'));
310
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
311
+ expect(openDiagnosticConnection).toHaveBeenCalledTimes(3);
312
+ expect(sleep).toHaveBeenCalledTimes(2);
313
+ expect(sleep).toHaveBeenCalledWith(5_000);
314
+ expect(unsetUserPublicKey).toHaveBeenCalledWith(discovery.user, 'RSA_PUBLIC_KEY');
315
+ expect(createProgrammaticAccessToken).toHaveBeenCalledAfter(unsetUserPublicKey);
316
+ });
317
+ it('falls back to a PAT when key registration lacks privileges', async () => {
318
+ vi.mocked(setUserPublicKey).mockRejectedValue(new Error('Insufficient privileges to operate on user'));
319
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).resolves.toBeUndefined();
320
+ expect(createProgrammaticAccessToken).toHaveBeenCalledWith('LIGHTDASH_ONBOARDING', 365, 1440);
321
+ expect(write).toHaveBeenCalledWith('Secured with a programmatic access token (expires 2027-07-14)');
322
+ });
323
+ it('prints administrator remediation when both durable paths fail', async () => {
324
+ vi.mocked(setUserPublicKey).mockRejectedValue(new Error('Insufficient privileges'));
325
+ vi.mocked(createProgrammaticAccessToken).mockRejectedValue(new Error('Authentication policy does not allow PROGRAMMATIC_ACCESS_TOKEN'));
326
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).rejects.toThrow('Snowflake programmatic access token creation failed');
327
+ expect(write).toHaveBeenCalledWith('GRANT MODIFY PROGRAMMATIC AUTHENTICATION METHODS ON USER lightdash_user TO ROLE lightdash_role;');
328
+ expect(write).toHaveBeenCalledWith(expect.stringContaining('PROGRAMMATIC_ACCESS_TOKEN'));
329
+ expect(fetchMock).not.toHaveBeenCalled();
330
+ });
331
+ it('reports an expired connect code without exposing it', async () => {
332
+ vi.mocked(fetchMock).mockResolvedValue(new node_fetch_1.Response(JSON.stringify({ status: 'error' }), {
333
+ status: 404,
334
+ headers: { 'Content-Type': 'application/json' },
335
+ }));
336
+ await expect((0, connectSnowflake_1.connectSnowflakeHandler)(options, getDependencies())).rejects.toThrow(common_1.WarehouseConnectionError);
337
+ expect(write).not.toHaveBeenCalledWith(expect.stringContaining(options.code));
338
+ const { LightdashAnalytics } = await Promise.resolve().then(() => __importStar(require('../analytics/analytics')));
339
+ expect(vi.mocked(LightdashAnalytics.track)).toHaveBeenCalledWith({
340
+ event: 'connect_snowflake.error',
341
+ properties: {
342
+ error: expect.any(String),
343
+ errorCategory: 'warehouse_connection',
344
+ },
345
+ });
346
+ });
347
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"customRoles.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/customRoles.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,uBAAuB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,uBAAuB,EAAE,CAAC;CACvC,CAAC;AAgBF,eAAO,MAAM,oBAAoB,GAAI,yBAAyB,MAAM,KAAG,MACjB,CAAC;AAEvD,eAAO,MAAM,6BAA6B,GACtC,SAAS,uBAAuB,KACjC,MACmH,CAAC;AAoGvH,eAAO,MAAM,mBAAmB,GAC5B,yBAAyB,MAAM,KAChC,OAAO,CAAC,OAAO,EAAE,CAUnB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC1B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,uBAAuB,CA6CjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC5B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAqDhB,CAAC"}
1
+ {"version":3,"file":"customRoles.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/customRoles.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,uBAAuB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,uBAAuB,EAAE,CAAC;CACvC,CAAC;AAgBF,eAAO,MAAM,oBAAoB,GAAI,yBAAyB,MAAM,KAAG,MACjB,CAAC;AAEvD,eAAO,MAAM,6BAA6B,GACtC,SAAS,uBAAuB,KACjC,MACmH,CAAC;AAkFvH,eAAO,MAAM,mBAAmB,GAC5B,yBAAyB,MAAM,KAChC,OAAO,CAAC,OAAO,EAAE,CAUnB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC1B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,uBAAuB,CA6CjC,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAC5B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAiChB,CAAC"}
@@ -4,13 +4,12 @@ exports.downloadCustomRoles = exports.uploadCustomRoles = exports.readCustomRole
4
4
  const tslib_1 = require("tslib");
5
5
  /* eslint-disable no-await-in-loop */
6
6
  const common_1 = require("@lightdash/common");
7
- const crypto_1 = require("crypto");
8
7
  const fs_1 = require("fs");
9
8
  const yaml = tslib_1.__importStar(require("js-yaml"));
10
9
  const groupBy_1 = tslib_1.__importDefault(require("lodash/groupBy"));
11
10
  const path = tslib_1.__importStar(require("path"));
12
11
  const apiClient_1 = require("../dbt/apiClient");
13
- const CUSTOM_ROLE_FILENAME_MAX_LENGTH = 200;
12
+ const fileNames_1 = require("./fileNames");
14
13
  const getCustomRolesFolder = (organizationContentPath) => path.join(organizationContentPath, 'custom-roles');
15
14
  exports.getCustomRolesFolder = getCustomRolesFolder;
16
15
  const formatCustomRoleUploadSummary = (summary) => `${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.failed} failed`;
@@ -23,19 +22,6 @@ const getCustomRoleName = (role) => {
23
22
  ? role.name
24
23
  : undefined;
25
24
  };
26
- const getCustomRoleFilenameBase = (name) => {
27
- const slug = name
28
- .normalize('NFKD')
29
- .replace(/[\u0300-\u036f]/g, '')
30
- .toLowerCase()
31
- .replace(/[^a-z0-9]+/g, '-')
32
- .replace(/^-+|-+$/g, '');
33
- const nameHash = (0, crypto_1.createHash)('sha256')
34
- .update(name)
35
- .digest('hex')
36
- .slice(0, 8);
37
- return (slug || `role-${nameHash}`).slice(0, CUSTOM_ROLE_FILENAME_MAX_LENGTH);
38
- };
39
25
  const readCustomRoleFileResults = async (organizationContentPath) => {
40
26
  const folder = (0, exports.getCustomRolesFolder)(organizationContentPath);
41
27
  let entries;
@@ -153,22 +139,12 @@ const downloadCustomRoles = async (organizationUuid, organizationContentPath) =>
153
139
  await Promise.all(existingFiles
154
140
  .filter((entry) => entry.isFile() && entry.name.endsWith('.yml'))
155
141
  .map((entry) => fs_1.promises.unlink(path.join(folder, entry.name))));
156
- const filenameBases = customRoles.map(({ name }) => getCustomRoleFilenameBase(name));
157
- const baseCounts = filenameBases.reduce((counts, filenameBase) => ({
158
- ...counts,
159
- [filenameBase]: (counts[filenameBase] ?? 0) + 1,
160
- }), {});
142
+ const filenames = (0, fileNames_1.getOrganizationContentFileNames)({
143
+ values: customRoles.map(({ name }) => name),
144
+ fallbackPrefix: 'role',
145
+ });
161
146
  await Promise.all(customRoles.map(async (role, index) => {
162
- const filenameBase = filenameBases[index];
163
- const collisionSuffix = `-${(0, crypto_1.createHash)('sha256')
164
- .update(role.name)
165
- .digest('hex')
166
- .slice(0, 8)}`;
167
- const uniqueFilenameBase = baseCounts[filenameBase] > 1
168
- ? `${filenameBase.slice(0, CUSTOM_ROLE_FILENAME_MAX_LENGTH -
169
- collisionSuffix.length)}${collisionSuffix}`
170
- : filenameBase;
171
- await fs_1.promises.writeFile(path.join(folder, `${uniqueFilenameBase}.yml`), yaml.dump(role, { quotingType: '"', sortKeys: true }));
147
+ await fs_1.promises.writeFile(path.join(folder, filenames[index]), yaml.dump(role, { quotingType: '"', sortKeys: true }));
172
148
  }));
173
149
  return customRoles.length;
174
150
  };
@@ -0,0 +1,5 @@
1
+ export declare const getOrganizationContentFileNames: ({ values, fallbackPrefix, }: {
2
+ values: string[];
3
+ fallbackPrefix: string;
4
+ }) => string[];
5
+ //# sourceMappingURL=fileNames.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fileNames.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/fileNames.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,+BAA+B,GAAI,6BAG7C;IACC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CAC1B,KAAG,MAAM,EAsBT,CAAC"}
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getOrganizationContentFileNames = void 0;
4
+ const common_1 = require("@lightdash/common");
5
+ const crypto_1 = require("crypto");
6
+ const ORGANIZATION_CONTENT_FILENAME_MAX_LENGTH = 200;
7
+ const FILENAME_HASH_LENGTH = 8;
8
+ const getStableHash = (value) => (0, crypto_1.createHash)('sha256')
9
+ .update(value)
10
+ .digest('hex')
11
+ .slice(0, FILENAME_HASH_LENGTH);
12
+ const getFilenameBase = (value, fallbackPrefix) => {
13
+ const normalizedValue = value
14
+ .normalize('NFKD')
15
+ .replace(/[\u0300-\u036f]/g, '');
16
+ const slug = /[a-z0-9]/i.test(normalizedValue)
17
+ ? (0, common_1.generateSlug)(normalizedValue)
18
+ : `${fallbackPrefix}-${getStableHash(value)}`;
19
+ return slug.slice(0, ORGANIZATION_CONTENT_FILENAME_MAX_LENGTH);
20
+ };
21
+ const getOrganizationContentFileNames = ({ values, fallbackPrefix, }) => {
22
+ const filenameBases = values.map((value) => getFilenameBase(value, fallbackPrefix));
23
+ const baseCounts = filenameBases.reduce((counts, filenameBase) => counts.set(filenameBase, (counts.get(filenameBase) ?? 0) + 1), new Map());
24
+ return filenameBases.map((filenameBase, index) => {
25
+ if ((baseCounts.get(filenameBase) ?? 0) === 1) {
26
+ return `${filenameBase}.yml`;
27
+ }
28
+ const collisionSuffix = `-${getStableHash(values[index])}`;
29
+ const uniqueFilenameBase = `${filenameBase.slice(0, ORGANIZATION_CONTENT_FILENAME_MAX_LENGTH - collisionSuffix.length)}${collisionSuffix}`;
30
+ return `${uniqueFilenameBase}.yml`;
31
+ });
32
+ };
33
+ exports.getOrganizationContentFileNames = getOrganizationContentFileNames;
@@ -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/organizationContent/fileNames.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const crypto_1 = require("crypto");
4
+ const fileNames_1 = require("./fileNames");
5
+ describe('organization content filenames', () => {
6
+ it('uses generateSlug with normalized accents', () => {
7
+ expect((0, fileNames_1.getOrganizationContentFileNames)({
8
+ values: ['Málaga Finance'],
9
+ fallbackPrefix: 'group',
10
+ })).toStrictEqual(['malaga-finance.yml']);
11
+ });
12
+ it('uses a deterministic hash when no slug can be generated', () => {
13
+ const value = '🎉';
14
+ const hash = (0, crypto_1.createHash)('sha256')
15
+ .update(value)
16
+ .digest('hex')
17
+ .slice(0, 8);
18
+ expect((0, fileNames_1.getOrganizationContentFileNames)({
19
+ values: [value],
20
+ fallbackPrefix: 'group',
21
+ })).toStrictEqual([`group-${hash}.yml`]);
22
+ });
23
+ it('adds stable hashes when different values generate the same slug', () => {
24
+ const values = ['Finance?', 'Finance!'];
25
+ expect((0, fileNames_1.getOrganizationContentFileNames)({
26
+ values,
27
+ fallbackPrefix: 'group',
28
+ })).toStrictEqual(values.map((value) => {
29
+ const hash = (0, crypto_1.createHash)('sha256')
30
+ .update(value)
31
+ .digest('hex')
32
+ .slice(0, 8);
33
+ return `finance-${hash}.yml`;
34
+ }));
35
+ });
36
+ });
@@ -0,0 +1,18 @@
1
+ export type GroupUploadFailure = {
2
+ message: string;
3
+ };
4
+ export type GroupUploadSummary = {
5
+ created: number;
6
+ updated: number;
7
+ unchanged: number;
8
+ failed: number;
9
+ dependencySkipped: number;
10
+ failures: GroupUploadFailure[];
11
+ };
12
+ export declare const getGroupsFolder: (organizationContentPath: string) => string;
13
+ export declare const formatGroupUploadSummary: (summary: GroupUploadSummary) => string;
14
+ export declare const readGroupFiles: (organizationContentPath: string) => Promise<unknown[]>;
15
+ export declare const countDependencySkippedGroups: (organizationContentPath: string) => Promise<number>;
16
+ export declare const uploadGroups: (organizationUuid: string, organizationContentPath: string) => Promise<GroupUploadSummary>;
17
+ export declare const downloadGroups: (organizationUuid: string, organizationContentPath: string) => Promise<number>;
18
+ //# sourceMappingURL=groups.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/groups.ts"],"names":[],"mappings":"AAgBA,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,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,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,EAAE,CAAC;CAClC,CAAC;AAgBF,eAAO,MAAM,eAAe,GAAI,yBAAyB,MAAM,KAAG,MAClB,CAAC;AAEjD,eAAO,MAAM,wBAAwB,GAAI,SAAS,kBAAkB,KAAG,MACgG,CAAC;AAiFxK,eAAO,MAAM,cAAc,GACvB,yBAAyB,MAAM,KAChC,OAAO,CAAC,OAAO,EAAE,CAUnB,CAAC;AAEF,eAAO,MAAM,4BAA4B,GACrC,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAahB,CAAC;AAEF,eAAO,MAAM,YAAY,GACrB,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,kBAAkB,CA8C5B,CAAC;AAEF,eAAO,MAAM,cAAc,GACvB,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAuChB,CAAC"}