@arkebcacy/beacon-cli-temp 0.1.5 → 0.1.7

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 (69) hide show
  1. package/dist/cs/labels/Label.d.ts +8 -0
  2. package/dist/cs/labels/Label.d.ts.map +1 -0
  3. package/dist/cs/labels/Label.js +5 -0
  4. package/dist/cs/labels/Label.js.map +1 -0
  5. package/dist/cs/labels/getAllLabels.d.ts +2 -1
  6. package/dist/cs/labels/getAllLabels.d.ts.map +1 -1
  7. package/dist/cs/labels/getAllLabels.js +24 -4
  8. package/dist/cs/labels/getAllLabels.js.map +1 -1
  9. package/dist/cs/locales/addLocale.d.ts +11 -0
  10. package/dist/cs/locales/addLocale.d.ts.map +1 -0
  11. package/dist/cs/locales/addLocale.js +36 -0
  12. package/dist/cs/locales/addLocale.js.map +1 -0
  13. package/dist/cs/locales/ensureLocaleExists.d.ts +11 -0
  14. package/dist/cs/locales/ensureLocaleExists.d.ts.map +1 -0
  15. package/dist/cs/locales/ensureLocaleExists.js +38 -0
  16. package/dist/cs/locales/ensureLocaleExists.js.map +1 -0
  17. package/dist/cs/locales/getLocales.d.ts +12 -0
  18. package/dist/cs/locales/getLocales.d.ts.map +1 -0
  19. package/dist/cs/locales/getLocales.js +23 -0
  20. package/dist/cs/locales/getLocales.js.map +1 -0
  21. package/dist/dto/labels/NormalizedLabels.d.ts +13 -0
  22. package/dist/dto/labels/NormalizedLabels.d.ts.map +1 -0
  23. package/dist/dto/labels/NormalizedLabels.js +25 -0
  24. package/dist/dto/labels/NormalizedLabels.js.map +1 -0
  25. package/dist/dto/labels/flatten.d.ts +4 -0
  26. package/dist/dto/labels/flatten.d.ts.map +1 -0
  27. package/dist/dto/labels/flatten.js +18 -0
  28. package/dist/dto/labels/flatten.js.map +1 -0
  29. package/dist/dto/labels/organize.d.ts +4 -0
  30. package/dist/dto/labels/organize.d.ts.map +1 -0
  31. package/dist/dto/labels/organize.js +41 -0
  32. package/dist/dto/labels/organize.js.map +1 -0
  33. package/dist/schema/entries/toContentstack.d.ts.map +1 -1
  34. package/dist/schema/entries/toContentstack.js +30 -5
  35. package/dist/schema/entries/toContentstack.js.map +1 -1
  36. package/dist/schema/entries/toFilesystem.js +23 -10
  37. package/dist/schema/entries/toFilesystem.js.map +1 -1
  38. package/dist/schema/labels/lib/labelHelpers.d.ts +3 -0
  39. package/dist/schema/labels/lib/labelHelpers.d.ts.map +1 -0
  40. package/dist/schema/labels/lib/labelHelpers.js +34 -0
  41. package/dist/schema/labels/lib/labelHelpers.js.map +1 -0
  42. package/dist/schema/labels/lib/labelOperations.d.ts +6 -0
  43. package/dist/schema/labels/lib/labelOperations.d.ts.map +1 -0
  44. package/dist/schema/labels/lib/labelOperations.js +57 -0
  45. package/dist/schema/labels/lib/labelOperations.js.map +1 -0
  46. package/dist/schema/labels/toContentstack.d.ts.map +1 -1
  47. package/dist/schema/labels/toContentstack.js +145 -85
  48. package/dist/schema/labels/toContentstack.js.map +1 -1
  49. package/dist/schema/labels/toFilesystem.d.ts.map +1 -1
  50. package/dist/schema/labels/toFilesystem.js +10 -8
  51. package/dist/schema/labels/toFilesystem.js.map +1 -1
  52. package/dist/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/src/cs/labels/Label.ts +12 -0
  55. package/src/cs/labels/getAllLabels.ts +30 -5
  56. package/src/cs/locales/addLocale.ts +59 -0
  57. package/src/cs/locales/ensureLocaleExists.ts +51 -0
  58. package/src/cs/locales/getLocales.ts +38 -0
  59. package/src/dto/labels/NormalizedLabels.ts +47 -0
  60. package/src/dto/labels/flatten.test.ts +118 -0
  61. package/src/dto/labels/flatten.ts +27 -0
  62. package/src/dto/labels/organize.test.ts +131 -0
  63. package/src/dto/labels/organize.ts +58 -0
  64. package/src/schema/entries/toContentstack.ts +75 -6
  65. package/src/schema/entries/toFilesystem.ts +29 -14
  66. package/src/schema/labels/lib/labelHelpers.ts +36 -0
  67. package/src/schema/labels/lib/labelOperations.ts +74 -0
  68. package/src/schema/labels/toContentstack.ts +208 -91
  69. package/src/schema/labels/toFilesystem.ts +15 -9
