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