grafeo-ogm 2.4.8 → 2.5.1

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 (78) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +106 -27
  3. package/assets/brand/grafeo-mark-on-dark.svg +1 -0
  4. package/assets/brand/grafeo-mark-on-light.svg +1 -0
  5. package/dist/cjs/cli/commands/db-push.d.ts.map +1 -1
  6. package/dist/cjs/cli/commands/db-push.js +17 -5
  7. package/dist/cjs/cli/commands/db-push.js.map +1 -1
  8. package/dist/cjs/cli/push-planner.d.ts +6 -0
  9. package/dist/cjs/cli/push-planner.d.ts.map +1 -1
  10. package/dist/cjs/cli/push-planner.js +24 -5
  11. package/dist/cjs/cli/push-planner.js.map +1 -1
  12. package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
  13. package/dist/cjs/compilers/mutation.compiler.js +92 -51
  14. package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
  15. package/dist/cjs/compilers/selection.compiler.d.ts +16 -0
  16. package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
  17. package/dist/cjs/compilers/selection.compiler.js +158 -18
  18. package/dist/cjs/compilers/selection.compiler.js.map +1 -1
  19. package/dist/cjs/generator/type-emitters/where-emitter.js +16 -0
  20. package/dist/cjs/generator/type-emitters/where-emitter.js.map +1 -1
  21. package/dist/cjs/interface-model.d.ts +12 -0
  22. package/dist/cjs/interface-model.d.ts.map +1 -1
  23. package/dist/cjs/interface-model.js +21 -13
  24. package/dist/cjs/interface-model.js.map +1 -1
  25. package/dist/cjs/model.d.ts.map +1 -1
  26. package/dist/cjs/model.js +6 -0
  27. package/dist/cjs/model.js.map +1 -1
  28. package/dist/cjs/subgraph/subgraph-operations.d.ts.map +1 -1
  29. package/dist/cjs/subgraph/subgraph-operations.js +71 -6
  30. package/dist/cjs/subgraph/subgraph-operations.js.map +1 -1
  31. package/dist/cjs/subgraph/types.d.ts +14 -0
  32. package/dist/cjs/subgraph/types.d.ts.map +1 -1
  33. package/dist/cjs/testing/cypher-assert.d.ts +19 -1
  34. package/dist/cjs/testing/cypher-assert.d.ts.map +1 -1
  35. package/dist/cjs/testing/cypher-assert.js +192 -11
  36. package/dist/cjs/testing/cypher-assert.js.map +1 -1
  37. package/dist/cjs/testing/neo4j-record-factory.d.ts +8 -4
  38. package/dist/cjs/testing/neo4j-record-factory.d.ts.map +1 -1
  39. package/dist/cjs/testing/neo4j-record-factory.js +18 -7
  40. package/dist/cjs/testing/neo4j-record-factory.js.map +1 -1
  41. package/dist/esm/cli/commands/db-push.d.ts.map +1 -1
  42. package/dist/esm/cli/commands/db-push.js +17 -5
  43. package/dist/esm/cli/commands/db-push.js.map +1 -1
  44. package/dist/esm/cli/push-planner.d.ts +6 -0
  45. package/dist/esm/cli/push-planner.d.ts.map +1 -1
  46. package/dist/esm/cli/push-planner.js +24 -5
  47. package/dist/esm/cli/push-planner.js.map +1 -1
  48. package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
  49. package/dist/esm/compilers/mutation.compiler.js +92 -51
  50. package/dist/esm/compilers/mutation.compiler.js.map +1 -1
  51. package/dist/esm/compilers/selection.compiler.d.ts +16 -0
  52. package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
  53. package/dist/esm/compilers/selection.compiler.js +160 -20
  54. package/dist/esm/compilers/selection.compiler.js.map +1 -1
  55. package/dist/esm/generator/type-emitters/where-emitter.js +16 -0
  56. package/dist/esm/generator/type-emitters/where-emitter.js.map +1 -1
  57. package/dist/esm/interface-model.d.ts +12 -0
  58. package/dist/esm/interface-model.d.ts.map +1 -1
  59. package/dist/esm/interface-model.js +21 -13
  60. package/dist/esm/interface-model.js.map +1 -1
  61. package/dist/esm/model.d.ts.map +1 -1
  62. package/dist/esm/model.js +6 -0
  63. package/dist/esm/model.js.map +1 -1
  64. package/dist/esm/subgraph/subgraph-operations.d.ts.map +1 -1
  65. package/dist/esm/subgraph/subgraph-operations.js +71 -6
  66. package/dist/esm/subgraph/subgraph-operations.js.map +1 -1
  67. package/dist/esm/subgraph/types.d.ts +14 -0
  68. package/dist/esm/subgraph/types.d.ts.map +1 -1
  69. package/dist/esm/testing/cypher-assert.d.ts +19 -1
  70. package/dist/esm/testing/cypher-assert.d.ts.map +1 -1
  71. package/dist/esm/testing/cypher-assert.js +192 -11
  72. package/dist/esm/testing/cypher-assert.js.map +1 -1
  73. package/dist/esm/testing/neo4j-record-factory.d.ts +8 -4
  74. package/dist/esm/testing/neo4j-record-factory.d.ts.map +1 -1
  75. package/dist/esm/testing/neo4j-record-factory.js +18 -7
  76. package/dist/esm/testing/neo4j-record-factory.js.map +1 -1
  77. package/package.json +1 -1
  78. package/assets/logo.jpg +0 -0
