grafeo-ogm 2.5.3 → 2.6.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/CHANGELOG.md +66 -0
- package/README.md +123 -0
- package/dist/cjs/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/fulltext.compiler.js +35 -3
- package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +39 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +165 -14
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +79 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +549 -28
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +21 -7
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +47 -13
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts +14 -10
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +25 -14
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +7 -0
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +20 -4
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts +14 -0
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +37 -3
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/utils/strict-mode.d.ts +39 -0
- package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
- package/dist/cjs/utils/strict-mode.js +59 -0
- package/dist/cjs/utils/strict-mode.js.map +1 -0
- package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/fulltext.compiler.js +35 -3
- package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +165 -14
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +79 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +549 -28
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +21 -7
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +47 -13
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts +14 -10
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +25 -14
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +7 -0
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +20 -4
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts +14 -0
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +37 -3
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/utils/strict-mode.d.ts +39 -0
- package/dist/esm/utils/strict-mode.d.ts.map +1 -0
- package/dist/esm/utils/strict-mode.js +54 -0
- package/dist/esm/utils/strict-mode.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ import { OGMError } from '../errors';
|
|
|
4
4
|
import { buildRelPattern, resolveTargetDef } from '../schema/utils';
|
|
5
5
|
import { buildInlineCypherFieldExpr, } from '../utils/cypher-field-projection';
|
|
6
6
|
import { assertSafeIdentifier, assertSortDirection, escapeIdentifier, isPlainObject, mergeParams, } from '../utils/validation';
|
|
7
|
+
import { lenientStrictMode } from '../utils/strict-mode';
|
|
7
8
|
import { stitchUserAndPolicy, traversalBundle, } from './where.compiler';
|
|
8
9
|
/**
|
|
9
10
|
* v2.5.3 — the printed arguments of a parsed selectionSet field, so a
|
|
@@ -11,6 +12,52 @@ import { stitchUserAndPolicy, traversalBundle, } from './where.compiler';
|
|
|
11
12
|
* selections carry the same arguments.
|
|
12
13
|
*/
|
|
13
14
|
const argumentKeys = new WeakMap();
|
|
15
|
+
/**
|
|
16
|
+
* v2.6.0 — the strict-mode warnings of each parsed selectionSet, replayed
|
|
17
|
+
* on cache hits (see `SelectionCompiler.replayParseWarnings`).
|
|
18
|
+
*/
|
|
19
|
+
const parseWarnings = new WeakMap();
|
|
20
|
+
/**
|
|
21
|
+
* v2.6.0 — whether two selections of one response key can apply to the
|
|
22
|
+
* same object. A selection outside any fragment applies to every object;
|
|
23
|
+
* fragment-only selections overlap when their type conditions share a
|
|
24
|
+
* possible concrete type. Only overlapping selections with different
|
|
25
|
+
* arguments conflict (GraphQL's "fields in set can merge" rule).
|
|
26
|
+
*/
|
|
27
|
+
function canApplyToSameObject(a, b, ctx) {
|
|
28
|
+
if (!a || !b)
|
|
29
|
+
return true;
|
|
30
|
+
const types = new Set(a.flatMap((t) => ctx.possibleTypes(t)));
|
|
31
|
+
return b.some((t) => ctx.possibleTypes(t).some((p) => types.has(p)));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* v2.6.0 — strict merge of a response key selected with different
|
|
35
|
+
* arguments: a conflict throws (as GraphQL validation does) when the two
|
|
36
|
+
* selections can apply to the same object; otherwise the incoming
|
|
37
|
+
* selection becomes (or joins) a per-type variant.
|
|
38
|
+
*/
|
|
39
|
+
function mergeTypeVariant(existing, incoming, key, ctx) {
|
|
40
|
+
const groups = [existing, ...(existing.typeVariants ?? [])];
|
|
41
|
+
for (const group of groups)
|
|
42
|
+
if (!sameArguments(group, incoming) &&
|
|
43
|
+
canApplyToSameObject(group.typeConditions, incoming.typeConditions, ctx))
|
|
44
|
+
throw new OGMError(`Fields "${key}" conflict because they have differing arguments. ` +
|
|
45
|
+
`Use different aliases on the fields to fetch both.`);
|
|
46
|
+
const same = groups.find((g) => sameArguments(g, incoming));
|
|
47
|
+
if (!same) {
|
|
48
|
+
existing.typeVariants = [...(existing.typeVariants ?? []), incoming];
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (same.typeConditions && incoming.typeConditions)
|
|
52
|
+
same.typeConditions = [
|
|
53
|
+
...new Set([...same.typeConditions, ...incoming.typeConditions]),
|
|
54
|
+
];
|
|
55
|
+
mergeSameArguments(same, incoming, ctx);
|
|
56
|
+
}
|
|
57
|
+
function parseWarn(ctx, warning) {
|
|
58
|
+
if (!ctx.warnings.some((w) => w.code === warning.code && w.detail === warning.detail))
|
|
59
|
+
ctx.warnings.push(warning);
|
|
60
|
+
}
|
|
14
61
|
function sameArguments(a, b) {
|
|
15
62
|
return (argumentKeys.get(a) ?? '') === (argumentKeys.get(b) ?? '');
|
|
16
63
|
}
|
|
@@ -26,7 +73,7 @@ function sameArguments(a, b) {
|
|
|
26
73
|
* different types: recorded in `fieldByType` for a per-type projection.
|
|
27
74
|
* - A selection outside any fragment makes the field unconditional.
|
|
28
75
|
*/
|
|
29
|
-
function mergeSelection(list, seen, incoming) {
|
|
76
|
+
function mergeSelection(list, seen, incoming, ctx) {
|
|
30
77
|
const key = incoming.alias ?? incoming.fieldName;
|
|
31
78
|
const existing = seen.get(key);
|
|
32
79
|
if (!existing) {
|
|
@@ -38,6 +85,12 @@ function mergeSelection(list, seen, incoming) {
|
|
|
38
85
|
mergeFragmentField(existing, incoming);
|
|
39
86
|
return;
|
|
40
87
|
}
|
|
88
|
+
if (ctx.strict &&
|
|
89
|
+
(!sameArguments(existing, incoming) || existing.typeVariants)) {
|
|
90
|
+
mergeTypeVariant(existing, incoming, key, ctx);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const priorTypes = existing.typeConditions;
|
|
41
94
|
if (existing.typeConditions && incoming.typeConditions)
|
|
42
95
|
existing.typeConditions = [
|
|
43
96
|
...new Set([...existing.typeConditions, ...incoming.typeConditions]),
|
|
@@ -45,12 +98,51 @@ function mergeSelection(list, seen, incoming) {
|
|
|
45
98
|
// Also selected outside any fragment → unconditional.
|
|
46
99
|
else
|
|
47
100
|
delete existing.typeConditions;
|
|
48
|
-
if (!sameArguments(existing, incoming))
|
|
101
|
+
if (!sameArguments(existing, incoming)) {
|
|
102
|
+
// v2.6.0 — GraphQL rejects one response key selected with different
|
|
103
|
+
// arguments when both selections can apply to the same object; 2.x
|
|
104
|
+
// keeps the first selection and drops the second. Fragments on types
|
|
105
|
+
// that can never be the same object are valid GraphQL: no warning
|
|
106
|
+
// (strict projects each per type, see `mergeTypeVariant`).
|
|
107
|
+
if (!canApplyToSameObject(priorTypes, incoming.typeConditions, ctx))
|
|
108
|
+
return;
|
|
109
|
+
parseWarn(ctx, {
|
|
110
|
+
code: 'selection-key-arguments',
|
|
111
|
+
detail: key,
|
|
112
|
+
problem: `The selectionSet selects "${key}" twice with different arguments; ` +
|
|
113
|
+
`2.x keeps the first selection and drops the second.`,
|
|
114
|
+
fix: 'reject the conflict, as GraphQL validation does',
|
|
115
|
+
});
|
|
49
116
|
return;
|
|
117
|
+
}
|
|
118
|
+
mergeSameArguments(existing, incoming, ctx);
|
|
119
|
+
}
|
|
120
|
+
/** Merge a repeat of a response key with the same arguments (v2.5.3). */
|
|
121
|
+
function mergeSameArguments(existing, incoming, ctx) {
|
|
122
|
+
const key = incoming.alias ?? incoming.fieldName;
|
|
123
|
+
if (existing.isConnection) {
|
|
124
|
+
if (ctx.strict) {
|
|
125
|
+
mergeConnectionEdgeSets(existing, incoming, ctx);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!existing.totalCountOnly &&
|
|
129
|
+
!incoming.totalCountOnly &&
|
|
130
|
+
edgesKeyOf(existing) !== edgesKeyOf(incoming))
|
|
131
|
+
warnAliasedEdges(ctx, key, edgesKeyOf(existing), edgesKeyOf(incoming));
|
|
132
|
+
else
|
|
133
|
+
for (const which of ['node', 'properties']) {
|
|
134
|
+
const a = which === 'node' ? existing.children : existing.edgeChildren;
|
|
135
|
+
const b = which === 'node' ? incoming.children : incoming.edgeChildren;
|
|
136
|
+
const ka = existing.connectionAliases?.[which] ?? which;
|
|
137
|
+
const kb = incoming.connectionAliases?.[which] ?? which;
|
|
138
|
+
if (a && b && ka !== kb)
|
|
139
|
+
warnAliasedEdgeKey(ctx, key, which, ka, kb);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
50
142
|
if (incoming.children)
|
|
51
|
-
existing.children = mergeSelectionLists(existing.children, incoming.children);
|
|
143
|
+
existing.children = mergeSelectionLists(existing.children, incoming.children, ctx);
|
|
52
144
|
if (incoming.edgeChildren)
|
|
53
|
-
existing.edgeChildren = mergeSelectionLists(existing.edgeChildren, incoming.edgeChildren);
|
|
145
|
+
existing.edgeChildren = mergeSelectionLists(existing.edgeChildren, incoming.edgeChildren, ctx);
|
|
54
146
|
if (existing.isConnection) {
|
|
55
147
|
if (incoming.totalCount)
|
|
56
148
|
existing.totalCount = true;
|
|
@@ -63,6 +155,168 @@ function mergeSelection(list, seen, incoming) {
|
|
|
63
155
|
};
|
|
64
156
|
}
|
|
65
157
|
}
|
|
158
|
+
/** The response key of a connection's (first) `edges` selection. */
|
|
159
|
+
function edgesKeyOf(node) {
|
|
160
|
+
return node.connectionAliases?.edges ?? 'edges';
|
|
161
|
+
}
|
|
162
|
+
/** v2.6.0 — 2.x merges differently aliased `edges` under the first alias. */
|
|
163
|
+
function warnAliasedEdges(ctx, connection, first, second) {
|
|
164
|
+
parseWarn(ctx, {
|
|
165
|
+
code: 'selection-aliased-edges',
|
|
166
|
+
detail: `${connection}:${first}:${second}`,
|
|
167
|
+
problem: `"${connection}" selects \`edges\` under two response keys ("${first}", "${second}"); ` +
|
|
168
|
+
`2.x returns only "${first}", with both selections merged into it.`,
|
|
169
|
+
fix: 'return each `edges` alias with its own selection',
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
/** v2.6.0 — 2.x merges differently aliased `node` / `properties` keys. */
|
|
173
|
+
function warnAliasedEdgeKey(ctx, connection, which, first, second) {
|
|
174
|
+
parseWarn(ctx, {
|
|
175
|
+
code: 'selection-aliased-edge-key',
|
|
176
|
+
detail: `${connection}:${which}:${first}:${second}`,
|
|
177
|
+
problem: `"${connection}" selects \`${which}\` under two response keys ("${first}", "${second}") in its edges; ` +
|
|
178
|
+
`2.x returns only "${first}", with both selections merged into it.`,
|
|
179
|
+
fix: `return each \`${which}\` alias with its own selection`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/** The `node` (or `properties`) keys of an edge set, main key first. */
|
|
183
|
+
function edgeKeyEntries(set, which) {
|
|
184
|
+
const main = which === 'node' ? set.children : set.edgeChildren;
|
|
185
|
+
const extras = (which === 'node' ? set.extraNodes : set.extraProperties) ?? [];
|
|
186
|
+
return [
|
|
187
|
+
...(main
|
|
188
|
+
? [{ key: set.connectionAliases?.[which] ?? which, children: main }]
|
|
189
|
+
: []),
|
|
190
|
+
...extras,
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
/** Store `entries` (main key first) back on an edge set (strict). */
|
|
194
|
+
function setEdgeKeyEntries(set, which, entries) {
|
|
195
|
+
const [main, ...rest] = entries;
|
|
196
|
+
const extrasProp = which === 'node' ? 'extraNodes' : 'extraProperties';
|
|
197
|
+
if (which === 'node')
|
|
198
|
+
set.children = main?.children;
|
|
199
|
+
else
|
|
200
|
+
set.edgeChildren = main?.children;
|
|
201
|
+
if (!main)
|
|
202
|
+
if (which === 'node')
|
|
203
|
+
delete set.children;
|
|
204
|
+
else
|
|
205
|
+
delete set.edgeChildren;
|
|
206
|
+
if (main && main.key !== which)
|
|
207
|
+
set.connectionAliases = { ...set.connectionAliases, [which]: main.key };
|
|
208
|
+
else if (set.connectionAliases) {
|
|
209
|
+
delete set.connectionAliases[which];
|
|
210
|
+
if (Object.keys(set.connectionAliases).length === 0)
|
|
211
|
+
delete set.connectionAliases;
|
|
212
|
+
}
|
|
213
|
+
if (rest.length > 0)
|
|
214
|
+
set[extrasProp] = rest;
|
|
215
|
+
else
|
|
216
|
+
delete set[extrasProp];
|
|
217
|
+
}
|
|
218
|
+
/** Merge two edge sets' `node` / `properties` keys by response key (strict). */
|
|
219
|
+
function mergeEdgeKeys(into, from, ctx) {
|
|
220
|
+
for (const which of ['node', 'properties']) {
|
|
221
|
+
const merged = edgeKeyEntries(into, which).map((e) => ({ ...e }));
|
|
222
|
+
for (const entry of edgeKeyEntries(from, which)) {
|
|
223
|
+
const same = merged.find((m) => m.key === entry.key);
|
|
224
|
+
if (same)
|
|
225
|
+
same.children = mergeSelectionLists(same.children, entry.children, ctx);
|
|
226
|
+
else
|
|
227
|
+
merged.push({ ...entry });
|
|
228
|
+
}
|
|
229
|
+
setEdgeKeyEntries(into, which, merged);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** A connection's edges selection as a standalone edge set (strict). */
|
|
233
|
+
function asEdgeSet(node) {
|
|
234
|
+
if (node.totalCountOnly)
|
|
235
|
+
return null;
|
|
236
|
+
const set = {
|
|
237
|
+
fieldName: node.fieldName,
|
|
238
|
+
isScalar: false,
|
|
239
|
+
isRelationship: false,
|
|
240
|
+
isConnection: true,
|
|
241
|
+
};
|
|
242
|
+
if (node.children)
|
|
243
|
+
set.children = node.children;
|
|
244
|
+
if (node.edgeChildren)
|
|
245
|
+
set.edgeChildren = node.edgeChildren;
|
|
246
|
+
if (node.extraNodes)
|
|
247
|
+
set.extraNodes = node.extraNodes;
|
|
248
|
+
if (node.extraProperties)
|
|
249
|
+
set.extraProperties = node.extraProperties;
|
|
250
|
+
const { totalCount: _total, ...aliases } = node.connectionAliases ?? {};
|
|
251
|
+
if (Object.keys(aliases).length > 0)
|
|
252
|
+
set.connectionAliases = aliases;
|
|
253
|
+
return set;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* v2.6.0 — strict merge of a connection selected twice (same arguments):
|
|
257
|
+
* edge sets merge by response key, so `a: edges` and `b: edges` stay apart.
|
|
258
|
+
*/
|
|
259
|
+
function mergeConnectionEdgeSets(existing, incoming, ctx) {
|
|
260
|
+
const sets = new Map();
|
|
261
|
+
const add = (set) => {
|
|
262
|
+
const k = edgesKeyOf(set);
|
|
263
|
+
const prev = sets.get(k);
|
|
264
|
+
if (!prev) {
|
|
265
|
+
sets.set(k, {
|
|
266
|
+
...set,
|
|
267
|
+
...(set.connectionAliases
|
|
268
|
+
? { connectionAliases: { ...set.connectionAliases } }
|
|
269
|
+
: {}),
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// v2.6.0 — `node` / `properties` keys merge by response key too.
|
|
274
|
+
mergeEdgeKeys(prev, set, ctx);
|
|
275
|
+
};
|
|
276
|
+
for (const node of [existing, incoming]) {
|
|
277
|
+
const main = asEdgeSet(node);
|
|
278
|
+
if (main)
|
|
279
|
+
add(main);
|
|
280
|
+
for (const extra of node.extraEdges ?? [])
|
|
281
|
+
add(extra);
|
|
282
|
+
}
|
|
283
|
+
const totalAlias = existing.connectionAliases?.totalCount ??
|
|
284
|
+
incoming.connectionAliases?.totalCount;
|
|
285
|
+
if (incoming.totalCount)
|
|
286
|
+
existing.totalCount = true;
|
|
287
|
+
const [main, ...rest] = [...sets.values()];
|
|
288
|
+
if (main) {
|
|
289
|
+
delete existing.totalCountOnly;
|
|
290
|
+
if (main.children)
|
|
291
|
+
existing.children = main.children;
|
|
292
|
+
else
|
|
293
|
+
delete existing.children;
|
|
294
|
+
if (main.edgeChildren)
|
|
295
|
+
existing.edgeChildren = main.edgeChildren;
|
|
296
|
+
else
|
|
297
|
+
delete existing.edgeChildren;
|
|
298
|
+
if (main.extraNodes)
|
|
299
|
+
existing.extraNodes = main.extraNodes;
|
|
300
|
+
else
|
|
301
|
+
delete existing.extraNodes;
|
|
302
|
+
if (main.extraProperties)
|
|
303
|
+
existing.extraProperties = main.extraProperties;
|
|
304
|
+
else
|
|
305
|
+
delete existing.extraProperties;
|
|
306
|
+
}
|
|
307
|
+
const aliases = {
|
|
308
|
+
...(main?.connectionAliases ?? {}),
|
|
309
|
+
...(totalAlias !== undefined ? { totalCount: totalAlias } : {}),
|
|
310
|
+
};
|
|
311
|
+
if (Object.keys(aliases).length > 0)
|
|
312
|
+
existing.connectionAliases = aliases;
|
|
313
|
+
else
|
|
314
|
+
delete existing.connectionAliases;
|
|
315
|
+
if (rest.length > 0)
|
|
316
|
+
existing.extraEdges = rest;
|
|
317
|
+
else
|
|
318
|
+
delete existing.extraEdges;
|
|
319
|
+
}
|
|
66
320
|
/** Two scalars under one key, from fragments on different types (v2.5.3). */
|
|
67
321
|
function mergeFragmentField(existing, incoming) {
|
|
68
322
|
// Only scalars selected by inline fragments can differ per type; any
|
|
@@ -84,11 +338,11 @@ function mergeFragmentField(existing, incoming) {
|
|
|
84
338
|
...new Set([...existing.typeConditions, ...incoming.typeConditions]),
|
|
85
339
|
];
|
|
86
340
|
}
|
|
87
|
-
function mergeSelectionLists(base, extra) {
|
|
341
|
+
function mergeSelectionLists(base, extra, ctx) {
|
|
88
342
|
const list = [];
|
|
89
343
|
const seen = new Map();
|
|
90
344
|
for (const node of [...(base ?? []), ...extra])
|
|
91
|
-
mergeSelection(list, seen, node);
|
|
345
|
+
mergeSelection(list, seen, node, ctx);
|
|
92
346
|
return list;
|
|
93
347
|
}
|
|
94
348
|
/** selectionSet field arguments the OGM honours, by field kind (v2.5.0). */
|
|
@@ -107,6 +361,43 @@ function assertNoVariables(value, where) {
|
|
|
107
361
|
for (const f of value.fields)
|
|
108
362
|
assertNoVariables(f.value, where);
|
|
109
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* v2.6.0 — whether `@include` / `@skip` keep a selectionSet field or inline
|
|
366
|
+
* fragment. 2.x ignores both directives (the selection is always kept) and
|
|
367
|
+
* warns; strict honours a literal `if: true | false` and rejects anything
|
|
368
|
+
* else, `$variables` included (selectionSet strings have no variables).
|
|
369
|
+
*/
|
|
370
|
+
function keptByDirectives(directives, where, ctx) {
|
|
371
|
+
const relevant = (directives ?? []).filter((d) => d.name.value === 'include' || d.name.value === 'skip');
|
|
372
|
+
if (relevant.length === 0)
|
|
373
|
+
return true;
|
|
374
|
+
if (!ctx.strict) {
|
|
375
|
+
parseWarn(ctx, {
|
|
376
|
+
code: 'selection-include-skip',
|
|
377
|
+
detail: where,
|
|
378
|
+
problem: `@include / @skip on "${where}" in a selectionSet is ignored in 2.x: ` +
|
|
379
|
+
`the selection is always kept.`,
|
|
380
|
+
fix: 'honour @include / @skip with a literal `if: true | false`',
|
|
381
|
+
});
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
384
|
+
let kept = true;
|
|
385
|
+
for (const directive of relevant) {
|
|
386
|
+
const name = directive.name.value;
|
|
387
|
+
const args = directive.arguments ?? [];
|
|
388
|
+
const arg = args.length === 1 ? args[0] : undefined;
|
|
389
|
+
if (!arg || arg.name.value !== 'if')
|
|
390
|
+
throw new OGMError(`@${name} on "${where}" takes exactly one argument: if: true | false.`);
|
|
391
|
+
if (arg.value.kind === Kind.VARIABLE)
|
|
392
|
+
throw new OGMError(`GraphQL variables are not supported in selectionSet directives (@${name} on "${where}": ` +
|
|
393
|
+
`$${arg.value.name.value}). Use a literal true or false.`);
|
|
394
|
+
if (arg.value.kind !== Kind.BOOLEAN)
|
|
395
|
+
throw new OGMError(`@${name}(if:) on "${where}" must be a literal true or false.`);
|
|
396
|
+
if (name === 'include' ? !arg.value.value : arg.value.value)
|
|
397
|
+
kept = false;
|
|
398
|
+
}
|
|
399
|
+
return kept;
|
|
400
|
+
}
|
|
110
401
|
function assertNoArguments(field, where) {
|
|
111
402
|
if (field.arguments && field.arguments.length > 0)
|
|
112
403
|
throw new OGMError(`Unsupported argument "${field.arguments[0].name.value}" on "${where}". ` +
|
|
@@ -188,7 +479,7 @@ function applyFieldArguments(field, node) {
|
|
|
188
479
|
* connection fields with edge properties, and nested traversals with depth limiting.
|
|
189
480
|
*/
|
|
190
481
|
export class SelectionCompiler {
|
|
191
|
-
constructor(schema, whereCompiler) {
|
|
482
|
+
constructor(schema, whereCompiler, strictMode) {
|
|
192
483
|
this.schema = schema;
|
|
193
484
|
/**
|
|
194
485
|
* Real LRU cache of parsed selection sets, capped at
|
|
@@ -205,6 +496,17 @@ export class SelectionCompiler {
|
|
|
205
496
|
*/
|
|
206
497
|
this.parseCache = new Map();
|
|
207
498
|
this.whereCompiler = whereCompiler;
|
|
499
|
+
this.strictMode = strictMode ?? lenientStrictMode();
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* v2.6.0 — re-log (through the warn-once log) the strict-mode warnings
|
|
503
|
+
* found when `parsed` was first parsed. Parsed selections are cached —
|
|
504
|
+
* here and in `Model`'s cross-OGM cache — so a cache hit must still warn
|
|
505
|
+
* on an OGM instance that has not seen the shape yet.
|
|
506
|
+
*/
|
|
507
|
+
replayParseWarnings(parsed) {
|
|
508
|
+
for (const w of parseWarnings.get(parsed) ?? [])
|
|
509
|
+
this.strictMode.warn(w.code, w.detail, w.problem, w.fix);
|
|
208
510
|
}
|
|
209
511
|
/** Clear the parse cache. Useful in tests to prevent cross-test pollution. */
|
|
210
512
|
clearCache() {
|
|
@@ -285,7 +587,7 @@ export class SelectionCompiler {
|
|
|
285
587
|
}
|
|
286
588
|
assertSafeIdentifier(node.fieldName, 'selection field');
|
|
287
589
|
if (isAbstract && node.typeConditions) {
|
|
288
|
-
const conditional = this.compileFragmentScalar(node, key, nodeVar, nodeDef, cypherScope);
|
|
590
|
+
const conditional = this.compileFragmentScalar(node, key, nodeVar, nodeDef, cypherScope, params, paramCounter, policyContext);
|
|
289
591
|
if (conditional) {
|
|
290
592
|
parts.push(conditional);
|
|
291
593
|
continue;
|
|
@@ -312,6 +614,15 @@ export class SelectionCompiler {
|
|
|
312
614
|
: `.${escapeIdentifier(node.fieldName)}`);
|
|
313
615
|
continue;
|
|
314
616
|
}
|
|
617
|
+
// v2.6.0 — strict: one key selected with different arguments by
|
|
618
|
+
// fragments on disjoint types, projected per concrete type.
|
|
619
|
+
if (node.typeVariants && node.typeVariants.length > 0) {
|
|
620
|
+
const variantCypher = this.compileTypeVariants(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
621
|
+
if (variantCypher) {
|
|
622
|
+
parts.push(`${key}: ${variantCypher}`);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
315
626
|
// v2.5.3 — a relationship / connection field selected through an
|
|
316
627
|
// inline fragment on an abstract type that doesn't declare it.
|
|
317
628
|
// (Union relationship fields keep their per-member CASE below.)
|
|
@@ -361,8 +672,15 @@ export class SelectionCompiler {
|
|
|
361
672
|
* when the abstract type declares the field as `@cypher` itself, when a
|
|
362
673
|
* condition isn't a concrete type, or when no fragment type declares it
|
|
363
674
|
* as `@cypher`.
|
|
675
|
+
*
|
|
676
|
+
* v2.5.4 — under a policy context, each `@cypher` branch is also guarded
|
|
677
|
+
* by that member's own `read` clause (`WHEN n:B AND <B read> THEN …`),
|
|
678
|
+
* so a member's statement only projects for a node that passes the
|
|
679
|
+
* member's policy. The node filter (`compileTargetPolicyClause`) already
|
|
680
|
+
* requires every carried member's clause; this is defense in depth.
|
|
681
|
+
* Without a policy context the branches are unchanged.
|
|
364
682
|
*/
|
|
365
|
-
compileFragmentScalar(node, key, nodeVar, nodeDef, cypherScope) {
|
|
683
|
+
compileFragmentScalar(node, key, nodeVar, nodeDef, cypherScope, params, paramCounter, policyContext = null) {
|
|
366
684
|
// v2.5.3 — `fieldByType`: sibling fragments selected different fields
|
|
367
685
|
// under this key, so each type projects its own field.
|
|
368
686
|
const perType = node.fieldByType;
|
|
@@ -390,10 +708,28 @@ export class SelectionCompiler {
|
|
|
390
708
|
: buildInlineCypherFieldExpr(prop.cypherStatement, nodeVar, `${def.typeName}.${field}`);
|
|
391
709
|
else
|
|
392
710
|
expr = `${nodeVar}.${escapeIdentifier(field)}`;
|
|
393
|
-
|
|
711
|
+
const guard = prop.isCypher && prop.cypherStatement
|
|
712
|
+
? this.memberReadGuard(def, nodeVar, params, paramCounter, policyContext)
|
|
713
|
+
: '';
|
|
714
|
+
return `WHEN ${nodeVar}:${escapeIdentifier(def.label)}${guard} THEN ${expr}`;
|
|
394
715
|
});
|
|
395
716
|
return `${escapeIdentifier(key)}: CASE ${branches.join(' ')} END`;
|
|
396
717
|
}
|
|
718
|
+
/**
|
|
719
|
+
* ` AND <member read clause>` guarding a fragment `@cypher` branch
|
|
720
|
+
* (v2.5.4); `''` without a policy context, when the member has no read
|
|
721
|
+
* clause, or when that clause needs `@cypher` preludes a projection
|
|
722
|
+
* cannot host (the node filter still enforces it).
|
|
723
|
+
*/
|
|
724
|
+
memberReadGuard(def, nodeVar, params, paramCounter, policyContext) {
|
|
725
|
+
if (!policyContext || !this.whereCompiler || !params || !paramCounter)
|
|
726
|
+
return '';
|
|
727
|
+
const clause = this.whereCompiler.compileTargetPolicyClause(def.typeName, nodeVar, 'read', policyContext, paramCounter);
|
|
728
|
+
if (clause.cypher === null || clause.preludes.length > 0)
|
|
729
|
+
return '';
|
|
730
|
+
mergeParams(params, clause.params);
|
|
731
|
+
return ` AND ${clause.cypher}`;
|
|
732
|
+
}
|
|
397
733
|
/**
|
|
398
734
|
* Parse a GraphQL selection set string into SelectionNode[].
|
|
399
735
|
*
|
|
@@ -407,6 +743,7 @@ export class SelectionCompiler {
|
|
|
407
743
|
// order is the LRU iteration order we rely on for eviction.
|
|
408
744
|
this.parseCache.delete(selectionSet);
|
|
409
745
|
this.parseCache.set(selectionSet, cached);
|
|
746
|
+
this.replayParseWarnings(cached);
|
|
410
747
|
return cached;
|
|
411
748
|
}
|
|
412
749
|
const trimmed = selectionSet.trim();
|
|
@@ -425,7 +762,16 @@ export class SelectionCompiler {
|
|
|
425
762
|
const dummyField = opDef.selectionSet.selections[0];
|
|
426
763
|
if (dummyField.kind !== Kind.FIELD || !dummyField.selectionSet)
|
|
427
764
|
return [];
|
|
428
|
-
const
|
|
765
|
+
const ctx = {
|
|
766
|
+
strict: this.strictMode.strict,
|
|
767
|
+
warnings: [],
|
|
768
|
+
possibleTypes: (typeName) => this.possibleTypes(typeName),
|
|
769
|
+
};
|
|
770
|
+
const result = this.convertSelectionSet(dummyField.selectionSet, undefined, ctx);
|
|
771
|
+
if (ctx.warnings.length > 0) {
|
|
772
|
+
parseWarnings.set(result, ctx.warnings);
|
|
773
|
+
this.replayParseWarnings(result);
|
|
774
|
+
}
|
|
429
775
|
// Real-LRU eviction: drop the oldest entry when the cache is full,
|
|
430
776
|
// then insert. Pre-1.7.5 we just stopped caching at saturation,
|
|
431
777
|
// forcing every subsequent miss to re-parse — a silent perf cliff
|
|
@@ -438,12 +784,24 @@ export class SelectionCompiler {
|
|
|
438
784
|
this.parseCache.set(selectionSet, result);
|
|
439
785
|
return result;
|
|
440
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* v2.6.0 — the concrete types a fragment on `typeName` can apply to (see
|
|
789
|
+
* `ParseContext.possibleTypes`). Unknown names stand for themselves.
|
|
790
|
+
*/
|
|
791
|
+
possibleTypes(typeName) {
|
|
792
|
+
if (this.schema.nodes.has(typeName))
|
|
793
|
+
return [typeName];
|
|
794
|
+
const iface = this.schema.interfaces?.get(typeName);
|
|
795
|
+
if (iface)
|
|
796
|
+
return iface.implementedBy;
|
|
797
|
+
return this.schema.unions?.get(typeName) ?? [typeName];
|
|
798
|
+
}
|
|
441
799
|
/**
|
|
442
800
|
* Convert a GraphQL SelectionSetNode into SelectionNode[].
|
|
443
801
|
*/
|
|
444
802
|
convertSelectionSet(selectionSet,
|
|
445
803
|
/** Type condition of the enclosing inline fragment, if any (v2.4.8). */
|
|
446
|
-
typeCondition) {
|
|
804
|
+
typeCondition, ctx) {
|
|
447
805
|
const nodes = [];
|
|
448
806
|
const seen = new Map();
|
|
449
807
|
for (const sel of selectionSet.selections) {
|
|
@@ -454,15 +812,19 @@ export class SelectionCompiler {
|
|
|
454
812
|
if (sel.kind === Kind.INLINE_FRAGMENT) {
|
|
455
813
|
const fragment = sel;
|
|
456
814
|
const condition = fragment.typeCondition?.name.value ?? typeCondition;
|
|
815
|
+
if (!keptByDirectives(fragment.directives, `... on ${condition ?? '(inline fragment)'}`, ctx))
|
|
816
|
+
continue;
|
|
457
817
|
if (fragment.selectionSet)
|
|
458
|
-
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition))
|
|
459
|
-
mergeSelection(nodes, seen, merged);
|
|
818
|
+
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition, ctx))
|
|
819
|
+
mergeSelection(nodes, seen, merged, ctx);
|
|
460
820
|
continue;
|
|
461
821
|
}
|
|
462
822
|
if (sel.kind !== Kind.FIELD)
|
|
463
823
|
continue;
|
|
464
824
|
const field = sel;
|
|
465
825
|
const fieldName = field.name.value;
|
|
826
|
+
if (!keptByDirectives(field.directives, field.alias?.value ?? fieldName, ctx))
|
|
827
|
+
continue;
|
|
466
828
|
// v2.5.0 — fields are keyed by their OUTPUT name, so the same field
|
|
467
829
|
// under two aliases (`a: kids { .. } b: kids { .. }`) stays two
|
|
468
830
|
// selections. Pre-2.5.0 they collapsed into one.
|
|
@@ -488,30 +850,82 @@ export class SelectionCompiler {
|
|
|
488
850
|
if (hasChildren && field.selectionSet)
|
|
489
851
|
if (isConnection)
|
|
490
852
|
// For connections, parse edges -> node and edges -> properties
|
|
491
|
-
this.parseConnectionChildren(field.selectionSet, node);
|
|
853
|
+
this.parseConnectionChildren(field.selectionSet, node, ctx);
|
|
492
854
|
else
|
|
493
|
-
node.children = this.convertSelectionSet(field.selectionSet);
|
|
494
|
-
mergeSelection(nodes, seen, node);
|
|
855
|
+
node.children = this.convertSelectionSet(field.selectionSet, undefined, ctx);
|
|
856
|
+
mergeSelection(nodes, seen, node, ctx);
|
|
495
857
|
}
|
|
496
858
|
return nodes;
|
|
497
859
|
}
|
|
498
860
|
/**
|
|
499
861
|
* Parse the children of a connection field, extracting node and edge children.
|
|
500
862
|
*/
|
|
501
|
-
parseConnectionChildren(selectionSet, parentNode) {
|
|
863
|
+
parseConnectionChildren(selectionSet, parentNode, ctx) {
|
|
502
864
|
let selectsEdges = false;
|
|
865
|
+
// v2.6.0 — the response key of the first `edges`, and (strict) the edge
|
|
866
|
+
// sets of `edges` selected under other keys.
|
|
867
|
+
let firstEdgesKey;
|
|
868
|
+
const extras = new Map();
|
|
869
|
+
// v2.6.0 — the first `node` / `properties` key of each edge set.
|
|
870
|
+
const firstKeys = new Map();
|
|
871
|
+
const connectionName = parentNode.alias ?? parentNode.fieldName;
|
|
872
|
+
/**
|
|
873
|
+
* Where a `node` / `properties` selection under `key` goes: the edge
|
|
874
|
+
* set's own (first) key, or — strict, another key — its own entry.
|
|
875
|
+
*/
|
|
876
|
+
const edgeKeyTarget = (set, which, key) => {
|
|
877
|
+
const firsts = firstKeys.get(set) ?? {};
|
|
878
|
+
firstKeys.set(set, firsts);
|
|
879
|
+
firsts[which] ?? (firsts[which] = key);
|
|
880
|
+
if (firsts[which] === key)
|
|
881
|
+
return null;
|
|
882
|
+
if (!ctx.strict) {
|
|
883
|
+
warnAliasedEdgeKey(ctx, connectionName, which, firsts[which], key);
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const prop = which === 'node' ? 'extraNodes' : 'extraProperties';
|
|
887
|
+
const list = (set[prop] ?? (set[prop] = []));
|
|
888
|
+
let entry = list.find((e) => e.key === key);
|
|
889
|
+
if (!entry) {
|
|
890
|
+
assertSafeIdentifier(key, 'selection alias');
|
|
891
|
+
entry = { key, children: [] };
|
|
892
|
+
list.push(entry);
|
|
893
|
+
}
|
|
894
|
+
return entry;
|
|
895
|
+
};
|
|
503
896
|
for (const sel of selectionSet.selections) {
|
|
504
897
|
if (sel.kind !== Kind.FIELD)
|
|
505
898
|
continue;
|
|
506
899
|
const field = sel;
|
|
507
900
|
assertNoArguments(field, `${parentNode.fieldName}.${field.name.value}`);
|
|
901
|
+
if (!keptByDirectives(field.directives, `${parentNode.fieldName}.${field.name.value}`, ctx))
|
|
902
|
+
continue;
|
|
508
903
|
if (field.name.value === 'totalCount') {
|
|
509
904
|
parentNode.totalCount = true;
|
|
510
905
|
this.recordConnectionAlias(parentNode, 'totalCount', field);
|
|
511
906
|
}
|
|
907
|
+
let target = parentNode;
|
|
512
908
|
if (field.name.value === 'edges') {
|
|
513
909
|
selectsEdges = true;
|
|
514
|
-
|
|
910
|
+
const edgesKey = field.alias?.value ?? 'edges';
|
|
911
|
+
firstEdgesKey ?? (firstEdgesKey = edgesKey);
|
|
912
|
+
if (edgesKey !== firstEdgesKey)
|
|
913
|
+
if (ctx.strict) {
|
|
914
|
+
let extra = extras.get(edgesKey);
|
|
915
|
+
if (!extra) {
|
|
916
|
+
extra = {
|
|
917
|
+
fieldName: parentNode.fieldName,
|
|
918
|
+
isScalar: false,
|
|
919
|
+
isRelationship: false,
|
|
920
|
+
isConnection: true,
|
|
921
|
+
};
|
|
922
|
+
extras.set(edgesKey, extra);
|
|
923
|
+
}
|
|
924
|
+
target = extra;
|
|
925
|
+
}
|
|
926
|
+
else
|
|
927
|
+
warnAliasedEdges(ctx, parentNode.alias ?? parentNode.fieldName, firstEdgesKey, edgesKey);
|
|
928
|
+
this.recordConnectionAlias(target, 'edges', field);
|
|
515
929
|
}
|
|
516
930
|
if (field.name.value === 'edges' && field.selectionSet)
|
|
517
931
|
for (const edgeSel of field.selectionSet.selections) {
|
|
@@ -519,20 +933,36 @@ export class SelectionCompiler {
|
|
|
519
933
|
continue;
|
|
520
934
|
const edgeField = edgeSel;
|
|
521
935
|
assertNoArguments(edgeField, `${parentNode.fieldName}.edges.${edgeField.name.value}`);
|
|
936
|
+
if (!keptByDirectives(edgeField.directives, `${parentNode.fieldName}.edges.${edgeField.name.value}`, ctx))
|
|
937
|
+
continue;
|
|
522
938
|
// v2.5.3 — a repeated `edges { node { … } }` merges its
|
|
523
939
|
// sub-selections; pre-2.5.3 the last one replaced the others.
|
|
524
940
|
if (edgeField.name.value === 'node' && edgeField.selectionSet) {
|
|
525
|
-
|
|
526
|
-
|
|
941
|
+
const extra = edgeKeyTarget(target, 'node', edgeField.alias?.value ?? 'node');
|
|
942
|
+
const parsed = this.convertSelectionSet(edgeField.selectionSet, undefined, ctx);
|
|
943
|
+
if (extra)
|
|
944
|
+
extra.children = mergeSelectionLists(extra.children, parsed, ctx);
|
|
945
|
+
else {
|
|
946
|
+
this.recordConnectionAlias(target, 'node', edgeField);
|
|
947
|
+
target.children = mergeSelectionLists(target.children, parsed, ctx);
|
|
948
|
+
}
|
|
527
949
|
}
|
|
528
950
|
if (edgeField.name.value === 'properties' && edgeField.selectionSet) {
|
|
529
|
-
|
|
530
|
-
|
|
951
|
+
const extra = edgeKeyTarget(target, 'properties', edgeField.alias?.value ?? 'properties');
|
|
952
|
+
const parsed = this.convertSelectionSet(edgeField.selectionSet, undefined, ctx);
|
|
953
|
+
if (extra)
|
|
954
|
+
extra.children = mergeSelectionLists(extra.children, parsed, ctx);
|
|
955
|
+
else {
|
|
956
|
+
this.recordConnectionAlias(target, 'properties', edgeField);
|
|
957
|
+
target.edgeChildren = mergeSelectionLists(target.edgeChildren, parsed, ctx);
|
|
958
|
+
}
|
|
531
959
|
}
|
|
532
960
|
}
|
|
533
961
|
}
|
|
534
962
|
if (parentNode.totalCount && !selectsEdges)
|
|
535
963
|
parentNode.totalCountOnly = true;
|
|
964
|
+
if (extras.size > 0)
|
|
965
|
+
parentNode.extraEdges = [...extras.values()];
|
|
536
966
|
}
|
|
537
967
|
/**
|
|
538
968
|
* Keep the selectionSet alias of a connection key (v2.5.3). The first
|
|
@@ -558,12 +988,40 @@ export class SelectionCompiler {
|
|
|
558
988
|
* Returns `null` (unchanged behaviour) when no fragment type declares it.
|
|
559
989
|
*/
|
|
560
990
|
compileFragmentRelationship(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
991
|
+
const { branches, singular } = this.fragmentRelationshipBranches(node, node.typeConditions ?? [], nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
992
|
+
if (branches.length === 0)
|
|
993
|
+
return null;
|
|
994
|
+
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* v2.6.0 — strict: a relationship / connection key with per-type
|
|
998
|
+
* variants (`SelectionNode.typeVariants`): one `CASE` whose branches
|
|
999
|
+
* compile each variant, with its own arguments and the target's read
|
|
1000
|
+
* policy, against the concrete types its fragments apply to.
|
|
1001
|
+
*/
|
|
1002
|
+
compileTypeVariants(node, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1003
|
+
const branches = [];
|
|
1004
|
+
let singular = false;
|
|
1005
|
+
for (const variant of [node, ...(node.typeVariants ?? [])]) {
|
|
1006
|
+
const types = [
|
|
1007
|
+
...new Set((variant.typeConditions ?? []).flatMap((t) => this.possibleTypes(t))),
|
|
1008
|
+
];
|
|
1009
|
+
const part = this.fragmentRelationshipBranches(variant, types, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext);
|
|
1010
|
+
branches.push(...part.branches);
|
|
1011
|
+
singular || (singular = part.singular);
|
|
1012
|
+
}
|
|
1013
|
+
if (branches.length === 0)
|
|
1014
|
+
return null;
|
|
1015
|
+
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
1016
|
+
}
|
|
1017
|
+
/** `WHEN n:T THEN <projection>` per type of `typeNames` declaring the field. */
|
|
1018
|
+
fragmentRelationshipBranches(node, typeNames, nodeVar, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
561
1019
|
const relField = node.isConnection
|
|
562
1020
|
? node.fieldName.replace(/Connection$/, '')
|
|
563
1021
|
: node.fieldName;
|
|
564
1022
|
const branches = [];
|
|
565
1023
|
let singular = node.isConnection;
|
|
566
|
-
for (const typeName of
|
|
1024
|
+
for (const typeName of typeNames) {
|
|
567
1025
|
const def = this.schema.nodes.get(typeName);
|
|
568
1026
|
const relDef = def?.relationships.get(relField);
|
|
569
1027
|
if (!def || !relDef)
|
|
@@ -576,9 +1034,7 @@ export class SelectionCompiler {
|
|
|
576
1034
|
if (expr)
|
|
577
1035
|
branches.push(`WHEN ${nodeVar}:${escapeIdentifier(def.label)} THEN ${expr}`);
|
|
578
1036
|
}
|
|
579
|
-
|
|
580
|
-
return null;
|
|
581
|
-
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
1037
|
+
return { branches, singular };
|
|
582
1038
|
}
|
|
583
1039
|
/**
|
|
584
1040
|
* Compile a relationship field into a Cypher pattern comprehension.
|
|
@@ -811,6 +1267,11 @@ export class SelectionCompiler {
|
|
|
811
1267
|
mapParts.push(`${propertiesKey}: ${edgeVar} { ${edgeFields} }`);
|
|
812
1268
|
projectionKeys.push(propertiesKey);
|
|
813
1269
|
}
|
|
1270
|
+
// v2.6.0 — strict: further `node` / `properties` keys of these edges.
|
|
1271
|
+
for (const [k, expr] of this.extraEdgeKeyParts(node, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext)) {
|
|
1272
|
+
mapParts.push(`${k}: ${expr}`);
|
|
1273
|
+
projectionKeys.push(k);
|
|
1274
|
+
}
|
|
814
1275
|
// Inject synthetic sort scalars at the top level of the edge map so
|
|
815
1276
|
// apoc.coll.sortMulti (which only accepts top-level keys) can order the
|
|
816
1277
|
// collection. The outer list comprehension strips them on the way out.
|
|
@@ -957,7 +1418,12 @@ export class SelectionCompiler {
|
|
|
957
1418
|
: '';
|
|
958
1419
|
if (node.totalCountOnly)
|
|
959
1420
|
return `{ ${totalCount} }`;
|
|
960
|
-
|
|
1421
|
+
// v2.6.0 — strict: `edges` selected under further response keys, each
|
|
1422
|
+
// its own list over the same pattern and filter. Empty otherwise.
|
|
1423
|
+
const extraEdges = (node.extraEdges ?? [])
|
|
1424
|
+
.map((extra) => `, ${escapeIdentifier(edgesKeyOf(extra))}: [${pattern}${whereClause} | ${this.compileEdgeSetMap(extra, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext)}]`)
|
|
1425
|
+
.join('');
|
|
1426
|
+
const withTotal = `${extraEdges}${totalCount ? `, ${totalCount}` : ''}`;
|
|
961
1427
|
if (sortKeyTokens.length === 0)
|
|
962
1428
|
return `{ ${edgesKey}: [${pattern}${whereClause} | ${innerMap}]${withTotal} }`;
|
|
963
1429
|
// Re-project only the user-facing keys so synthetic __skN scalars don't
|
|
@@ -967,6 +1433,61 @@ export class SelectionCompiler {
|
|
|
967
1433
|
: `{}`;
|
|
968
1434
|
return `{ ${edgesKey}: [x IN apoc.coll.sortMulti([${pattern}${whereClause} | ${innerMap}], [${sortKeyTokens.join(', ')}]) | ${outerProjection}]${withTotal} }`;
|
|
969
1435
|
}
|
|
1436
|
+
/**
|
|
1437
|
+
* v2.6.0 — the `{ node: …, properties: … }` map of one strict-mode extra
|
|
1438
|
+
* edge set (see `SelectionNode.extraEdges`), mirroring `compileConnection`.
|
|
1439
|
+
*/
|
|
1440
|
+
compileEdgeSetMap(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1441
|
+
const parts = [];
|
|
1442
|
+
const aliases = set.connectionAliases ?? {};
|
|
1443
|
+
if (set.children && set.children.length > 0)
|
|
1444
|
+
parts.push(`${aliases.node ? escapeIdentifier(aliases.node) : 'node'}: ${this.compile(set.children, childVar, targetDef, maxDepth, currentDepth + 1, params, paramCounter, null, policyContext)}`);
|
|
1445
|
+
if (set.edgeChildren && set.edgeChildren.length > 0) {
|
|
1446
|
+
const edgeFields = set.edgeChildren
|
|
1447
|
+
.filter((c) => c.isScalar)
|
|
1448
|
+
.map((c) => {
|
|
1449
|
+
if (c.alias === undefined || c.alias === c.fieldName)
|
|
1450
|
+
return `.${escapeIdentifier(c.fieldName)}`;
|
|
1451
|
+
assertSafeIdentifier(c.alias, 'selection alias');
|
|
1452
|
+
return `${escapeIdentifier(c.alias)}: ${edgeVar}.${escapeIdentifier(c.fieldName)}`;
|
|
1453
|
+
})
|
|
1454
|
+
.join(', ');
|
|
1455
|
+
parts.push(`${aliases.properties ? escapeIdentifier(aliases.properties) : 'properties'}: ${edgeVar} { ${edgeFields} }`);
|
|
1456
|
+
}
|
|
1457
|
+
for (const [k, expr] of this.extraEdgeKeyParts(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext))
|
|
1458
|
+
parts.push(`${k}: ${expr}`);
|
|
1459
|
+
return `{ ${parts.join(', ')} }`;
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* v2.6.0 — strict: the projections of an edge set's further `node` /
|
|
1463
|
+
* `properties` keys (`SelectionNode.extraNodes` / `extraProperties`),
|
|
1464
|
+
* as `[escapedKey, expression]` pairs. Empty in 2.x default mode.
|
|
1465
|
+
*/
|
|
1466
|
+
extraEdgeKeyParts(set, childVar, edgeVar, targetDef, maxDepth, currentDepth, params, paramCounter, policyContext = null) {
|
|
1467
|
+
const out = [];
|
|
1468
|
+
for (const extra of set.extraNodes ?? [])
|
|
1469
|
+
if (extra.children.length > 0)
|
|
1470
|
+
out.push([
|
|
1471
|
+
escapeIdentifier(extra.key),
|
|
1472
|
+
this.compile(extra.children, childVar, targetDef, maxDepth, currentDepth + 1, params, paramCounter, null, policyContext),
|
|
1473
|
+
]);
|
|
1474
|
+
for (const extra of set.extraProperties ?? []) {
|
|
1475
|
+
const fields = extra.children
|
|
1476
|
+
.filter((c) => c.isScalar)
|
|
1477
|
+
.map((c) => {
|
|
1478
|
+
if (c.alias === undefined || c.alias === c.fieldName)
|
|
1479
|
+
return `.${escapeIdentifier(c.fieldName)}`;
|
|
1480
|
+
assertSafeIdentifier(c.alias, 'selection alias');
|
|
1481
|
+
return `${escapeIdentifier(c.alias)}: ${edgeVar}.${escapeIdentifier(c.fieldName)}`;
|
|
1482
|
+
});
|
|
1483
|
+
if (fields.length > 0)
|
|
1484
|
+
out.push([
|
|
1485
|
+
escapeIdentifier(extra.key),
|
|
1486
|
+
`${edgeVar} { ${fields.join(', ')} }`,
|
|
1487
|
+
]);
|
|
1488
|
+
}
|
|
1489
|
+
return out;
|
|
1490
|
+
}
|
|
970
1491
|
/**
|
|
971
1492
|
* AND-combine an optional user where (`select.where` / `connectionWhere`)
|
|
972
1493
|
* with the target type's `'read'` policy and emit a `WHERE …` fragment
|