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.
Files changed (65) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +123 -0
  3. package/dist/cjs/compilers/fulltext.compiler.d.ts +17 -1
  4. package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
  5. package/dist/cjs/compilers/fulltext.compiler.js +35 -3
  6. package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
  7. package/dist/cjs/compilers/mutation.compiler.d.ts +39 -0
  8. package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
  9. package/dist/cjs/compilers/mutation.compiler.js +161 -11
  10. package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
  11. package/dist/cjs/compilers/selection.compiler.d.ts +65 -1
  12. package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
  13. package/dist/cjs/compilers/selection.compiler.js +521 -25
  14. package/dist/cjs/compilers/selection.compiler.js.map +1 -1
  15. package/dist/cjs/compilers/where.compiler.d.ts +4 -0
  16. package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
  17. package/dist/cjs/compilers/where.compiler.js +23 -0
  18. package/dist/cjs/compilers/where.compiler.js.map +1 -1
  19. package/dist/cjs/interface-model.d.ts.map +1 -1
  20. package/dist/cjs/interface-model.js +8 -1
  21. package/dist/cjs/interface-model.js.map +1 -1
  22. package/dist/cjs/model.d.ts +7 -0
  23. package/dist/cjs/model.d.ts.map +1 -1
  24. package/dist/cjs/model.js +20 -4
  25. package/dist/cjs/model.js.map +1 -1
  26. package/dist/cjs/ogm.d.ts +14 -0
  27. package/dist/cjs/ogm.d.ts.map +1 -1
  28. package/dist/cjs/ogm.js +36 -2
  29. package/dist/cjs/ogm.js.map +1 -1
  30. package/dist/cjs/utils/strict-mode.d.ts +39 -0
  31. package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
  32. package/dist/cjs/utils/strict-mode.js +59 -0
  33. package/dist/cjs/utils/strict-mode.js.map +1 -0
  34. package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
  35. package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
  36. package/dist/esm/compilers/fulltext.compiler.js +35 -3
  37. package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
  38. package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
  39. package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
  40. package/dist/esm/compilers/mutation.compiler.js +161 -11
  41. package/dist/esm/compilers/mutation.compiler.js.map +1 -1
  42. package/dist/esm/compilers/selection.compiler.d.ts +65 -1
  43. package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
  44. package/dist/esm/compilers/selection.compiler.js +521 -25
  45. package/dist/esm/compilers/selection.compiler.js.map +1 -1
  46. package/dist/esm/compilers/where.compiler.d.ts +4 -0
  47. package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
  48. package/dist/esm/compilers/where.compiler.js +23 -0
  49. package/dist/esm/compilers/where.compiler.js.map +1 -1
  50. package/dist/esm/interface-model.d.ts.map +1 -1
  51. package/dist/esm/interface-model.js +8 -1
  52. package/dist/esm/interface-model.js.map +1 -1
  53. package/dist/esm/model.d.ts +7 -0
  54. package/dist/esm/model.d.ts.map +1 -1
  55. package/dist/esm/model.js +20 -4
  56. package/dist/esm/model.js.map +1 -1
  57. package/dist/esm/ogm.d.ts +14 -0
  58. package/dist/esm/ogm.d.ts.map +1 -1
  59. package/dist/esm/ogm.js +36 -2
  60. package/dist/esm/ogm.js.map +1 -1
  61. package/dist/esm/utils/strict-mode.d.ts +39 -0
  62. package/dist/esm/utils/strict-mode.d.ts.map +1 -0
  63. package/dist/esm/utils/strict-mode.js +54 -0
  64. package/dist/esm/utils/strict-mode.js.map +1 -0
  65. 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() {
@@ -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.)
@@ -435,6 +746,7 @@ class SelectionCompiler {
435
746
  // order is the LRU iteration order we rely on for eviction.
436
747
  this.parseCache.delete(selectionSet);
437
748
  this.parseCache.set(selectionSet, cached);
749
+ this.replayParseWarnings(cached);
438
750
  return cached;
439
751
  }
440
752
  const trimmed = selectionSet.trim();
@@ -453,7 +765,16 @@ class SelectionCompiler {
453
765
  const dummyField = opDef.selectionSet.selections[0];
454
766
  if (dummyField.kind !== graphql_1.Kind.FIELD || !dummyField.selectionSet)
455
767
  return [];
456
- const result = this.convertSelectionSet(dummyField.selectionSet);
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
+ }
457
778
  // Real-LRU eviction: drop the oldest entry when the cache is full,
