fast-tree-builder 2.0.0-alpha.4 → 2.0.0-beta.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.
- package/README.md +9 -9
- package/index.cjs +29 -13
- package/index.d.cts +47 -43
- package/index.d.mts +47 -43
- package/index.mjs +29 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
- **Arbitary Node Access** – Returns a `Map` that allows constant-time access to any node.
|
|
30
30
|
- **Tree Validation** – Detects cycles or nodes reachable through multiple paths.
|
|
31
31
|
- **Reference Validation** – Optionally enforce that all parent/child links are valid.
|
|
32
|
-
- **Depth Values** – Optionally
|
|
32
|
+
- **Depth Values** – Optionally include a depth value in each node.
|
|
33
33
|
|
|
34
34
|
## Installation
|
|
35
35
|
|
|
@@ -66,13 +66,13 @@ Builds a tree structure from an iterable list of items.
|
|
|
66
66
|
|
|
67
67
|
##### Optional
|
|
68
68
|
|
|
69
|
-
- `nodeValueMapper`: Function to map an item to a custom value stored in the node.
|
|
70
|
-
- `nodeValueKey`: Key where the item
|
|
71
|
-
- `nodeParentKey`: Key where the node's parent reference is stored. Set to `false` to omit parent links.
|
|
72
|
-
- `nodeChildrenKey`: Key where the node's children are stored.
|
|
73
|
-
- `
|
|
74
|
-
- `validateReferences`: When `true`, verifies all `parentId` or `childIds` resolve to real items. Errors are thrown on invalid references.
|
|
75
|
-
- `validateTree`: When `true`, verifies that the final structure is a valid tree (no cycles or nodes reachable via multipla paths). Errors are thrown if the check fails.
|
|
69
|
+
- `nodeValueMapper`: Function to map an item to a custom value stored in the node.
|
|
70
|
+
- `nodeValueKey`: Key where the item is stored in the output node. Set to `false` to inline the item directly into the node. Defaults to `'value'`.
|
|
71
|
+
- `nodeParentKey`: Key where the node's parent reference is stored. Set to `false` to omit parent links. Defaults to `'parent'`.
|
|
72
|
+
- `nodeChildrenKey`: Key where the node's children are stored. Defaults to `'children'`.
|
|
73
|
+
- `nodeDepthKey`: Object key used to store the node's depth in the tree (root = 0). Set to `false` to omit depth values. Turns on `validateTree` when a string value is set here. Defaults to `false`.
|
|
74
|
+
- `validateReferences`: When `true`, verifies all `parentId` or `childIds` resolve to real items. Errors are thrown on invalid references. Defaults to `false`.
|
|
75
|
+
- `validateTree`: When `true`, verifies that the final structure is a valid tree (no cycles or nodes reachable via multipla paths). Errors are thrown if the check fails. Defaults to `false`.
|
|
76
76
|
|
|
77
77
|
#### Returns
|
|
78
78
|
|
|
@@ -88,7 +88,7 @@ Builds a tree structure from an iterable list of items.
|
|
|
88
88
|
- Missing required `id`, `parentId`/`childIds`, or `options` parameter
|
|
89
89
|
- Duplicate item identifiers in input
|
|
90
90
|
- Invalid reference (if `validateReferences` is enabled)
|
|
91
|
-
- Cycle or structural error (if `validateTree` or `
|
|
91
|
+
- Cycle or structural error (if `validateTree` is enabled or `nodeDepthKey` is string)
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
## Usage
|
package/index.cjs
CHANGED
|
@@ -23,7 +23,7 @@ __export(index_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(index_exports);
|
|
24
24
|
function buildTree(items, options) {
|
|
25
25
|
if (!options) {
|
|
26
|
-
throw new Error(
|
|
26
|
+
throw new Error(`Missing required 'options' parameter.`);
|
|
27
27
|
}
|
|
28
28
|
const {
|
|
29
29
|
id: idAccessor,
|
|
@@ -33,18 +33,18 @@ function buildTree(items, options) {
|
|
|
33
33
|
nodeValueKey = "value",
|
|
34
34
|
nodeParentKey = "parent",
|
|
35
35
|
nodeChildrenKey = "children",
|
|
36
|
-
|
|
36
|
+
nodeDepthKey = false,
|
|
37
37
|
validateTree = false,
|
|
38
38
|
validateReferences = false
|
|
39
39
|
} = options;
|
|
40
40
|
if (!idAccessor) {
|
|
41
|
-
throw new Error(
|
|
41
|
+
throw new Error(`Option 'id' is required.`);
|
|
42
42
|
}
|
|
43
43
|
if (!parentIdAccessor && !childIdsAccessor) {
|
|
44
|
-
throw new Error(
|
|
44
|
+
throw new Error(`Either 'parentId' or 'childIds' must be provided.`);
|
|
45
45
|
}
|
|
46
46
|
if (parentIdAccessor && childIdsAccessor) {
|
|
47
|
-
throw new Error('
|
|
47
|
+
throw new Error(`'parentId' and 'childIds' cannot be used together.`);
|
|
48
48
|
}
|
|
49
49
|
const roots = [];
|
|
50
50
|
const nodes = /* @__PURE__ */ new Map();
|
|
@@ -53,9 +53,15 @@ function buildTree(items, options) {
|
|
|
53
53
|
for (const item of items) {
|
|
54
54
|
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
55
55
|
if (nodes.has(id)) {
|
|
56
|
-
throw new Error(`Duplicate identifier
|
|
56
|
+
throw new Error(`Duplicate identifier '${id}'.`);
|
|
57
57
|
}
|
|
58
58
|
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
59
|
+
if (nodeValueKey === false) {
|
|
60
|
+
if (nodeParentKey !== false) {
|
|
61
|
+
delete node[nodeParentKey];
|
|
62
|
+
}
|
|
63
|
+
delete node[nodeChildrenKey];
|
|
64
|
+
}
|
|
59
65
|
nodes.set(id, node);
|
|
60
66
|
const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor];
|
|
61
67
|
const parentNode = nodes.get(parentId);
|
|
@@ -86,7 +92,7 @@ function buildTree(items, options) {
|
|
|
86
92
|
}
|
|
87
93
|
for (const [parentId, nodes2] of waitingForParent.entries()) {
|
|
88
94
|
if (validateReferences && parentId != null) {
|
|
89
|
-
throw new Error(`Referential integrity violation: parentId
|
|
95
|
+
throw new Error(`Referential integrity violation: parentId '${parentId}' does not match any item in the input.`);
|
|
90
96
|
}
|
|
91
97
|
for (const node of nodes2) {
|
|
92
98
|
roots.push(node);
|
|
@@ -98,14 +104,20 @@ function buildTree(items, options) {
|
|
|
98
104
|
for (const item of items) {
|
|
99
105
|
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
100
106
|
if (nodes.has(id)) {
|
|
101
|
-
throw new Error(`Duplicate identifier
|
|
107
|
+
throw new Error(`Duplicate identifier '${id}'.`);
|
|
102
108
|
}
|
|
103
109
|
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
110
|
+
if (nodeValueKey === false) {
|
|
111
|
+
if (nodeParentKey !== false) {
|
|
112
|
+
delete node[nodeParentKey];
|
|
113
|
+
}
|
|
114
|
+
delete node[nodeChildrenKey];
|
|
115
|
+
}
|
|
104
116
|
nodes.set(id, node);
|
|
105
117
|
const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor];
|
|
106
118
|
if (childIds != null) {
|
|
107
119
|
if (typeof childIds[Symbol.iterator] !== "function") {
|
|
108
|
-
throw new Error(`Item
|
|
120
|
+
throw new Error(`Item '${id}' has invalid children: expected an iterable value.`);
|
|
109
121
|
}
|
|
110
122
|
node[nodeChildrenKey] = [];
|
|
111
123
|
for (const childId of childIds) {
|
|
@@ -114,12 +126,15 @@ function buildTree(items, options) {
|
|
|
114
126
|
node[nodeChildrenKey].push(childNode);
|
|
115
127
|
if (nodeParentKey !== false) {
|
|
116
128
|
if (childNode[nodeParentKey] && childNode[nodeParentKey] !== node) {
|
|
117
|
-
throw new Error(`Node
|
|
129
|
+
throw new Error(`Node '${childId}' already has a different parent, refusing to overwrite.`);
|
|
118
130
|
}
|
|
119
131
|
childNode[nodeParentKey] = node;
|
|
120
132
|
}
|
|
121
133
|
rootCandidates.delete(childId);
|
|
122
134
|
} else {
|
|
135
|
+
if (waitingChildren.has(childId)) {
|
|
136
|
+
throw new Error(`Multiple parents reference the same unresolved child '${childId}'.`);
|
|
137
|
+
}
|
|
123
138
|
waitingChildren.set(childId, {
|
|
124
139
|
parentNode: node,
|
|
125
140
|
childIndex: node[nodeChildrenKey].length
|
|
@@ -137,7 +152,7 @@ function buildTree(items, options) {
|
|
|
137
152
|
parentNode[nodeChildrenKey][childIndex] = node;
|
|
138
153
|
if (nodeParentKey !== false) {
|
|
139
154
|
if (node[nodeParentKey] && node[nodeParentKey] !== parentNode) {
|
|
140
|
-
throw new Error(`Node
|
|
155
|
+
throw new Error(`Node '${id}' already has a different parent, refusing to overwrite.`);
|
|
141
156
|
}
|
|
142
157
|
node[nodeParentKey] = parentNode;
|
|
143
158
|
}
|
|
@@ -149,7 +164,7 @@ function buildTree(items, options) {
|
|
|
149
164
|
if (waitingChildren.size > 0) {
|
|
150
165
|
if (validateReferences) {
|
|
151
166
|
const childId = waitingChildren.keys().next().value;
|
|
152
|
-
throw new Error(`Referential integrity violation: child reference
|
|
167
|
+
throw new Error(`Referential integrity violation: child reference '${childId}' does not match any item in the input.`);
|
|
153
168
|
}
|
|
154
169
|
const pending = Array.from(waitingChildren.values());
|
|
155
170
|
for (let i = pending.length - 1; i >= 0; i--) {
|
|
@@ -164,6 +179,7 @@ function buildTree(items, options) {
|
|
|
164
179
|
roots.push(node);
|
|
165
180
|
}
|
|
166
181
|
}
|
|
182
|
+
const withDepth = typeof nodeDepthKey === "string" || typeof nodeDepthKey === "symbol" || typeof nodeDepthKey === "number";
|
|
167
183
|
if (validateTree || withDepth) {
|
|
168
184
|
if (roots.length === 0 && nodes.size > 0) {
|
|
169
185
|
throw new Error("Tree validation failed: detected a cycle.");
|
|
@@ -179,7 +195,7 @@ function buildTree(items, options) {
|
|
|
179
195
|
}
|
|
180
196
|
visited.add(node);
|
|
181
197
|
if (withDepth) {
|
|
182
|
-
node
|
|
198
|
+
node[nodeDepthKey] = depth;
|
|
183
199
|
}
|
|
184
200
|
if (node[nodeChildrenKey]) {
|
|
185
201
|
for (const child of node[nodeChildrenKey]) {
|
package/index.d.cts
CHANGED
|
@@ -1,51 +1,31 @@
|
|
|
1
|
-
type TreeNode<TValue, TValueKey extends
|
|
2
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
3
|
-
} : Omit<TValue, Exclude<TParentKey, false> | TChildrenKey> & {
|
|
4
|
-
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
5
|
-
} & {
|
|
6
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
7
|
-
}) : (TParentKey extends false ? {
|
|
1
|
+
type TreeNode<TValue, TValueKey extends PropertyKey | false, TParentKey extends PropertyKey | false, TChildrenKey extends PropertyKey, TDepthKey extends PropertyKey | false> = (TValueKey extends false ? Omit<TValue, Exclude<TParentKey, false> | TChildrenKey | Exclude<TDepthKey, false>> : {
|
|
8
2
|
[k in Exclude<TValueKey, false>]: TValue;
|
|
9
|
-
} & {
|
|
10
|
-
[k in
|
|
11
|
-
} : {
|
|
12
|
-
[k in
|
|
13
|
-
} & {
|
|
14
|
-
[k in Exclude<
|
|
15
|
-
} & {
|
|
16
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
17
|
-
})) & (TWithDepth extends true ? {
|
|
18
|
-
depth: number;
|
|
3
|
+
}) & (TParentKey extends false ? {
|
|
4
|
+
[k in Exclude<TParentKey, false>]: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TDepthKey>;
|
|
5
|
+
} : {}) & {
|
|
6
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TDepthKey>[];
|
|
7
|
+
} & (TDepthKey extends false ? {
|
|
8
|
+
[k in Exclude<TDepthKey, false>]: number;
|
|
19
9
|
} : {});
|
|
20
|
-
type AccessorReturnType<
|
|
21
|
-
type
|
|
22
|
-
[K in keyof T]: T[K] extends Iterable<unknown> & object ? K : never;
|
|
10
|
+
type AccessorReturnType<O, P extends (keyof O) | ((item: O) => any)> = P extends ((item: O) => infer R) ? R : P extends (keyof O) ? O[P] : never;
|
|
11
|
+
type ObjectKeysOfIterableProperties<T> = {
|
|
12
|
+
[K in keyof T]: T[K] extends (Iterable<unknown> & object) | null | undefined ? K : never;
|
|
23
13
|
}[keyof T];
|
|
24
|
-
declare function buildTree<
|
|
14
|
+
declare function buildTree<TIdAccessor extends (keyof NoInfer<TInputValue>) | ((item: NoInfer<TInputValue>) => unknown), TNodeValueKey extends PropertyKey | false = 'value', TNodeParentKey extends PropertyKey | false = 'parent', TNodeChildrenKey extends PropertyKey = 'children', TNodeDepthKey extends PropertyKey | false = false, TInputValue extends (TNodeValueKey extends false ? object : TIdAccessor extends PropertyKey ? object : unknown) = any, TMappedValue extends (TNodeValueKey extends false ? object : unknown) = TInputValue>(items: Iterable<TInputValue>, options: {
|
|
25
15
|
/**
|
|
26
16
|
* A string key or function used to get the item's unique identifier.
|
|
27
17
|
*/
|
|
28
18
|
id: TIdAccessor;
|
|
29
|
-
/**
|
|
30
|
-
* A string key or function used to get the item's parent identifier.
|
|
31
|
-
*
|
|
32
|
-
* Either `parentId` or `childIds` must be provided.
|
|
33
|
-
*/
|
|
34
|
-
parentId?: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
35
|
-
/**
|
|
36
|
-
* A string key or function to retrieve a list of child identifiers from an item.
|
|
37
|
-
*
|
|
38
|
-
* Either `parentId` or `childIds` must be provided.
|
|
39
|
-
*/
|
|
40
|
-
childIds?: IterableKeys<TInputValue> | ((item: TInputValue) => (Iterable<unknown> & object) | null | undefined);
|
|
41
19
|
/**
|
|
42
20
|
* Maps the input item to a different value stored in the resulting tree node.
|
|
43
21
|
*/
|
|
44
22
|
nodeValueMapper?: {
|
|
45
|
-
(item: TInputValue): TMappedValue;
|
|
23
|
+
(item: NoInfer<TInputValue>): TMappedValue;
|
|
46
24
|
};
|
|
47
25
|
/**
|
|
48
|
-
* Object key used to store the
|
|
26
|
+
* Object key used to store the input item in the output tree node.
|
|
27
|
+
*
|
|
28
|
+
* Set to `false` to merge the item into the node itself.
|
|
49
29
|
*
|
|
50
30
|
* Defaults to `'value'`.
|
|
51
31
|
*/
|
|
@@ -53,6 +33,8 @@ declare function buildTree<TInputValue extends (TIdAccessor extends (keyof TInpu
|
|
|
53
33
|
/**
|
|
54
34
|
* Object key used to store the reference to the parent node in the output tree node.
|
|
55
35
|
*
|
|
36
|
+
* Set to `false` to omit parent links.
|
|
37
|
+
*
|
|
56
38
|
* Defaults to `'parent'`.
|
|
57
39
|
*/
|
|
58
40
|
nodeParentKey?: TNodeParentKey;
|
|
@@ -63,15 +45,17 @@ declare function buildTree<TInputValue extends (TIdAccessor extends (keyof TInpu
|
|
|
63
45
|
*/
|
|
64
46
|
nodeChildrenKey?: TNodeChildrenKey;
|
|
65
47
|
/**
|
|
66
|
-
*
|
|
48
|
+
* Object key used to store a value indicating the node depth in the tree on each output node.
|
|
67
49
|
* Root nodes have a depth of 0.
|
|
68
50
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
51
|
+
* Set to `false` to omit depth values.
|
|
52
|
+
*
|
|
53
|
+
* Enables `validateTree`, as depth assignment requires a valid tree structure,
|
|
54
|
+
* and both operations also share the same traversal logic.
|
|
71
55
|
*
|
|
72
56
|
* Defaults to `false`.
|
|
73
57
|
*/
|
|
74
|
-
|
|
58
|
+
nodeDepthKey?: TNodeDepthKey;
|
|
75
59
|
/**
|
|
76
60
|
* Validates that the final structure forms a tree.
|
|
77
61
|
*
|
|
@@ -97,14 +81,34 @@ declare function buildTree<TInputValue extends (TIdAccessor extends (keyof TInpu
|
|
|
97
81
|
*/
|
|
98
82
|
validateReferences?: boolean;
|
|
99
83
|
} & ({
|
|
84
|
+
/**
|
|
85
|
+
* A string key or function used to get the item's parent identifier.
|
|
86
|
+
*
|
|
87
|
+
* Either `parentId` or `childIds` must be provided.
|
|
88
|
+
*/
|
|
100
89
|
parentId?: never;
|
|
101
|
-
|
|
90
|
+
/**
|
|
91
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
92
|
+
*
|
|
93
|
+
* Either `parentId` or `childIds` must be provided.
|
|
94
|
+
*/
|
|
95
|
+
childIds: ObjectKeysOfIterableProperties<NoInfer<TInputValue>> | ((item: NoInfer<TInputValue>) => (Iterable<unknown> & object) | null | undefined);
|
|
102
96
|
} | {
|
|
103
|
-
|
|
97
|
+
/**
|
|
98
|
+
* A string key or function used to get the item's parent identifier.
|
|
99
|
+
*
|
|
100
|
+
* Either `parentId` or `childIds` must be provided.
|
|
101
|
+
*/
|
|
102
|
+
parentId: (keyof NoInfer<TInputValue>) | ((item: NoInfer<TInputValue>) => unknown);
|
|
103
|
+
/**
|
|
104
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
105
|
+
*
|
|
106
|
+
* Either `parentId` or `childIds` must be provided.
|
|
107
|
+
*/
|
|
104
108
|
childIds?: never;
|
|
105
109
|
})): {
|
|
106
|
-
roots:
|
|
107
|
-
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>,
|
|
110
|
+
roots: TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TNodeDepthKey>[];
|
|
111
|
+
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>, TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TNodeDepthKey>>;
|
|
108
112
|
};
|
|
109
113
|
declare const _default: { default: typeof buildTree };
|
|
110
114
|
export = _default;
|
package/index.d.mts
CHANGED
|
@@ -1,51 +1,31 @@
|
|
|
1
|
-
type TreeNode<TValue, TValueKey extends
|
|
2
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
3
|
-
} : Omit<TValue, Exclude<TParentKey, false> | TChildrenKey> & {
|
|
4
|
-
[k in Exclude<TParentKey, false>]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>;
|
|
5
|
-
} & {
|
|
6
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
7
|
-
}) : (TParentKey extends false ? {
|
|
1
|
+
type TreeNode<TValue, TValueKey extends PropertyKey | false, TParentKey extends PropertyKey | false, TChildrenKey extends PropertyKey, TDepthKey extends PropertyKey | false> = (TValueKey extends false ? Omit<TValue, Exclude<TParentKey, false> | TChildrenKey | Exclude<TDepthKey, false>> : {
|
|
8
2
|
[k in Exclude<TValueKey, false>]: TValue;
|
|
9
|
-
} & {
|
|
10
|
-
[k in
|
|
11
|
-
} : {
|
|
12
|
-
[k in
|
|
13
|
-
} & {
|
|
14
|
-
[k in Exclude<
|
|
15
|
-
} & {
|
|
16
|
-
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TWithDepth>[];
|
|
17
|
-
})) & (TWithDepth extends true ? {
|
|
18
|
-
depth: number;
|
|
3
|
+
}) & (TParentKey extends false ? {
|
|
4
|
+
[k in Exclude<TParentKey, false>]: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TDepthKey>;
|
|
5
|
+
} : {}) & {
|
|
6
|
+
[k in TChildrenKey]?: TreeNode<TValue, TValueKey, TParentKey, TChildrenKey, TDepthKey>[];
|
|
7
|
+
} & (TDepthKey extends false ? {
|
|
8
|
+
[k in Exclude<TDepthKey, false>]: number;
|
|
19
9
|
} : {});
|
|
20
|
-
type AccessorReturnType<
|
|
21
|
-
type
|
|
22
|
-
[K in keyof T]: T[K] extends Iterable<unknown> & object ? K : never;
|
|
10
|
+
type AccessorReturnType<O, P extends (keyof O) | ((item: O) => any)> = P extends ((item: O) => infer R) ? R : P extends (keyof O) ? O[P] : never;
|
|
11
|
+
type ObjectKeysOfIterableProperties<T> = {
|
|
12
|
+
[K in keyof T]: T[K] extends (Iterable<unknown> & object) | null | undefined ? K : never;
|
|
23
13
|
}[keyof T];
|
|
24
|
-
export default function buildTree<
|
|
14
|
+
export default function buildTree<TIdAccessor extends (keyof NoInfer<TInputValue>) | ((item: NoInfer<TInputValue>) => unknown), TNodeValueKey extends PropertyKey | false = 'value', TNodeParentKey extends PropertyKey | false = 'parent', TNodeChildrenKey extends PropertyKey = 'children', TNodeDepthKey extends PropertyKey | false = false, TInputValue extends (TNodeValueKey extends false ? object : TIdAccessor extends PropertyKey ? object : unknown) = any, TMappedValue extends (TNodeValueKey extends false ? object : unknown) = TInputValue>(items: Iterable<TInputValue>, options: {
|
|
25
15
|
/**
|
|
26
16
|
* A string key or function used to get the item's unique identifier.
|
|
27
17
|
*/
|
|
28
18
|
id: TIdAccessor;
|
|
29
|
-
/**
|
|
30
|
-
* A string key or function used to get the item's parent identifier.
|
|
31
|
-
*
|
|
32
|
-
* Either `parentId` or `childIds` must be provided.
|
|
33
|
-
*/
|
|
34
|
-
parentId?: (keyof TInputValue) | ((item: TInputValue) => unknown);
|
|
35
|
-
/**
|
|
36
|
-
* A string key or function to retrieve a list of child identifiers from an item.
|
|
37
|
-
*
|
|
38
|
-
* Either `parentId` or `childIds` must be provided.
|
|
39
|
-
*/
|
|
40
|
-
childIds?: IterableKeys<TInputValue> | ((item: TInputValue) => (Iterable<unknown> & object) | null | undefined);
|
|
41
19
|
/**
|
|
42
20
|
* Maps the input item to a different value stored in the resulting tree node.
|
|
43
21
|
*/
|
|
44
22
|
nodeValueMapper?: {
|
|
45
|
-
(item: TInputValue): TMappedValue;
|
|
23
|
+
(item: NoInfer<TInputValue>): TMappedValue;
|
|
46
24
|
};
|
|
47
25
|
/**
|
|
48
|
-
* Object key used to store the
|
|
26
|
+
* Object key used to store the input item in the output tree node.
|
|
27
|
+
*
|
|
28
|
+
* Set to `false` to merge the item into the node itself.
|
|
49
29
|
*
|
|
50
30
|
* Defaults to `'value'`.
|
|
51
31
|
*/
|
|
@@ -53,6 +33,8 @@ export default function buildTree<TInputValue extends (TIdAccessor extends (keyo
|
|
|
53
33
|
/**
|
|
54
34
|
* Object key used to store the reference to the parent node in the output tree node.
|
|
55
35
|
*
|
|
36
|
+
* Set to `false` to omit parent links.
|
|
37
|
+
*
|
|
56
38
|
* Defaults to `'parent'`.
|
|
57
39
|
*/
|
|
58
40
|
nodeParentKey?: TNodeParentKey;
|
|
@@ -63,15 +45,17 @@ export default function buildTree<TInputValue extends (TIdAccessor extends (keyo
|
|
|
63
45
|
*/
|
|
64
46
|
nodeChildrenKey?: TNodeChildrenKey;
|
|
65
47
|
/**
|
|
66
|
-
*
|
|
48
|
+
* Object key used to store a value indicating the node depth in the tree on each output node.
|
|
67
49
|
* Root nodes have a depth of 0.
|
|
68
50
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
51
|
+
* Set to `false` to omit depth values.
|
|
52
|
+
*
|
|
53
|
+
* Enables `validateTree`, as depth assignment requires a valid tree structure,
|
|
54
|
+
* and both operations also share the same traversal logic.
|
|
71
55
|
*
|
|
72
56
|
* Defaults to `false`.
|
|
73
57
|
*/
|
|
74
|
-
|
|
58
|
+
nodeDepthKey?: TNodeDepthKey;
|
|
75
59
|
/**
|
|
76
60
|
* Validates that the final structure forms a tree.
|
|
77
61
|
*
|
|
@@ -97,13 +81,33 @@ export default function buildTree<TInputValue extends (TIdAccessor extends (keyo
|
|
|
97
81
|
*/
|
|
98
82
|
validateReferences?: boolean;
|
|
99
83
|
} & ({
|
|
84
|
+
/**
|
|
85
|
+
* A string key or function used to get the item's parent identifier.
|
|
86
|
+
*
|
|
87
|
+
* Either `parentId` or `childIds` must be provided.
|
|
88
|
+
*/
|
|
100
89
|
parentId?: never;
|
|
101
|
-
|
|
90
|
+
/**
|
|
91
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
92
|
+
*
|
|
93
|
+
* Either `parentId` or `childIds` must be provided.
|
|
94
|
+
*/
|
|
95
|
+
childIds: ObjectKeysOfIterableProperties<NoInfer<TInputValue>> | ((item: NoInfer<TInputValue>) => (Iterable<unknown> & object) | null | undefined);
|
|
102
96
|
} | {
|
|
103
|
-
|
|
97
|
+
/**
|
|
98
|
+
* A string key or function used to get the item's parent identifier.
|
|
99
|
+
*
|
|
100
|
+
* Either `parentId` or `childIds` must be provided.
|
|
101
|
+
*/
|
|
102
|
+
parentId: (keyof NoInfer<TInputValue>) | ((item: NoInfer<TInputValue>) => unknown);
|
|
103
|
+
/**
|
|
104
|
+
* A string key or function to retrieve a list of child identifiers from an item.
|
|
105
|
+
*
|
|
106
|
+
* Either `parentId` or `childIds` must be provided.
|
|
107
|
+
*/
|
|
104
108
|
childIds?: never;
|
|
105
109
|
})): {
|
|
106
|
-
roots:
|
|
107
|
-
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>,
|
|
110
|
+
roots: TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TNodeDepthKey>[];
|
|
111
|
+
nodes: Map<AccessorReturnType<TInputValue, TIdAccessor>, TreeNode<TMappedValue extends undefined ? TInputValue : TMappedValue, TNodeValueKey, TNodeParentKey, TNodeChildrenKey, TNodeDepthKey>>;
|
|
108
112
|
};
|
|
109
113
|
export {};
|
package/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function buildTree(items, options) {
|
|
2
2
|
if (!options) {
|
|
3
|
-
throw new Error(
|
|
3
|
+
throw new Error(`Missing required 'options' parameter.`);
|
|
4
4
|
}
|
|
5
5
|
const {
|
|
6
6
|
id: idAccessor,
|
|
@@ -10,18 +10,18 @@ function buildTree(items, options) {
|
|
|
10
10
|
nodeValueKey = "value",
|
|
11
11
|
nodeParentKey = "parent",
|
|
12
12
|
nodeChildrenKey = "children",
|
|
13
|
-
|
|
13
|
+
nodeDepthKey = false,
|
|
14
14
|
validateTree = false,
|
|
15
15
|
validateReferences = false
|
|
16
16
|
} = options;
|
|
17
17
|
if (!idAccessor) {
|
|
18
|
-
throw new Error(
|
|
18
|
+
throw new Error(`Option 'id' is required.`);
|
|
19
19
|
}
|
|
20
20
|
if (!parentIdAccessor && !childIdsAccessor) {
|
|
21
|
-
throw new Error(
|
|
21
|
+
throw new Error(`Either 'parentId' or 'childIds' must be provided.`);
|
|
22
22
|
}
|
|
23
23
|
if (parentIdAccessor && childIdsAccessor) {
|
|
24
|
-
throw new Error('
|
|
24
|
+
throw new Error(`'parentId' and 'childIds' cannot be used together.`);
|
|
25
25
|
}
|
|
26
26
|
const roots = [];
|
|
27
27
|
const nodes = /* @__PURE__ */ new Map();
|
|
@@ -30,9 +30,15 @@ function buildTree(items, options) {
|
|
|
30
30
|
for (const item of items) {
|
|
31
31
|
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
32
32
|
if (nodes.has(id)) {
|
|
33
|
-
throw new Error(`Duplicate identifier
|
|
33
|
+
throw new Error(`Duplicate identifier '${id}'.`);
|
|
34
34
|
}
|
|
35
35
|
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
36
|
+
if (nodeValueKey === false) {
|
|
37
|
+
if (nodeParentKey !== false) {
|
|
38
|
+
delete node[nodeParentKey];
|
|
39
|
+
}
|
|
40
|
+
delete node[nodeChildrenKey];
|
|
41
|
+
}
|
|
36
42
|
nodes.set(id, node);
|
|
37
43
|
const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor];
|
|
38
44
|
const parentNode = nodes.get(parentId);
|
|
@@ -63,7 +69,7 @@ function buildTree(items, options) {
|
|
|
63
69
|
}
|
|
64
70
|
for (const [parentId, nodes2] of waitingForParent.entries()) {
|
|
65
71
|
if (validateReferences && parentId != null) {
|
|
66
|
-
throw new Error(`Referential integrity violation: parentId
|
|
72
|
+
throw new Error(`Referential integrity violation: parentId '${parentId}' does not match any item in the input.`);
|
|
67
73
|
}
|
|
68
74
|
for (const node of nodes2) {
|
|
69
75
|
roots.push(node);
|
|
@@ -75,14 +81,20 @@ function buildTree(items, options) {
|
|
|
75
81
|
for (const item of items) {
|
|
76
82
|
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
|
|
77
83
|
if (nodes.has(id)) {
|
|
78
|
-
throw new Error(`Duplicate identifier
|
|
84
|
+
throw new Error(`Duplicate identifier '${id}'.`);
|
|
79
85
|
}
|
|
80
86
|
const node = nodeValueKey !== false ? { [nodeValueKey]: nodeValueMapper ? nodeValueMapper(item) : item } : { ...nodeValueMapper ? nodeValueMapper(item) : item };
|
|
87
|
+
if (nodeValueKey === false) {
|
|
88
|
+
if (nodeParentKey !== false) {
|
|
89
|
+
delete node[nodeParentKey];
|
|
90
|
+
}
|
|
91
|
+
delete node[nodeChildrenKey];
|
|
92
|
+
}
|
|
81
93
|
nodes.set(id, node);
|
|
82
94
|
const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor];
|
|
83
95
|
if (childIds != null) {
|
|
84
96
|
if (typeof childIds[Symbol.iterator] !== "function") {
|
|
85
|
-
throw new Error(`Item
|
|
97
|
+
throw new Error(`Item '${id}' has invalid children: expected an iterable value.`);
|
|
86
98
|
}
|
|
87
99
|
node[nodeChildrenKey] = [];
|
|
88
100
|
for (const childId of childIds) {
|
|
@@ -91,12 +103,15 @@ function buildTree(items, options) {
|
|
|
91
103
|
node[nodeChildrenKey].push(childNode);
|
|
92
104
|
if (nodeParentKey !== false) {
|
|
93
105
|
if (childNode[nodeParentKey] && childNode[nodeParentKey] !== node) {
|
|
94
|
-
throw new Error(`Node
|
|
106
|
+
throw new Error(`Node '${childId}' already has a different parent, refusing to overwrite.`);
|
|
95
107
|
}
|
|
96
108
|
childNode[nodeParentKey] = node;
|
|
97
109
|
}
|
|
98
110
|
rootCandidates.delete(childId);
|
|
99
111
|
} else {
|
|
112
|
+
if (waitingChildren.has(childId)) {
|
|
113
|
+
throw new Error(`Multiple parents reference the same unresolved child '${childId}'.`);
|
|
114
|
+
}
|
|
100
115
|
waitingChildren.set(childId, {
|
|
101
116
|
parentNode: node,
|
|
102
117
|
childIndex: node[nodeChildrenKey].length
|
|
@@ -114,7 +129,7 @@ function buildTree(items, options) {
|
|
|
114
129
|
parentNode[nodeChildrenKey][childIndex] = node;
|
|
115
130
|
if (nodeParentKey !== false) {
|
|
116
131
|
if (node[nodeParentKey] && node[nodeParentKey] !== parentNode) {
|
|
117
|
-
throw new Error(`Node
|
|
132
|
+
throw new Error(`Node '${id}' already has a different parent, refusing to overwrite.`);
|
|
118
133
|
}
|
|
119
134
|
node[nodeParentKey] = parentNode;
|
|
120
135
|
}
|
|
@@ -126,7 +141,7 @@ function buildTree(items, options) {
|
|
|
126
141
|
if (waitingChildren.size > 0) {
|
|
127
142
|
if (validateReferences) {
|
|
128
143
|
const childId = waitingChildren.keys().next().value;
|
|
129
|
-
throw new Error(`Referential integrity violation: child reference
|
|
144
|
+
throw new Error(`Referential integrity violation: child reference '${childId}' does not match any item in the input.`);
|
|
130
145
|
}
|
|
131
146
|
const pending = Array.from(waitingChildren.values());
|
|
132
147
|
for (let i = pending.length - 1; i >= 0; i--) {
|
|
@@ -141,6 +156,7 @@ function buildTree(items, options) {
|
|
|
141
156
|
roots.push(node);
|
|
142
157
|
}
|
|
143
158
|
}
|
|
159
|
+
const withDepth = typeof nodeDepthKey === "string" || typeof nodeDepthKey === "symbol" || typeof nodeDepthKey === "number";
|
|
144
160
|
if (validateTree || withDepth) {
|
|
145
161
|
if (roots.length === 0 && nodes.size > 0) {
|
|
146
162
|
throw new Error("Tree validation failed: detected a cycle.");
|
|
@@ -156,7 +172,7 @@ function buildTree(items, options) {
|
|
|
156
172
|
}
|
|
157
173
|
visited.add(node);
|
|
158
174
|
if (withDepth) {
|
|
159
|
-
node
|
|
175
|
+
node[nodeDepthKey] = depth;
|
|
160
176
|
}
|
|
161
177
|
if (node[nodeChildrenKey]) {
|
|
162
178
|
for (const child of node[nodeChildrenKey]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fast-tree-builder",
|
|
3
|
-
"version": "2.0.0-
|
|
3
|
+
"version": "2.0.0-beta.1",
|
|
4
4
|
"description": "Easily construct highly customizable bi-directional tree structures from iterable data.",
|
|
5
5
|
"types": "./index.d.mts",
|
|
6
6
|
"module": "./index.mjs",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"build": "node scripts/build.mjs",
|
|
23
23
|
"watch": "node scripts/watch.mjs",
|
|
24
24
|
"test": "mocha",
|
|
25
|
-
"coverage": "c8 -r text -r text-summary -r lcov --include \"*.
|
|
25
|
+
"coverage": "c8 -r text -r text-summary -r lcov --include \"*.mjs\" npm test"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"c8": "^10.1.3",
|