@lightdash/cli 0.3405.1 → 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,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const fs_1 = require("fs");
5
+ const os_1 = tslib_1.__importDefault(require("os"));
6
+ const path_1 = tslib_1.__importDefault(require("path"));
7
+ const vitest_1 = require("vitest");
8
+ const projectResources_1 = require("./projectResources");
9
+ const resource_1 = require("./resource");
10
+ const temporaryDirectories = [];
11
+ (0, vitest_1.afterEach)(async () => {
12
+ await Promise.all(temporaryDirectories.splice(0).map((directory) => fs_1.promises.rm(directory, {
13
+ recursive: true,
14
+ force: true,
15
+ })));
16
+ });
17
+ (0, vitest_1.describe)('content-as-code resource files', () => {
18
+ (0, vitest_1.it)('ignores other scheduled content types in a shared legacy folder', async () => {
19
+ const basePath = await fs_1.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'lightdash-content-as-code-'));
20
+ temporaryDirectories.push(basePath);
21
+ const alertFolder = path_1.default.join(basePath, 'alerts', 'charts', 'orders');
22
+ await fs_1.promises.mkdir(alertFolder, { recursive: true });
23
+ await Promise.all([
24
+ fs_1.promises.writeFile(path_1.default.join(alertFolder, 'alert.yml'), 'contentType: alert\nversion: 1\nslug: revenue-alert\nname: Revenue alert\n'),
25
+ fs_1.promises.writeFile(path_1.default.join(alertFolder, 'delivery.yml'), 'contentType: scheduled_delivery\nversion: 1\nslug: revenue-delivery\nname: Revenue delivery\n'),
26
+ fs_1.promises.writeFile(path_1.default.join(alertFolder, 'invalid-alert.yml'), 'contentType: alert\nslug: invalid-alert\nname: Invalid alert\n'),
27
+ ]);
28
+ const result = await (0, resource_1.readCodeResourceFiles)({
29
+ definition: projectResources_1.ALERT_CODE_RESOURCE,
30
+ basePath,
31
+ });
32
+ (0, vitest_1.expect)(result.files.map(({ document }) => document.slug)).toEqual([
33
+ 'revenue-alert',
34
+ ]);
35
+ (0, vitest_1.expect)(result.failures).toHaveLength(1);
36
+ (0, vitest_1.expect)(result.failures[0].message).toContain('invalid-alert.yml');
37
+ (0, vitest_1.expect)(result.failures[0].message).toContain('expected version 1');
38
+ });
39
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"download.d.ts","sourceRoot":"","sources":["../../src/handlers/download.ts"],"names":[],"mappings":"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"}
@@ -19,6 +19,8 @@ const styles = tslib_1.__importStar(require("../styles"));
19
19
  const appCodeFiles_1 = require("./apps/appCodeFiles");
20
20
  const appsDownload_1 = require("./apps/appsDownload");
21
21
  const scaffolding_1 = require("./apps/scaffolding");
22
+ const projectResources_1 = require("./contentAsCode/projectResources");
23
+ const resource_1 = require("./contentAsCode/resource");
22
24
  const contentAsCodePaths_1 = require("./contentAsCodePaths");
23
25
  const apiClient_1 = require("./dbt/apiClient");
24
26
  const metadataFile_1 = require("./metadataFile");
