@weborigami/async-tree 0.2.11 → 0.3.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.
- package/package.json +2 -2
- package/shared.js +5 -1
- package/src/operations/addNextPrevious.js +56 -0
- package/src/operations/cache.js +2 -6
- package/src/operations/cachedKeyFunctions.js +24 -10
- package/src/operations/concat.js +3 -10
- package/src/operations/concatTrees.js +25 -0
- package/src/operations/deepReverse.js +2 -7
- package/src/operations/deepTake.js +3 -6
- package/src/operations/deepValuesIterator.js +3 -6
- package/src/operations/{keyFunctionsForExtensions.js → extensionKeyFunctions.js} +5 -5
- package/src/operations/filter.js +27 -39
- package/src/operations/globKeys.js +5 -1
- package/src/operations/group.js +2 -6
- package/src/operations/map.js +49 -38
- package/src/operations/mask.js +55 -0
- package/src/operations/paginate.js +56 -0
- package/src/operations/regExpKeys.js +3 -8
- package/src/operations/reverse.js +3 -6
- package/src/operations/scope.js +2 -6
- package/src/operations/sort.js +3 -6
- package/src/operations/take.js +2 -6
- package/src/utilities.d.ts +1 -0
- package/src/utilities.js +24 -0
- package/test/drivers/constantTree.test.js +1 -1
- package/test/jsonKeys.test.js +1 -1
- package/test/operations/addNextPrevious.test.js +55 -0
- package/test/operations/concatTrees.test.js +12 -0
- package/test/operations/{keyFunctionsForExtensions.test.js → extensionKeyFunctions.test.js} +6 -19
- package/test/operations/filter.test.js +20 -7
- package/test/operations/globKeys.test.js +1 -1
- package/test/operations/mask.test.js +32 -0
- package/test/operations/paginate.test.js +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weborigami/async-tree",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Asynchronous tree drivers based on standard JavaScript classes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./main.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"typescript": "5.8.2"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@weborigami/types": "0.
|
|
14
|
+
"@weborigami/types": "0.3.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"test": "node --test --test-reporter=spec",
|
package/shared.js
CHANGED
|
@@ -11,20 +11,24 @@ export { default as SetTree } from "./src/drivers/SetTree.js";
|
|
|
11
11
|
export { default as SiteTree } from "./src/drivers/SiteTree.js";
|
|
12
12
|
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
|
|
13
13
|
export * as jsonKeys from "./src/jsonKeys.js";
|
|
14
|
+
export { default as addNextPrevious } from "./src/operations/addNextPrevious.js";
|
|
14
15
|
export { default as cache } from "./src/operations/cache.js";
|
|
15
16
|
export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
|
|
16
17
|
export { default as concat } from "./src/operations/concat.js";
|
|
18
|
+
export { default as concatTrees } from "./src/operations/concatTrees.js";
|
|
17
19
|
export { default as deepMerge } from "./src/operations/deepMerge.js";
|
|
18
20
|
export { default as deepReverse } from "./src/operations/deepReverse.js";
|
|
19
21
|
export { default as deepTake } from "./src/operations/deepTake.js";
|
|
20
22
|
export { default as deepValues } from "./src/operations/deepValues.js";
|
|
21
23
|
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
|
|
24
|
+
export { default as extensionKeyFunctions } from "./src/operations/extensionKeyFunctions.js";
|
|
22
25
|
export { default as filter } from "./src/operations/filter.js";
|
|
23
26
|
export { default as group } from "./src/operations/group.js";
|
|
24
27
|
export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
|
|
25
|
-
export { default as keyFunctionsForExtensions } from "./src/operations/keyFunctionsForExtensions.js";
|
|
26
28
|
export { default as map } from "./src/operations/map.js";
|
|
29
|
+
export { default as mask } from "./src/operations/mask.js";
|
|
27
30
|
export { default as merge } from "./src/operations/merge.js";
|
|
31
|
+
export { default as paginate } from "./src/operations/paginate.js";
|
|
28
32
|
export { default as reverse } from "./src/operations/reverse.js";
|
|
29
33
|
export { default as scope } from "./src/operations/scope.js";
|
|
30
34
|
export { default as sort } from "./src/operations/sort.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Add nextKey/previousKey properties to values.
|
|
6
|
+
*
|
|
7
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
+
* @typedef {import("../../index.ts").PlainObject} PlainObject
|
|
9
|
+
*
|
|
10
|
+
* @param {import("../../index.ts").Treelike} treelike
|
|
11
|
+
* @returns {Promise<PlainObject|Array>}
|
|
12
|
+
*/
|
|
13
|
+
export default async function addNextPrevious(treelike) {
|
|
14
|
+
assertIsTreelike(treelike, "addNextPrevious");
|
|
15
|
+
const tree = Tree.from(treelike);
|
|
16
|
+
|
|
17
|
+
const entries = [...(await Tree.entries(tree))];
|
|
18
|
+
const keys = entries.map(([key]) => key);
|
|
19
|
+
|
|
20
|
+
// Map to an array of [key, result] pairs, where the result includes
|
|
21
|
+
// nextKey/previousKey properties.
|
|
22
|
+
const mappedEntries = await Promise.all(
|
|
23
|
+
entries.map(async ([key, value], index) => {
|
|
24
|
+
let resultValue;
|
|
25
|
+
if (value === undefined) {
|
|
26
|
+
resultValue = undefined;
|
|
27
|
+
} else if (Tree.isTreelike(value)) {
|
|
28
|
+
resultValue = await Tree.plain(value);
|
|
29
|
+
} else if (typeof value === "object") {
|
|
30
|
+
// Clone value to avoid modifying the original object
|
|
31
|
+
resultValue = { ...value };
|
|
32
|
+
} else {
|
|
33
|
+
// Take the object as the `value` property
|
|
34
|
+
resultValue = { value };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (resultValue) {
|
|
38
|
+
// Extend result with nextKey/previousKey properties.
|
|
39
|
+
const nextKey = keys[index + 1];
|
|
40
|
+
if (nextKey) {
|
|
41
|
+
resultValue.nextKey = nextKey;
|
|
42
|
+
}
|
|
43
|
+
const previousKey = keys[index - 1];
|
|
44
|
+
if (previousKey) {
|
|
45
|
+
resultValue.previousKey = previousKey;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return [key, resultValue];
|
|
50
|
+
})
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
return treelike instanceof Array
|
|
54
|
+
? mappedEntries.map(([_, value]) => value)
|
|
55
|
+
: Object.fromEntries(mappedEntries);
|
|
56
|
+
}
|
package/src/operations/cache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ObjectTree, Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Caches values from a source tree in a second cache tree. Cache source tree
|
|
@@ -15,12 +16,7 @@ import { ObjectTree, Tree } from "../internal.js";
|
|
|
15
16
|
* @returns {AsyncTree & { description: string }}
|
|
16
17
|
*/
|
|
17
18
|
export default function treeCache(sourceTreelike, cacheTreelike) {
|
|
18
|
-
|
|
19
|
-
const error = new TypeError(`cache: The source tree isn't defined.`);
|
|
20
|
-
/** @type {any} */ (error).position = 0;
|
|
21
|
-
throw error;
|
|
22
|
-
}
|
|
23
|
-
|
|
19
|
+
assertIsTreelike(sourceTreelike, "cache");
|
|
24
20
|
const source = Tree.from(sourceTreelike);
|
|
25
21
|
|
|
26
22
|
/** @type {AsyncMutableTree} */
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as trailingSlash from "../trailingSlash.js";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
// For each (tree, keyFn) combination, we maintain a cache mapping a source key to
|
|
4
|
+
// a result key and vice versa. We have to maintain three levels of Map: tree ->
|
|
5
|
+
// keyFn -> sourceKey -> resultKey. This is the top level map.
|
|
6
|
+
const treeMap = new Map();
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Given a key function, return a new key function and inverse key function that
|
|
@@ -19,7 +22,7 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
|
|
|
19
22
|
return {
|
|
20
23
|
async inverseKey(resultKey, tree) {
|
|
21
24
|
const { resultKeyToSourceKey, sourceKeyToResultKey } =
|
|
22
|
-
|
|
25
|
+
getKeyMapsForTreeKeyFn(tree, keyFn);
|
|
23
26
|
|
|
24
27
|
const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey);
|
|
25
28
|
if (cachedSourceKey !== undefined) {
|
|
@@ -57,7 +60,7 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
|
|
|
57
60
|
},
|
|
58
61
|
|
|
59
62
|
async key(sourceKey, tree) {
|
|
60
|
-
const { sourceKeyToResultKey } =
|
|
63
|
+
const { sourceKeyToResultKey } = getKeyMapsForTreeKeyFn(tree, keyFn);
|
|
61
64
|
|
|
62
65
|
const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey);
|
|
63
66
|
if (cachedResultKey !== undefined) {
|
|
@@ -76,8 +79,10 @@ export default function cachedKeyFunctions(keyFn, deep = false) {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
async function computeAndCacheResultKey(tree, keyFn, deep, sourceKey) {
|
|
79
|
-
const { resultKeyToSourceKey, sourceKeyToResultKey } =
|
|
80
|
-
|
|
82
|
+
const { resultKeyToSourceKey, sourceKeyToResultKey } = getKeyMapsForTreeKeyFn(
|
|
83
|
+
tree,
|
|
84
|
+
keyFn
|
|
85
|
+
);
|
|
81
86
|
|
|
82
87
|
const resultKey =
|
|
83
88
|
deep && trailingSlash.has(sourceKey)
|
|
@@ -90,17 +95,26 @@ async function computeAndCacheResultKey(tree, keyFn, deep, sourceKey) {
|
|
|
90
95
|
return resultKey;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
// Maintain key->inverseKey and inverseKey->key mappings for each tree
|
|
94
|
-
// store subtree keys in either direction with a trailing slash.
|
|
95
|
-
function
|
|
96
|
-
|
|
98
|
+
// Maintain key->inverseKey and inverseKey->key mappings for each (tree, keyFn)
|
|
99
|
+
// pair. These store subtree keys in either direction with a trailing slash.
|
|
100
|
+
function getKeyMapsForTreeKeyFn(tree, keyFn) {
|
|
101
|
+
// Check if we already have a cache for this tree
|
|
102
|
+
let keyFnMap = treeMap.get(tree);
|
|
103
|
+
if (!keyFnMap) {
|
|
104
|
+
keyFnMap = new Map();
|
|
105
|
+
treeMap.set(tree, keyFnMap);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if we have a cache for this keyFn
|
|
109
|
+
let keyMaps = keyFnMap.get(keyFn);
|
|
97
110
|
if (!keyMaps) {
|
|
98
111
|
keyMaps = {
|
|
99
112
|
resultKeyToSourceKey: new Map(),
|
|
100
113
|
sourceKeyToResultKey: new Map(),
|
|
101
114
|
};
|
|
102
|
-
|
|
115
|
+
keyFnMap.set(keyFn, keyMaps);
|
|
103
116
|
}
|
|
117
|
+
|
|
104
118
|
return keyMaps;
|
|
105
119
|
}
|
|
106
120
|
|
package/src/operations/concat.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import { toString } from "../utilities.js";
|
|
1
|
+
import { assertIsTreelike, toString } from "../utilities.js";
|
|
2
2
|
import deepValuesIterator from "./deepValuesIterator.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Concatenate the deep text values in a tree.
|
|
6
6
|
*
|
|
7
|
-
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
8
|
-
*
|
|
9
|
-
* @this {AsyncTree|null}
|
|
10
7
|
* @param {import("../../index.ts").Treelike} treelike
|
|
11
8
|
*/
|
|
12
|
-
export default async function
|
|
13
|
-
|
|
14
|
-
const error = new TypeError(`concat: The tree isn't defined.`);
|
|
15
|
-
/** @type {any} */ (error).position = 0;
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
9
|
+
export default async function concat(treelike) {
|
|
10
|
+
assertIsTreelike(treelike, "concat");
|
|
18
11
|
|
|
19
12
|
const strings = [];
|
|
20
13
|
for await (const value of deepValuesIterator(treelike, { expand: true })) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
import { toString } from "../utilities.js";
|
|
3
|
+
import concat from "./concat.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A tagged template literal function that concatenate the deep text values in a
|
|
7
|
+
* tree. Any treelike values will be concatenated using `concat`.
|
|
8
|
+
*
|
|
9
|
+
* @param {TemplateStringsArray} strings
|
|
10
|
+
* @param {...any} values
|
|
11
|
+
*/
|
|
12
|
+
export default async function concatTrees(strings, ...values) {
|
|
13
|
+
// Convert all the values to strings
|
|
14
|
+
const valueTexts = await Promise.all(
|
|
15
|
+
values.map((value) =>
|
|
16
|
+
Tree.isTreelike(value) ? concat(value) : toString(value)
|
|
17
|
+
)
|
|
18
|
+
);
|
|
19
|
+
// Splice all the strings together
|
|
20
|
+
let result = strings[0];
|
|
21
|
+
for (let i = 0; i < valueTexts.length; i++) {
|
|
22
|
+
result += valueTexts[i] + strings[i + 1];
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Reverse the order of keys at all levels of the tree.
|
|
@@ -10,13 +11,7 @@ import { Tree } from "../internal.js";
|
|
|
10
11
|
* @returns {AsyncTree}
|
|
11
12
|
*/
|
|
12
13
|
export default function deepReverse(treelike) {
|
|
13
|
-
|
|
14
|
-
const error = new TypeError(
|
|
15
|
-
`deepReverse: The tree to reverse isn't defined.`
|
|
16
|
-
);
|
|
17
|
-
/** @type {any} */ (error).position = 0;
|
|
18
|
-
throw error;
|
|
19
|
-
}
|
|
14
|
+
assertIsTreelike(treelike, "deepReverse");
|
|
20
15
|
|
|
21
16
|
const tree = Tree.from(treelike, { deep: true });
|
|
22
17
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Returns a function that traverses a tree deeply and returns the values of the
|
|
@@ -11,13 +12,9 @@ import { Tree } from "../internal.js";
|
|
|
11
12
|
* @param {number} count
|
|
12
13
|
*/
|
|
13
14
|
export default async function deepTake(treelike, count) {
|
|
14
|
-
|
|
15
|
-
const error = new TypeError(`deepTake: The tree isn't defined.`);
|
|
16
|
-
/** @type {any} */ (error).position = 0;
|
|
17
|
-
throw error;
|
|
18
|
-
}
|
|
19
|
-
|
|
15
|
+
assertIsTreelike(treelike, "deepTake");
|
|
20
16
|
const tree = await Tree.from(treelike, { deep: true });
|
|
17
|
+
|
|
21
18
|
const { values } = await traverse(tree, count);
|
|
22
19
|
return Tree.from(values, { deep: true });
|
|
23
20
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return an iterator that yields all values in a tree, including nested trees.
|
|
@@ -14,13 +15,9 @@ export default async function* deepValuesIterator(
|
|
|
14
15
|
treelike,
|
|
15
16
|
options = { expand: false }
|
|
16
17
|
) {
|
|
17
|
-
|
|
18
|
-
const error = new TypeError(`deepValues: The tree isn't defined.`);
|
|
19
|
-
/** @type {any} */ (error).position = 0;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
|
|
18
|
+
assertIsTreelike(treelike, "deepValuesIterator");
|
|
23
19
|
const tree = Tree.from(treelike, { deep: true });
|
|
20
|
+
|
|
24
21
|
for (const key of await tree.keys()) {
|
|
25
22
|
let value = await tree.get(key);
|
|
26
23
|
|
|
@@ -8,13 +8,13 @@ import * as trailingSlash from "../trailingSlash.js";
|
|
|
8
8
|
* The resulting `inverseKey` and `key` functions are compatible with those
|
|
9
9
|
* expected by map and other transforms.
|
|
10
10
|
*
|
|
11
|
-
* @
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {string} sourceExtension
|
|
12
|
+
* @param {string} [resultExtension]
|
|
13
13
|
*/
|
|
14
|
-
export default function
|
|
15
|
-
resultExtension,
|
|
14
|
+
export default function extensionKeyFunctions(
|
|
16
15
|
sourceExtension,
|
|
17
|
-
|
|
16
|
+
resultExtension
|
|
17
|
+
) {
|
|
18
18
|
if (resultExtension === undefined) {
|
|
19
19
|
resultExtension = sourceExtension;
|
|
20
20
|
}
|
package/src/operations/filter.js
CHANGED
|
@@ -1,51 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
|
+
import map from "./map.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Given
|
|
5
|
-
*
|
|
6
|
-
* deep: if a value from `a` is a subtree, it will be filtered recursively.
|
|
5
|
+
* Given a tree an a test function, return a new tree whose keys correspond to
|
|
6
|
+
* the values that pass the test function.
|
|
7
7
|
*
|
|
8
8
|
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
9
9
|
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
10
10
|
*
|
|
11
|
-
* @param {Treelike}
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {Treelike} treelike
|
|
12
|
+
* @param {function|any} options
|
|
13
13
|
* @returns {AsyncTree}
|
|
14
14
|
*/
|
|
15
|
-
export default function filter(
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
export default function filter(treelike, options) {
|
|
16
|
+
assertIsTreelike(treelike, "map");
|
|
17
|
+
let testFn;
|
|
18
|
+
let deep;
|
|
19
|
+
if (typeof options === "function") {
|
|
20
|
+
testFn = options;
|
|
21
|
+
deep = false;
|
|
22
|
+
} else {
|
|
23
|
+
testFn = options.test;
|
|
24
|
+
deep = options.deep ?? false;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
|
-
return {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
let aValue = await a.get(key);
|
|
27
|
-
if (Tree.isTreelike(aValue)) {
|
|
28
|
-
// Filter the subtree
|
|
29
|
-
return filter(aValue, bValue);
|
|
30
|
-
} else {
|
|
31
|
-
return aValue;
|
|
32
|
-
}
|
|
33
|
-
},
|
|
27
|
+
return map(treelike, {
|
|
28
|
+
deep,
|
|
29
|
+
|
|
30
|
+
// Assume source key is the same as result key
|
|
31
|
+
inverseKey: async (resultKey) => resultKey,
|
|
34
32
|
|
|
35
|
-
async
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
// An async tree value in b implies that the a key should have a slash
|
|
40
|
-
const aKeySlashes = aKeys.map((key, index) =>
|
|
41
|
-
trailingSlash.toggle(
|
|
42
|
-
key,
|
|
43
|
-
trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
|
|
44
|
-
)
|
|
45
|
-
);
|
|
46
|
-
// Remove keys that don't have values in b
|
|
47
|
-
const keys = aKeySlashes.filter((key, index) => bValues[index] ?? false);
|
|
48
|
-
return keys;
|
|
33
|
+
key: async (sourceKey, tree) => {
|
|
34
|
+
const value = await tree.get(sourceKey);
|
|
35
|
+
const passes = await testFn(value, sourceKey, tree);
|
|
36
|
+
return passes ? sourceKey : undefined;
|
|
49
37
|
},
|
|
50
|
-
};
|
|
38
|
+
});
|
|
51
39
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { ObjectTree, Tree
|
|
1
|
+
import { ObjectTree, Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
4
|
+
import merge from "./merge.js";
|
|
2
5
|
|
|
3
6
|
const globstar = "**";
|
|
4
7
|
const globstarSlash = `${globstar}/`;
|
|
5
8
|
|
|
6
9
|
export default function globKeys(treelike) {
|
|
10
|
+
assertIsTreelike(treelike, "globKeys");
|
|
7
11
|
const globs = Tree.from(treelike, { deep: true });
|
|
8
12
|
return {
|
|
9
13
|
async get(key) {
|
package/src/operations/group.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ObjectTree, Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Given a function that returns a grouping key for a value, returns a transform
|
|
@@ -8,12 +9,7 @@ import { ObjectTree, Tree } from "../internal.js";
|
|
|
8
9
|
* @param {import("../../index.ts").ValueKeyFn} groupKeyFn
|
|
9
10
|
*/
|
|
10
11
|
export default async function group(treelike, groupKeyFn) {
|
|
11
|
-
|
|
12
|
-
const error = new TypeError(`groupBy: The tree to group isn't defined.`);
|
|
13
|
-
/** @type {any} */ (error).position = 0;
|
|
14
|
-
throw error;
|
|
15
|
-
}
|
|
16
|
-
|
|
12
|
+
assertIsTreelike(treelike, "group");
|
|
17
13
|
const tree = Tree.from(treelike);
|
|
18
14
|
|
|
19
15
|
const keys = Array.from(await tree.keys());
|
package/src/operations/map.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Transform the keys and/or values of a tree.
|
|
@@ -12,44 +13,9 @@ import * as trailingSlash from "../trailingSlash.js";
|
|
|
12
13
|
* @param {MapOptions|ValueKeyFn} options
|
|
13
14
|
*/
|
|
14
15
|
export default function map(treelike, options = {}) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let keyFn;
|
|
19
|
-
let needsSourceValue;
|
|
20
|
-
let valueFn;
|
|
21
|
-
|
|
22
|
-
if (!treelike) {
|
|
23
|
-
const error = new TypeError(`map: The tree to map isn't defined.`);
|
|
24
|
-
/** @type {any} */ (error).position = 0;
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (typeof options === "function") {
|
|
29
|
-
// Take the single function argument as the valueFn
|
|
30
|
-
valueFn = options;
|
|
31
|
-
} else {
|
|
32
|
-
deep = options.deep;
|
|
33
|
-
description = options.description;
|
|
34
|
-
inverseKeyFn = options.inverseKey;
|
|
35
|
-
keyFn = options.key;
|
|
36
|
-
needsSourceValue = options.needsSourceValue;
|
|
37
|
-
valueFn = options.value;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
deep ??= false;
|
|
41
|
-
description ??= "key/value map";
|
|
42
|
-
// @ts-ignore
|
|
43
|
-
inverseKeyFn ??= valueFn?.inverseKey;
|
|
44
|
-
// @ts-ignore
|
|
45
|
-
keyFn ??= valueFn?.key;
|
|
46
|
-
needsSourceValue ??= true;
|
|
47
|
-
|
|
48
|
-
if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
|
|
49
|
-
throw new TypeError(
|
|
50
|
-
`map: You must specify both key and inverseKey functions, or neither.`
|
|
51
|
-
);
|
|
52
|
-
}
|
|
16
|
+
assertIsTreelike(treelike, "map");
|
|
17
|
+
const { deep, description, inverseKeyFn, keyFn, needsSourceValue, valueFn } =
|
|
18
|
+
validateOptions(options);
|
|
53
19
|
|
|
54
20
|
/**
|
|
55
21
|
* @param {import("@weborigami/types").AsyncTree} tree
|
|
@@ -144,3 +110,48 @@ export default function map(treelike, options = {}) {
|
|
|
144
110
|
const tree = Tree.from(treelike, { deep });
|
|
145
111
|
return mapFn(tree);
|
|
146
112
|
}
|
|
113
|
+
|
|
114
|
+
// Extract and validate options
|
|
115
|
+
function validateOptions(options) {
|
|
116
|
+
let deep;
|
|
117
|
+
let description;
|
|
118
|
+
let inverseKeyFn;
|
|
119
|
+
let keyFn;
|
|
120
|
+
let needsSourceValue;
|
|
121
|
+
let valueFn;
|
|
122
|
+
|
|
123
|
+
if (typeof options === "function") {
|
|
124
|
+
// Take the single function argument as the valueFn
|
|
125
|
+
valueFn = options;
|
|
126
|
+
} else {
|
|
127
|
+
deep = options.deep;
|
|
128
|
+
description = options.description;
|
|
129
|
+
inverseKeyFn = options.inverseKey;
|
|
130
|
+
keyFn = options.key;
|
|
131
|
+
needsSourceValue = options.needsSourceValue;
|
|
132
|
+
valueFn = options.value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
deep ??= false;
|
|
136
|
+
description ??= "key/value map";
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
inverseKeyFn ??= valueFn?.inverseKey;
|
|
139
|
+
// @ts-ignore
|
|
140
|
+
keyFn ??= valueFn?.key;
|
|
141
|
+
needsSourceValue ??= true;
|
|
142
|
+
|
|
143
|
+
if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
|
|
144
|
+
throw new TypeError(
|
|
145
|
+
`map: You must specify both key and inverseKey functions, or neither.`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
deep,
|
|
151
|
+
description,
|
|
152
|
+
inverseKeyFn,
|
|
153
|
+
keyFn,
|
|
154
|
+
needsSourceValue,
|
|
155
|
+
valueFn,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Given trees `a` and `b`, return a masked version of `a` where only the keys
|
|
7
|
+
* that exist in `b` and have truthy values are kept. The filter operation is
|
|
8
|
+
* deep: if a value from `a` is a subtree, it will be filtered recursively.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
11
|
+
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
12
|
+
*
|
|
13
|
+
* @param {Treelike} a
|
|
14
|
+
* @param {Treelike} b
|
|
15
|
+
* @returns {AsyncTree}
|
|
16
|
+
*/
|
|
17
|
+
export default function mask(a, b) {
|
|
18
|
+
assertIsTreelike(a, "filter", 0);
|
|
19
|
+
assertIsTreelike(b, "filter", 1);
|
|
20
|
+
a = Tree.from(a);
|
|
21
|
+
b = Tree.from(b, { deep: true });
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
async get(key) {
|
|
25
|
+
// The key must exist in b and return a truthy value
|
|
26
|
+
const bValue = await b.get(key);
|
|
27
|
+
if (!bValue) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
let aValue = await a.get(key);
|
|
31
|
+
if (Tree.isTreelike(aValue)) {
|
|
32
|
+
// Filter the subtree
|
|
33
|
+
return mask(aValue, bValue);
|
|
34
|
+
} else {
|
|
35
|
+
return aValue;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async keys() {
|
|
40
|
+
// Use a's keys as the basis
|
|
41
|
+
const aKeys = [...(await a.keys())];
|
|
42
|
+
const bValues = await Promise.all(aKeys.map((key) => b.get(key)));
|
|
43
|
+
// An async tree value in b implies that the a key should have a slash
|
|
44
|
+
const aKeySlashes = aKeys.map((key, index) =>
|
|
45
|
+
trailingSlash.toggle(
|
|
46
|
+
key,
|
|
47
|
+
trailingSlash.has(key) || Tree.isAsyncTree(bValues[index])
|
|
48
|
+
)
|
|
49
|
+
);
|
|
50
|
+
// Remove keys that don't have values in b
|
|
51
|
+
const keys = aKeySlashes.filter((key, index) => bValues[index] ?? false);
|
|
52
|
+
return keys;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return a new grouping of the treelike's values into chunks of the specified
|
|
6
|
+
* size.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
|
|
9
|
+
* @typedef {import("../../index.ts").Treelike} Treelike
|
|
10
|
+
*
|
|
11
|
+
* @param {Treelike} [treelike]
|
|
12
|
+
* @param {number} [size=10]
|
|
13
|
+
*/
|
|
14
|
+
export default async function paginate(treelike, size = 10) {
|
|
15
|
+
assertIsTreelike(treelike, "paginate");
|
|
16
|
+
const tree = Tree.from(treelike);
|
|
17
|
+
|
|
18
|
+
const keys = Array.from(await tree.keys());
|
|
19
|
+
const pageCount = Math.ceil(keys.length / size);
|
|
20
|
+
|
|
21
|
+
const paginated = {
|
|
22
|
+
async get(pageKey) {
|
|
23
|
+
// Note: page numbers are 1-based.
|
|
24
|
+
const pageNumber = Number(pageKey);
|
|
25
|
+
if (Number.isNaN(pageNumber)) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const nextPage = pageNumber + 1 <= pageCount ? pageNumber + 1 : null;
|
|
29
|
+
const previousPage = pageNumber - 1 >= 1 ? pageNumber - 1 : null;
|
|
30
|
+
const items = {};
|
|
31
|
+
for (
|
|
32
|
+
let index = (pageNumber - 1) * size;
|
|
33
|
+
index < Math.min(keys.length, pageNumber * size);
|
|
34
|
+
index++
|
|
35
|
+
) {
|
|
36
|
+
const key = keys[index];
|
|
37
|
+
items[key] = await tree.get(keys[index]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
items,
|
|
42
|
+
nextPage,
|
|
43
|
+
pageCount,
|
|
44
|
+
pageNumber,
|
|
45
|
+
previousPage,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async keys() {
|
|
50
|
+
// Return an array from 1..totalPages
|
|
51
|
+
return Array.from({ length: pageCount }, (_, index) => index + 1);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return paginated;
|
|
56
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
2
|
import * as trailingSlash from "../trailingSlash.js";
|
|
3
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* A tree whose keys are strings interpreted as regular expressions.
|
|
@@ -12,15 +13,9 @@ import * as trailingSlash from "../trailingSlash.js";
|
|
|
12
13
|
* @type {import("../../index.ts").TreeTransform}
|
|
13
14
|
*/
|
|
14
15
|
export default async function regExpKeys(treelike) {
|
|
15
|
-
|
|
16
|
-
const error = new TypeError(
|
|
17
|
-
`regExpKeys: The tree of regular expressions isn't defined.`
|
|
18
|
-
);
|
|
19
|
-
/** @type {any} */ (error).position = 0;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
|
|
16
|
+
assertIsTreelike(treelike, "regExpKeys");
|
|
23
17
|
const tree = Tree.from(treelike);
|
|
18
|
+
|
|
24
19
|
const map = new Map();
|
|
25
20
|
|
|
26
21
|
// We build the output tree first so that we can refer to it when setting
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Reverse the order of the top-level keys in the tree.
|
|
@@ -10,13 +11,9 @@ import { Tree } from "../internal.js";
|
|
|
10
11
|
* @returns {AsyncTree}
|
|
11
12
|
*/
|
|
12
13
|
export default function reverse(treelike) {
|
|
13
|
-
|
|
14
|
-
const error = new TypeError(`reverse: The tree to reverse isn't defined.`);
|
|
15
|
-
/** @type {any} */ (error).position = 0;
|
|
16
|
-
throw error;
|
|
17
|
-
}
|
|
18
|
-
|
|
14
|
+
assertIsTreelike(treelike, "reverse");
|
|
19
15
|
const tree = Tree.from(treelike);
|
|
16
|
+
|
|
20
17
|
return {
|
|
21
18
|
async get(key) {
|
|
22
19
|
return tree.get(key);
|
package/src/operations/scope.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* A tree's "scope" is the collection of everything in that tree and all of its
|
|
@@ -11,12 +12,7 @@ import { Tree } from "../internal.js";
|
|
|
11
12
|
* @returns {AsyncTree & {trees: AsyncTree[]}}
|
|
12
13
|
*/
|
|
13
14
|
export default function scope(treelike) {
|
|
14
|
-
|
|
15
|
-
const error = new TypeError(`scope: The tree isn't defined.`);
|
|
16
|
-
/** @type {any} */ (error).position = 0;
|
|
17
|
-
throw error;
|
|
18
|
-
}
|
|
19
|
-
|
|
15
|
+
assertIsTreelike(treelike, "scope");
|
|
20
16
|
const tree = Tree.from(treelike);
|
|
21
17
|
|
|
22
18
|
return {
|
package/src/operations/sort.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Return a new tree with the original's keys sorted. A comparison function can
|
|
@@ -14,16 +15,12 @@ import { Tree } from "../internal.js";
|
|
|
14
15
|
* @param {SortOptions} [options]
|
|
15
16
|
*/
|
|
16
17
|
export default function sort(treelike, options) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
/** @type {any} */ (error).position = 0;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
18
|
+
assertIsTreelike(treelike, "sort");
|
|
19
|
+
const tree = Tree.from(treelike);
|
|
22
20
|
|
|
23
21
|
const sortKey = options?.sortKey;
|
|
24
22
|
let compare = options?.compare;
|
|
25
23
|
|
|
26
|
-
const tree = Tree.from(treelike);
|
|
27
24
|
const transformed = Object.create(tree);
|
|
28
25
|
transformed.keys = async () => {
|
|
29
26
|
const keys = Array.from(await tree.keys());
|
package/src/operations/take.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Tree } from "../internal.js";
|
|
2
|
+
import { assertIsTreelike } from "../utilities.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Returns a new tree with the number of keys limited to the indicated count.
|
|
@@ -7,12 +8,7 @@ import { Tree } from "../internal.js";
|
|
|
7
8
|
* @param {number} count
|
|
8
9
|
*/
|
|
9
10
|
export default function take(treelike, count) {
|
|
10
|
-
|
|
11
|
-
const error = new TypeError(`take: The tree to take from isn't defined.`);
|
|
12
|
-
/** @type {any} */ (error).position = 0;
|
|
13
|
-
throw error;
|
|
14
|
-
}
|
|
15
|
-
|
|
11
|
+
assertIsTreelike(treelike, "take");
|
|
16
12
|
const tree = Tree.from(treelike);
|
|
17
13
|
|
|
18
14
|
return {
|
package/src/utilities.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AsyncTree } from "@weborigami/types";
|
|
2
2
|
import { Packed, PlainObject, StringLike } from "../index.ts";
|
|
3
3
|
|
|
4
|
+
export function assertIsTreelike(object: any, operation: string, position?: number): void;
|
|
4
5
|
export function box(value: any): any;
|
|
5
6
|
export function castArrayLike(keys: any[], values: any[]): any;
|
|
6
7
|
export function getRealmObjectPrototype(object: any): any;
|
package/src/utilities.js
CHANGED
|
@@ -7,6 +7,30 @@ const TypedArray = Object.getPrototypeOf(Uint8Array);
|
|
|
7
7
|
|
|
8
8
|
/** @typedef {import("@weborigami/types").AsyncTree} AsyncTree */
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* If the given object isn't treelike, throw an exception.
|
|
12
|
+
*
|
|
13
|
+
* @param {any} object
|
|
14
|
+
* @param {string} operation
|
|
15
|
+
* @param {number} [position]
|
|
16
|
+
*/
|
|
17
|
+
export function assertIsTreelike(object, operation, position = 0) {
|
|
18
|
+
let message;
|
|
19
|
+
if (!object) {
|
|
20
|
+
message = `${operation}: The tree argument wasn't defined.`;
|
|
21
|
+
} else if (object instanceof Promise) {
|
|
22
|
+
// A common mistake
|
|
23
|
+
message = `${operation}: The tree argument was a Promise. Did you mean to use await?`;
|
|
24
|
+
} else if (!Tree.isTreelike) {
|
|
25
|
+
message = `${operation}: The tree argument wasn't a treelike object.`;
|
|
26
|
+
}
|
|
27
|
+
if (message) {
|
|
28
|
+
const error = new TypeError(message);
|
|
29
|
+
/** @type {any} */ (error).position = position;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
10
34
|
/**
|
|
11
35
|
* Return the value as an object. If the value is already an object it will be
|
|
12
36
|
* returned as is. If the value is a primitive, it will be wrapped in an object:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
4
3
|
import constantTree from "../../src/drivers/constantTree.js";
|
|
4
|
+
import { Tree } from "../../src/internal.js";
|
|
5
5
|
|
|
6
6
|
describe("constantTree", () => {
|
|
7
7
|
test("returns a deep tree that returns constant for all keys", async () => {
|
package/test/jsonKeys.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DeepObjectTree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { DeepObjectTree } from "../src/internal.js";
|
|
4
4
|
import * as jsonKeys from "../src/jsonKeys.js";
|
|
5
5
|
|
|
6
6
|
describe("jsonKeys", () => {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import addNextPrevious from "../../src/operations/addNextPrevious.js";
|
|
4
|
+
|
|
5
|
+
describe("addNextPrevious", () => {
|
|
6
|
+
test("adds next/previous properties to values", async () => {
|
|
7
|
+
const tree = {
|
|
8
|
+
alice: {
|
|
9
|
+
name: "Alice",
|
|
10
|
+
},
|
|
11
|
+
bob: {
|
|
12
|
+
name: "Bob",
|
|
13
|
+
},
|
|
14
|
+
carol: {
|
|
15
|
+
name: "Carol",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
const result = await addNextPrevious(tree);
|
|
19
|
+
assert.deepEqual(result, {
|
|
20
|
+
alice: {
|
|
21
|
+
name: "Alice",
|
|
22
|
+
nextKey: "bob",
|
|
23
|
+
},
|
|
24
|
+
bob: {
|
|
25
|
+
name: "Bob",
|
|
26
|
+
nextKey: "carol",
|
|
27
|
+
previousKey: "alice",
|
|
28
|
+
},
|
|
29
|
+
carol: {
|
|
30
|
+
name: "Carol",
|
|
31
|
+
previousKey: "bob",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns a non-object value as a 'value' property", async () => {
|
|
37
|
+
const array = ["Alice", "Bob", "Carol"];
|
|
38
|
+
const result = await addNextPrevious(array);
|
|
39
|
+
assert.deepEqual(result, [
|
|
40
|
+
{
|
|
41
|
+
value: "Alice",
|
|
42
|
+
nextKey: 1,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
value: "Bob",
|
|
46
|
+
nextKey: 2,
|
|
47
|
+
previousKey: 0,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: "Carol",
|
|
51
|
+
previousKey: 1,
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import concatTrees from "../../src/operations/concatTrees.js";
|
|
4
|
+
|
|
5
|
+
describe("concatTrees", () => {
|
|
6
|
+
test("joins strings and values together", async () => {
|
|
7
|
+
const array = [1, 2, 3];
|
|
8
|
+
const object = { person1: "Alice", person2: "Bob" };
|
|
9
|
+
const result = await concatTrees`a ${array} b ${object} c`;
|
|
10
|
+
assert.equal(result, "a 123 b AliceBob c");
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
2
|
import { describe, test } from "node:test";
|
|
3
3
|
import { ObjectTree, Tree } from "../../src/internal.js";
|
|
4
|
-
import
|
|
4
|
+
import extensionKeyFunctions from "../../src/operations/extensionKeyFunctions.js";
|
|
5
5
|
import map from "../../src/operations/map.js";
|
|
6
6
|
|
|
7
7
|
describe("keyMapsForExtensions", () => {
|
|
8
8
|
test("returns key functions that pass a matching key through", async () => {
|
|
9
|
-
const { inverseKey, key } =
|
|
10
|
-
sourceExtension: ".txt",
|
|
11
|
-
});
|
|
9
|
+
const { inverseKey, key } = extensionKeyFunctions(".txt");
|
|
12
10
|
assert.equal(await inverseKey("file.txt"), "file.txt");
|
|
13
11
|
assert.equal(await inverseKey("file.txt/"), "file.txt");
|
|
14
12
|
assert.equal(await key("file.txt"), "file.txt");
|
|
@@ -18,10 +16,7 @@ describe("keyMapsForExtensions", () => {
|
|
|
18
16
|
});
|
|
19
17
|
|
|
20
18
|
test("returns key functions that can map extensions", async () => {
|
|
21
|
-
const { inverseKey, key } =
|
|
22
|
-
resultExtension: ".json",
|
|
23
|
-
sourceExtension: ".md",
|
|
24
|
-
});
|
|
19
|
+
const { inverseKey, key } = extensionKeyFunctions(".md", ".json");
|
|
25
20
|
assert.equal(await inverseKey("file.json"), "file.md");
|
|
26
21
|
assert.equal(await inverseKey("file.json/"), "file.md");
|
|
27
22
|
assert.equal(await key("file.md"), "file.json");
|
|
@@ -31,10 +26,7 @@ describe("keyMapsForExtensions", () => {
|
|
|
31
26
|
});
|
|
32
27
|
|
|
33
28
|
test("key functions can handle a slash as an explicit extension", async () => {
|
|
34
|
-
const { inverseKey, key } =
|
|
35
|
-
resultExtension: ".html",
|
|
36
|
-
sourceExtension: "/",
|
|
37
|
-
});
|
|
29
|
+
const { inverseKey, key } = extensionKeyFunctions("/", ".html");
|
|
38
30
|
assert.equal(await inverseKey("file.html"), "file/");
|
|
39
31
|
assert.equal(await inverseKey("file.html/"), "file/");
|
|
40
32
|
assert.equal(await key("file"), undefined);
|
|
@@ -47,9 +39,7 @@ describe("keyMapsForExtensions", () => {
|
|
|
47
39
|
file2: "won't be mapped",
|
|
48
40
|
"file3.foo": "won't be mapped",
|
|
49
41
|
});
|
|
50
|
-
const { inverseKey, key } =
|
|
51
|
-
sourceExtension: ".txt",
|
|
52
|
-
});
|
|
42
|
+
const { inverseKey, key } = extensionKeyFunctions(".txt");
|
|
53
43
|
const fixture = map(files, {
|
|
54
44
|
inverseKey,
|
|
55
45
|
key,
|
|
@@ -66,10 +56,7 @@ describe("keyMapsForExtensions", () => {
|
|
|
66
56
|
file2: "won't be mapped",
|
|
67
57
|
"file3.foo": "won't be mapped",
|
|
68
58
|
});
|
|
69
|
-
const { inverseKey, key } =
|
|
70
|
-
resultExtension: ".upper",
|
|
71
|
-
sourceExtension: ".txt",
|
|
72
|
-
});
|
|
59
|
+
const { inverseKey, key } = extensionKeyFunctions(".txt", ".upper");
|
|
73
60
|
const fixture = map(files, {
|
|
74
61
|
inverseKey,
|
|
75
62
|
key,
|
|
@@ -4,8 +4,24 @@ import { Tree } from "../../src/internal.js";
|
|
|
4
4
|
import filter from "../../src/operations/filter.js";
|
|
5
5
|
|
|
6
6
|
describe("filter", () => {
|
|
7
|
-
test("
|
|
8
|
-
const result = filter(
|
|
7
|
+
test("returns values that pass a filter function", async () => {
|
|
8
|
+
const result = await filter(
|
|
9
|
+
{
|
|
10
|
+
a: 1,
|
|
11
|
+
b: 2,
|
|
12
|
+
c: 3,
|
|
13
|
+
d: 4,
|
|
14
|
+
},
|
|
15
|
+
(value) => value % 2 === 1 // odd
|
|
16
|
+
);
|
|
17
|
+
assert.deepEqual(await Tree.plain(result), {
|
|
18
|
+
a: 1,
|
|
19
|
+
c: 3,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("returns deep values that pass a filter function", async () => {
|
|
24
|
+
const result = await filter(
|
|
9
25
|
{
|
|
10
26
|
a: 1,
|
|
11
27
|
b: 2,
|
|
@@ -15,13 +31,10 @@ describe("filter", () => {
|
|
|
15
31
|
},
|
|
16
32
|
},
|
|
17
33
|
{
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
d: true,
|
|
21
|
-
},
|
|
34
|
+
deep: true,
|
|
35
|
+
test: (value) => value % 2 === 1, // odd
|
|
22
36
|
}
|
|
23
37
|
);
|
|
24
|
-
assert.deepEqual(await result.keys(), ["a", "c/"]);
|
|
25
38
|
assert.deepEqual(await Tree.plain(result), {
|
|
26
39
|
a: 1,
|
|
27
40
|
c: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Tree } from "@weborigami/async-tree";
|
|
2
1
|
import assert from "node:assert";
|
|
3
2
|
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
4
|
import globKeys from "../../src/operations/globKeys.js";
|
|
5
5
|
|
|
6
6
|
describe("globKeys", () => {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
|
+
import mask from "../../src/operations/mask.js";
|
|
5
|
+
|
|
6
|
+
describe("mask", () => {
|
|
7
|
+
test("removes keys and values whose mask values are falsy", async () => {
|
|
8
|
+
const result = mask(
|
|
9
|
+
{
|
|
10
|
+
a: 1,
|
|
11
|
+
b: 2,
|
|
12
|
+
c: {
|
|
13
|
+
d: 3,
|
|
14
|
+
e: 4,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
a: true,
|
|
19
|
+
c: {
|
|
20
|
+
d: true,
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
assert.deepEqual(await result.keys(), ["a", "c/"]);
|
|
25
|
+
assert.deepEqual(await Tree.plain(result), {
|
|
26
|
+
a: 1,
|
|
27
|
+
c: {
|
|
28
|
+
d: 3,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, test } from "node:test";
|
|
3
|
+
import { Tree } from "../../src/internal.js";
|
|
4
|
+
import paginate from "../../src/operations/paginate.js";
|
|
5
|
+
|
|
6
|
+
describe("paginate", () => {
|
|
7
|
+
test("divides tree keys into fixed-length chunks", async () => {
|
|
8
|
+
const treelike = {
|
|
9
|
+
a: 1,
|
|
10
|
+
b: 2,
|
|
11
|
+
c: 3,
|
|
12
|
+
d: 4,
|
|
13
|
+
e: 5,
|
|
14
|
+
};
|
|
15
|
+
const paginated = await paginate.call(null, treelike, 2);
|
|
16
|
+
assert.deepEqual(await Tree.plain(paginated), {
|
|
17
|
+
1: {
|
|
18
|
+
items: { a: 1, b: 2 },
|
|
19
|
+
nextPage: 2,
|
|
20
|
+
pageCount: 3,
|
|
21
|
+
pageNumber: 1,
|
|
22
|
+
previousPage: null,
|
|
23
|
+
},
|
|
24
|
+
2: {
|
|
25
|
+
items: { c: 3, d: 4 },
|
|
26
|
+
nextPage: 3,
|
|
27
|
+
pageCount: 3,
|
|
28
|
+
pageNumber: 2,
|
|
29
|
+
previousPage: 1,
|
|
30
|
+
},
|
|
31
|
+
3: {
|
|
32
|
+
items: { e: 5 },
|
|
33
|
+
nextPage: null,
|
|
34
|
+
pageCount: 3,
|
|
35
|
+
pageNumber: 3,
|
|
36
|
+
previousPage: 2,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|