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.
Files changed (67) hide show
  1. package/CHANGELOG.md +66 -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 +165 -14
  10. package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
  11. package/dist/cjs/compilers/selection.compiler.d.ts +79 -1
  12. package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
  13. package/dist/cjs/compilers/selection.compiler.js +549 -28
  14. package/dist/cjs/compilers/selection.compiler.js.map +1 -1
  15. package/dist/cjs/compilers/where.compiler.d.ts +21 -7
  16. package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
  17. package/dist/cjs/compilers/where.compiler.js +47 -13
  18. package/dist/cjs/compilers/where.compiler.js.map +1 -1
  19. package/dist/cjs/interface-model.d.ts +14 -10
  20. package/dist/cjs/interface-model.d.ts.map +1 -1
  21. package/dist/cjs/interface-model.js +25 -14
  22. package/dist/cjs/interface-model.js.map +1 -1
  23. package/dist/cjs/model.d.ts +7 -0
  24. package/dist/cjs/model.d.ts.map +1 -1
  25. package/dist/cjs/model.js +20 -4
  26. package/dist/cjs/model.js.map +1 -1
  27. package/dist/cjs/ogm.d.ts +14 -0
  28. package/dist/cjs/ogm.d.ts.map +1 -1
  29. package/dist/cjs/ogm.js +37 -3
  30. package/dist/cjs/ogm.js.map +1 -1
  31. package/dist/cjs/utils/strict-mode.d.ts +39 -0
  32. package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
  33. package/dist/cjs/utils/strict-mode.js +59 -0
  34. package/dist/cjs/utils/strict-mode.js.map +1 -0
  35. package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
  36. package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
  37. package/dist/esm/compilers/fulltext.compiler.js +35 -3
  38. package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
  39. package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
  40. package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
  41. package/dist/esm/compilers/mutation.compiler.js +165 -14
  42. package/dist/esm/compilers/mutation.compiler.js.map +1 -1
  43. package/dist/esm/compilers/selection.compiler.d.ts +79 -1
  44. package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
  45. package/dist/esm/compilers/selection.compiler.js +549 -28
  46. package/dist/esm/compilers/selection.compiler.js.map +1 -1
  47. package/dist/esm/compilers/where.compiler.d.ts +21 -7
  48. package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
  49. package/dist/esm/compilers/where.compiler.js +47 -13
  50. package/dist/esm/compilers/where.compiler.js.map +1 -1
  51. package/dist/esm/interface-model.d.ts +14 -10
  52. package/dist/esm/interface-model.d.ts.map +1 -1
  53. package/dist/esm/interface-model.js +25 -14
  54. package/dist/esm/interface-model.js.map +1 -1
  55. package/dist/esm/model.d.ts +7 -0
  56. package/dist/esm/model.d.ts.map +1 -1
  57. package/dist/esm/model.js +20 -4
  58. package/dist/esm/model.js.map +1 -1
  59. package/dist/esm/ogm.d.ts +14 -0
  60. package/dist/esm/ogm.d.ts.map +1 -1
  61. package/dist/esm/ogm.js +37 -3
  62. package/dist/esm/ogm.js.map +1 -1
  63. package/dist/esm/utils/strict-mode.d.ts +39 -0
  64. package/dist/esm/utils/strict-mode.d.ts.map +1 -0
  65. package/dist/esm/utils/strict-mode.js +54 -0
  66. package/dist/esm/utils/strict-mode.js.map +1 -0
  67. package/package.json +1 -1
@@ -11,6 +11,7 @@ const types_1 = require("../policy/types");
11
11
  const utils_1 = require("../schema/utils");
12
12
  const validation_1 = require("../utils/validation");
13
13
  const write_coercion_1 = require("../utils/write-coercion");
14
+ const strict_mode_1 = require("../utils/strict-mode");
14
15
  const where_compiler_1 = require("./where.compiler");
15
16
  /** The root node `n` of an update — the pre-2.4.8 names, unprefixed. */