@@ -288,19 +290,19 @@ const getContentTypeConfig = (type, projectId) => {
288
290
  switch (type) {
289
291
  case 'charts':
290
292
  return {
291
- endpoint: `/api/v1/projects/${projectId}/charts/code`,
293
+ endpoint: `/api/v1/projects/${projectId}/code/charts`,
292
294
  displayName: 'charts',
293
295
  supportsLanguageMap: true,
294
296
  };
295
297
  case 'dashboards':
296
298
  return {
297
- endpoint: `/api/v1/projects/${projectId}/dashboards/code`,
299
+ endpoint: `/api/v1/projects/${projectId}/code/dashboards`,
298
300
  displayName: 'dashboards',
299
301
  supportsLanguageMap: true,
300
302
  };
301
303
  case 'sqlCharts':
302
304
  return {
303
- endpoint: `/api/v1/projects/${projectId}/sqlCharts/code`,
305
+ endpoint: `/api/v1/projects/${projectId}/code/sqlCharts`,
304
306
  displayName: 'SQL charts',
305
307
  supportsLanguageMap: false,
306
308
  };
@@ -421,55 +423,32 @@ const downloadContent = async (ids, type, projectId, projectName, customPath, la
421
423
  return [total, [...new Set(chartSlugs)], allMetadataEntries, allSpaces];
422
424
  };
423
425
  exports.downloadContent = downloadContent;
424
- const getScheduledDeliveriesFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), 'scheduled-deliveries');
425
- const getAiAgentsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), 'ai-agents');
426
- const getAlertsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), 'alerts');
427
- const getGoogleSheetsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), 'google-sheets');
428
- const getVirtualViewsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), 'virtual-views');
426
+ const getScheduledDeliveriesFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE.folderName);
427
+ const getAlertsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), projectResources_1.ALERT_CODE_RESOURCE.folderName);
428
+ const getGoogleSheetsFolder = (customPath) => path.join((0, contentAsCodePaths_1.getDownloadFolder)(customPath), projectResources_1.GOOGLE_SHEETS_CODE_RESOURCE.folderName);
429
429
  const downloadVirtualViews = async (projectId, slugs, customPath) => {
430
- const folder = getVirtualViewsFolder(customPath);
431
- await fs_1.promises.mkdir(folder, { recursive: true });
432
430
  const query = new URLSearchParams(slugs.map((slug) => ['slugs', slug])).toString();
433
431
  const results = await (0, apiClient_1.lightdashApi)({
434
432
  method: 'GET',
435
- url: `/api/v1/projects/${projectId}/virtualViews/code${query ? `?${query}` : ''}`,
433
+ url: `/api/v1/projects/${projectId}/code/virtualViews${query ? `?${query}` : ''}`,
436
434
  body: undefined,
437
435
  });
438
- for (const virtualView of results.virtualViews) {
439
- await fs_1.promises.writeFile(path.join(folder, `${encodeURIComponent(virtualView.slug)}.yml`), yaml.dump(virtualView, { quotingType: '"', sortKeys: true }));
440
- }
436
+ await (0, resource_1.writeCodeResourceDocuments)({
437
+ definition: projectResources_1.VIRTUAL_VIEW_CODE_RESOURCE,
438
+ basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
439
+ documents: results.virtualViews,
440
+ });
441
441
  results.skipped.forEach(({ slug, reason }) => globalState_1.default.log(styles.warning(`Skipped virtual view "${slug}": ${reason}`)));
442
442
  results.missingSlugs.forEach((slug) => globalState_1.default.log(styles.warning(`Virtual view "${slug}" was not found`)));
443
443
  return results.virtualViews.length;
444
444
  };