458
779
  // then insert. Pre-1.7.5 we just stopped caching at saturation,
459
780
  // forcing every subsequent miss to re-parse — a silent perf cliff
@@ -466,12 +787,24 @@ class SelectionCompiler {
466
787
  this.parseCache.set(selectionSet, result);
467
788
  return result;
468
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
+ }
469
802
  /**
470
803
  * Convert a GraphQL SelectionSetNode into SelectionNode[].
471
804
  */
472
805
  convertSelectionSet(selectionSet,
473
806
  /** Type condition of the enclosing inline fragment, if any (v2.4.8). */
474
- typeCondition) {
807
+ typeCondition, ctx) {
475
808
  const nodes = [];
476
809
  const seen = new Map();
477
810
  for (const sel of selectionSet.selections) {
@@ -482,15 +815,19 @@ class SelectionCompiler {
482
815
  if (sel.kind === graphql_1.Kind.INLINE_FRAGMENT) {
483
816
  const fragment = sel;
484
817
  const condition = fragment.typeCondition?.name.value ?? typeCondition;
818
+ if (!keptByDirectives(fragment.directives, `... on ${condition ?? '(inline fragment)'}`, ctx))
819
+ continue;
485
820
  if (fragment.selectionSet)
486
- for (const merged of this.convertSelectionSet(fragment.selectionSet, condition))
487
- mergeSelection(nodes, seen, merged);
821
+ for (const merged of this.convertSelectionSet(fragment.selectionSet, condition, ctx))
822
+ mergeSelection(nodes, seen, merged, ctx);
488
823
  continue;
489
824
  }
490
825
  if (sel.kind !== graphql_1.Kind.FIELD)
491
826
  continue;
492
827
  const field = sel;
493
828
  const fieldName = field.name.value;
829
+ if (!keptByDirectives(field.directives, field.alias?.value ?? fieldName, ctx))
830
+ continue;
494
831
  // v2.5.0 — fields are keyed by their OUTPUT name, so the same field
495
832
  // under two aliases (`a: kids { .. } b: kids { .. }`) stays two
496
833
  // selections. Pre-2.5.0 they collapsed into one.
@@ -516,30 +853,82 @@ class SelectionCompiler {
516
853
  if (hasChildren && field.selectionSet)
517
854
  if (isConnection)
518
855
  // For connections, parse edges -> node and edges -> properties
519
- this.parseConnectionChildren(field.selectionSet, node);
856
+ this.parseConnectionChildren(field.selectionSet, node, ctx);
520
857
  else
521
- node.children = this.convertSelectionSet(field.selectionSet);
522
- mergeSelection(nodes, seen, node);
858
+ node.children = this.convertSelectionSet(field.selectionSet, undefined, ctx);
859
+ mergeSelection(nodes, seen, node, ctx);
523
860
  }
524
861
  return nodes;
525
862
  }
526
863
  /**
527
864
  * Parse the children of a connection field, extracting node and edge children.
528
865
  */
529
- parseConnectionChildren(selectionSet, parentNode) {
866
+ parseConnectionChildren(selectionSet, parentNode, ctx) {
530
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
+ };
531
899
  for (const sel of selectionSet.selections) {
532
900
  if (sel.kind !== graphql_1.Kind.FIELD)
533
901
  continue;
534
902
  const field = sel;
535
903
  assertNoArguments(field, `${parentNode.fieldName}.${field.name.value}`);
904
+ if (!keptByDirectives(field.directives, `${parentNode.fieldName}.${field.name.value}`, ctx))
905
+ continue;
536
906
  if (field.name.value === 'totalCount') {
537
907
  parentNode.totalCount = true;
538
908
  this.recordConnectionAlias(parentNode, 'totalCount', field);
539
909
  }
910
+ let target = parentNode;
540
911
  if (field.name.value === 'edges') {
541
912
  selectsEdges = true;
542
- this.recordConnectionAlias(parentNode, 'edges', field);
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);
543
932
  }
544
933
  if (field.name.value === 'edges' && field.selectionSet)
545
934
  for (const edgeSel of field.selectionSet.selections) {
@@ -547,20 +936,36 @@ class SelectionCompiler {
547
936
  continue;
548
937
  const edgeField = edgeSel;
549
938
  assertNoArguments(edgeField, `${parentNode.fieldName}.edges.${edgeField.name.value}`);
939
+ if (!keptByDirectives(edgeField.directives, `${parentNode.fieldName}.edges.${edgeField.name.value}`, ctx))
940
+ continue;
550
941
  // v2.5.3 — a repeated `edges { node { … } }` merges its
551
942
  // sub-selections; pre-2.5.3 the last one replaced the others.
552
943
  if (edgeField.name.value === 'node' && edgeField.selectionSet) {
553
- this.recordConnectionAlias(parentNode, 'node', edgeField);
554
- parentNode.children = mergeSelectionLists(parentNode.children, this.convertSelectionSet(edgeField.selectionSet));
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
+ }
555
952
  }
556
953
  if (edgeField.name.value === 'properties' && edgeField.selectionSet) {
557
- this.recordConnectionAlias(parentNode, 'properties', edgeField);
558
- parentNode.edgeChildren = mergeSelectionLists(parentNode.edgeChildren, this.convertSelectionSet(edgeField.selectionSet));
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
+ }
559
962
  }
560
963
  }
561
964
  }
562
965
  if (parentNode.totalCount && !selectsEdges)
563
966
  parentNode.totalCountOnly = true;
967
+ if (extras.size > 0)
968
+ parentNode.extraEdges = [...extras.values()];
564
969
  }
