@graffy/link 0.19.0 → 0.19.1-alpha.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/index.d.ts +2 -0
- package/index.js +36 -0
- package/linkGraph.js +197 -0
- package/package.json +12 -7
- package/prepQueryLinks.js +176 -0
- package/index.cjs +0 -319
- package/index.mjs +0 -320
- package/types/index.d.ts +0 -2
- /package/{types/linkGraph.d.ts → linkGraph.d.ts} +0 -0
- /package/{types/prepQueryLinks.d.ts → prepQueryLinks.d.ts} +0 -0
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { add, encodePath, finalize, pack, unpack, unwrap, wrap, } from '@graffy/common';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import linkGraph from "./linkGraph.js";
|
|
4
|
+
import prepQueryLinks from "./prepQueryLinks.js";
|
|
5
|
+
const log = debug('graffy:link');
|
|
6
|
+
export default (defs) => (store) => {
|
|
7
|
+
const prefix = encodePath(store.path);
|
|
8
|
+
const defEntries = Object.entries(defs).map(([prop, def]) => ({
|
|
9
|
+
path: prop.split('.'),
|
|
10
|
+
def,
|
|
11
|
+
}));
|
|
12
|
+
store.on('read', async (query, options, next) => {
|
|
13
|
+
const unwrappedQuery = clone(unwrap(query, prefix));
|
|
14
|
+
const usedDefs = prepQueryLinks(unwrappedQuery, defEntries);
|
|
15
|
+
// Shortcut for queries that don't interact with any links
|
|
16
|
+
if (!usedDefs.length)
|
|
17
|
+
return next(query, options);
|
|
18
|
+
const result = await next(wrap(unwrappedQuery, prefix), options);
|
|
19
|
+
const version = result[0].version;
|
|
20
|
+
const unwrappedResult = unwrap(result, prefix);
|
|
21
|
+
// PrepQueryLinks have removed the parts of the query that are
|
|
22
|
+
// provided by links, so the result would not have "finalized"
|
|
23
|
+
// them.
|
|
24
|
+
add(unwrappedQuery, unwrap(query, prefix));
|
|
25
|
+
log('finalizing', prefix, unwrappedQuery);
|
|
26
|
+
const finalizedResult = finalize(unwrappedResult, unwrappedQuery, version);
|
|
27
|
+
log('beforeAddingLinks', prefix, finalizedResult);
|
|
28
|
+
linkGraph(finalizedResult, usedDefs);
|
|
29
|
+
log('afterAddingLinks', prefix, finalizedResult);
|
|
30
|
+
return wrap(finalizedResult, prefix, version);
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
function clone(tree) {
|
|
34
|
+
// TODO: Do better
|
|
35
|
+
return unpack(JSON.parse(JSON.stringify(pack(tree))));
|
|
36
|
+
}
|
package/linkGraph.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { cmp, encodeArgs, encodePath, findFirst, merge, splitArgs, splitRef, unwrap, wrap, } from '@graffy/common';
|
|
2
|
+
export default function linkGraph(rootGraph, defs) {
|
|
3
|
+
const version = rootGraph[0].version;
|
|
4
|
+
/*
|
|
5
|
+
Braids and Strands are different representations of the set of paths
|
|
6
|
+
you obtain by replacing placeholders in the path.
|
|
7
|
+
|
|
8
|
+
This is better explained using an example.
|
|
9
|
+
|
|
10
|
+
Say we have a blogging application. Posts have multiple authors identified by
|
|
11
|
+
authorId, and a single post category. Authors have different "taglines" for each
|
|
12
|
+
category. We want to add a link "tagline" from the post to the relevant author
|
|
13
|
+
tagline.
|
|
14
|
+
|
|
15
|
+
We use the link definition:
|
|
16
|
+
'post.$i.authors.$j.tagline': [
|
|
17
|
+
'user',
|
|
18
|
+
'$$post.$i.authors.$j.id',
|
|
19
|
+
'taglines',
|
|
20
|
+
'$$post.$i.category
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
By traversing the graph, we get all possible values of ($i, $j); say
|
|
24
|
+
they are (p0, 0), (p1, 0), (p1, 1).
|
|
25
|
+
|
|
26
|
+
We can replace the placeholders with these values to get the "braid":
|
|
27
|
+
[
|
|
28
|
+
[{ value: 'user', vars: {} }],
|
|
29
|
+
[
|
|
30
|
+
{ value: u0, vars: {i: p0, j: 0} },
|
|
31
|
+
{ value: u1, vars: {i: p1, j: 0} },
|
|
32
|
+
{ value: u2, vars: {i: p1, j: 1} },
|
|
33
|
+
],
|
|
34
|
+
[{ value: 'taglines', vars: {} }],
|
|
35
|
+
[
|
|
36
|
+
{ value: 'cooking', vars: {i: p0} },
|
|
37
|
+
{ value: 'fitness', vars: {i: p1} },
|
|
38
|
+
]
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
We then "unbraid" the "braid" into an array of "strands":
|
|
42
|
+
|
|
43
|
+
[
|
|
44
|
+
{ value: ['user', u0, 'taglines', 'cooking'], vars: {p0, 0} },
|
|
45
|
+
{ value: ['user', u1, 'taglines', 'fitness'], vars: {p1, 0} },
|
|
46
|
+
{ value: ['user', u2, 'taglines', 'fitness'], vars: {p1, 1} },
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
Note that combinations with incompatible vars such as are removed during
|
|
50
|
+
the unbraid operation (e.g. ['user', u0, 'taglines', 'fitness'])
|
|
51
|
+
*/
|
|
52
|
+
for (const { path, def } of defs) {
|
|
53
|
+
/** @type {{ value: any, vars: Record<string, any> }[][]} */
|
|
54
|
+
const braid = def.map(getChoices);
|
|
55
|
+
const strands = unbraid(braid);
|
|
56
|
+
for (const { value, vars } of strands) {
|
|
57
|
+
const realPath = makeRef(path, vars);
|
|
58
|
+
const realRef = makeRef(value, vars);
|
|
59
|
+
const node = { key: realPath.pop(), path: encodePath(realRef), version };
|
|
60
|
+
const [range] = splitRef(value);
|
|
61
|
+
if (range)
|
|
62
|
+
node.prefix = true;
|
|
63
|
+
let target = rootGraph;
|
|
64
|
+
do {
|
|
65
|
+
const key = realPath.shift();
|
|
66
|
+
const nextTarget = target[findFirst(target, key)];
|
|
67
|
+
if (!nextTarget || cmp(nextTarget.key, key) !== 0 || nextTarget.end) {
|
|
68
|
+
realPath.unshift(key);
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
target = nextTarget.path
|
|
72
|
+
? unwrap(rootGraph, nextTarget.path)
|
|
73
|
+
: nextTarget.children;
|
|
74
|
+
} while (target && realPath.length);
|
|
75
|
+
if (!target)
|
|
76
|
+
return;
|
|
77
|
+
merge(target, realPath.length ? wrap([node], realPath, version) : [node]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return rootGraph;
|
|
81
|
+
function getChoices(key) {
|
|
82
|
+
if (typeof key === 'string' && key[0] === '$' && key[1] === '$') {
|
|
83
|
+
return lookupValues(rootGraph, key.slice(2).split('.'));
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(key)) {
|
|
86
|
+
if (!key.length)
|
|
87
|
+
return [{ value: [], vars: {} }];
|
|
88
|
+
return unbraid(key.map(getChoices));
|
|
89
|
+
}
|
|
90
|
+
if (typeof key === 'object' && key) {
|
|
91
|
+
const [range = {}, filter = {}] = splitArgs(key);
|
|
92
|
+
const entries = Object.entries(filter).flat();
|
|
93
|
+
if (!entries.length)
|
|
94
|
+
return [{ value: {}, vars: {} }];
|
|
95
|
+
const strands = unbraid(entries.map(getChoices));
|
|
96
|
+
return strands.map(({ value, vars }) => ({
|
|
97
|
+
value: {
|
|
98
|
+
...range,
|
|
99
|
+
...Object.fromEntries(value.reduce((acc, item, i) => {
|
|
100
|
+
if (i % 2) {
|
|
101
|
+
acc[acc.length - 1].push(item);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
acc.push([item]);
|
|
105
|
+
}
|
|
106
|
+
return acc;
|
|
107
|
+
}, [])),
|
|
108
|
+
},
|
|
109
|
+
vars,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
return [{ value: key, vars: {} }];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
Takes a lookup expression which may optionally contain named placeholders,
|
|
116
|
+
and returns an array of possible values in the graph that it matches.
|
|
117
|
+
For each such value, it also returns the corresponding placeholder values.
|
|
118
|
+
@param {string[]} path
|
|
119
|
+
@return {{ value: any, vars: Record<string,any> }[]}
|
|
120
|
+
*/
|
|
121
|
+
function lookupValues(graph, path, vars = {}) {
|
|
122
|
+
const [key, ...rest] = path;
|
|
123
|
+
if (key[0] === '$') {
|
|
124
|
+
return graph.flatMap((node) => {
|
|
125
|
+
if (node.end)
|
|
126
|
+
return [];
|
|
127
|
+
const newVars = { ...vars, [key.slice(1)]: node.key };
|
|
128
|
+
return recurse(node, rest, newVars);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const encodedKey = encodeArgs(key).key;
|
|
132
|
+
const node = graph[findFirst(graph, encodedKey)];
|
|
133
|
+
if (!node || cmp(node.key, encodedKey) !== 0 || node.end)
|
|
134
|
+
return [];
|
|
135
|
+
return recurse(node, rest, vars);
|
|
136
|
+
}
|
|
137
|
+
function recurse(node, path, vars) {
|
|
138
|
+
if (!path.length)
|
|
139
|
+
return [{ value: node.value, vars }];
|
|
140
|
+
if (node.children)
|
|
141
|
+
return lookupValues(node.children, path, vars);
|
|
142
|
+
if (node.path) {
|
|
143
|
+
const linked = unwrap(rootGraph, node.path);
|
|
144
|
+
if (Array.isArray(linked))
|
|
145
|
+
return lookupValues(linked, path, vars);
|
|
146
|
+
}
|
|
147
|
+
throw Error(`link.no_children ${JSON.stringify(node)}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function unbraid(braid) {
|
|
151
|
+
if (!braid.length)
|
|
152
|
+
return [];
|
|
153
|
+
const [options, ...rest] = braid;
|
|
154
|
+
if (!rest.length) {
|
|
155
|
+
return options.map((option) => ({
|
|
156
|
+
value: [option.value],
|
|
157
|
+
vars: option.vars,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
const strands = unbraid(rest);
|
|
161
|
+
return options.flatMap((option) => strands
|
|
162
|
+
.filter((strand) => isCompatible(option.vars, strand.vars))
|
|
163
|
+
.map((strand) => ({
|
|
164
|
+
value: [option.value, ...strand.value],
|
|
165
|
+
vars: { ...option.vars, ...strand.vars },
|
|
166
|
+
})));
|
|
167
|
+
}
|
|
168
|
+
function isCompatible(oVars, sVars) {
|
|
169
|
+
for (const name in oVars) {
|
|
170
|
+
if (name in sVars && oVars[name] !== sVars[name])
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
// If you find yourself editing this function, you probably want
|
|
176
|
+
// to edit prepareDef in prepQueryLinks too.
|
|
177
|
+
function makeRef(def, vars) {
|
|
178
|
+
function getValue(key) {
|
|
179
|
+
if (typeof key !== 'string')
|
|
180
|
+
return key;
|
|
181
|
+
return key[0] === '$' && key.slice(1) in vars ? vars[key.slice(1)] : key;
|
|
182
|
+
}
|
|
183
|
+
function replacePlaceholders(key) {
|
|
184
|
+
if (Array.isArray(key)) {
|
|
185
|
+
return key.map(replacePlaceholders);
|
|
186
|
+
}
|
|
187
|
+
if (typeof key === 'object' && !ArrayBuffer.isView(key) && key) {
|
|
188
|
+
const result = {};
|
|
189
|
+
for (const prop in key) {
|
|
190
|
+
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
return getValue(key);
|
|
195
|
+
}
|
|
196
|
+
return encodePath(def.map(replacePlaceholders));
|
|
197
|
+
}
|
package/package.json
CHANGED
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
"name": "@graffy/link",
|
|
3
3
|
"description": "Graffy module for constructing links using an intuitive, declarative notation.",
|
|
4
4
|
"author": "aravind (https://github.com/aravindet)",
|
|
5
|
-
"version": "0.19.
|
|
6
|
-
"main": "./index.
|
|
5
|
+
"version": "0.19.1-alpha.1",
|
|
6
|
+
"main": "./cjs/index.js",
|
|
7
7
|
"exports": {
|
|
8
|
-
"
|
|
9
|
-
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./index.js",
|
|
10
|
+
"types": "./index.d.ts"
|
|
11
|
+
},
|
|
12
|
+
"./*": {
|
|
13
|
+
"import": "./*.js",
|
|
14
|
+
"types": "./*.d.ts"
|
|
15
|
+
}
|
|
10
16
|
},
|
|
11
|
-
"
|
|
12
|
-
"types": "./types/index.d.ts",
|
|
17
|
+
"types": "./index.d.ts",
|
|
13
18
|
"repository": {
|
|
14
19
|
"type": "git",
|
|
15
20
|
"url": "git+https://github.com/usegraffy/graffy.git"
|
|
16
21
|
},
|
|
17
22
|
"license": "Apache-2.0",
|
|
18
23
|
"dependencies": {
|
|
19
|
-
"@graffy/common": "0.19.
|
|
24
|
+
"@graffy/common": "0.19.1-alpha.1",
|
|
20
25
|
"debug": "^4.4.3"
|
|
21
26
|
}
|
|
22
27
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { add, cmp, decodeArgs, encodeArgs, encodeQuery, findFirst, isBranch, MIN_KEY, splitRef, } from '@graffy/common';
|
|
2
|
+
/*
|
|
3
|
+
Given a query and an array of link definitions, it:
|
|
4
|
+
- modifies the query in-place to remove the link properties
|
|
5
|
+
and add data properties required to construct the link.
|
|
6
|
+
- returns an array of link definitions used in this query,
|
|
7
|
+
along with the subQueries for each.
|
|
8
|
+
- as far as possible, placeholders in the returned defs are
|
|
9
|
+
replaced with actual keys from the query. In cases where
|
|
10
|
+
the keys are not known before the query is evaluated
|
|
11
|
+
(i.e. pagination), placeholders are preserved to be
|
|
12
|
+
replaced in the linkGraph function.
|
|
13
|
+
*/
|
|
14
|
+
export default function prepQueryLinks(rootQuery, defs) {
|
|
15
|
+
return defs.flatMap(({ path, def }) => prepQueryDef(rootQuery, path, def));
|
|
16
|
+
function prepQueryDef(query, path, def, vars = {}, version = 0) {
|
|
17
|
+
function addDefQuery(subQuery) {
|
|
18
|
+
// Request the data we will need later to construct the link.
|
|
19
|
+
add(rootQuery, getDefQuery(def, vars, version));
|
|
20
|
+
// Return the "used defs" for use by linkGraph
|
|
21
|
+
const [range, filter] = splitRef(def);
|
|
22
|
+
if (range && subQuery.length) {
|
|
23
|
+
return subQuery.map((node) => {
|
|
24
|
+
if (!(node.prefix || node.end)) {
|
|
25
|
+
throw Error(`link.range_expected: ${path.concat(node.key).join('.')}`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
// Range queries that don't specify prefix:true are those
|
|
29
|
+
// without filters; we expect that the result will contain
|
|
30
|
+
// a prefix node with MIN_KEY to indicate this, so we add
|
|
31
|
+
// MIN_KEY here to match that.
|
|
32
|
+
path: path.concat(node.prefix ? decodeArgs(node) : MIN_KEY),
|
|
33
|
+
def: prepareDef(def.slice(0, -1).concat({
|
|
34
|
+
...filter,
|
|
35
|
+
...(node.prefix ? decodeArgs(node) : {}),
|
|
36
|
+
...range,
|
|
37
|
+
}), vars),
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return [{ path, def: prepareDef(def, vars) }];
|
|
42
|
+
}
|
|
43
|
+
function prefixKey(defs, key) {
|
|
44
|
+
return defs.map(({ path, def }) => ({
|
|
45
|
+
path: [key, ...path],
|
|
46
|
+
def,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
if (!(Array.isArray(query) && query.length))
|
|
50
|
+
return [];
|
|
51
|
+
const [key, ...rest] = path;
|
|
52
|
+
const encodedKey = encodeArgs(key).key;
|
|
53
|
+
if (rest.length === 0) {
|
|
54
|
+
if (key[0] === '$') {
|
|
55
|
+
return query.splice(0).flatMap((node) => {
|
|
56
|
+
vars[key.slice(1)] = decodeArgs(node);
|
|
57
|
+
return addDefQuery(node.children);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
const ix = findFirst(query, encodedKey);
|
|
61
|
+
if (!query[ix] || cmp(query[ix].key, encodedKey) !== 0)
|
|
62
|
+
return []; // Not using this def
|
|
63
|
+
// Remove the request for the link itself.
|
|
64
|
+
const [{ children: subQuery }] = query.splice(ix, 1);
|
|
65
|
+
return addDefQuery(subQuery);
|
|
66
|
+
}
|
|
67
|
+
let used = [];
|
|
68
|
+
if (key[0] === '$') {
|
|
69
|
+
for (const node of query) {
|
|
70
|
+
if (!isBranch(node))
|
|
71
|
+
continue;
|
|
72
|
+
let usedHere;
|
|
73
|
+
if (node.prefix) {
|
|
74
|
+
usedHere = node.children.flatMap((subNode) => {
|
|
75
|
+
return prefixKey(prepQueryDef(subNode.children, rest, def, {
|
|
76
|
+
...vars,
|
|
77
|
+
[key.slice(1)]: {
|
|
78
|
+
...decodeArgs(node),
|
|
79
|
+
...decodeArgs(subNode),
|
|
80
|
+
},
|
|
81
|
+
}, node.version), key);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
usedHere = prepQueryDef(node.children, rest, def, { ...vars, [key.slice(1)]: decodeArgs(node) }, node.version);
|
|
86
|
+
}
|
|
87
|
+
if (!node.prefix)
|
|
88
|
+
usedHere = prefixKey(usedHere, decodeArgs(node));
|
|
89
|
+
used = used.concat(usedHere);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const node = query[findFirst(query, encodedKey)];
|
|
94
|
+
if (!node || cmp(node.key, encodedKey) !== 0 || !node.children)
|
|
95
|
+
return [];
|
|
96
|
+
used = prepQueryDef(node.children, rest, def, vars, node.version);
|
|
97
|
+
if (!node.prefix)
|
|
98
|
+
used = prefixKey(used, decodeArgs(node));
|
|
99
|
+
}
|
|
100
|
+
// Remove any childnodes whose children are now empty.
|
|
101
|
+
for (let i = 0; i < query.length; i++) {
|
|
102
|
+
if (query[i].children && query[i].children.length === 0) {
|
|
103
|
+
query.splice(i, 1);
|
|
104
|
+
i--;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// console.log('Used def', used);
|
|
108
|
+
return used;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function getDefQuery(def, vars, version) {
|
|
112
|
+
// console.log('getDefQuery', def, vars);
|
|
113
|
+
function getValue(key) {
|
|
114
|
+
return key[0] === '$' ? vars[key.slice(1)] : key;
|
|
115
|
+
}
|
|
116
|
+
function getPath(template) {
|
|
117
|
+
return template.split('.').flatMap(getValue);
|
|
118
|
+
}
|
|
119
|
+
const defQuery = [];
|
|
120
|
+
function addDefQueries(key) {
|
|
121
|
+
if (typeof key === 'string' && key[0] === '$' && key[1] === '$') {
|
|
122
|
+
const path = getPath(key.slice(2));
|
|
123
|
+
// We do this to ensure that range queries are made correctly.
|
|
124
|
+
let porcelainQuery = { $key: path.pop() };
|
|
125
|
+
let $key;
|
|
126
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: idiomatic while-pop pattern
|
|
127
|
+
while (($key = path.pop())) {
|
|
128
|
+
porcelainQuery = { $key, $chi: [porcelainQuery] };
|
|
129
|
+
}
|
|
130
|
+
const query = encodeQuery(porcelainQuery, version);
|
|
131
|
+
add(defQuery, query);
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(key)) {
|
|
134
|
+
key.map(addDefQueries);
|
|
135
|
+
}
|
|
136
|
+
if (typeof key === 'object' && key) {
|
|
137
|
+
for (const prop in key) {
|
|
138
|
+
addDefQueries(prop);
|
|
139
|
+
addDefQueries(key[prop]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
def.map(addDefQueries);
|
|
144
|
+
return defQuery;
|
|
145
|
+
}
|
|
146
|
+
// If you find yourself editing this function, you probably want
|
|
147
|
+
// to edit makeRef in linkGraph too.
|
|
148
|
+
function prepareDef(def, vars) {
|
|
149
|
+
function getValue(key) {
|
|
150
|
+
if (typeof key !== 'string')
|
|
151
|
+
return key;
|
|
152
|
+
if (key[0] === '$' && key.slice(1) in vars) {
|
|
153
|
+
const value = vars[key.slice(1)];
|
|
154
|
+
return typeof value === 'object' && value !== null ? key : value;
|
|
155
|
+
}
|
|
156
|
+
return key;
|
|
157
|
+
}
|
|
158
|
+
function replacePlaceholders(key) {
|
|
159
|
+
if (typeof key === 'string' && key[0] === '$' && key[1] === '$') {
|
|
160
|
+
return `$$${key.slice(2).split('.').flatMap(getValue).join('.')}`;
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(key)) {
|
|
163
|
+
return key.map(replacePlaceholders);
|
|
164
|
+
}
|
|
165
|
+
if (typeof key === 'object' && key) {
|
|
166
|
+
const result = {};
|
|
167
|
+
for (const prop in key) {
|
|
168
|
+
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
return getValue(key);
|
|
173
|
+
}
|
|
174
|
+
const ref = def.map(replacePlaceholders);
|
|
175
|
+
return ref;
|
|
176
|
+
}
|
package/index.cjs
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
const common = require("@graffy/common");
|
|
3
|
-
const debug = require("debug");
|
|
4
|
-
function linkGraph(rootGraph, defs) {
|
|
5
|
-
const version = rootGraph[0].version;
|
|
6
|
-
for (const { path, def } of defs) {
|
|
7
|
-
const braid = def.map(getChoices);
|
|
8
|
-
const strands = unbraid(braid);
|
|
9
|
-
for (const { value, vars } of strands) {
|
|
10
|
-
const realPath = makeRef(path, vars);
|
|
11
|
-
const realRef = makeRef(value, vars);
|
|
12
|
-
const node = { key: realPath.pop(), path: common.encodePath(realRef), version };
|
|
13
|
-
const [range] = common.splitRef(value);
|
|
14
|
-
if (range) node.prefix = true;
|
|
15
|
-
let target = rootGraph;
|
|
16
|
-
do {
|
|
17
|
-
const key = realPath.shift();
|
|
18
|
-
const nextTarget = target[common.findFirst(target, key)];
|
|
19
|
-
if (!nextTarget || common.cmp(nextTarget.key, key) !== 0 || nextTarget.end) {
|
|
20
|
-
realPath.unshift(key);
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
target = nextTarget.path ? common.unwrap(rootGraph, nextTarget.path) : nextTarget.children;
|
|
24
|
-
} while (target && realPath.length);
|
|
25
|
-
if (!target) return;
|
|
26
|
-
common.merge(target, realPath.length ? common.wrap([node], realPath, version) : [node]);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return rootGraph;
|
|
30
|
-
function getChoices(key) {
|
|
31
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
32
|
-
return lookupValues(rootGraph, key.slice(2).split("."));
|
|
33
|
-
}
|
|
34
|
-
if (Array.isArray(key)) {
|
|
35
|
-
if (!key.length) return [{ value: [], vars: {} }];
|
|
36
|
-
return unbraid(key.map(getChoices));
|
|
37
|
-
}
|
|
38
|
-
if (typeof key === "object" && key) {
|
|
39
|
-
const [range = {}, filter = {}] = common.splitArgs(key);
|
|
40
|
-
const entries = Object.entries(filter).flat();
|
|
41
|
-
if (!entries.length) return [{ value: {}, vars: {} }];
|
|
42
|
-
const strands = unbraid(entries.map(getChoices));
|
|
43
|
-
return strands.map(({ value, vars }) => ({
|
|
44
|
-
value: {
|
|
45
|
-
...range,
|
|
46
|
-
...Object.fromEntries(
|
|
47
|
-
value.reduce((acc, item, i) => {
|
|
48
|
-
if (i % 2) {
|
|
49
|
-
acc[acc.length - 1].push(item);
|
|
50
|
-
} else {
|
|
51
|
-
acc.push([item]);
|
|
52
|
-
}
|
|
53
|
-
return acc;
|
|
54
|
-
}, [])
|
|
55
|
-
)
|
|
56
|
-
},
|
|
57
|
-
vars
|
|
58
|
-
}));
|
|
59
|
-
}
|
|
60
|
-
return [{ value: key, vars: {} }];
|
|
61
|
-
}
|
|
62
|
-
function lookupValues(graph, path, vars = {}) {
|
|
63
|
-
const [key, ...rest] = path;
|
|
64
|
-
if (key[0] === "$") {
|
|
65
|
-
return graph.flatMap((node2) => {
|
|
66
|
-
if (node2.end) return [];
|
|
67
|
-
const newVars = { ...vars, [key.slice(1)]: node2.key };
|
|
68
|
-
return recurse(node2, rest, newVars);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
const encodedKey = common.encodeArgs(key).key;
|
|
72
|
-
const node = graph[common.findFirst(graph, encodedKey)];
|
|
73
|
-
if (!node || common.cmp(node.key, encodedKey) !== 0 || node.end) return [];
|
|
74
|
-
return recurse(node, rest, vars);
|
|
75
|
-
}
|
|
76
|
-
function recurse(node, path, vars) {
|
|
77
|
-
if (!path.length) return [{ value: node.value, vars }];
|
|
78
|
-
if (node.children) return lookupValues(node.children, path, vars);
|
|
79
|
-
if (node.path) {
|
|
80
|
-
const linked = common.unwrap(rootGraph, node.path);
|
|
81
|
-
if (Array.isArray(linked)) return lookupValues(linked, path, vars);
|
|
82
|
-
}
|
|
83
|
-
throw Error(`link.no_children ${JSON.stringify(node)}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
function unbraid(braid) {
|
|
87
|
-
if (!braid.length) return [];
|
|
88
|
-
const [options, ...rest] = braid;
|
|
89
|
-
if (!rest.length) {
|
|
90
|
-
return options.map((option) => ({
|
|
91
|
-
value: [option.value],
|
|
92
|
-
vars: option.vars
|
|
93
|
-
}));
|
|
94
|
-
}
|
|
95
|
-
const strands = unbraid(rest);
|
|
96
|
-
return options.flatMap(
|
|
97
|
-
(option) => strands.filter((strand) => isCompatible(option.vars, strand.vars)).map((strand) => ({
|
|
98
|
-
value: [option.value, ...strand.value],
|
|
99
|
-
vars: { ...option.vars, ...strand.vars }
|
|
100
|
-
}))
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
function isCompatible(oVars, sVars) {
|
|
104
|
-
for (const name in oVars) {
|
|
105
|
-
if (name in sVars && oVars[name] !== sVars[name]) return false;
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
function makeRef(def, vars) {
|
|
110
|
-
function getValue(key) {
|
|
111
|
-
if (typeof key !== "string") return key;
|
|
112
|
-
return key[0] === "$" && key.slice(1) in vars ? vars[key.slice(1)] : key;
|
|
113
|
-
}
|
|
114
|
-
function replacePlaceholders(key) {
|
|
115
|
-
if (Array.isArray(key)) {
|
|
116
|
-
return key.map(replacePlaceholders);
|
|
117
|
-
}
|
|
118
|
-
if (typeof key === "object" && !ArrayBuffer.isView(key) && key) {
|
|
119
|
-
const result = {};
|
|
120
|
-
for (const prop in key) {
|
|
121
|
-
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
return getValue(key);
|
|
126
|
-
}
|
|
127
|
-
return common.encodePath(def.map(replacePlaceholders));
|
|
128
|
-
}
|
|
129
|
-
function prepQueryLinks(rootQuery, defs) {
|
|
130
|
-
return defs.flatMap(({ path, def }) => prepQueryDef(rootQuery, path, def));
|
|
131
|
-
function prepQueryDef(query, path, def, vars = {}, version = 0) {
|
|
132
|
-
function addDefQuery(subQuery) {
|
|
133
|
-
common.add(rootQuery, getDefQuery(def, vars, version));
|
|
134
|
-
const [range, filter] = common.splitRef(def);
|
|
135
|
-
if (range && subQuery.length) {
|
|
136
|
-
return subQuery.map((node) => {
|
|
137
|
-
if (!(node.prefix || node.end)) {
|
|
138
|
-
throw Error(
|
|
139
|
-
`link.range_expected: ${path.concat(node.key).join(".")}`
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
return {
|
|
143
|
-
// Range queries that don't specify prefix:true are those
|
|
144
|
-
// without filters; we expect that the result will contain
|
|
145
|
-
// a prefix node with MIN_KEY to indicate this, so we add
|
|
146
|
-
// MIN_KEY here to match that.
|
|
147
|
-
path: path.concat(node.prefix ? common.decodeArgs(node) : common.MIN_KEY),
|
|
148
|
-
def: prepareDef(
|
|
149
|
-
def.slice(0, -1).concat({
|
|
150
|
-
...filter,
|
|
151
|
-
...node.prefix ? common.decodeArgs(node) : {},
|
|
152
|
-
...range
|
|
153
|
-
}),
|
|
154
|
-
vars
|
|
155
|
-
)
|
|
156
|
-
};
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return [{ path, def: prepareDef(def, vars) }];
|
|
160
|
-
}
|
|
161
|
-
function prefixKey(defs2, key2) {
|
|
162
|
-
return defs2.map(({ path: path2, def: def2 }) => ({
|
|
163
|
-
path: [key2, ...path2],
|
|
164
|
-
def: def2
|
|
165
|
-
}));
|
|
166
|
-
}
|
|
167
|
-
if (!(Array.isArray(query) && query.length)) return [];
|
|
168
|
-
const [key, ...rest] = path;
|
|
169
|
-
const encodedKey = common.encodeArgs(key).key;
|
|
170
|
-
if (rest.length === 0) {
|
|
171
|
-
if (key[0] === "$") {
|
|
172
|
-
return query.splice(0).flatMap((node) => {
|
|
173
|
-
vars[key.slice(1)] = common.decodeArgs(node);
|
|
174
|
-
return addDefQuery(node.children);
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
const ix = common.findFirst(query, encodedKey);
|
|
178
|
-
if (!query[ix] || common.cmp(query[ix].key, encodedKey) !== 0) return [];
|
|
179
|
-
const [{ children: subQuery }] = query.splice(ix, 1);
|
|
180
|
-
return addDefQuery(subQuery);
|
|
181
|
-
}
|
|
182
|
-
let used = [];
|
|
183
|
-
if (key[0] === "$") {
|
|
184
|
-
for (const node of query) {
|
|
185
|
-
if (!common.isBranch(node)) continue;
|
|
186
|
-
let usedHere;
|
|
187
|
-
if (node.prefix) {
|
|
188
|
-
usedHere = node.children.flatMap((subNode) => {
|
|
189
|
-
return prefixKey(
|
|
190
|
-
prepQueryDef(
|
|
191
|
-
subNode.children,
|
|
192
|
-
rest,
|
|
193
|
-
def,
|
|
194
|
-
{
|
|
195
|
-
...vars,
|
|
196
|
-
[key.slice(1)]: {
|
|
197
|
-
...common.decodeArgs(node),
|
|
198
|
-
...common.decodeArgs(subNode)
|
|
199
|
-
}
|
|
200
|
-
},
|
|
201
|
-
node.version
|
|
202
|
-
),
|
|
203
|
-
key
|
|
204
|
-
);
|
|
205
|
-
});
|
|
206
|
-
} else {
|
|
207
|
-
usedHere = prepQueryDef(
|
|
208
|
-
node.children,
|
|
209
|
-
rest,
|
|
210
|
-
def,
|
|
211
|
-
{ ...vars, [key.slice(1)]: common.decodeArgs(node) },
|
|
212
|
-
node.version
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
if (!node.prefix) usedHere = prefixKey(usedHere, common.decodeArgs(node));
|
|
216
|
-
used = used.concat(usedHere);
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
const node = query[common.findFirst(query, encodedKey)];
|
|
220
|
-
if (!node || common.cmp(node.key, encodedKey) !== 0 || !node.children) return [];
|
|
221
|
-
used = prepQueryDef(node.children, rest, def, vars, node.version);
|
|
222
|
-
if (!node.prefix) used = prefixKey(used, common.decodeArgs(node));
|
|
223
|
-
}
|
|
224
|
-
for (let i = 0; i < query.length; i++) {
|
|
225
|
-
if (query[i].children && query[i].children.length === 0) {
|
|
226
|
-
query.splice(i, 1);
|
|
227
|
-
i--;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return used;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function getDefQuery(def, vars, version) {
|
|
234
|
-
function getValue(key) {
|
|
235
|
-
return key[0] === "$" ? vars[key.slice(1)] : key;
|
|
236
|
-
}
|
|
237
|
-
function getPath(template) {
|
|
238
|
-
return template.split(".").flatMap(getValue);
|
|
239
|
-
}
|
|
240
|
-
const defQuery = [];
|
|
241
|
-
function addDefQueries(key) {
|
|
242
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
243
|
-
const path = getPath(key.slice(2));
|
|
244
|
-
let porcelainQuery = { $key: path.pop() };
|
|
245
|
-
let $key;
|
|
246
|
-
while ($key = path.pop()) {
|
|
247
|
-
porcelainQuery = { $key, $chi: [porcelainQuery] };
|
|
248
|
-
}
|
|
249
|
-
const query = common.encodeQuery(porcelainQuery, version);
|
|
250
|
-
common.add(defQuery, query);
|
|
251
|
-
}
|
|
252
|
-
if (Array.isArray(key)) {
|
|
253
|
-
key.map(addDefQueries);
|
|
254
|
-
}
|
|
255
|
-
if (typeof key === "object" && key) {
|
|
256
|
-
for (const prop in key) {
|
|
257
|
-
addDefQueries(prop);
|
|
258
|
-
addDefQueries(key[prop]);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
def.map(addDefQueries);
|
|
263
|
-
return defQuery;
|
|
264
|
-
}
|
|
265
|
-
function prepareDef(def, vars) {
|
|
266
|
-
function getValue(key) {
|
|
267
|
-
if (typeof key !== "string") return key;
|
|
268
|
-
if (key[0] === "$" && key.slice(1) in vars) {
|
|
269
|
-
const value = vars[key.slice(1)];
|
|
270
|
-
return typeof value === "object" && value !== null ? key : value;
|
|
271
|
-
}
|
|
272
|
-
return key;
|
|
273
|
-
}
|
|
274
|
-
function replacePlaceholders(key) {
|
|
275
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
276
|
-
return `$$${key.slice(2).split(".").flatMap(getValue).join(".")}`;
|
|
277
|
-
}
|
|
278
|
-
if (Array.isArray(key)) {
|
|
279
|
-
return key.map(replacePlaceholders);
|
|
280
|
-
}
|
|
281
|
-
if (typeof key === "object" && key) {
|
|
282
|
-
const result = {};
|
|
283
|
-
for (const prop in key) {
|
|
284
|
-
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
285
|
-
}
|
|
286
|
-
return result;
|
|
287
|
-
}
|
|
288
|
-
return getValue(key);
|
|
289
|
-
}
|
|
290
|
-
const ref = def.map(replacePlaceholders);
|
|
291
|
-
return ref;
|
|
292
|
-
}
|
|
293
|
-
const log = debug("graffy:link");
|
|
294
|
-
const index = (defs) => (store) => {
|
|
295
|
-
const prefix = common.encodePath(store.path);
|
|
296
|
-
const defEntries = Object.entries(defs).map(([prop, def]) => ({
|
|
297
|
-
path: prop.split("."),
|
|
298
|
-
def
|
|
299
|
-
}));
|
|
300
|
-
store.on("read", async (query, options, next) => {
|
|
301
|
-
const unwrappedQuery = clone(common.unwrap(query, prefix));
|
|
302
|
-
const usedDefs = prepQueryLinks(unwrappedQuery, defEntries);
|
|
303
|
-
if (!usedDefs.length) return next(query, options);
|
|
304
|
-
const result = await next(common.wrap(unwrappedQuery, prefix), options);
|
|
305
|
-
const version = result[0].version;
|
|
306
|
-
const unwrappedResult = common.unwrap(result, prefix);
|
|
307
|
-
common.add(unwrappedQuery, common.unwrap(query, prefix));
|
|
308
|
-
log("finalizing", prefix, unwrappedQuery);
|
|
309
|
-
const finalizedResult = common.finalize(unwrappedResult, unwrappedQuery, version);
|
|
310
|
-
log("beforeAddingLinks", prefix, finalizedResult);
|
|
311
|
-
linkGraph(finalizedResult, usedDefs);
|
|
312
|
-
log("afterAddingLinks", prefix, finalizedResult);
|
|
313
|
-
return common.wrap(finalizedResult, prefix, version);
|
|
314
|
-
});
|
|
315
|
-
};
|
|
316
|
-
function clone(tree) {
|
|
317
|
-
return common.unpack(JSON.parse(JSON.stringify(common.pack(tree))));
|
|
318
|
-
}
|
|
319
|
-
module.exports = index;
|
package/index.mjs
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { encodePath, splitRef, findFirst, cmp, unwrap, merge, wrap, splitArgs, encodeArgs, decodeArgs, isBranch, add, MIN_KEY, encodeQuery, finalize, unpack, pack } from "@graffy/common";
|
|
2
|
-
import debug from "debug";
|
|
3
|
-
function linkGraph(rootGraph, defs) {
|
|
4
|
-
const version = rootGraph[0].version;
|
|
5
|
-
for (const { path, def } of defs) {
|
|
6
|
-
const braid = def.map(getChoices);
|
|
7
|
-
const strands = unbraid(braid);
|
|
8
|
-
for (const { value, vars } of strands) {
|
|
9
|
-
const realPath = makeRef(path, vars);
|
|
10
|
-
const realRef = makeRef(value, vars);
|
|
11
|
-
const node = { key: realPath.pop(), path: encodePath(realRef), version };
|
|
12
|
-
const [range] = splitRef(value);
|
|
13
|
-
if (range) node.prefix = true;
|
|
14
|
-
let target = rootGraph;
|
|
15
|
-
do {
|
|
16
|
-
const key = realPath.shift();
|
|
17
|
-
const nextTarget = target[findFirst(target, key)];
|
|
18
|
-
if (!nextTarget || cmp(nextTarget.key, key) !== 0 || nextTarget.end) {
|
|
19
|
-
realPath.unshift(key);
|
|
20
|
-
break;
|
|
21
|
-
}
|
|
22
|
-
target = nextTarget.path ? unwrap(rootGraph, nextTarget.path) : nextTarget.children;
|
|
23
|
-
} while (target && realPath.length);
|
|
24
|
-
if (!target) return;
|
|
25
|
-
merge(target, realPath.length ? wrap([node], realPath, version) : [node]);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return rootGraph;
|
|
29
|
-
function getChoices(key) {
|
|
30
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
31
|
-
return lookupValues(rootGraph, key.slice(2).split("."));
|
|
32
|
-
}
|
|
33
|
-
if (Array.isArray(key)) {
|
|
34
|
-
if (!key.length) return [{ value: [], vars: {} }];
|
|
35
|
-
return unbraid(key.map(getChoices));
|
|
36
|
-
}
|
|
37
|
-
if (typeof key === "object" && key) {
|
|
38
|
-
const [range = {}, filter = {}] = splitArgs(key);
|
|
39
|
-
const entries = Object.entries(filter).flat();
|
|
40
|
-
if (!entries.length) return [{ value: {}, vars: {} }];
|
|
41
|
-
const strands = unbraid(entries.map(getChoices));
|
|
42
|
-
return strands.map(({ value, vars }) => ({
|
|
43
|
-
value: {
|
|
44
|
-
...range,
|
|
45
|
-
...Object.fromEntries(
|
|
46
|
-
value.reduce((acc, item, i) => {
|
|
47
|
-
if (i % 2) {
|
|
48
|
-
acc[acc.length - 1].push(item);
|
|
49
|
-
} else {
|
|
50
|
-
acc.push([item]);
|
|
51
|
-
}
|
|
52
|
-
return acc;
|
|
53
|
-
}, [])
|
|
54
|
-
)
|
|
55
|
-
},
|
|
56
|
-
vars
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
return [{ value: key, vars: {} }];
|
|
60
|
-
}
|
|
61
|
-
function lookupValues(graph, path, vars = {}) {
|
|
62
|
-
const [key, ...rest] = path;
|
|
63
|
-
if (key[0] === "$") {
|
|
64
|
-
return graph.flatMap((node2) => {
|
|
65
|
-
if (node2.end) return [];
|
|
66
|
-
const newVars = { ...vars, [key.slice(1)]: node2.key };
|
|
67
|
-
return recurse(node2, rest, newVars);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
const encodedKey = encodeArgs(key).key;
|
|
71
|
-
const node = graph[findFirst(graph, encodedKey)];
|
|
72
|
-
if (!node || cmp(node.key, encodedKey) !== 0 || node.end) return [];
|
|
73
|
-
return recurse(node, rest, vars);
|
|
74
|
-
}
|
|
75
|
-
function recurse(node, path, vars) {
|
|
76
|
-
if (!path.length) return [{ value: node.value, vars }];
|
|
77
|
-
if (node.children) return lookupValues(node.children, path, vars);
|
|
78
|
-
if (node.path) {
|
|
79
|
-
const linked = unwrap(rootGraph, node.path);
|
|
80
|
-
if (Array.isArray(linked)) return lookupValues(linked, path, vars);
|
|
81
|
-
}
|
|
82
|
-
throw Error(`link.no_children ${JSON.stringify(node)}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
function unbraid(braid) {
|
|
86
|
-
if (!braid.length) return [];
|
|
87
|
-
const [options, ...rest] = braid;
|
|
88
|
-
if (!rest.length) {
|
|
89
|
-
return options.map((option) => ({
|
|
90
|
-
value: [option.value],
|
|
91
|
-
vars: option.vars
|
|
92
|
-
}));
|
|
93
|
-
}
|
|
94
|
-
const strands = unbraid(rest);
|
|
95
|
-
return options.flatMap(
|
|
96
|
-
(option) => strands.filter((strand) => isCompatible(option.vars, strand.vars)).map((strand) => ({
|
|
97
|
-
value: [option.value, ...strand.value],
|
|
98
|
-
vars: { ...option.vars, ...strand.vars }
|
|
99
|
-
}))
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
function isCompatible(oVars, sVars) {
|
|
103
|
-
for (const name in oVars) {
|
|
104
|
-
if (name in sVars && oVars[name] !== sVars[name]) return false;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
function makeRef(def, vars) {
|
|
109
|
-
function getValue(key) {
|
|
110
|
-
if (typeof key !== "string") return key;
|
|
111
|
-
return key[0] === "$" && key.slice(1) in vars ? vars[key.slice(1)] : key;
|
|
112
|
-
}
|
|
113
|
-
function replacePlaceholders(key) {
|
|
114
|
-
if (Array.isArray(key)) {
|
|
115
|
-
return key.map(replacePlaceholders);
|
|
116
|
-
}
|
|
117
|
-
if (typeof key === "object" && !ArrayBuffer.isView(key) && key) {
|
|
118
|
-
const result = {};
|
|
119
|
-
for (const prop in key) {
|
|
120
|
-
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
123
|
-
}
|
|
124
|
-
return getValue(key);
|
|
125
|
-
}
|
|
126
|
-
return encodePath(def.map(replacePlaceholders));
|
|
127
|
-
}
|
|
128
|
-
function prepQueryLinks(rootQuery, defs) {
|
|
129
|
-
return defs.flatMap(({ path, def }) => prepQueryDef(rootQuery, path, def));
|
|
130
|
-
function prepQueryDef(query, path, def, vars = {}, version = 0) {
|
|
131
|
-
function addDefQuery(subQuery) {
|
|
132
|
-
add(rootQuery, getDefQuery(def, vars, version));
|
|
133
|
-
const [range, filter] = splitRef(def);
|
|
134
|
-
if (range && subQuery.length) {
|
|
135
|
-
return subQuery.map((node) => {
|
|
136
|
-
if (!(node.prefix || node.end)) {
|
|
137
|
-
throw Error(
|
|
138
|
-
`link.range_expected: ${path.concat(node.key).join(".")}`
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
return {
|
|
142
|
-
// Range queries that don't specify prefix:true are those
|
|
143
|
-
// without filters; we expect that the result will contain
|
|
144
|
-
// a prefix node with MIN_KEY to indicate this, so we add
|
|
145
|
-
// MIN_KEY here to match that.
|
|
146
|
-
path: path.concat(node.prefix ? decodeArgs(node) : MIN_KEY),
|
|
147
|
-
def: prepareDef(
|
|
148
|
-
def.slice(0, -1).concat({
|
|
149
|
-
...filter,
|
|
150
|
-
...node.prefix ? decodeArgs(node) : {},
|
|
151
|
-
...range
|
|
152
|
-
}),
|
|
153
|
-
vars
|
|
154
|
-
)
|
|
155
|
-
};
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
return [{ path, def: prepareDef(def, vars) }];
|
|
159
|
-
}
|
|
160
|
-
function prefixKey(defs2, key2) {
|
|
161
|
-
return defs2.map(({ path: path2, def: def2 }) => ({
|
|
162
|
-
path: [key2, ...path2],
|
|
163
|
-
def: def2
|
|
164
|
-
}));
|
|
165
|
-
}
|
|
166
|
-
if (!(Array.isArray(query) && query.length)) return [];
|
|
167
|
-
const [key, ...rest] = path;
|
|
168
|
-
const encodedKey = encodeArgs(key).key;
|
|
169
|
-
if (rest.length === 0) {
|
|
170
|
-
if (key[0] === "$") {
|
|
171
|
-
return query.splice(0).flatMap((node) => {
|
|
172
|
-
vars[key.slice(1)] = decodeArgs(node);
|
|
173
|
-
return addDefQuery(node.children);
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
const ix = findFirst(query, encodedKey);
|
|
177
|
-
if (!query[ix] || cmp(query[ix].key, encodedKey) !== 0) return [];
|
|
178
|
-
const [{ children: subQuery }] = query.splice(ix, 1);
|
|
179
|
-
return addDefQuery(subQuery);
|
|
180
|
-
}
|
|
181
|
-
let used = [];
|
|
182
|
-
if (key[0] === "$") {
|
|
183
|
-
for (const node of query) {
|
|
184
|
-
if (!isBranch(node)) continue;
|
|
185
|
-
let usedHere;
|
|
186
|
-
if (node.prefix) {
|
|
187
|
-
usedHere = node.children.flatMap((subNode) => {
|
|
188
|
-
return prefixKey(
|
|
189
|
-
prepQueryDef(
|
|
190
|
-
subNode.children,
|
|
191
|
-
rest,
|
|
192
|
-
def,
|
|
193
|
-
{
|
|
194
|
-
...vars,
|
|
195
|
-
[key.slice(1)]: {
|
|
196
|
-
...decodeArgs(node),
|
|
197
|
-
...decodeArgs(subNode)
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
node.version
|
|
201
|
-
),
|
|
202
|
-
key
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
} else {
|
|
206
|
-
usedHere = prepQueryDef(
|
|
207
|
-
node.children,
|
|
208
|
-
rest,
|
|
209
|
-
def,
|
|
210
|
-
{ ...vars, [key.slice(1)]: decodeArgs(node) },
|
|
211
|
-
node.version
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
if (!node.prefix) usedHere = prefixKey(usedHere, decodeArgs(node));
|
|
215
|
-
used = used.concat(usedHere);
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
const node = query[findFirst(query, encodedKey)];
|
|
219
|
-
if (!node || cmp(node.key, encodedKey) !== 0 || !node.children) return [];
|
|
220
|
-
used = prepQueryDef(node.children, rest, def, vars, node.version);
|
|
221
|
-
if (!node.prefix) used = prefixKey(used, decodeArgs(node));
|
|
222
|
-
}
|
|
223
|
-
for (let i = 0; i < query.length; i++) {
|
|
224
|
-
if (query[i].children && query[i].children.length === 0) {
|
|
225
|
-
query.splice(i, 1);
|
|
226
|
-
i--;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return used;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
function getDefQuery(def, vars, version) {
|
|
233
|
-
function getValue(key) {
|
|
234
|
-
return key[0] === "$" ? vars[key.slice(1)] : key;
|
|
235
|
-
}
|
|
236
|
-
function getPath(template) {
|
|
237
|
-
return template.split(".").flatMap(getValue);
|
|
238
|
-
}
|
|
239
|
-
const defQuery = [];
|
|
240
|
-
function addDefQueries(key) {
|
|
241
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
242
|
-
const path = getPath(key.slice(2));
|
|
243
|
-
let porcelainQuery = { $key: path.pop() };
|
|
244
|
-
let $key;
|
|
245
|
-
while ($key = path.pop()) {
|
|
246
|
-
porcelainQuery = { $key, $chi: [porcelainQuery] };
|
|
247
|
-
}
|
|
248
|
-
const query = encodeQuery(porcelainQuery, version);
|
|
249
|
-
add(defQuery, query);
|
|
250
|
-
}
|
|
251
|
-
if (Array.isArray(key)) {
|
|
252
|
-
key.map(addDefQueries);
|
|
253
|
-
}
|
|
254
|
-
if (typeof key === "object" && key) {
|
|
255
|
-
for (const prop in key) {
|
|
256
|
-
addDefQueries(prop);
|
|
257
|
-
addDefQueries(key[prop]);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
def.map(addDefQueries);
|
|
262
|
-
return defQuery;
|
|
263
|
-
}
|
|
264
|
-
function prepareDef(def, vars) {
|
|
265
|
-
function getValue(key) {
|
|
266
|
-
if (typeof key !== "string") return key;
|
|
267
|
-
if (key[0] === "$" && key.slice(1) in vars) {
|
|
268
|
-
const value = vars[key.slice(1)];
|
|
269
|
-
return typeof value === "object" && value !== null ? key : value;
|
|
270
|
-
}
|
|
271
|
-
return key;
|
|
272
|
-
}
|
|
273
|
-
function replacePlaceholders(key) {
|
|
274
|
-
if (typeof key === "string" && key[0] === "$" && key[1] === "$") {
|
|
275
|
-
return `$$${key.slice(2).split(".").flatMap(getValue).join(".")}`;
|
|
276
|
-
}
|
|
277
|
-
if (Array.isArray(key)) {
|
|
278
|
-
return key.map(replacePlaceholders);
|
|
279
|
-
}
|
|
280
|
-
if (typeof key === "object" && key) {
|
|
281
|
-
const result = {};
|
|
282
|
-
for (const prop in key) {
|
|
283
|
-
result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
|
|
284
|
-
}
|
|
285
|
-
return result;
|
|
286
|
-
}
|
|
287
|
-
return getValue(key);
|
|
288
|
-
}
|
|
289
|
-
const ref = def.map(replacePlaceholders);
|
|
290
|
-
return ref;
|
|
291
|
-
}
|
|
292
|
-
const log = debug("graffy:link");
|
|
293
|
-
const index = (defs) => (store) => {
|
|
294
|
-
const prefix = encodePath(store.path);
|
|
295
|
-
const defEntries = Object.entries(defs).map(([prop, def]) => ({
|
|
296
|
-
path: prop.split("."),
|
|
297
|
-
def
|
|
298
|
-
}));
|
|
299
|
-
store.on("read", async (query, options, next) => {
|
|
300
|
-
const unwrappedQuery = clone(unwrap(query, prefix));
|
|
301
|
-
const usedDefs = prepQueryLinks(unwrappedQuery, defEntries);
|
|
302
|
-
if (!usedDefs.length) return next(query, options);
|
|
303
|
-
const result = await next(wrap(unwrappedQuery, prefix), options);
|
|
304
|
-
const version = result[0].version;
|
|
305
|
-
const unwrappedResult = unwrap(result, prefix);
|
|
306
|
-
add(unwrappedQuery, unwrap(query, prefix));
|
|
307
|
-
log("finalizing", prefix, unwrappedQuery);
|
|
308
|
-
const finalizedResult = finalize(unwrappedResult, unwrappedQuery, version);
|
|
309
|
-
log("beforeAddingLinks", prefix, finalizedResult);
|
|
310
|
-
linkGraph(finalizedResult, usedDefs);
|
|
311
|
-
log("afterAddingLinks", prefix, finalizedResult);
|
|
312
|
-
return wrap(finalizedResult, prefix, version);
|
|
313
|
-
});
|
|
314
|
-
};
|
|
315
|
-
function clone(tree) {
|
|
316
|
-
return unpack(JSON.parse(JSON.stringify(pack(tree))));
|
|
317
|
-
}
|
|
318
|
-
export {
|
|
319
|
-
index as default
|
|
320
|
-
};
|
package/types/index.d.ts
DELETED
|
File without changes
|
|
File without changes
|