445
445
  const readVirtualViewFiles = async (customPath) => {
446
- const folder = getVirtualViewsFolder(customPath);
447
- try {
448
- const entries = await fs_1.promises.readdir(folder, { withFileTypes: true });
449
- const virtualViews = [];
450
- for (const entry of entries) {
451
- if (entry.isFile() &&
452
- (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml'))) {
453
- const parsed = yaml.load(await fs_1.promises.readFile(path.join(folder, entry.name), 'utf-8'));
454
- if (parsed.contentType !== common_1.ContentAsCodeType.VIRTUAL_VIEW) {
455
- throw new common_1.ParameterError(`Invalid virtual view contentType in ${entry.name}`);
456
- }
457
- virtualViews.push(parsed);
458
- }
459
- }
460
- const duplicates = Object.entries((0, groupBy_1.default)(virtualViews, 'slug'))
461
- .filter(([, items]) => items.length > 1)
462
- .map(([slug]) => slug);
463
- if (duplicates.length > 0) {
464
- throw new common_1.ParameterError(`Duplicate virtual view slugs: ${duplicates.join(', ')}`);
465
- }
466
- return virtualViews;
467
- }
468
- catch (error) {
469
- if (error.code === 'ENOENT')
470
- return [];
471
- throw error;
472
- }
446
+ const result = await (0, resource_1.readCodeResourceFiles)({
447
+ definition: projectResources_1.VIRTUAL_VIEW_CODE_RESOURCE,
448
+ basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
449
+ });
450
+ (0, resource_1.assertCodeResourceFilesValid)(result);
451
+ return result.files.map(({ document }) => document);
473
452
  };
474
453
  const upsertVirtualViews = async (projectId, slugs, changes, force, canUpload, customPath) => {
475
454
  const virtualViews = await readVirtualViewFiles(customPath);
@@ -497,7 +476,7 @@ const upsertVirtualViews = async (projectId, slugs, changes, force, canUpload, c
497
476
  }
498
477
  const result = await (0, apiClient_1.lightdashApi)({
499
478
  method: 'POST',
500
- url: `/api/v1/projects/${projectId}/virtualViews/${encodeURIComponent(virtualView.slug)}/code?force=${force}`,
479
+ url: `/api/v1/projects/${projectId}/code/virtualViews/${encodeURIComponent(virtualView.slug)}?force=${force}`,
501
480
  body: JSON.stringify(virtualView),
502
481
  });
503
482
  const action = `virtual views ${getPromoteAction(result.action)}`;
@@ -516,6 +495,7 @@ const getScheduledContentConfig = (contentType, customPath) => {
516
495
  case common_1.ContentAsCodeType.ALERT:
517
496
  return {
518
497
  folder: getAlertsFolder(customPath),
498
+ definition: projectResources_1.ALERT_CODE_RESOURCE,
519
499
  route: 'alerts',
520
500
  singular: 'alert',
521
501
  plural: 'alerts',
@@ -523,6 +503,7 @@ const getScheduledContentConfig = (contentType, customPath) => {
523
503
  case common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC:
524
504
  return {
525
505
  folder: getGoogleSheetsFolder(customPath),
506
+ definition: projectResources_1.GOOGLE_SHEETS_CODE_RESOURCE,
526
507
  route: 'googleSheets',
527
508
  singular: 'Google Sheets sync',
528
509
  plural: 'Google Sheets syncs',
@@ -530,6 +511,7 @@ const getScheduledContentConfig = (contentType, customPath) => {
530
511
  case common_1.ContentAsCodeType.SCHEDULED_DELIVERY:
531
512
  return {
532
513
  folder: getScheduledDeliveriesFolder(customPath),
514
+ definition: projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE,
533
515
  route: 'scheduledDeliveries',
534
516
  singular: 'scheduled delivery',
535
517
  plural: 'scheduled deliveries',
@@ -539,12 +521,11 @@ const getScheduledContentConfig = (contentType, customPath) => {
539
521
  }
540
522
  };
541
523
  const downloadAiAgents = async (projectId, ids, customPath) => {
542
- const outputDir = getAiAgentsFolder(customPath);
543
- await fs_1.promises.mkdir(outputDir, { recursive: true });
544
524
  const idQuery = ids.map((id) => ['ids', id]);
545
525
  let offset = 0;
546
526
  let total = 0;
547
527
  let downloaded = 0;
528
+ const agents = [];
548
529
  do {
549
530
  const query = new URLSearchParams([
550
531
  ...idQuery,
@@ -552,54 +533,29 @@ const downloadAiAgents = async (projectId, ids, customPath) => {
552
533
  ]).toString();
553
534
  const results = await (0, apiClient_1.lightdashApi)({
554
535
  method: 'GET',
555
- url: `/api/v1/projects/${projectId}/aiAgents/code?${query}`,
536
+ url: `/api/v1/projects/${projectId}/code/aiAgents?${query}`,
556
537
  body: undefined,
557
538
  });
558
- for (const agent of results.agents) {
559
- const { updatedAt, downloadedAt, ...agentConfig } = agent;
560
- // eslint-disable-next-line no-await-in-loop
561
- await fs_1.promises.writeFile(path.join(outputDir, `${agent.slug}.yml`), yaml.dump(agentConfig, { quotingType: '"', sortKeys: true }));
562
- }
539
+ agents.push(...results.agents);
563
540
  results.missingIds.forEach((id) => globalState_1.default.debug(`No AI agent with id "${id}"`));
564
541
  downloaded += results.agents.length;
565
542
  offset = results.offset;
566
543
  total = results.total;
567
544
  } while (offset < total);
545
+ await (0, resource_1.writeCodeResourceDocuments)({
546
+ definition: projectResources_1.AI_AGENT_CODE_RESOURCE,
547
+ basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
548
+ documents: agents,
549
+ });
568
550
  return downloaded;
569
551
  };
570
552
  const readAiAgentFiles = async (customPath) => {
571
- const folder = getAiAgentsFolder(customPath);
572
- try {
573
- const entries = await fs_1.promises.readdir(folder, {
574
- recursive: true,
575
- withFileTypes: true,
576
- });
577
- const agents = [];
578
- for (const entry of entries) {
579
- if (entry.isFile() &&
580
- (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml'))) {
581
- const filePath = path.join(entry.parentPath, entry.name);
582
- // eslint-disable-next-line no-await-in-loop
583
- const parsed = yaml.load(await fs_1.promises.readFile(filePath, 'utf-8'));
584
- if (typeof parsed !== 'object' ||
585
- parsed === null ||
586
- Array.isArray(parsed)) {
587
- throw new common_1.ParameterError(`Invalid AI agent file "${filePath}": expected a YAML object`);
588
- }
589
- if (!('contentType' in parsed) ||
590
- parsed.contentType !== common_1.ContentAsCodeType.AI_AGENT) {
591
- throw new common_1.ParameterError(`Invalid contentType in AI agent file "${filePath}": expected "${common_1.ContentAsCodeType.AI_AGENT}"`);
592
- }
593
- agents.push(parsed);
594
- }
595
- }
596
- return agents;
597
- }
598
- catch (error) {
599
- if (error.code === 'ENOENT')
600
- return [];
601
- throw error;
602
- }
553
+ const result = await (0, resource_1.readCodeResourceFiles)({
554
+ definition: projectResources_1.AI_AGENT_CODE_RESOURCE,
555
+ basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
556
+ });
557
+ (0, resource_1.assertCodeResourceFilesValid)(result);
558
+ return result.files.map(({ document }) => document);
603
559
  };
604
560
  class AiAgentAsCodeUploadError extends Error {
605
561
  originalError;
@@ -624,7 +580,7 @@ const upsertAiAgents = async (projectId, slugs, changes, force, customPath) => {
624
580
  globalState_1.default.log(`Found ${filteredAgents.length} AI agent files`);
625
581
  const results = await (0, apiClient_1.lightdashApi)({
626
582
  method: 'POST',
627
- url: `/api/v1/projects/${projectId}/aiAgents/code?force=${force}`,
583
+ url: `/api/v1/projects/${projectId}/code/aiAgents?force=${force}`,
628
584
  body: JSON.stringify({ agents: filteredAgents }),
629
585
  });
630
586
  const counts = {
@@ -644,7 +600,7 @@ const downloadScheduledContent = async (projectId, slugs, contentType, customPat
644
600
  const query = new URLSearchParams(slugs.map((slug) => ['slugs', slug])).toString();
645
601
  const results = await (0, apiClient_1.lightdashApi)({
646
602
  method: 'GET',
647
- url: `/api/v1/projects/${projectId}/${config.route}/code${query ? `?${query}` : ''}`,
603
+ url: `/api/v1/projects/${projectId}/code/${config.route}${query ? `?${query}` : ''}`,
648
604
  body: undefined,
649
605
  });
650
606
  let scheduledContent;
@@ -666,29 +622,23 @@ const downloadScheduledContent = async (projectId, slugs, contentType, customPat
666
622
  return scheduledContent.length;
667
623
  };
668
624
  const readScheduledContentFiles = async (contentType, customPath) => {
669
- const config = getScheduledContentConfig(contentType, customPath);
670
- try {
671
- const entries = await fs_1.promises.readdir(config.folder, {
672
- recursive: true,
673
- withFileTypes: true,
625
+ const read = async (definition) => {
626
+ const result = await (0, resource_1.readCodeResourceFiles)({
627
+ definition,
628
+ basePath: (0, contentAsCodePaths_1.getDownloadFolder)(customPath),
674
629
  });
675
- const scheduledContent = [];
676
- for (const entry of entries) {
677
- if (entry.isFile() &&
678
- (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml'))) {
679
- const filePath = path.join(entry.parentPath, entry.name);
680
- const parsed = yaml.load(await fs_1.promises.readFile(filePath, 'utf-8'));
681
- if (parsed.contentType === contentType) {
682
- scheduledContent.push(parsed);
683
- }
684
- }
685
- }
686
- return scheduledContent;
687
- }
688
- catch (error) {
689
- if (error.code === 'ENOENT')
690
- return [];
691
- throw error;
630
+ (0, resource_1.assertCodeResourceFilesValid)(result);
631
+ return result.files.map(({ document }) => document);
632
+ };
633
+ switch (contentType) {
634
+ case common_1.ContentAsCodeType.ALERT:
635
+ return read(projectResources_1.ALERT_CODE_RESOURCE);
636
+ case common_1.ContentAsCodeType.GOOGLE_SHEETS_SYNC:
637
+ return read(projectResources_1.GOOGLE_SHEETS_CODE_RESOURCE);
638
+ case common_1.ContentAsCodeType.SCHEDULED_DELIVERY:
639
+ return read(projectResources_1.SCHEDULED_DELIVERY_CODE_RESOURCE);
640
+ default:
641
+ return (0, common_1.assertUnreachable)(contentType, 'Unknown scheduled content type');
692
642
  }
693
643
  };
694
644
  const upsertScheduledContent = async (projectId, slugs, changes, force, contentType, canUpload, customPath) => {
@@ -709,7 +659,7 @@ const upsertScheduledContent = async (projectId, slugs, changes, force, contentT
709
659
  try {
710
660
  const result = await (0, apiClient_1.lightdashApi)({
711
661
  method: 'POST',
712
- url: `/api/v1/projects/${projectId}/${config.route}/${item.slug}/code?force=${force}`,
662
+ url: `/api/v1/projects/${projectId}/code/${config.route}/${item.slug}?force=${force}`,
713
663
  body: JSON.stringify(item),
714
664
  });
715
665
  const action = getPromoteAction(result.action);
@@ -1160,8 +1110,8 @@ const upsertSingleItem = async (item, type, projectId, changes, force, config, s
1160
1110
  // SQL charts use a different endpoint
1161
1111
  const isSqlChartItem = type === 'charts' && isSqlChart(item);
1162
1112
  const endpoint = isSqlChartItem
1163
- ? `/api/v1/projects/${projectId}/sqlCharts/${item.slug}/code`
1164
- : `/api/v1/projects/${projectId}/${type}/${item.slug}/code`;
1113
+ ? `/api/v1/projects/${projectId}/code/sqlCharts/${item.slug}`
1114
+ : `/api/v1/projects/${projectId}/code/${type}/${item.slug}`;
1165
1115
  const upsertData = await (0, apiClient_1.lightdashApi)({
1166
1116
  method: 'POST',
1167
1117
  url: endpoint,
@@ -1,6 +1,6 @@
1
- export type CustomRoleUploadFailure = {
2
- message: string;
3
- };
1
+ import { type CustomRoleAsCode } from '@lightdash/common';
2
+ import { type CodeFileFailure, type CodeResourceDefinition } from '../contentAsCode/resource';
3
+ export type CustomRoleUploadFailure = CodeFileFailure;
4
4
  export type CustomRoleUploadSummary = {
5
5
  created: number;
6
6
  updated: number;
@@ -8,6 +8,7 @@ export type CustomRoleUploadSummary = {
8
8
  failed: number;
9
9
  failures: CustomRoleUploadFailure[];
10
10
  };
11
+ export declare const CUSTOM_ROLE_CODE_RESOURCE: CodeResourceDefinition<CustomRoleAsCode>;
11
12
  export declare const getCustomRolesFolder: (organizationContentPath: string) => string;
12
13
  export declare const formatCustomRoleUploadSummary: (summary: CustomRoleUploadSummary) => string;
13
14
  export declare const readCustomRoleFiles: (organizationContentPath: string) => Promise<unknown[]>;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"customRoles.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/customRoles.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,gBAAgB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAKH,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC9B,MAAM,2BAA2B,CAAC;AAGnC,MAAM,MAAM,uBAAuB,GAAG,eAAe,CAAC;AAEtD,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;AAEF,eAAO,MAAM,yBAAyB,EAAE,sBAAsB,CAAC,gBAAgB,CAkB1E,CAAC;AAEN,eAAO,MAAM,oBAAoB,GAAI,yBAAyB,MAAM,KAAG,MACK,CAAC;AAE7E,eAAO,MAAM,6BAA6B,GACtC,SAAS,uBAAuB,KACjC,MACmH,CAAC;AAEvH,eAAO,MAAM,mBAAmB,GAC5B,yBAAyB,MAAM,KAChC,OAAO,CAAC,OAAO,EAAE,CAOnB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC1B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,uBAAuB,CAc5B,CAAC;AAEP,eAAO,MAAM,mBAAmB,GAC5B,kBAAkB,MAAM,EACxB,yBAAyB,MAAM,KAChC,OAAO,CAAC,MAAM,CAcX,CAAC"}
@@ -1,151 +1,65 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.downloadCustomRoles = exports.uploadCustomRoles = exports.readCustomRoleFiles = exports.formatCustomRoleUploadSummary = exports.getCustomRolesFolder = void 0;
3
+ exports.downloadCustomRoles = exports.uploadCustomRoles = exports.readCustomRoleFiles = exports.formatCustomRoleUploadSummary = exports.getCustomRolesFolder = exports.CUSTOM_ROLE_CODE_RESOURCE = void 0;
4
4
  const tslib_1 = require("tslib");
5
- /* eslint-disable no-await-in-loop */
6
5
  const common_1 = require("@lightdash/common");
7
- const fs_1 = require("fs");
8
- const yaml = tslib_1.__importStar(require("js-yaml"));
9
- const groupBy_1 = tslib_1.__importDefault(require("lodash/groupBy"));
10
6
  const path = tslib_1.__importStar(require("path"));
7
+ const resource_1 = require("../contentAsCode/resource");
11
8
  const apiClient_1 = require("../dbt/apiClient");
12
- const fileNames_1 = require("./fileNames");
13
- const getCustomRolesFolder = (organizationContentPath) => path.join(organizationContentPath, 'custom-roles');
9
+ exports.CUSTOM_ROLE_CODE_RESOURCE = {
10
+ kind: 'custom_role',
11
+ displayLabel: 'custom role',
12
+ identityLabel: 'name',
13
+ scope: 'organization',
14
+ folderName: 'custom-roles',
15
+ acceptedExtensions: ['.yml'],
16
+ fileName: {
17
+ strategy: 'normalizedDisplayName',
18
+ fallbackPrefix: 'role',
19
+ extension: '.yml',
20
+ },
21
+ dependencies: [],
22
+ identity: ({ name }) => name,
23
+ displayName: ({ name }) => name,
24
+ parse: common_1.parseCustomRoleAsCode,
25
+ sort: (left, right) => left.name.localeCompare(right.name),
26
+ };
27
+ const getCustomRolesFolder = (organizationContentPath) => path.join(organizationContentPath, exports.CUSTOM_ROLE_CODE_RESOURCE.folderName);
14
28
  exports.getCustomRolesFolder = getCustomRolesFolder;
15
29
  const formatCustomRoleUploadSummary = (summary) => `${summary.created} created, ${summary.updated} updated, ${summary.unchanged} unchanged, ${summary.failed} failed`;
16
30
  exports.formatCustomRoleUploadSummary = formatCustomRoleUploadSummary;
17
- const getCustomRoleName = (role) => {
18
- if (typeof role !== 'object' || role === null || Array.isArray(role)) {
19
- return undefined;
20
- }
21
- return typeof role.name === 'string'
22
- ? role.name
23
- : undefined;
24
- };
25
- const readCustomRoleFileResults = async (organizationContentPath) => {
26
- const folder = (0, exports.getCustomRolesFolder)(organizationContentPath);
27
- let entries;
28
- try {
29
- entries = await fs_1.promises.readdir(folder, { withFileTypes: true });
30
- }
31
- catch (error) {
32
- if (error.code === 'ENOENT') {
33
- return { roleFiles: [], failures: [] };
34
- }
35
- throw error;
36
- }
37
- const files = entries
38
- .filter((entry) => entry.isFile() && entry.name.endsWith('.yml'))
39
- .sort((left, right) => left.name.localeCompare(right.name));
40
- const results = await Promise.all(files.map(async (file) => {
41
- const filePath = path.join(folder, file.name);
42
- try {
43
- return {
44
- kind: 'role',
45
- roleFile: {
46
- filePath,
47
- role: yaml.load(await fs_1.promises.readFile(filePath, 'utf8')),
48
- },
49
- };
50
- }
51
- catch (error) {
52
- return {
53
- kind: 'failure',
54
- failure: {
55
- message: `Unable to parse custom role file "${filePath}": ${(0, common_1.getErrorMessage)(error)}`,
56
- },
57
- };
58
- }
59
- }));
60
- const parsedRoleFiles = results.flatMap((result) => result.kind === 'role' ? [result.roleFile] : []);
61
- const failures = results.flatMap((result) => result.kind === 'failure' ? [result.failure] : []);
62
- const namedRoleFiles = parsedRoleFiles.flatMap((roleFile) => {
63
- const name = getCustomRoleName(roleFile.role);
64
- return name === undefined ? [] : [{ name, roleFile }];
65
- });
66
- const roleFilesByName = (0, groupBy_1.default)(namedRoleFiles, ({ name }) => name);
67
- const duplicateNames = new Set(Object.entries(roleFilesByName)
68
- .filter(([, roleFiles]) => roleFiles.length > 1)
69
- .map(([name]) => name));
70
- for (const duplicateName of [...duplicateNames].sort()) {
71
- for (const { roleFile } of roleFilesByName[duplicateName]) {
72
- failures.push({
73
- message: `Duplicate custom role name "${duplicateName}" in "${roleFile.filePath}"`,
74
- });
75
- }
76
- }
77
- return {
78
- roleFiles: parsedRoleFiles.filter(({ role }) => !duplicateNames.has(getCustomRoleName(role) ?? '')),
79
- failures,
80
- };
81
- };
82
31
  const readCustomRoleFiles = async (organizationContentPath) => {
83
- const { roleFiles, failures } = await readCustomRoleFileResults(organizationContentPath);
84
- if (failures.length > 0) {
85
- throw new common_1.ParameterError(failures.map(({ message }) => message).join('\n'));
86
- }
87
- return roleFiles.map(({ role }) => role);
32
+ const result = await (0, resource_1.readCodeResourceFiles)({
33
+ definition: exports.CUSTOM_ROLE_CODE_RESOURCE,
34
+ basePath: organizationContentPath,
35
+ });
36
+ (0, resource_1.assertCodeResourceFilesValid)(result);
37
+ return result.files.map(({ document }) => document);
88
38
  };
89
39
  exports.readCustomRoleFiles = readCustomRoleFiles;
90
- const uploadCustomRoles = async (organizationUuid, organizationContentPath) => {
91
- const { roleFiles, failures } = await readCustomRoleFileResults(organizationContentPath);
92
- const summary = {
93
- created: 0,
94
- updated: 0,
95
- unchanged: 0,
96
- failed: 0,
97
- failures,
98
- };
99
- for (const { filePath, role } of roleFiles) {
100
- try {
101
- const result = await (0, apiClient_1.lightdashApi)({
102
- method: 'POST',
103
- url: `/api/v2/orgs/${organizationUuid}/roles/code`,
104
- body: JSON.stringify(role),
105
- });
106
- switch (result.action) {
107
- case common_1.PromotionAction.CREATE:
108
- summary.created += 1;
109
- break;
110
- case common_1.PromotionAction.UPDATE:
111
- summary.updated += 1;
112
- break;
113
- case common_1.PromotionAction.NO_CHANGES:
114
- summary.unchanged += 1;
115
- break;
116
- default:
117
- throw new common_1.ParameterError(`Unsupported custom role promotion action: ${result.action}`);
118
- }
119
- }
120
- catch (error) {
121
- summary.failures.push({
122
- message: `Invalid custom role file "${filePath}": ${(0, common_1.getErrorMessage)(error)}`,
123
- });
124
- }
125
- }
126
- summary.failed = summary.failures.length;
127
- return summary;
128
- };
40
+ const uploadCustomRoles = async (organizationUuid, organizationContentPath) => (0, resource_1.uploadCodeResource)({
41
+ definition: exports.CUSTOM_ROLE_CODE_RESOURCE,
42
+ basePath: organizationContentPath,
43
+ upsert: async (role) => {
44
+ const result = await (0, apiClient_1.lightdashApi)({
45
+ method: 'POST',
46
+ url: `/api/v2/orgs/${organizationUuid}/code/roles`,
47
+ body: JSON.stringify(role),
48
+ });
49
+ return result.action;
50
+ },
51
+ });
129
52
  exports.uploadCustomRoles = uploadCustomRoles;
130
- const downloadCustomRoles = async (organizationUuid, organizationContentPath) => {
131
- const { customRoles } = await (0, apiClient_1.lightdashApi)({
132
- method: 'GET',
133
- url: `/api/v2/orgs/${organizationUuid}/roles/code`,
134
- body: undefined,
135
- });
136
- const folder = (0, exports.getCustomRolesFolder)(organizationContentPath);
137
- await fs_1.promises.mkdir(folder, { recursive: true });
138
- const existingFiles = await fs_1.promises.readdir(folder, { withFileTypes: true });
139
- await Promise.all(existingFiles
140
- .filter((entry) => entry.isFile() && entry.name.endsWith('.yml'))
141
- .map((entry) => fs_1.promises.unlink(path.join(folder, entry.name))));
142
- const filenames = (0, fileNames_1.getOrganizationContentFileNames)({
143
- values: customRoles.map(({ name }) => name),
144
- fallbackPrefix: 'role',
145
- });
146
- await Promise.all(customRoles.map(async (role, index) => {
147
- await fs_1.promises.writeFile(path.join(folder, filenames[index]), yaml.dump(role, { quotingType: '"', sortKeys: true }));
148
- }));
149
- return customRoles.length;
150
- };
53
+ const downloadCustomRoles = async (organizationUuid, organizationContentPath) => (0, resource_1.downloadCodeResource)({
54
+ definition: exports.CUSTOM_ROLE_CODE_RESOURCE,
55
+ basePath: organizationContentPath,
56
+ list: async () => {
57
+ const { customRoles } = await (0, apiClient_1.lightdashApi)({
58
+ method: 'GET',
59
+ url: `/api/v2/orgs/${organizationUuid}/code/roles`,
60
+ body: undefined,
61
+ });
62
+ return customRoles;
63
+ },
64
+ });
151
65
  exports.downloadCustomRoles = downloadCustomRoles;
@@ -59,7 +59,7 @@ describe('custom roles as code', () => {
59
59
  expect(total).toBe(1);
60
60
  expect(apiClient_1.lightdashApi).toHaveBeenCalledWith({
61
61
  method: 'GET',
62
- url: '/api/v2/orgs/organization-uuid/roles/code',
62
+ url: '/api/v2/orgs/organization-uuid/code/roles',
63
63
  body: undefined,
64
64
  });
65
65
  expect(yaml.load(await fs_1.promises.readFile(path.join(tmpDir, 'custom-roles', 'custom-role-name.yml'), 'utf8'))).toStrictEqual({
@@ -71,6 +71,7 @@ describe('custom roles as code', () => {
71
71
  });
72
72
  expect((await fs_1.promises.readdir(folder)).sort()).toStrictEqual([
73
73
  'custom-role-name.yml',
74
+ 'stale.yml',
74
75
  ]);
75
76
  });
76
77
  it('uses stable hashes for normalized filename collisions', async () => {
@@ -82,6 +83,16 @@ describe('custom roles as code', () => {
82
83
  expect(filenames).toHaveLength(2);
83
84
  expect(filenames.every((filename) => /^data-admin-[a-f0-9]{8}\.yml$/.test(filename))).toBe(true);
84
85
  });
86
+ it('reuses an existing filename owned by the same identity', async () => {
87
+ await writeCustomRole('hand-authored-name.yml', customRole('Data Admin'));
88
+ vitest_1.vi.mocked(apiClient_1.lightdashApi).mockResolvedValueOnce({
89
+ customRoles: [customRole('Data Admin')],
90
+ });
91
+ await (0, customRoles_1.downloadCustomRoles)('organization-uuid', tmpDir);
92
+ expect(await fs_1.promises.readdir((0, customRoles_1.getCustomRolesFolder)(tmpDir))).toStrictEqual([
93
+ 'hand-authored-name.yml',
94
+ ]);
95
+ });
85
96
  it('counts backend create, update, and no-op actions', async () => {
86
97
  await writeCustomRole('a.yml', customRole('Role A'));
87
98
  await writeCustomRole('b.yml', customRole('Role B'));
@@ -102,7 +113,7 @@ describe('custom roles as code', () => {
102
113
  });
103
114
  expect(apiClient_1.lightdashApi).toHaveBeenNthCalledWith(1, {
104
115
  method: 'POST',
105
- url: '/api/v2/orgs/organization-uuid/roles/code',
116
+ url: '/api/v2/orgs/organization-uuid/code/roles',
106
117
  body: expect.any(String),
107
118
  });
108
119
  expect(JSON.parse(vitest_1.vi.mocked(apiClient_1.lightdashApi).mock.calls[0][0].body)).toStrictEqual(customRole('Role A'));
@@ -1 +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"}
1
+ {"version":3,"file":"fileNames.d.ts","sourceRoot":"","sources":["../../../src/handlers/organizationContent/fileNames.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,+BAA+B,GAAI,6BAG7C;IACC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CAC1B,KAAG,MAAM,EAOJ,CAAC"}