16
17
  const TOP_SCOPE = {
@@ -67,6 +68,7 @@ class MutationCompiler {
67
68
  this.narrowedRelDefs = new WeakSet();
68
69
  this.logger = options?.logger;
69
70
  this.policyWhereCompiler = options?.whereCompiler;
71
+ this.strictMode = options?.strictMode ?? (0, strict_mode_1.lenientStrictMode)();
70
72
  }
71
73
  /**
72
74
  * Resolve and compile the TARGET type's `read` policy predicate for a
@@ -117,9 +119,10 @@ class MutationCompiler {
117
119
  reason: 'no-permissive-matched',
118
120
  });
119
121
  }
120
- // Abstract (interface / union) targets get a CASE over their concrete
121
- // members' own clauses — pre-2.3.0 only the abstract type's own
122
- // policies (usually none) were applied.
122
+ // Abstract (interface / union) targets get a conjunction over their
123
+ // concrete members' own clauses — every member label the node carries
124
+ // must pass (v2.5.4; pre-2.5.4 a first-match CASE) — pre-2.3.0 only
125
+ // the abstract type's own policies (usually none) were applied.
123
126
  const clause = this.getPolicyWhereCompiler().compileTargetPolicyClause(targetNodeDef.typeName, targetVar, op, policyContext, counter);
124
127
  if (clause.preludes.length > 0)
125
128
  throw new errors_1.OGMError(`Policy on "${targetNodeDef.typeName}" requires @cypher field ` +
@@ -284,6 +287,68 @@ class MutationCompiler {
284
287
  throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
285
288
  return targetNodeDef;
286
289
  }
290
+ /**
291
+ * v2.6.0 — keys of a connect / disconnect item that the compiler does not
292
+ * read. Pre-2.6.0 (and still by default) they are ignored, so a typo such
293
+ * as `{ wehre: … }` compiles to an item with NO filter: a connect links
294
+ * every target node, a disconnect unlinks every related one. Default: one
295
+ * warning per OGM; strict: `OGMError`, like an unknown write field.
296
+ */
297
+ checkLinkItemKeys(item, kind, relDef) {
298
+ const allowed = kind === 'connect' ? CONNECT_ITEM_KEYS : DISCONNECT_ITEM_KEYS;
299
+ const unknown = Object.keys(item).filter((k) => item[k] !== undefined && !allowed.has(k));
300
+ if (unknown.length === 0)
301
+ return;
302
+ const list = unknown.map((k) => `"${k}"`).join(', ');
303
+ const allowedList = [...allowed].map((k) => `"${k}"`).join(', ');
304
+ if (this.strictMode.strict)
305
+ throw new errors_1.OGMError(`Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}". Allowed: ${allowedList}.`);
306
+ const unfiltered = item.where == null;
307
+ this.strictMode.warn(`${kind}-unknown-key`, `${relDef.fieldName}\u0000${unknown.join(',')}`, `Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}" ` +
308
+ `${unknown.length > 1 ? 'are' : 'is'} ignored` +
309
+ (unfiltered
310
+ ? `; with no "where", the item ${kind === 'connect'
311
+ ? `connects EVERY "${relDef.target}" node`
312
+ : `unlinks EVERY related "${relDef.target}" node`}.`
313
+ : '.') +
314
+ ` Allowed: ${allowedList}.`, `throw an OGMError for unknown ${kind} item keys`);
315
+ }
316
+ /**
317
+ * v2.6.0 — `labels` on a MERGE path (upsert, createMany with
318
+ * skipDuplicates) are SET after the MERGE, so they are also added to an
319
+ * EXISTING node it matched. Default: unchanged, one warning; strict: the
320
+ * labels go in `ON CREATE SET`, onto newly created nodes only.
321
+ */
322
+ labelsOnMatchedNodes(nodeDef, op) {
323
+ const call = op === 'upsert' ? 'upsert' : 'createMany with skipDuplicates';
324
+ this.strictMode.warn('labels-on-matched-nodes', `${nodeDef.typeName}\u0000${op}`, `"labels" on ${call} (${nodeDef.typeName}) are added to every node the MERGE returns, so an existing node ` +
325
+ `it matched is relabelled too.`, 'apply "labels" only to the nodes it creates');
326
+ }
327
+ /**
328
+ * v2.6.0 — `edge` keys that are not properties of the relationship's
329
+ * `@relationshipProperties` type. Pre-2.6.0 (and still by default) each
330
+ * is written to the relationship as a stray property. Default: one
331
+ * warning per OGM; strict: `OGMError` (`Unknown field …`).
332
+ */
333
+ checkEdgeKeys(relDef, edge, input) {
334
+ if (!(0, validation_1.isPlainObject)(edge))
335
+ return;
336
+ const declared = relDef.properties
337
+ ? this.schema.relationshipProperties.get(relDef.properties)?.properties
338
+ : undefined;
339
+ for (const key of Object.keys(edge)) {
340
+ if (edge[key] === undefined || declared?.has(key))
341
+ continue;
342
+ const owner = relDef.properties
343
+ ? `the relationship properties type "${relDef.properties}"`
344
+ : `the relationship, which declares no @relationshipProperties`;
345
+ if (this.strictMode.strict)
346
+ throw new errors_1.OGMError(`Unknown field "${key}" in the edge input (${input}) of "${relDef.fieldName}": it is not a property of ` +
347
+ `${owner}. Declare it in the schema, or fix the key.`);
348
+ this.strictMode.warn('edge-unknown-key', `${relDef.fieldName}\u0000${key}`, `Unknown field "${key}" in the edge input of "${relDef.fieldName}" is not a property of ${owner}; ` +
349
+ `it is written to the relationship as a stray property.`, `throw an OGMError for unknown edge fields`);
350
+ }
351
+ }
287
352
  /**
288
353
  * Generate CREATE Cypher for one or more nodes.
289
354
  * Handles scalar properties, nested relationship creates, and connects.
@@ -577,6 +642,19 @@ class MutationCompiler {
577
642
  if (mergeProps.length === 0)
578
643
  throw new errors_1.OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
579
644
  `an @id or @unique field of ${nodeDef.typeName}.`);
645
+ // v2.6.0 — MERGE matches ALL its keys together, so `{ id, email }`
646
+ // misses a node that has the email under another id and creates a
647
+ // duplicate beside it (or fails on the unique constraint). Default:
648
+ // compiled as before, with one warning; strict: exactly one key.
649
+ if (mergeKeyNames.length > 1) {
650
+ const keys = mergeKeyNames.map((k) => `"${k}"`).join(', ');
651
+ if (this.strictMode.strict)
652
+ throw new errors_1.OGMError(`upsert "where" must name exactly one @id / @unique field of ${nodeDef.typeName}; got ${keys}. ` +
653
+ `MERGE matches all of them together, so a node that has one but not the others is not found ` +
654
+ `and a duplicate is created. Pass a single unique key.`);
655
+ this.strictMode.warn('upsert-multiple-unique-keys', `${nodeDef.typeName}\u0000${mergeKeyNames.join(',')}`, `upsert on ${nodeDef.typeName} with several unique keys in "where" (${keys}) MERGEs on all of them ` +
656
+ `together: a node that has one but not the others is not matched and a duplicate is created.`, 'require exactly one unique key in an upsert "where"');
657
+ }
580
658
  lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
581
659
  // ON CREATE SET — all properties from create input (scalar only)
582
660
  const createSets = [];
@@ -627,6 +705,11 @@ class MutationCompiler {
627
705
  continue;
628
706
  createSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = randomUUID()`);
629
707
  }
708
+ // v2.6.0 — strict: runtime labels go on the node only when MERGE creates
709
+ // it (see `labelsOnMatchedNodes`).
710
+ const labelsOnCreate = this.strictMode.strict && labels && labels.length > 0;
711
+ if (labelsOnCreate)
712
+ createSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
630
713
  if (createSets.length > 0)
631
714
  lines.push(`ON CREATE SET ${createSets.join(', ')}`);
632
715
  // ON MATCH SET — all properties from update input (scalar only)
@@ -650,9 +733,10 @@ class MutationCompiler {
650
733
  if (updateSets.length > 0)
651
734
  lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
652
735
  // Extra labels
653
- if (labels && labels.length > 0) {
736
+ if (labels && labels.length > 0 && !labelsOnCreate) {
654
737
  const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
655
738
  lines.push(`SET n:${validatedLabels}`);
739
+ this.labelsOnMatchedNodes(nodeDef, 'upsert');
656
740
  }
657
741
  lines.push('RETURN n');
658
742
  return { cypher: lines.join('\n'), params };
@@ -802,6 +886,9 @@ class MutationCompiler {
802
886
  onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
803
887
  params[paramName] = bound;
804
888
  }
889
+ // v2.6.0 — strict: labels only on the nodes MERGE creates.
890
+ if (this.strictMode.strict && labels && labels.length > 0)
891
+ onCreateSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
805
892
  if (onCreateSets.length > 0)
806
893
  lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
807
894
  }
@@ -821,9 +908,13 @@ class MutationCompiler {
821
908
  lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
822
909
  }
823
910
  // Extra labels
824
- if (labels && labels.length > 0) {
911
+ if (labels &&
912
+ labels.length > 0 &&
913
+ !(skipDuplicates && this.strictMode.strict)) {
825
914
  const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
826
915
  lines.push(`SET n:${validatedLabels}`);
916
+ if (skipDuplicates)
917
+ this.labelsOnMatchedNodes(nodeDef, 'createMany');
827
918
  }
828
919
  lines.push('RETURN count(n) AS count');
829
920
  return { cypher: lines.join('\n'), params };
@@ -973,6 +1064,7 @@ class MutationCompiler {
973
1064
  (0, validation_1.mergeParams)(params, propParams);
974
1065
  lines.push(`CREATE ${relPattern}`);
975
1066
  // Set edge properties if present
1067
+ this.checkEdgeKeys(relDef, edgeInput, 'nested create');
976
1068
  if (edgeInput && Object.keys(edgeInput).length > 0) {
977
1069
  const relVar = `r_edge_${startCounter + counter - 1}`;
978
1070
  // Re-match the relationship to set edge props
@@ -1111,6 +1203,11 @@ class MutationCompiler {
1111
1203
  scope,
1112
1204
  }));
1113
1205
  else {
1206
+ // Every item is a plain object here (fast-path eligibility).
1207
+ for (const item of spec) {
1208
+ this.checkLinkItemKeys(item, 'connect', relDef);
1209
+ this.checkEdgeKeys(relDef, item.edge, 'connect');
1210
+ }
1114
1211
  const targetVar = `${varPrefix}target`;
1115
1212
  const relVar = `${varPrefix}r`;
1116
1213
  const targetLabelStr = this.getCachedLabelString(targetNodeDef);
@@ -1237,6 +1334,8 @@ class MutationCompiler {
1237
1334
  // compiling it would MATCH, and link, every target node.
1238
1335
  if (!(0, validation_1.isPlainObject)(connectSpec))
1239
1336
  throw new errors_1.OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
1337
+ this.checkLinkItemKeys(connectSpec, 'connect', relDef);
1338
+ this.checkEdgeKeys(relDef, connectSpec.edge, 'connect');
1240
1339
  const lines = [];
1241
1340
  const whereSpec = connectSpec.where;
1242
1341
  const edgeInput = connectSpec.edge;
@@ -1378,21 +1477,25 @@ class MutationCompiler {
1378
1477
  const predicate = targetNodeDef
1379
1478
  ? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
1380
1479
  : undefined;
1480
+ const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
1381
1481
  const pattern = (0, utils_1.buildRelPattern)({
1382
1482
  sourceVar,
1383
1483
  relDef,
1384
- targetVar: predicate ? blanketTarget : '',
1484
+ targetVar: predicate || labelPredicate ? blanketTarget : '',
1385
1485
  edgeVar: relVar,
1386
- targetLabelRaw: this.blanketTargetLabel(relDef),
1486
+ targetLabelRaw: labelRaw,
1387
1487
  });
1388
1488
  lines.push(`OPTIONAL MATCH ${pattern}`);
1389
- if (predicate)
1390
- lines.push(`WHERE ${predicate}`);
1489
+ const blanketConditions = [labelPredicate, predicate].filter(Boolean);
1490
+ if (blanketConditions.length > 0)
1491
+ lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
1391
1492
  lines.push(`DELETE ${relVar}`);
1392
1493
  }
1393
1494
  else {
1394
1495
  // Specific disconnect with conditions
1395
1496
  const disconnectSpec = item;
1497
+ if ((0, validation_1.isPlainObject)(disconnectSpec))
1498
+ this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
1396
1499
  const whereSpec = disconnectSpec.where;
1397
1500
  const targetVar = `${varPrefix}target_${tag}_${si}`;
1398
1501
  const pattern = (0, utils_1.buildRelPattern)({
@@ -1431,6 +1534,33 @@ class MutationCompiler {
1431
1534
  this.narrowedRelDefs.add(narrowed);
1432
1535
  return narrowed;
1433
1536
  }
1537
+ /**
1538
+ * v2.6.0 — how a blanket disconnect (`null` / `{}`) restricts its target.
1539
+ *
1540
+ * Default (v2.5.3): only a relationship narrowed to a union / interface
1541
+ * member keeps that member's label; otherwise the target is anonymous,
1542
+ * so with two fields on one relationship type and direction
1543
+ * (`pets: [Pet]`, `toys: [Toy]`, both `HAS`) a blanket disconnect of one
1544
+ * also unlinks the other's nodes (warned once at OGM construction).
1545
+ *
1546
+ * Strict: the field's own target type — its label, or for an abstract
1547
+ * target a predicate over its members' labels on `targetVar`.
1548
+ */
1549
+ blanketRestriction(relDef, targetNodeDef, targetVar) {
1550
+ const narrowed = this.blanketTargetLabel(relDef);
1551
+ if (narrowed || !this.strictMode.strict || !targetNodeDef)
1552
+ return { labelRaw: narrowed, labelPredicate: null };
1553
+ if (abstractTarget(targetNodeDef.typeName, this.schema))
1554
+ return {
1555
+ labelRaw: undefined,
1556
+ labelPredicate: this.targetMatch(targetNodeDef, targetVar)
1557
+ .labelPredicate,
1558
+ };
1559
+ return {
1560
+ labelRaw: this.getCachedLabelString(targetNodeDef),
1561
+ labelPredicate: null,
1562
+ };
1563
+ }
1434
1564
  /** The label a blanket disconnect over `relDef` must keep (see above). */
1435
1565
  blanketTargetLabel(relDef) {
1436
1566
  if (!this.narrowedRelDefs.has(relDef))
@@ -1521,6 +1651,8 @@ class MutationCompiler {
1521
1651
  : [item.disconnect];
1522
1652
  for (let di = 0; di < disconnectItems.length; di++) {
1523
1653
  const disconnectSpec = disconnectItems[di];
1654
+ if ((0, validation_1.isPlainObject)(disconnectSpec))
1655
+ this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
1524
1656
  const whereSpec = disconnectSpec.where;
1525
1657
  // A nested `disconnect` needs the target bound, so an item that
1526
1658
  // carries one is never the anonymous blanket form.
@@ -1537,18 +1669,20 @@ class MutationCompiler {
1537
1669
  // disconnect: only unlink targets the caller can read.
1538
1670
  const blanketTarget = `${sourceVar}_disc${varCounter}`;
1539
1671
  const predicate = this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter);
1540
- if (predicate)
1672
+ const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
1673
+ if (predicate || labelPredicate)
1541
1674
  varCounter++;
1542
1675
  const pattern = (0, utils_1.buildRelPattern)({
1543
1676
  sourceVar,
1544
1677
  relDef,
1545
- targetVar: predicate ? blanketTarget : '',
1678
+ targetVar: predicate || labelPredicate ? blanketTarget : '',
1546
1679
  edgeVar: relVar,
1547
- targetLabelRaw: this.blanketTargetLabel(relDef),
1680
+ targetLabelRaw: labelRaw,
1548
1681
  });
1549
1682
  lines.push(`OPTIONAL MATCH ${pattern}`);
1550
- if (predicate)
1551
- lines.push(`WHERE ${predicate}`);
1683
+ const blanketConditions = [labelPredicate, predicate].filter(Boolean);
1684
+ if (blanketConditions.length > 0)
1685
+ lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
1552
1686
  lines.push(`DELETE ${relVar}`);
1553
1687
  }
1554
1688
  else {
@@ -1656,6 +1790,7 @@ class MutationCompiler {
1656
1790
  // create() accepts. Pre-2.4.5 this path never read
1657
1791
  // `createSpec.edge`, so they were silently dropped.
1658
1792
  const edgeInput = createSpec.edge;
1793
+ this.checkEdgeKeys(relDef, edgeInput, 'nested create');
1659
1794
  const edgeEntries = Object.entries(edgeInput ?? {}).filter(([, v]) => v !== undefined);
1660
1795
  const edgeVar = edgeEntries.length ? `${createVar}_e` : undefined;
1661
1796
  const relPattern = (0, utils_1.buildRelPattern)({
@@ -1726,6 +1861,7 @@ class MutationCompiler {
1726
1861
  // Pre-compute SET clauses for edge (relationship) properties
1727
1862
  const edgeClauses = [];
1728
1863
  const edgeParams = {};
1864
+ this.checkEdgeKeys(relDef, edgeUpdate, 'nested update');
1729
1865
  if (edgeUpdate)
1730
1866
  for (const [prop, val] of Object.entries(edgeUpdate)) {
1731
1867
  if (val === undefined)
@@ -1874,6 +2010,21 @@ MutationCompiler.OPERATOR_SUFFIXES = [
1874
2010
  * path to validate that every item shares the same shape; mismatched
1875
2011
  * shapes silently drop keys from the compiled WHERE / SET.
1876
2012
  */
2013
+ /**
2014
+ * v2.6.0 — the keys a connect / disconnect item is compiled from. `overwrite`
2015
+ * is offered by the generated `ConnectFieldInput` and accepted as before
2016
+ * (MERGE + SET always overwrites the edge properties it is given).
2017
+ */
2018
+ const CONNECT_ITEM_KEYS = new Set([
2019
+ 'where',
2020
+ 'edge',
2021
+ 'overwrite',
2022
+ 'connect',
2023
+ ]);
2024
+ const DISCONNECT_ITEM_KEYS = new Set([
2025
+ 'where',
2026
+ 'disconnect',
2027
+ ]);
1877
2028
  /** Keys that make a nested-mutation `where` connection-shaped. */
1878
2029
  const CONNECTION_WHERE_KEYS = new Set([
1879
2030
  'node',