grafeo-ogm 2.5.4 → 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 +54 -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 +161 -11
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +65 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +521 -25
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +4 -0
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +23 -0
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +8 -1
- 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 +36 -2
- 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 +161 -11
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +65 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +521 -25
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +4 -0
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +23 -0
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +8 -1
- 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 +36 -2
- 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() {
|
|
@@ -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.)
|
|
@@ -432,6 +743,7 @@ export class SelectionCompiler {
|
|
|
432
743
|
// order is the LRU iteration order we rely on for eviction.
|
|
433
744
|
this.parseCache.delete(selectionSet);
|
|
434
745
|
this.parseCache.set(selectionSet, cached);
|
|
746
|
+
this.replayParseWarnings(cached);
|
|
435
747
|
return cached;
|
|
436
748
|
}
|
|
437
749
|
const trimmed = selectionSet.trim();
|
|
@@ -450,7 +762,16 @@ export class SelectionCompiler {
|
|
|
450
762
|
const dummyField = opDef.selectionSet.selections[0];
|
|
451
763
|
if (dummyField.kind !== Kind.FIELD || !dummyField.selectionSet)
|
|
452
764
|
return [];
|
|
453
|
-
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
|
+
}
|
|
454
775
|
// Real-LRU eviction: drop the oldest entry when the cache is full,
|
|
455
776
|
// then insert. Pre-1.7.5 we just stopped caching at saturation,
|
|
456
777
|
// forcing every subsequent miss to re-parse — a silent perf cliff
|
|
@@ -463,12 +784,24 @@ export class SelectionCompiler {
|
|
|
463
784
|
this.parseCache.set(selectionSet, result);
|
|
464
785
|
return result;
|
|
465
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
|
+
}
|
|
466
799
|
/**
|
|
467
800
|
* Convert a GraphQL SelectionSetNode into SelectionNode[].
|
|
468
801
|
*/
|
|
469
802
|
convertSelectionSet(selectionSet,
|
|
470
803
|
/** Type condition of the enclosing inline fragment, if any (v2.4.8). */
|
|
471
|
-
typeCondition) {
|
|
804
|
+
typeCondition, ctx) {
|
|
472
805
|
const nodes = [];
|
|
473
806
|
const seen = new Map();
|
|
474
807
|
for (const sel of selectionSet.selections) {
|
|
@@ -479,15 +812,19 @@ export class SelectionCompiler {
|
|
|
479
812
|
if (sel.kind === Kind.INLINE_FRAGMENT) {
|
|
480
813
|
const fragment = sel;
|
|
481
814
|
const condition = fragment.typeCondition?.name.value ?? typeCondition;
|
|
815
|
+
if (!keptByDirectives(fragment.directives, `... on ${condition ?? '(inline fragment)'}`, ctx))
|
|
816
|
+
continue;
|
|
482
817
|
if (fragment.selectionSet)
|
|
483
|
-
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition))
|
|
484
|
-
mergeSelection(nodes, seen, merged);
|
|
818
|
+
for (const merged of this.convertSelectionSet(fragment.selectionSet, condition, ctx))
|
|
819
|
+
mergeSelection(nodes, seen, merged, ctx);
|
|
485
820
|
continue;
|
|
486
821
|
}
|
|
487
822
|
if (sel.kind !== Kind.FIELD)
|
|
488
823
|
continue;
|
|
489
824
|
const field = sel;
|
|
490
825
|
const fieldName = field.name.value;
|
|
826
|
+
if (!keptByDirectives(field.directives, field.alias?.value ?? fieldName, ctx))
|
|
827
|
+
continue;
|
|
491
828
|
// v2.5.0 — fields are keyed by their OUTPUT name, so the same field
|
|
492
829
|
// under two aliases (`a: kids { .. } b: kids { .. }`) stays two
|
|
493
830
|
// selections. Pre-2.5.0 they collapsed into one.
|
|
@@ -513,30 +850,82 @@ export class SelectionCompiler {
|
|
|
513
850
|
if (hasChildren && field.selectionSet)
|
|
514
851
|
if (isConnection)
|
|
515
852
|
// For connections, parse edges -> node and edges -> properties
|
|
516
|
-
this.parseConnectionChildren(field.selectionSet, node);
|
|
853
|
+
this.parseConnectionChildren(field.selectionSet, node, ctx);
|
|
517
854
|
else
|
|
518
|
-
node.children = this.convertSelectionSet(field.selectionSet);
|
|
519
|
-
mergeSelection(nodes, seen, node);
|
|
855
|
+
node.children = this.convertSelectionSet(field.selectionSet, undefined, ctx);
|
|
856
|
+
mergeSelection(nodes, seen, node, ctx);
|
|
520
857
|
}
|
|
521
858
|
return nodes;
|
|
522
859
|
}
|
|
523
860
|
/**
|
|
524
861
|
* Parse the children of a connection field, extracting node and edge children.
|
|
525
862
|
*/
|
|
526
|
-
parseConnectionChildren(selectionSet, parentNode) {
|
|
863
|
+
parseConnectionChildren(selectionSet, parentNode, ctx) {
|
|
527
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
|
+
};
|
|
528
896
|
for (const sel of selectionSet.selections) {
|
|
529
897
|
if (sel.kind !== Kind.FIELD)
|
|
530
898
|
continue;
|
|
531
899
|
const field = sel;
|
|
532
900
|
assertNoArguments(field, `${parentNode.fieldName}.${field.name.value}`);
|
|
901
|
+
if (!keptByDirectives(field.directives, `${parentNode.fieldName}.${field.name.value}`, ctx))
|
|
902
|
+
continue;
|
|
533
903
|
if (field.name.value === 'totalCount') {
|
|
534
904
|
parentNode.totalCount = true;
|
|
535
905
|
this.recordConnectionAlias(parentNode, 'totalCount', field);
|
|
536
906
|
}
|
|
907
|
+
let target = parentNode;
|
|
537
908
|
if (field.name.value === 'edges') {
|
|
538
909
|
selectsEdges = true;
|
|
539
|
-
|
|
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);
|
|
540
929
|
}
|
|
541
930
|
if (field.name.value === 'edges' && field.selectionSet)
|
|
542
931
|
for (const edgeSel of field.selectionSet.selections) {
|
|
@@ -544,20 +933,36 @@ export class SelectionCompiler {
|
|
|
544
933
|
continue;
|
|
545
934
|
const edgeField = edgeSel;
|
|
546
935
|
assertNoArguments(edgeField, `${parentNode.fieldName}.edges.${edgeField.name.value}`);
|
|
936
|
+
if (!keptByDirectives(edgeField.directives, `${parentNode.fieldName}.edges.${edgeField.name.value}`, ctx))
|
|
937
|
+
continue;
|
|
547
938
|
// v2.5.3 — a repeated `edges { node { … } }` merges its
|
|
548
939
|
// sub-selections; pre-2.5.3 the last one replaced the others.
|
|
549
940
|
if (edgeField.name.value === 'node' && edgeField.selectionSet) {
|
|
550
|
-
|
|
551
|
-
|
|
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
|
+
}
|
|
552
949
|
}
|
|
553
950
|
if (edgeField.name.value === 'properties' && edgeField.selectionSet) {
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
}
|
|
556
959
|
}
|
|
557
960
|
}
|
|
558
961
|
}
|
|
559
962
|
if (parentNode.totalCount && !selectsEdges)
|
|
560
963
|
parentNode.totalCountOnly = true;
|
|
964
|
+
if (extras.size > 0)
|
|
965
|
+
parentNode.extraEdges = [...extras.values()];
|
|
561
966
|
}
|
|
562
967
|
/**
|
|
563
968
|
* Keep the selectionSet alias of a connection key (v2.5.3). The first
|
|
@@ -583,12 +988,40 @@ export class SelectionCompiler {
|
|
|
583
988
|
* Returns `null` (unchanged behaviour) when no fragment type declares it.
|
|
584
989
|
*/
|
|
585
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) {
|
|
586
1019
|
const relField = node.isConnection
|
|
587
1020
|
? node.fieldName.replace(/Connection$/, '')
|
|
588
1021
|
: node.fieldName;
|
|
589
1022
|
const branches = [];
|
|
590
1023
|
let singular = node.isConnection;
|
|
591
|
-
for (const typeName of
|
|
1024
|
+
for (const typeName of typeNames) {
|
|
592
1025
|
const def = this.schema.nodes.get(typeName);
|
|
593
1026
|
const relDef = def?.relationships.get(relField);
|
|
594
1027
|
if (!def || !relDef)
|
|
@@ -601,9 +1034,7 @@ export class SelectionCompiler {
|
|
|
601
1034
|
if (expr)
|
|
602
1035
|
branches.push(`WHEN ${nodeVar}:${escapeIdentifier(def.label)} THEN ${expr}`);
|
|
603
1036
|
}
|
|
604
|
-
|
|
605
|
-
return null;
|
|
606
|
-
return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
|
|
1037
|
+
return { branches, singular };
|
|
607
1038
|
}
|
|
608
1039
|
/**
|
|
609
1040
|
* Compile a relationship field into a Cypher pattern comprehension.
|
|
@@ -836,6 +1267,11 @@ export class SelectionCompiler {
|
|
|
836
1267
|
mapParts.push(`${propertiesKey}: ${edgeVar} { ${edgeFields} }`);
|
|
837
1268
|
projectionKeys.push(propertiesKey);
|
|
838
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
|
+
}
|
|
839
1275
|
// Inject synthetic sort scalars at the top level of the edge map so
|
|
840
1276
|
// apoc.coll.sortMulti (which only accepts top-level keys) can order the
|
|
841
1277
|
// collection. The outer list comprehension strips them on the way out.
|
|
@@ -982,7 +1418,12 @@ export class SelectionCompiler {
|
|
|
982
1418
|
: '';
|
|
983
1419
|
if (node.totalCountOnly)
|
|
984
1420
|
return `{ ${totalCount} }`;
|
|
985
|
-
|
|
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}` : ''}`;
|
|
986
1427
|
if (sortKeyTokens.length === 0)
|
|
987
1428
|
return `{ ${edgesKey}: [${pattern}${whereClause} | ${innerMap}]${withTotal} }`;
|
|
988
1429
|
// Re-project only the user-facing keys so synthetic __skN scalars don't
|
|
@@ -992,6 +1433,61 @@ export class SelectionCompiler {
|
|
|
992
1433
|
: `{}`;
|
|
993
1434
|
return `{ ${edgesKey}: [x IN apoc.coll.sortMulti([${pattern}${whereClause} | ${innerMap}], [${sortKeyTokens.join(', ')}]) | ${outerProjection}]${withTotal} }`;
|
|
994
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
|
+
}
|
|
995
1491
|
/**
|
|
996
1492
|
* AND-combine an optional user where (`select.where` / `connectionWhere`)
|
|
997
1493
|
* with the target type's `'read'` policy and emit a `WHERE …` fragment
|