package/package.json CHANGED
@@ -50,5 +50,5 @@
50
50
  "prepack": "node ./build/prepack.js"
51
51
  },
52
52
  "type": "module",
53
- "version": "0.1.5"
53
+ "version": "0.1.7"
54
54
  }
@@ -0,0 +1,12 @@
1
+ import type OmitIndex from '../../util/OmitIndex.js';
2
+ import type { Item } from '../Types.js';
3
+ import { isItem } from '../Types.js';
4
+
5
+ export default interface Label extends OmitIndex<Item> {
6
+ readonly name: string;
7
+ readonly parent: readonly string[];
8
+ }
9
+
10
+ export function isLabel(o: unknown): o is Label {
11
+ return isItem(o) && typeof o.name === 'string' && Array.isArray(o.parent);
12
+ }
@@ -1,18 +1,43 @@
1
1
  import ContentstackError from '../api/ContentstackError.js';
2
2
  import type Client from '../api/Client.js';
3
3
  import isRecord from '#cli/util/isRecord.js';
4
+ import type Label from './Label.js';
5
+ import { isLabel } from './Label.js';
4
6
 
5
- export default async function getAllLabels(client: Client): Promise<unknown[]> {
7
+ export default async function getAllLabels(client: Client): Promise<Label[]> {
6
8
  const res = (await client.GET('/v3/labels')) as unknown;
7
9
  const data = (res as { data?: unknown } | undefined)?.data;
8
10
  const error = (res as { error?: unknown } | undefined)?.error;
9
11
  const msg = `Failed to fetch labels`;
10
12
  ContentstackError.throwIfError(error, msg);
13
+
14
+ let rawLabels: unknown[] = [];
11
15
  if (isRecord(data) && Array.isArray(data.labels)) {
12
- return data.labels as unknown[];
16
+ rawLabels = data.labels;
17
+ } else if (Array.isArray(data)) {
18
+ rawLabels = data;
13
19
  }
14
- if (Array.isArray(data)) {
15
- return data as unknown[];
20
+
21
+ // Transform raw labels to ensure they have parent_uid (null if not present)
22
+ const labels: Label[] = [];
23
+ for (const raw of rawLabels) {
24
+ if (
25
+ isRecord(raw) &&
26
+ typeof raw.uid === 'string' &&
27
+ typeof raw.name === 'string'
28
+ ) {
29
+ // Preserve all fields from the remote label
30
+ const label: Label = {
31
+ ...raw,
32
+ name: raw.name,
33
+ parent: Array.isArray(raw.parent) ? raw.parent : [],
34
+ uid: raw.uid,
35
+ };
36
+ if (isLabel(label)) {
37
+ labels.push(label);
38
+ }
39
+ }
16
40
  }
17
- return [];
41
+
42
+ return labels;
18
43
  }
@@ -0,0 +1,59 @@
1
+ import type Client from '../api/Client.js';
2
+ import ContentstackError from '../api/ContentstackError.js';
3
+ import isRecord from '#cli/util/isRecord.js';
4
+ import type { Locale } from './getLocales.js';
5
+
6
+ interface AddLocaleRequest {
7
+ readonly locale: {
8
+ readonly code: string;
9
+ readonly fallback_locale?: string;
10
+ readonly name?: string;
11
+ };
12
+ }
13
+
14
+ /**
15
+ * Adds a new locale to the stack.
16
+ * @param client - Contentstack API client
17
+ * @param code - Locale code (e.g., 'zh-cn', 'zh-chs')
18
+ * @param name - Display name for the locale (e.g., 'Chinese - China')
19
+ * @param fallbackLocale - Optional fallback locale code
20
+ */
21
+ export async function addLocale(
22
+ client: Client,
23
+ code: string,
24
+ name?: string,
25
+ fallbackLocale?: string,
26
+ ): Promise<Locale> {
27
+ const requestBody: AddLocaleRequest = {
28
+ locale: {
29
+ code,
30
+ ...(fallbackLocale !== undefined && { fallback_locale: fallbackLocale }),
31
+ ...(name !== undefined && { name }),
32
+ },
33
+ };
34
+
35
+ const response = await client.POST('/v3/locales', {
36
+ body: requestBody as never,
37
+ });
38
+
39
+ const msg = `Failed to add locale '${code}'`;
40
+ ContentstackError.throwIfError(response.error, msg);
41
+
42
+ if (!response.response.ok) {
43
+ throw new Error(msg);
44
+ }
45
+
46
+ const { data } = response;
47
+
48
+ if (!isRecord(data)) {
49
+ throw new TypeError(msg);
50
+ }
51
+
52
+ const { locale } = data;
53
+
54
+ if (!isRecord(locale)) {
55
+ throw new TypeError(msg);
56
+ }
57
+
58
+ return locale as Locale;
59
+ }
@@ -0,0 +1,51 @@
1
+ import type Client from '../api/Client.js';
2
+ import { addLocale } from './addLocale.js';
3
+ import { getLocales } from './getLocales.js';
4
+ import type { Locale } from './getLocales.js';
5
+
6
+ /**
7
+ * Ensures a locale exists in the target stack. If it doesn't exist, creates it.
8
+ * @param client - Contentstack API client
9
+ * @param localeCode - Locale code to ensure exists (e.g., 'zh-cn')
10
+ * @param fallbackLocale - Optional fallback locale code (defaults to 'en-us')
11
+ * @returns The locale object (either existing or newly created)
12
+ */
13
+ export async function ensureLocaleExists(
14
+ client: Client,
15
+ localeCode: string,
16
+ fallbackLocale = 'en-us',
17
+ ): Promise<Locale> {
18
+ const existingLocales = await getLocales(client);
19
+ const existing = existingLocales.find((loc) => loc.code === localeCode);
20
+
21
+ if (existing !== undefined) {
22
+ return existing;
23
+ }
24
+
25
+ // Locale doesn't exist - create it
26
+
27
+ // Derive a reasonable display name from the locale code
28
+ const name = deriveLocaleName(localeCode);
29
+
30
+ const newLocale = await addLocale(client, localeCode, name, fallbackLocale);
31
+
32
+ return newLocale;
33
+ }
34
+
35
+ /**
36
+ * Derives a human-readable name from a locale code.
37
+ * This is a basic implementation that can be expanded as needed.
38
+ */
39
+ function deriveLocaleName(code: string): string {
40
+ const nameMap: Record<string, string> = {
41
+ 'en-us': 'English - United States',
42
+ 'zh-chs': 'Chinese (simplified)',
43
+ 'zh-cht': 'Chinese (traditional)',
44
+ 'zh-cn': 'Chinese - China',
45
+ 'zh-hans': 'Chinese (simplified)',
46
+ 'zh-hant': 'Chinese (traditional)',
47
+ 'zh-tw': 'Chinese - Taiwan',
48
+ };
49
+
50
+ return nameMap[code.toLowerCase()] ?? code.toUpperCase();
51
+ }
@@ -0,0 +1,38 @@
1
+ import type Client from '../api/Client.js';
2
+ import ContentstackError from '../api/ContentstackError.js';
3
+ import isRecord from '#cli/util/isRecord.js';
4
+
5
+ export interface Locale {
6
+ readonly code: string;
7
+ readonly fallback_locale?: string;
8
+ readonly name: string;
9
+ readonly uid: string;
10
+ }
11
+
12
+ /**
13
+ * Fetches all locales configured for a stack.
14
+ */
15
+ export async function getLocales(client: Client): Promise<readonly Locale[]> {
16
+ const response = await client.GET('/v3/locales', {});
17
+
18
+ const msg = 'Failed to fetch locales';
19
+ ContentstackError.throwIfError(response.error, msg);
20
+
21
+ if (!response.response.ok) {
22
+ throw new Error(msg);
23
+ }
24
+
25
+ const { data } = response;
26
+
27
+ if (!isRecord(data)) {
28
+ throw new TypeError(msg);
29
+ }
30
+
31
+ const { locales } = data;
32
+
33
+ if (!Array.isArray(locales)) {
34
+ throw new TypeError(msg);
35
+ }
36
+
37
+ return locales as Locale[];
38
+ }
@@ -0,0 +1,47 @@
1
+ import isRecord from '#cli/util/isRecord.js';
2
+ import type Label from '#cli/cs/labels/Label.js';
3
+ import { isItem } from '#cli/cs/Types.js';
4
+
5
+ export default interface NormalizedLabels {
6
+ readonly labels: readonly LabelTreeNode[];
7
+ }
8
+
9
+ export interface LabelTreeNode {
10
+ // Allow any additional fields from the label
11
+ readonly [key: string]: unknown;
12
+ readonly uid: Label['uid'];
13
+ readonly name: Label['name'];
14
+ readonly children?: readonly LabelTreeNode[];
15
+ }
16
+
17
+ export function key() {
18
+ return 'labels';
19
+ }
20
+
21
+ export function isNormalizedLabels(
22
+ value: unknown,
23
+ ): value is NormalizedLabels & Record<string, unknown> {
24
+ if (!isRecord(value)) {
25
+ return false;
26
+ }
27
+
28
+ return Array.isArray(value.labels) && value.labels.every(isLabelTreeNode);
29
+ }
30
+
31
+ function isLabelTreeNode(value: unknown): value is LabelTreeNode {
32
+ if (!isItem(value)) {
33
+ return false;
34
+ }
35
+
36
+ if (typeof value.name !== 'string') {
37
+ return false;
38
+ }
39
+
40
+ if (!('children' in value)) {
41
+ return true;
42
+ }
43
+
44
+ const { children } = value;
45
+
46
+ return Array.isArray(children) && children.every(isLabelTreeNode);
47
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import flatten from './flatten.js';
3
+ import type { LabelTreeNode } from './NormalizedLabels.js';
4
+
5
+ describe('flatten', () => {
6
+ it('flattens a simple hierarchy', () => {
7
+ const tree: LabelTreeNode[] = [
8
+ {
9
+ children: [
10
+ { name: 'Child 1', uid: 'child1' },
11
+ { name: 'Child 2', uid: 'child2' },
12
+ ],
13
+ name: 'Parent',
14
+ uid: 'parent',
15
+ },
16
+ ];
17
+
18
+ const result = flatten(tree);
19
+
20
+ expect(result).toEqual([
21
+ { name: 'Parent', parent: [], uid: 'parent' },
22
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
23
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
24
+ ]);
25
+ });
26
+
27
+ it('flattens nested hierarchy', () => {
28
+ const tree: LabelTreeNode[] = [
29
+ {
30
+ children: [
31
+ { name: 'Calculator', uid: 'calculator' },
32
+ { name: 'Data', uid: 'data' },
33
+ { name: 'Page', uid: 'page' },
34
+ ],
35
+ name: 'Component',
36
+ uid: 'component',
37
+ },
38
+ ];
39
+
40
+ const result = flatten(tree);
41
+
42
+ expect(result).toEqual([
43
+ { name: 'Component', parent: [], uid: 'component' },
44
+ { name: 'Calculator', parent: ['component'], uid: 'calculator' },
45
+ { name: 'Data', parent: ['component'], uid: 'data' },
46
+ { name: 'Page', parent: ['component'], uid: 'page' },
47
+ ]);
48
+ });
49
+
50
+ it('handles deeply nested hierarchy', () => {
51
+ const tree: LabelTreeNode[] = [
52
+ {
53
+ children: [
54
+ {
55
+ children: [
56
+ {
57
+ children: [{ name: 'Level 3', uid: 'level3' }],
58
+ name: 'Level 2',
59
+ uid: 'level2',
60
+ },
61
+ ],
62
+ name: 'Level 1',
63
+ uid: 'level1',
64
+ },
65
+ ],
66
+ name: 'Root',
67
+ uid: 'root',
68
+ },
69
+ ];
70
+
71
+ const result = flatten(tree);
72
+
73
+ expect(result).toEqual([
74
+ { name: 'Root', parent: [], uid: 'root' },
75
+ { name: 'Level 1', parent: ['root'], uid: 'level1' },
76
+ { name: 'Level 2', parent: ['level1'], uid: 'level2' },
77
+ { name: 'Level 3', parent: ['level2'], uid: 'level3' },
78
+ ]);
79
+ });
80
+
81
+ it('handles multiple top-level labels', () => {
82
+ const tree: LabelTreeNode[] = [
83
+ { name: 'Label 1', uid: 'label1' },
84
+ {
85
+ children: [{ name: 'Child', uid: 'child' }],
86
+ name: 'Label 2',
87
+ uid: 'label2',
88
+ },
89
+ ];
90
+
91
+ const result = flatten(tree);
92
+
93
+ expect(result).toEqual([
94
+ { name: 'Label 1', parent: [], uid: 'label1' },
95
+ { name: 'Label 2', parent: [], uid: 'label2' },
96
+ { name: 'Child', parent: ['label2'], uid: 'child' },
97
+ ]);
98
+ });
99
+
100
+ it('handles empty array', () => {
101
+ const result = flatten([]);
102
+ expect(result).toEqual([]);
103
+ });
104
+
105
+ it('handles labels without children', () => {
106
+ const tree: LabelTreeNode[] = [
107
+ { name: 'Label 1', uid: 'label1' },
108
+ { name: 'Label 2', uid: 'label2' },
109
+ ];
110
+
111
+ const result = flatten(tree);
112
+
113
+ expect(result).toEqual([
114
+ { name: 'Label 1', parent: [], uid: 'label1' },
115
+ { name: 'Label 2', parent: [], uid: 'label2' },
116
+ ]);
117
+ });
118
+ });
@@ -0,0 +1,27 @@
1
+ import type Label from '#cli/cs/labels/Label.js';
2
+ import type { LabelTreeNode } from './NormalizedLabels.js';
3
+
4
+ export default function flatten(
5
+ labels: readonly LabelTreeNode[],
6
+ ): readonly Label[] {
7
+ const result: Label[] = [];
8
+
9
+ function traverse(nodes: readonly LabelTreeNode[], parentUid: string | null) {
10
+ for (const node of nodes) {
11
+ const { children, ...label } = node;
12
+
13
+ result.push({
14
+ ...label,
15
+ parent: parentUid ? [parentUid] : [],
16
+ } as Label);
17
+
18
+ if (children) {
19
+ traverse(children, node.uid);
20
+ }
21
+ }
22
+ }
23
+
24
+ traverse(labels, null);
25
+
26
+ return result;
27
+ }
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import organize from './organize.js';
3
+ import type Label from '#cli/cs/labels/Label.js';
4
+
5
+ describe('organize', () => {
6
+ it('organizes flat labels into hierarchy', () => {
7
+ const labels: Label[] = [
8
+ { name: 'Parent', parent: [], uid: 'parent' },
9
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
10
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
11
+ ];
12
+
13
+ const result = organize(labels);
14
+
15
+ expect(result).toEqual([
16
+ {
17
+ children: [
18
+ { name: 'Child 1', uid: 'child1' },
19
+ { name: 'Child 2', uid: 'child2' },
20
+ ],
21
+ name: 'Parent',
22
+ uid: 'parent',
23
+ },
24
+ ]);
25
+ });
26
+
27
+ it('organizes component label structure from screenshot', () => {
28
+ const labels: Label[] = [
29
+ { name: 'Component', parent: [], uid: 'component' },
30
+ { name: 'Calculator', parent: ['component'], uid: 'calculator' },
31
+ { name: 'Data', parent: ['component'], uid: 'data' },
32
+ { name: 'Page', parent: ['component'], uid: 'page' },
33
+ ];
34
+
35
+ const result = organize(labels);
36
+
37
+ expect(result).toEqual([
38
+ {
39
+ children: [
40
+ { name: 'Calculator', uid: 'calculator' },
41
+ { name: 'Data', uid: 'data' },
42
+ { name: 'Page', uid: 'page' },
43
+ ],
44
+ name: 'Component',
45
+ uid: 'component',
46
+ },
47
+ ]);
48
+ });
49
+
50
+ it('handles deeply nested labels', () => {
51
+ const labels: Label[] = [
52
+ { name: 'Root', parent: [], uid: 'root' },
53
+ { name: 'Level 1', parent: ['root'], uid: 'level1' },
54
+ { name: 'Level 2', parent: ['level1'], uid: 'level2' },
55
+ { name: 'Level 3', parent: ['level2'], uid: 'level3' },
56
+ ];
57
+
58
+ const result = organize(labels);
59
+
60
+ expect(result).toEqual([
61
+ {
62
+ children: [
63
+ {
64
+ children: [
65
+ {
66
+ children: [{ name: 'Level 3', uid: 'level3' }],
67
+ name: 'Level 2',
68
+ uid: 'level2',
69
+ },
70
+ ],
71
+ name: 'Level 1',
72
+ uid: 'level1',
73
+ },
74
+ ],
75
+ name: 'Root',
76
+ uid: 'root',
77
+ },
78
+ ]);
79
+ });
80
+
81
+ it('handles multiple top-level labels', () => {
82
+ const labels: Label[] = [
83
+ { name: 'Label 1', parent: [], uid: 'label1' },
84
+ { name: 'Label 2', parent: [], uid: 'label2' },
85
+ { name: 'Child', parent: ['label2'], uid: 'child' },
86
+ ];
87
+
88
+ const result = organize(labels);
89
+
90
+ expect(result).toEqual([
91
+ { name: 'Label 1', uid: 'label1' },
92
+ {
93
+ children: [{ name: 'Child', uid: 'child' }],
94
+ name: 'Label 2',
95
+ uid: 'label2',
96
+ },
97
+ ]);
98
+ });
99
+
100
+ it('handles empty array', () => {
101
+ const result = organize([]);
102
+ expect(result).toEqual([]);
103
+ });
104
+
105
+ it('throws error for orphaned label', () => {
106
+ const labels: Label[] = [
107
+ { name: 'Child', parent: ['nonexistent'], uid: 'child' },
108
+ ];
109
+
110
+ expect(() => organize(labels)).toThrow(
111
+ 'Orphaned label child with parent nonexistent',
112
+ );
113
+ });
114
+
115
+ it('preserves order of siblings', () => {
116
+ const labels: Label[] = [
117
+ { name: 'Parent', parent: [], uid: 'parent' },
118
+ { name: 'Child 3', parent: ['parent'], uid: 'child3' },
119
+ { name: 'Child 1', parent: ['parent'], uid: 'child1' },
120
+ { name: 'Child 2', parent: ['parent'], uid: 'child2' },
121
+ ];
122
+
123
+ const result = organize(labels);
124
+
125
+ expect(result[0]?.children).toEqual([
126
+ { name: 'Child 3', uid: 'child3' },
127
+ { name: 'Child 1', uid: 'child1' },
128
+ { name: 'Child 2', uid: 'child2' },
129
+ ]);
130
+ });
131
+ });
@@ -0,0 +1,58 @@
1
+ import type Label from '#cli/cs/labels/Label.js';
2
+ import type { LabelTreeNode } from './NormalizedLabels.js';
3
+
4
+ // Labels have a tree structure defined by uid/parent array.
5
+ //
6
+ // The API returns a flat array that includes uid/parent, where parent is an array
7
+ // containing the parent UID (or empty array for top-level labels).
8
+ //
9
+ // We organize labels into a tree structure for serialization.
10
+ //
11
+ // The sort order amongst siblings is maintained equal to the order
12
+ // as it appears in the array.
13
+ export default function organize(
14
+ labels: readonly Label[],
15
+ ): readonly LabelTreeNode[] {
16
+ const byUid = new Map<string, MutableNode>();
17
+ const topLevel: MutableNode[] = [];
18
+
19
+ for (const label of labels) {
20
+ const { parent, ...labelWithoutParent } = label;
21
+ // Preserve all fields except parent (which is represented by the tree structure)
22
+ byUid.set(label.uid, {
23
+ ...labelWithoutParent,
24
+ name: label.name,
25
+ uid: label.uid,
26
+ });
27
+ }
28
+
29
+ for (const label of labels) {
30
+ const node = byUid.get(label.uid);
31
+ if (!node) {
32
+ throw new Error(`Label ${label.uid} not found`);
33
+ }
34
+
35
+ const parentUid = label.parent.length > 0 ? label.parent[0] : null;
36
+ if (!parentUid) {
37
+ topLevel.push(node);
38
+ continue;
39
+ }
40
+
41
+ const parent = byUid.get(parentUid);
42
+ if (!parent) {
43
+ const msg = `Orphaned label ${label.uid} with parent ${parentUid}`;
44
+ throw new Error(msg);
45
+ }
46
+
47
+ (parent.children ??= []).push(node);
48
+ }
49
+
50
+ return topLevel;
51
+ }
52
+
53
+ interface MutableNode {
54
+ [key: string]: unknown;
55
+ readonly uid: string;
56
+ readonly name: string;
57
+ children?: MutableNode[];
58
+ }