@@ -20,7 +20,15 @@ const MAX_NESTED_LINK_DEPTH = 5;
20
20
  * bound as `targetVar`: that node becomes the source, everything in scope
21
21
  * stays carried, and its own names are prefixed by it.
22
22
  */
23
- function nestedScope(carryVars, targetVar, depth) {
23
+ function nestedScope(carryVars, targetVar, depth,
24
+ /**
25
+ * A token unique along the whole write path (v2.5.1). Pre-2.5.1 the
26
+ * prefix was `targetVar` alone, which sibling connects share (`target`)
27
+ * and which per-member union / interface dispatch restarts (`n_conn0`),
28
+ * so their nested params collided and the last writer won: links went
29
+ * to the wrong node and edge values were overwritten.
30
+ */
31
+ uniqueToken) {
24
32
  if (depth >= MAX_NESTED_LINK_DEPTH)
25
33
  throw new OGMError(`Nested connect / disconnect deeper than ${MAX_NESTED_LINK_DEPTH} levels is not supported.`);
26
34
  const vars = [...new Set([...carryVars, targetVar])];
@@ -28,7 +36,7 @@ function nestedScope(carryVars, targetVar, depth) {
28
36
  sourceVar: targetVar,
29
37
  carryVars: vars,
30
38
  withClause: `WITH ${vars.join(', ')}`,
31
- varPrefix: `${targetVar}_`,
39
+ varPrefix: `${uniqueToken}_`,
32
40
  depth: depth + 1,
33
41
  };
34
42
  }
@@ -168,6 +176,9 @@ export class MutationCompiler {
168
176
  */
169
177
  isConnectFastPathEligible(items, targetNodeDef) {
170
178
  for (const item of items) {
179
+ // A non-object item takes the per-item path, which rejects it.
180
+ if (!isPlainObject(item))
181
+ return false;
171
182
  // A nested `connect` is per item (v2.4.8): the UNWIND cannot run it.
172
183
  if (item.connect != null)
173
184
  return false;
@@ -276,8 +287,10 @@ export class MutationCompiler {
276
287
  const input = inputs[i];
277
288
  const prefix = `create${i}`;
278
289
  const nodeVar = i === 0 ? 'n' : `n_${i}`;
279
- const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
290
+ // Labels first: an unsafe label must fail as such, before any
291
+ // input-key check (v2.5.0 rejects unknown keys).
280
292
  const labelStr = this.getCachedLabelString(nodeDef);
293
+ const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
281
294
  lines.push(`CREATE (${nodeVar}:${labelStr} { ${propString} })`);
282
295
  mergeParams(params, propParams);
283
296
  createdVars.push(nodeVar);
@@ -352,6 +365,7 @@ export class MutationCompiler {
352
365
  if (value === undefined)
353
366
  continue;
354
367
  assertSafePropertyName(key, 'update property');
368
+ assertKnownField(nodeDef, key, 'update');
355
369
  const propDef = nodeDef.properties.get(key);
356
370
  const paramName = `update_${key}`;
357
371
  const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
@@ -480,6 +494,11 @@ export class MutationCompiler {
480
494
  schema: this.schema,
481
495
  });
482
496
  const conditions = [
497
+ // v2.5.1 — an interface / union target binds no label in the
498
+ // pattern, so restrict the match to its member types. Without it a
499
+ // nested or cascade delete also DETACH DELETEd any other node on the
500
+ // same relationship type (a Toy deleted through `pets`).
501
+ this.targetMatch(targetNodeDef, targetVar).labelPredicate,
483
502
  this.compileTargetWhere(item.where, targetVar, targetNodeDef, args.params, args.policyContext, args.paramCounter),
484
503
  this.buildTargetPolicyPredicate(targetNodeDef, targetVar, args.params, args.policyContext, args.paramCounter, 'delete'),
485
504
  ].filter((c) => Boolean(c));
@@ -503,19 +522,31 @@ export class MutationCompiler {
503
522
  const labelStr = this.getCachedLabelString(nodeDef);
504
523
  const lines = [];
505
524
  const params = {};
506
- // Build MERGE key properties from where clause (scalar only)
525
+ // MERGE key: the `@id` / `@unique` fields of `where`, and nothing else
526
+ // (v2.5.0). Any other key throws:
527
+ // - a plain field (`{ email, name }`) became part of the MERGE
528
+ // pattern, so an existing node with that email but another name did
529
+ // not match — MERGE created a DUPLICATE beside it;
530
+ // - an operator key (`name_CONTAINS`) was dropped, silently widening
531
+ // the match to the remaining keys;
532
+ // - a typo (`usernme`) became a phantom property on the new node.
533
+ // Pre-1.7.4 any key set was accepted; 1.7.4 required at least one
534
+ // unique key but still let the others through.
507
535
  const mergeProps = [];
508
536
  const mergeKeyNames = [];
509
537
  for (const [key, value] of Object.entries(where)) {
510
- if (value === undefined || value === null)
511
- continue;
512
- // Skip relationship suffixes and operators
513
- if (key === 'AND' || key === 'OR' || key === 'NOT')
514
- continue;
515
- if (key.includes('_') && !nodeDef.properties.has(key))
538
+ if (value === undefined)
516
539
  continue;
517
- assertSafePropertyName(key, 'merge key');
518
540
  const propDef = nodeDef.properties.get(key);
541
+ if (!propDef ||
542
+ propDef.isCypher ||
543
+ !(propDef.isUnique || propDef.isGenerated))
544
+ throw new OGMError(`upsert "where" accepts only @id / @unique fields of ${nodeDef.typeName}; "${key}" is not one. ` +
545
+ `MERGE matches on exactly these keys — put other fields in "create" / "update", ` +
546
+ `or use findFirst() + create() / update() for a non-unique match.`);
547
+ if (value === null)
548
+ throw new OGMError(`upsert "where.${key}" is null; a MERGE key must have a value.`);
549
+ assertSafePropertyName(key, 'merge key');
519
550
  const paramName = `merge_${key}`;
520
551
  const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
521
552
  mergeProps.push(`${escapeIdentifier(key)}: ${valueExpr}`);
@@ -523,30 +554,8 @@ export class MutationCompiler {
523
554
  params[paramName] = coerceWriteValue(value, propDef);
524
555
  }
525
556
  if (mergeProps.length === 0)
526
- throw new OGMError('upsert requires at least one scalar property in "where" for MERGE key');
527
- // Validate that the MERGE key set contains at least one `@unique` or
528
- // `@id` property. Pre-1.7.4 we accepted any key set, so:
529
- // - A typo in `where` (e.g. `usernme`) became a phantom MERGE
530
- // property — Neo4j happily created a node with that mis-named
531
- // attribute on first use.
532
- // - A non-unique `where` (e.g. `where: { country: 'AR' }`) matched
533
- // multiple existing nodes; MERGE then fans out across all of
534
- // them, applying ON MATCH SET to every match. Almost always a
535
- // bug, never the developer's intent.
536
- // Requiring at least one unique/id key forces MERGE to target at
537
- // most one row.
538
- const hasUniqueOrId = mergeKeyNames.some((name) => {
539
- const prop = nodeDef.properties.get(name);
540
- // `isGenerated` is the parser's flag for the `@id` directive;
541
- // `isUnique` is the flag for `@unique`. Either is sufficient
542
- // to guarantee the MERGE pattern targets at most one row.
543
- return prop !== undefined && (prop.isUnique || prop.isGenerated);
544
- });
545
- if (!hasUniqueOrId)
546
- throw new OGMError(`upsert "where" must contain at least one property marked with @id or @unique. ` +
547
- `Got [${mergeKeyNames.map((n) => `"${n}"`).join(', ')}] — none of these are unique on ${nodeDef.typeName}. ` +
548
- `Without a unique key, MERGE can fan out across multiple existing nodes (applying ON MATCH SET to each) ` +
549
- `or create phantom properties on typo. If you really want a non-unique merge, use create() + update().`);
557
+ throw new OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
558
+ `an @id or @unique field of ${nodeDef.typeName}.`);
550
559
  lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
551
560
  // ON CREATE SET — all properties from create input (scalar only)
552
561
  const createSets = [];
@@ -559,6 +568,7 @@ export class MutationCompiler {
559
568
  throw upsertRelationshipError(nodeDef, key, 'create');
560
569
  }
561
570
  assertSafePropertyName(key, 'create property');
571
+ assertKnownField(nodeDef, key, 'create');
562
572
  const propDef = nodeDef.properties.get(key);
563
573
  const paramName = `onCreate_${key}`;
564
574
  const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
@@ -609,6 +619,7 @@ export class MutationCompiler {
609
619
  throw upsertRelationshipError(nodeDef, key, 'update');
610
620
  }
611
621
  assertSafePropertyName(key, 'update property');
622
+ assertKnownField(nodeDef, key, 'update');
612
623
  const propDef = nodeDef.properties.get(key);
613
624
  const paramName = `onMatch_${key}`;
614
625
  const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
@@ -682,16 +693,13 @@ export class MutationCompiler {
682
693
  for (const key of Object.keys(item)) {
683
694
  if (item[key] === undefined || scalarKeys.includes(key))
684
695
  continue;
685
- if (nodeDef.relationships.has(key)) {
686
- // Only the first item's relationship fields have ever been
687
- // rejected; in later items they were (and still are) skipped
688
- // unwritten. Rejecting them everywhere would make calls that
689
- // succeed today throw — tracked as a separate, breaking change.
690
- if (index > 0)
691
- continue;
692
- throw new OGMError(`createMany does not support relationship fields. Found: "${key}". Use create() for nested operations.`);
693
- }
696
+ // v2.5.0 — rejected in EVERY item. Pre-2.5.0 only the first item's
697
+ // relationship fields were; in later items the nested input was
698
+ // skipped unwritten.
699
+ if (nodeDef.relationships.has(key))
700
+ throw new OGMError(`createMany does not support relationship fields. Found: "${key}" in data[${index}]. Use create() for nested operations.`);
694
701
  assertSafePropertyName(key, 'createMany property');
702
+ assertKnownField(nodeDef, key, 'createMany');
695
703
  scalarKeys.push(key);
696
704
  }
697
705
  // Identify @id (generated) fields that are NOT provided in data
@@ -823,6 +831,7 @@ export class MutationCompiler {
823
831
  if (value === undefined)
824
832
  continue;
825
833
  assertSafePropertyName(key, 'create property');
834
+ assertKnownField(nodeDef, key, 'create');
826
835
  const propDef = nodeDef.properties.get(key);
827
836
  const paramName = `${prefix}_${key}`;
828
837
  const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
@@ -1022,6 +1031,11 @@ export class MutationCompiler {
1022
1031
  const relDef = nodeDef.relationships.get(fieldName);
1023
1032
  if (!relDef)
1024
1033
  continue;
1034
+ // v2.5.1 — `{ rel: null }` (the generated input is `InputMaybe`)
1035
+ // connects nothing. Since 2.4.8 it reached the item compiler with no
1036
+ // `where`, so the MATCH had no filter and EVERY target was linked.
1037
+ if (spec == null)
1038
+ continue;
1025
1039
  const abstract = abstractTarget(relDef.target, this.schema);
1026
1040
  const memberEntries = abstract && !isBlanketSpec(spec)
1027
1041
  ? perMemberEntries(spec, abstract, fieldName)
@@ -1193,9 +1207,14 @@ export class MutationCompiler {
1193
1207
  */
1194
1208
  buildConnectItem(a) {
1195
1209
  const { connectSpec, relDef, targetNodeDef, sourceVar, connectVar } = a;
1210
+ // v2.5.1 — every connect item reaches here (top-level, array, nested in
1211
+ // create / update / connect). A null or non-object item has no filter:
1212
+ // compiling it would MATCH, and link, every target node.
1213
+ if (!isPlainObject(connectSpec))
1214
+ throw new OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
1196
1215
  const lines = [];
1197
- const whereSpec = connectSpec?.where;
1198
- const edgeInput = connectSpec?.edge;
1216
+ const whereSpec = connectSpec.where;
1217
+ const edgeInput = connectSpec.edge;
1199
1218
  lines.push(a.withClause);
1200
1219
  lines.push('CALL {');
1201
1220
  lines.push(a.withClause);
@@ -1241,12 +1260,12 @@ export class MutationCompiler {
1241
1260
  // A `connect` nested in the item links the CONNECTED node onward
1242
1261
  // (`connect: { posts: [{ where, connect: { tags: [...] } }] }`), with
1243
1262
  // the same policy predicates at its level. Pre-2.4.8 it was ignored.
1244
- const nested = connectSpec?.connect;
1263
+ const nested = connectSpec.connect;
1245
1264
  if (nested != null) {
1246
1265
  if (abstractTarget(targetNodeDef.typeName, this.schema))
1247
1266
  throw new OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
1248
1267
  `must be keyed by the implementing type, e.g. { ${relDef.fieldName}: { Member: [{ where, connect }] } }.`);
1249
- const inner = nestedScope(a.scope.carryVars, connectVar, a.scope.depth);
1268
+ const inner = nestedScope(a.scope.carryVars, connectVar, a.scope.depth, a.edgeParamPrefix);
1250
1269
  for (const nestedConnect of Array.isArray(nested) ? nested : [nested]) {
1251
1270
  if (!isPlainObject(nestedConnect))
1252
1271
  throw new OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" must be an object keyed by relationship.`);
@@ -1289,13 +1308,15 @@ export class MutationCompiler {
1289
1308
  * { tags: [...] } }] }`), with the same policy predicates at its level.
1290
1309
  * Pre-2.4.8 it was ignored. `''`-free: no input, no lines.
1291
1310
  */
1292
- buildNestedDisconnect(nested, relDef, targetNodeDef, targetVar, carryVars, params, policyContext, paramCounter, depth = 0) {
1311
+ buildNestedDisconnect(nested, relDef, targetNodeDef, targetVar, carryVars, params, policyContext, paramCounter, depth = 0,
1312
+ /** Unique along the write path; see `nestedScope`. */
1313
+ uniqueToken = targetVar) {
1293
1314
  if (nested == null)
1294
1315
  return [];
1295
1316
  if (abstractTarget(targetNodeDef.typeName, this.schema))
1296
1317
  throw new OGMError(`A nested "disconnect" inside a disconnect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
1297
1318
  `must be keyed by the implementing type, e.g. { ${relDef.fieldName}: { Member: [{ where, disconnect }] } }.`);
1298
- const inner = nestedScope(carryVars, targetVar, depth);
1319
+ const inner = nestedScope(carryVars, targetVar, depth, uniqueToken);
1299
1320
  const lines = [];
1300
1321
  for (const nestedDisconnect of Array.isArray(nested) ? nested : [nested]) {
1301
1322
  if (!isPlainObject(nestedDisconnect))
@@ -1537,7 +1558,10 @@ export class MutationCompiler {
1537
1558
  if (nestedDiscConditions.length > 0)
1538
1559
  lines.push(`WHERE ${nestedDiscConditions.join(' AND ')}`);
1539
1560
  lines.push(`DELETE ${relVar}`);
1540
- lines.push(...this.buildNestedDisconnect(nestedDisconnect, relDef, targetNodeDef, discTarget, allVars, params, policyContext, paramCounter));
1561
+ lines.push(...this.buildNestedDisconnect(nestedDisconnect, relDef, targetNodeDef, discTarget, allVars, params, policyContext, paramCounter, 0,
1562
+ // `discTarget` restarts per member under union dispatch;
1563
+ // `itemPrefix` is unique along the path.
1564
+ `${itemPrefix}_disc${di}`));
1541
1565
  }
1542
1566
  lines.push(`RETURN count(*) AS _${relVar}`);
1543
1567
  lines.push('}');
@@ -1667,6 +1691,7 @@ export class MutationCompiler {
1667
1691
  if (val === undefined)
1668
1692
  continue;
1669
1693
  assertSafePropertyName(prop, 'nested update property');
1694
+ assertKnownField(targetNodeDef, prop, 'nested update');
1670
1695
  const nodePropDef = targetNodeDef.properties.get(prop);
1671
1696
  const paramName = `${itemPrefix}_set_${prop}`;
1672
1697
  setClauses.push(`${updateVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${paramName}`, val, nodePropDef)}`);
@@ -1723,6 +1748,10 @@ export class MutationCompiler {
1723
1748
  // `update` policy, exactly like a direct update on that type
1724
1749
  // (pre-2.3.0 it enforced no target policy at all).
1725
1750
  const updateConditions = [
1751
+ // v2.5.1 — restrict an interface / union target to its member
1752
+ // types; without it the nested update also SET properties on
1753
+ // other nodes on the same relationship type.
1754
+ this.targetMatch(targetNodeDef, updateVar).labelPredicate,
1726
1755
  this.compileTargetWhere(updateWhere, updateVar, targetNodeDef, params, policyContext, paramCounter),
1727
1756
  this.buildTargetPolicyPredicate(targetNodeDef, updateVar, params, policyContext, paramCounter, 'update'),
1728
1757
  ].filter((c) => Boolean(c));
@@ -1969,6 +1998,18 @@ export function unwrapAbstractCreateNode(nodeSpec, target, fieldName) {
1969
1998
  `e.g. { node: { ${target.members[0] ?? 'Member'}: { … } } }. Allowed: ${target.members.map((m) => `"${m}"`).join(', ')}.`);
1970
1999
  return [keys[0], nodeSpec[keys[0]]];
1971
2000
  }
2001
+ /**
2002
+ * Reject a write key that is neither a property nor a relationship of the
2003
+ * type (v2.5.0). Pre-2.5.0 only its spelling was checked, so a typo
2004
+ * (`nmae`) was stored as a stray property and the intended field was left
2005
+ * untouched — at every depth (nested creates and updates included).
2006
+ */
2007
+ function assertKnownField(nodeDef, key, input) {
2008
+ if (nodeDef.properties.has(key) || nodeDef.relationships.has(key))
2009
+ return;
2010
+ throw new OGMError(`Unknown field "${key}" in the ${input} input of ${nodeDef.typeName}: it is neither a property nor a ` +
2011
+ `relationship of the type. Declare it in the schema, or fix the key.`);
2012
+ }
1972
2013
  /**
1973
2014
  * MERGE writes scalars only. Pre-2.4.8 a relationship field in an upsert's
1974
2015
  * `create` / `update` was skipped, so its nested connect / create never