@graffy/link 0.19.0 → 0.19.1-alpha.2

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 ADDED
@@ -0,0 +1,2 @@
1
+ declare const _default: (defs: any) => (store: any) => void;
2
+ export default _default;
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,201 @@
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 = {
60
+ key: realPath.pop(),
61
+ path: encodePath(realRef),
62
+ version,
63
+ };
64
+ const [range] = splitRef(value);
65
+ if (range)
66
+ node.prefix = true;
67
+ let target = rootGraph;
68
+ do {
69
+ const key = realPath.shift();
70
+ const nextTarget = target[findFirst(target, key)];
71
+ if (!nextTarget || cmp(nextTarget.key, key) !== 0 || nextTarget.end) {
72
+ realPath.unshift(key);
73
+ break;
74
+ }
75
+ target = nextTarget.path
76
+ ? unwrap(rootGraph, nextTarget.path)
77
+ : nextTarget.children;
78
+ } while (target && realPath.length);
79
+ if (!target)
80
+ return;
81
+ merge(target, realPath.length ? wrap([node], realPath, version) : [node]);
82
+ }
83
+ }
84
+ return rootGraph;
85
+ function getChoices(key) {
86
+ if (typeof key === 'string' && key[0] === '$' && key[1] === '$') {
87
+ return lookupValues(rootGraph, key.slice(2).split('.'));
88
+ }
89
+ if (Array.isArray(key)) {
90
+ if (!key.length)
91
+ return [{ value: [], vars: {} }];
92
+ return unbraid(key.map(getChoices));
93
+ }
94
+ if (typeof key === 'object' && key) {
95
+ const [range = {}, filter = {}] = splitArgs(key);
96
+ const entries = Object.entries(filter).flat();
97
+ if (!entries.length)
98
+ return [{ value: {}, vars: {} }];
99
+ const strands = unbraid(entries.map(getChoices));
100
+ return strands.map(({ value, vars }) => ({
101
+ value: {
102
+ ...range,
103
+ ...Object.fromEntries(value.reduce((acc, item, i) => {
104
+ if (i % 2) {
105
+ acc[acc.length - 1].push(item);
106
+ }
107
+ else {
108
+ acc.push([item]);
109
+ }
110
+ return acc;
111
+ }, [])),
112
+ },
113
+ vars,
114
+ }));
115
+ }
116
+ return [{ value: key, vars: {} }];
117
+ }
118
+ /**
119
+ Takes a lookup expression which may optionally contain named placeholders,
120
+ and returns an array of possible values in the graph that it matches.
121
+ For each such value, it also returns the corresponding placeholder values.
122
+ @param {string[]} path
123
+ @return {{ value: any, vars: Record<string,any> }[]}
124
+ */
125
+ function lookupValues(graph, path, vars = {}) {
126
+ const [key, ...rest] = path;
127
+ if (key[0] === '$') {
128
+ return graph.flatMap((node) => {
129
+ if (node.end)
130
+ return [];
131
+ const newVars = { ...vars, [key.slice(1)]: node.key };
132
+ return recurse(node, rest, newVars);
133
+ });
134
+ }
135
+ const encodedKey = encodeArgs(key).key;
136
+ const node = graph[findFirst(graph, encodedKey)];
137
+ if (!node || cmp(node.key, encodedKey) !== 0 || node.end)
138
+ return [];
139
+ return recurse(node, rest, vars);
140
+ }
141
+ function recurse(node, path, vars) {
142
+ if (!path.length)
143
+ return [{ value: node.value, vars }];
144
+ if (node.children)
145
+ return lookupValues(node.children, path, vars);
146
+ if (node.path) {
147
+ const linked = unwrap(rootGraph, node.path);
148
+ if (Array.isArray(linked))
149
+ return lookupValues(linked, path, vars);
150
+ }
151
+ throw Error(`link.no_children ${JSON.stringify(node)}`);
152
+ }
153
+ }
154
+ function unbraid(braid) {
155
+ if (!braid.length)
156
+ return [];
157
+ const [options, ...rest] = braid;
158
+ if (!rest.length) {
159
+ return options.map((option) => ({
160
+ value: [option.value],
161
+ vars: option.vars,
162
+ }));
163
+ }
164
+ const strands = unbraid(rest);
165
+ return options.flatMap((option) => strands
166
+ .filter((strand) => isCompatible(option.vars, strand.vars))
167
+ .map((strand) => ({
168
+ value: [option.value, ...strand.value],
169
+ vars: { ...option.vars, ...strand.vars },
170
+ })));
171
+ }
172
+ function isCompatible(oVars, sVars) {
173
+ for (const name in oVars) {
174
+ if (name in sVars && oVars[name] !== sVars[name])
175
+ return false;
176
+ }
177
+ return true;
178
+ }
179
+ // If you find yourself editing this function, you probably want
180
+ // to edit prepareDef in prepQueryLinks too.
181
+ function makeRef(def, vars) {
182
+ function getValue(key) {
183
+ if (typeof key !== 'string')
184
+ return key;
185
+ return key[0] === '$' && key.slice(1) in vars ? vars[key.slice(1)] : key;
186
+ }
187
+ function replacePlaceholders(key) {
188
+ if (Array.isArray(key)) {
189
+ return key.map(replacePlaceholders);
190
+ }
191
+ if (typeof key === 'object' && !ArrayBuffer.isView(key) && key) {
192
+ const result = {};
193
+ for (const prop in key) {
194
+ result[replacePlaceholders(prop)] = replacePlaceholders(key[prop]);
195
+ }
196
+ return result;
197
+ }
198
+ return getValue(key);
199
+ }
200
+ return encodePath(def.map(replacePlaceholders));
201
+ }
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.0",
6
- "main": "./index.cjs",
5
+ "version": "0.19.1-alpha.2",
6
+ "main": "./cjs/index.js",
7
7
  "exports": {
8
- "import": "./index.mjs",
9
- "require": "./index.cjs"
8
+ ".": {
9
+ "import": "./index.js",
10
+ "types": "./index.d.ts"
11
+ },
12
+ "./*": {
13
+ "import": "./*.js",
14
+ "types": "./*.d.ts"
15
+ }
10
16
  },
11
- "module": "./index.mjs",
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.0",
24
+ "@graffy/common": "0.19.1-alpha.2",
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
@@ -1,2 +0,0 @@
1
- declare function _default(defs: any): (store: any) => void;
2
- export default _default;
File without changes