565
970
  /**
566
971
  * Keep the selectionSet alias of a connection key (v2.5.3). The first
@@ -586,12 +991,40 @@ class SelectionCompiler {
586
991
  * Returns `null` (unchanged behaviour) when no fragment type declares it.
587
992
  */
588
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) {
589
1022
  const relField = node.isConnection
590
1023
  ? node.fieldName.replace(/Connection$/, '')
591
1024
  : node.fieldName;
592
1025
  const branches = [];
593
1026
  let singular = node.isConnection;
594
- for (const typeName of node.typeConditions ?? []) {
1027
+ for (const typeName of typeNames) {
595
1028
  const def = this.schema.nodes.get(typeName);
596
1029
  const relDef = def?.relationships.get(relField);
597
1030
  if (!def || !relDef)
@@ -604,9 +1037,7 @@ class SelectionCompiler {
604
1037
  if (expr)
605
1038
  branches.push(`WHEN ${nodeVar}:${(0, validation_1.escapeIdentifier)(def.label)} THEN ${expr}`);
606
1039
  }
607
- if (branches.length === 0)
608
- return null;
609
- return `CASE ${branches.join(' ')} ELSE ${singular ? 'null' : '[]'} END`;
1040
+ return { branches, singular };
610
1041
  }
611
1042
  /**
612
1043
  * Compile a relationship field into a Cypher pattern comprehension.
@@ -839,6 +1270,11 @@ class SelectionCompiler {
839
1270
  mapParts.push(`${propertiesKey}: ${edgeVar} { ${edgeFields} }`);
840
1271
  projectionKeys.push(propertiesKey);
841
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
+ }
842
1278
  // Inject synthetic sort scalars at the top level of the edge map so
843
1279
  // apoc.coll.sortMulti (which only accepts top-level keys) can order the
844
1280
  // collection. The outer list comprehension strips them on the way out.
@@ -985,7 +1421,12 @@ class SelectionCompiler {
985
1421
  : '';
986
1422
  if (node.totalCountOnly)
987
1423
  return `{ ${totalCount} }`;
988
- const withTotal = totalCount ? `, ${totalCount}` : '';
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}` : ''}`;
989
1430
  if (sortKeyTokens.length === 0)
990
1431
  return `{ ${edgesKey}: [${pattern}${whereClause} | ${innerMap}]${withTotal} }`;
991
1432
  // Re-project only the user-facing keys so synthetic __skN scalars don't
@@ -995,6 +1436,61 @@ class SelectionCompiler {
995
1436
  : `{}`;
996
1437
  return `{ ${edgesKey}: [x IN apoc.coll.sortMulti([${pattern}${whereClause} | ${innerMap}], [${sortKeyTokens.join(', ')}]) | ${outerProjection}]${withTotal} }`;
997
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
+ }
998
1494
  /**
999
1495
  * AND-combine an optional user where (`select.where` / `connectionWhere`)
1000
1496
  * with the target type's `'read'` policy and emit a `WHERE …` fragment