grafeo-ogm 2.4.8 → 2.5.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 (61) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +20 -6
  3. package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
  4. package/dist/cjs/compilers/mutation.compiler.js +48 -42
  5. package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
  6. package/dist/cjs/compilers/selection.compiler.d.ts +16 -0
  7. package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
  8. package/dist/cjs/compilers/selection.compiler.js +158 -18
  9. package/dist/cjs/compilers/selection.compiler.js.map +1 -1
  10. package/dist/cjs/generator/type-emitters/where-emitter.js +16 -0
  11. package/dist/cjs/generator/type-emitters/where-emitter.js.map +1 -1
  12. package/dist/cjs/interface-model.d.ts +12 -0
  13. package/dist/cjs/interface-model.d.ts.map +1 -1
  14. package/dist/cjs/interface-model.js +21 -13
  15. package/dist/cjs/interface-model.js.map +1 -1
  16. package/dist/cjs/model.d.ts.map +1 -1
  17. package/dist/cjs/model.js +6 -0
  18. package/dist/cjs/model.js.map +1 -1
  19. package/dist/cjs/subgraph/subgraph-operations.d.ts.map +1 -1
  20. package/dist/cjs/subgraph/subgraph-operations.js +71 -6
  21. package/dist/cjs/subgraph/subgraph-operations.js.map +1 -1
  22. package/dist/cjs/subgraph/types.d.ts +14 -0
  23. package/dist/cjs/subgraph/types.d.ts.map +1 -1
  24. package/dist/cjs/testing/cypher-assert.d.ts +19 -1
  25. package/dist/cjs/testing/cypher-assert.d.ts.map +1 -1
  26. package/dist/cjs/testing/cypher-assert.js +192 -11
  27. package/dist/cjs/testing/cypher-assert.js.map +1 -1
  28. package/dist/cjs/testing/neo4j-record-factory.d.ts +8 -4
  29. package/dist/cjs/testing/neo4j-record-factory.d.ts.map +1 -1
  30. package/dist/cjs/testing/neo4j-record-factory.js +18 -7
  31. package/dist/cjs/testing/neo4j-record-factory.js.map +1 -1
  32. package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
  33. package/dist/esm/compilers/mutation.compiler.js +48 -42
  34. package/dist/esm/compilers/mutation.compiler.js.map +1 -1
  35. package/dist/esm/compilers/selection.compiler.d.ts +16 -0
  36. package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
  37. package/dist/esm/compilers/selection.compiler.js +160 -20
  38. package/dist/esm/compilers/selection.compiler.js.map +1 -1
  39. package/dist/esm/generator/type-emitters/where-emitter.js +16 -0
  40. package/dist/esm/generator/type-emitters/where-emitter.js.map +1 -1
  41. package/dist/esm/interface-model.d.ts +12 -0
  42. package/dist/esm/interface-model.d.ts.map +1 -1
  43. package/dist/esm/interface-model.js +21 -13
  44. package/dist/esm/interface-model.js.map +1 -1
  45. package/dist/esm/model.d.ts.map +1 -1
  46. package/dist/esm/model.js +6 -0
  47. package/dist/esm/model.js.map +1 -1
  48. package/dist/esm/subgraph/subgraph-operations.d.ts.map +1 -1
  49. package/dist/esm/subgraph/subgraph-operations.js +71 -6
  50. package/dist/esm/subgraph/subgraph-operations.js.map +1 -1
  51. package/dist/esm/subgraph/types.d.ts +14 -0
  52. package/dist/esm/subgraph/types.d.ts.map +1 -1
  53. package/dist/esm/testing/cypher-assert.d.ts +19 -1
  54. package/dist/esm/testing/cypher-assert.d.ts.map +1 -1
  55. package/dist/esm/testing/cypher-assert.js +192 -11
  56. package/dist/esm/testing/cypher-assert.js.map +1 -1
  57. package/dist/esm/testing/neo4j-record-factory.d.ts +8 -4
  58. package/dist/esm/testing/neo4j-record-factory.d.ts.map +1 -1
  59. package/dist/esm/testing/neo4j-record-factory.js +18 -7
  60. package/dist/esm/testing/neo4j-record-factory.js.map +1 -1
  61. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.5.0 (2026-10-18) — breaking fixes from the bug scan
4
+
5
+ > **Minor release. Every item below changes behaviour that callers may depend on.** Each change fixes a confirmed bug, and the old behaviour was wrong (see each entry). The non-breaking fixes from the same scan shipped in **2.4.8**.
6
+
7
+ ### Upgrade checklist
8
+
9
+ Use this to check whether you're affected. If none of these apply, 2.5.0 is a drop-in upgrade.
10
+
11
+ | If you… | Then… |
12
+ |---|---|
13
+ | use `InterfaceModel` | it now finds implementor nodes **without** the interface label, so result counts can grow (see below) |
14
+ | put arguments or aliases in `selectionSet` strings (`posts(where: …)`, `r: rank`) | they now take effect, and unsupported ones throw |
15
+ | pass non-`@id`/`@unique` keys in `upsert` `where` | they now throw |
16
+ | rely on `createMany({ skipDuplicates: true })` counting the skipped duplicates | it now returns the created count |
17
+ | write keys that aren't in the schema | they now throw `Unknown field …` |
18
+ | pass relationship fields in a later `createMany` item | they now throw |
19
+ | filter list fields with element-typed operators (`tags: 'x'`, `tags_CONTAINS`) | those no longer compile (they never matched) |
20
+ | use `CypherAssert` or `Neo4jRecordFactory.dateTime` in tests | stricter assertions may expose tests that passed by accident |
21
+ | use `cloneSubgraph` / `deleteSubgraph` | traversal is now outgoing-only by default, and the root is matched by label |
22
+
23
+ ### Reads
24
+
25
+ - **`InterfaceModel` matches implementors, not the interface label.** `find`, `count` and `aggregate` used to `MATCH (n:<Interface>)`. Nodes created with `Model.create` never carry that label, so these methods found nothing for nodes the OGM itself wrote. They now `MATCH (n:(A|B|…))` over the implementors, as `@neo4j/graphql` does. Per-member policies, `__typename` and the `labels` filter work as before.
26
+ - **What to do:** nodes carrying *only* the interface label, with no implementor label, are no longer returned; give them their concrete label. Recheck counts and pagination that depended on the old, smaller result. Listing the interface in `@node(labels: [...])` still works but is no longer needed.
27
+ - **`selectionSet` arguments and aliases take effect.** They were silently ignored, so `posts(where: …)` and `posts(options: { sort, limit, offset })` returned every related node, `r: rank` came back as `rank`, and two aliases of one field collapsed into one key. Now relationship fields honour `where` (with the target `read` policy applied) and `options` (`sort` needs APOC; `limit` and `offset` are bound parameters). Connection fields honour `where`, and aliases name the output keys.
28
+ - **Now throw `OGMError`:** any other argument, an unknown option or sort field, `options` on a singular relationship, GraphQL `$variables`, and arguments on a field reached through a union-typed parent.
29
+ - **What to do:** search for `selectionSet` strings with arguments or aliases. Their results now match what they ask for: filtered, paged and re-keyed.
30
+
31
+ ### Writes
32
+
33
+ - **`upsert` `where` accepts only `@id` / `@unique` fields.** A plain key used to join the `MERGE` pattern, so `where: { email, name: 'Bob' }` against an existing `{ email, name: 'Alice' }` created a **duplicate** with the same email. An operator key (`name_CONTAINS`) was silently dropped, which widened the match so the **wrong row** could be updated. These now throw `OGMError`: a plain, operator, logical or unknown key, and a `null` value. At least one unique key is required.
34
+ - **What to do:** keep only unique keys in `where` and move the other fields into `create` / `update`. For a non-unique match, use `findFirst()` followed by `create()` / `update()`.
35
+ - **Unknown keys in write input throw.** A key that is neither a property nor a relationship of the type (a typo like `nmae`) was written as a stray property, and the intended field was left unchanged. It now throws `Unknown field …`, in `create`, `update`, `updateMany`, `createMany`, `upsert`, and nested creates and updates at any depth.
36
+ - **What to do:** declare every property you write in the schema.
37
+ - **`createMany({ skipDuplicates: true })` returns the number of nodes created.** It used to include the duplicates it skipped. It now matches Prisma. Calls without `skipDuplicates` are unchanged.
38
+ - **`createMany` rejects relationship fields in every item.** Only `data[0]` used to be checked; later items were created without their relationship input. The error names the item, e.g. `"tags" in data[1]`. Use `create()` for nested relationship writes.
39
+
40
+ ### Generated types
41
+
42
+ - **List-scalar where operators are typed as lists (compile-time only).** For `tags: [String!]`, equality and `_NOT` take `Array<T>`, and `_IN` / `_NOT_IN` take `Array<Array<T>>`. String and comparison operators are no longer generated for list fields. The old element-typed forms (`{ tags: 'x' }`, `tags_CONTAINS`) compiled but **never matched**, while the working form `{ tags: ['x', 'y'] }` didn't compile. Runtime behaviour is unchanged.
43
+
44
+ ### Testing helpers
45
+
46
+ - **`CypherAssert.assertContainsClause` looks for the pattern inside the clause.** The clause and the pattern were checked independently, so `assertContainsClause(q, 'WHERE', 'n { .name }')` passed when the pattern was in the `RETURN`. `'SET'` also matched `$offset`. Keywords now match whole words, outside strings, names and comments, and the pattern must sit within that clause.
47
+ - **`CypherAssert.normalize` and `assertStructurallyEqual` keep string literals and backticked names verbatim.** `'a b'` used to equal `'a b'`, and a `$name` inside a string was renamed as a parameter.
48
+ - **`Neo4jRecordFactory.dateTime` reads an offset-less string as UTC**, as Neo4j does. It used to use the machine's local zone, so results differed between a laptop and CI. A string that isn't a date now throws a `TypeError`.
49
+
50
+ Assertions that only passed by accident will now fail.
51
+
52
+ ### Subgraph operations (APOC)
53
+
54
+ - **Owned relationships are followed outgoing by default.** A bare type was expanded in **both** directions. With two books sharing a chapter, `deleteSubgraph(bookA)` climbed back up the shared chapter and **deleted book B**, `deleteSubgraph(chapter)` deleted its parent book, and `cloneSubgraph` pulled in the other parents. Now a bare `'TYPE'` is outgoing, `'<TYPE'` is incoming, and `'<TYPE>'` is both directions (the old behaviour).
55
+ - **What to do:** mark relationships you traverse backwards as `'<TYPE'` or `'<TYPE>'`.
56
+ - **The root is matched by label, and owned nodes without an `id` are refused.** The root was matched by `id` alone, so any node with that id became a root, found by scanning every node. It's now `(:rootLabel {id})`. The new optional `SubgraphConfig.rootLabel` defaults to `ownedLabels[0]`. `cloneSubgraph` now aborts before writing anything if an owned node has no `id` or shares one with another owned node; previously those nodes collapsed onto one mapping entry.
57
+ - **What to do:** set `rootLabel` if your root isn't of the first owned label.
58
+
59
+ Every entry above has a regression test, live wherever behaviour depends on the server. The subgraph changes are verified against Neo4j 5.26 with APOC 5.26.
60
+
3
61
  ## 2.4.8 (2026-10-18) — bug-scan fixes across reads, writes, codegen, CLI and testing helpers
4
62
 
5
63
  > **A large patch release. Upgrading is safe, with no breaking changes.** Most of these bugs were found by a third, multi-area bug scan. Each fix was reproduced on Neo4j 5.26 before it was fixed, and is pinned by a regression test, live wherever behaviour depends on the server. The breaking fixes from the same scan ship separately in **2.5.0**. Cypher for queries the fixes don't touch is unchanged; the policy byte-identical suite pins this.
package/README.md CHANGED
@@ -397,7 +397,7 @@ grafeo-ogm uses standard GraphQL SDL with Neo4j directives to define your graph
397
397
 
398
398
  | Directive | Target | Description |
399
399
  |---|---|---|
400
- | `@node` | Type | Marks a type as a Neo4j node. Optional `labels` argument for multi-label nodes (implementors of an interface must include the interface name, see [Interfaces](#interfaces)) |
400
+ | `@node` | Type | Marks a type as a Neo4j node. Optional `labels` argument for multi-label nodes (implementors of an interface no longer need to include the interface name since v2.5.0, see [Interfaces](#interfaces)) |
401
401
  | `@relationship` | Field | Defines a relationship with `type`, `direction` (exactly `IN` or `OUT`; anything else is a schema error), and optional `properties` |
402
402
  | `@declareRelationship` | Interface field | Declares a relationship on an interface. Its `type`, `direction` and `properties` come from the implementors' `@relationship`, which must all agree |
403
403
  | `@relationshipProperties` | Type | Marks a type as relationship properties (edge data) |
@@ -483,7 +483,7 @@ type Resource @node {
483
483
 
484
484
  Interface types can be queried polymorphically via `ogm.interfaceModel('Entity')`. Results include `__typename` to discriminate concrete types.
485
485
 
486
- - The interface model matches nodes by the **interface label** (`MATCH (n:Entity)`), so every implementor must carry it: list the interface name in `@node(labels: [...])`. An implementor declared with a bare `@node` is stored with only its own label and never appears in interface queries.
486
+ - The interface model matches nodes by their **implementors' labels** (`MATCH (n:(User|Organization))`), so a node created with `ogm.model('User').create(...)` appears in interface queries with no extra labels. Since v2.5.0 an implementor does **not** need to carry the interface name; before v2.5.0 it had to list it in `@node(labels: [...])`, which still works.
487
487
  - A `@declareRelationship` field takes its relationship `type`, `direction` and `properties` from the implementors. They must all declare the same ones; if they disagree, or an implementor has no `@relationship` on that field, `parseSchema` throws.
488
488
 
489
489
  ### Enums
@@ -587,6 +587,15 @@ const book = await Book.findUniqueOrThrow({
587
587
  | `_NOT_ENDS_WITH` | `NOT ENDS WITH` | `{ name_NOT_ENDS_WITH: 'x' }` |
588
588
  | `_MATCHES` | `=~` (regex) | `{ name_MATCHES: '^A.*' }` |
589
589
 
590
+ **List fields** (`tags: [String!]`) are compared as a whole list, so only these operators exist for them:
591
+
592
+ | Operator | Matches when | Example |
593
+ |---|---|---|
594
+ | *(equality)* | the stored list equals the given list | `{ tags: ['a', 'b'] }` |
595
+ | `_NOT` | the stored list differs from the given list | `{ tags_NOT: ['a'] }` |
596
+ | `_IN` | the stored list equals one of the given lists | `{ tags_IN: [['a'], ['b', 'c']] }` |
597
+ | `_NOT_IN` | the stored list equals none of the given lists | `{ tags_NOT_IN: [['a']] }` |
598
+
590
599
  **Relationship quantifiers:**
591
600
 
592
601
  | Operator | Meaning | Example |
@@ -1044,6 +1053,10 @@ The compiler reads `OR`, then `AND`, then `NOT`, and stops at the first one it f
1044
1053
 
1045
1054
  Clone or delete entire subgraphs using APOC procedures. Requires the APOC plugin installed in Neo4j.
1046
1055
 
1056
+ The traversal starts at the root and follows `ownedRelationships` into nodes carrying one of `ownedLabels`. A bare type such as `'HAS_CHAPTER'` is followed **outgoing only**; write `'<CHILD_OF'` to follow a relationship incoming, or `'<LINKED>'` to follow it both ways. Before v2.5.0 a bare type was followed both ways, so deleting one book could climb from a chapter it shared with another book up to that book and delete it too; add `<…>` to keep the old behavior for a type.
1057
+
1058
+ The root is matched as `(:rootLabel {id: rootId})`, with `rootLabel` defaulting to the first entry of `ownedLabels` (v2.5.0; before, a node of any label with that id was a root). Set `rootLabel` when the root is not of that label — a delete whose root isn't found deletes nothing and returns `deletedCount: 0`. `cloneSubgraph` maps every owned node by its `id`, so it refuses, before cloning anything, a subgraph in which an owned node has no `id` or shares one with another owned node.
1059
+
1047
1060
  ```typescript
1048
1061
  import { cloneSubgraph, deleteSubgraph } from 'grafeo-ogm';
1049
1062
 
@@ -1069,12 +1082,13 @@ const cloneResult = await cloneSubgraph(
1069
1082
  // v1.13.0+ — post-process only the nodes of one label, no broad-IN filtering:
1070
1083
  const chapterIds = cloneResult.nodesByLabel.get('Chapter') ?? [];
1071
1084
 
1072
- // Delete a subgraph
1085
+ // Delete a subgraph rooted at a Chapter: its sections, not its Book
1073
1086
  const deleteResult = await deleteSubgraph(
1074
- 'root-node-id',
1087
+ 'chapter-node-id',
1075
1088
  {
1076
- ownedLabels: ['Book', 'Chapter'],
1077
- ownedRelationships: ['HAS_CHAPTER'],
1089
+ rootLabel: 'Chapter', // v2.5.0+ — defaults to ownedLabels[0]
1090
+ ownedLabels: ['Chapter', 'Section'],
1091
+ ownedRelationships: ['HAS_SECTION'], // outgoing; '<TYPE' incoming, '<TYPE>' both
1078
1092
  maxLevel: 5,
1079
1093
  referenceRelationships: [],
1080
1094
  },
@@ -1 +1 @@
1
- {"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
@@ -283,8 +283,10 @@ class MutationCompiler {
283
283
  const input = inputs[i];
284
284
  const prefix = `create${i}`;
285
285
  const nodeVar = i === 0 ? 'n' : `n_${i}`;
286
- const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
286
+ // Labels first: an unsafe label must fail as such, before any
287
+ // input-key check (v2.5.0 rejects unknown keys).
287
288
  const labelStr = this.getCachedLabelString(nodeDef);
289
+ const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
288
290
  lines.push(`CREATE (${nodeVar}:${labelStr} { ${propString} })`);
289
291
  (0, validation_1.mergeParams)(params, propParams);
290
292
  createdVars.push(nodeVar);
@@ -359,6 +361,7 @@ class MutationCompiler {
359
361
  if (value === undefined)
360
362
  continue;
361
363
  (0, validation_1.assertSafePropertyName)(key, 'update property');
364
+ assertKnownField(nodeDef, key, 'update');
362
365
  const propDef = nodeDef.properties.get(key);
363
366
  const paramName = `update_${key}`;
364
367
  const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
@@ -510,19 +513,31 @@ class MutationCompiler {
510
513
  const labelStr = this.getCachedLabelString(nodeDef);
511
514
  const lines = [];
512
515
  const params = {};
513
- // Build MERGE key properties from where clause (scalar only)
516
+ // MERGE key: the `@id` / `@unique` fields of `where`, and nothing else
517
+ // (v2.5.0). Any other key throws:
518
+ // - a plain field (`{ email, name }`) became part of the MERGE
519
+ // pattern, so an existing node with that email but another name did
520
+ // not match — MERGE created a DUPLICATE beside it;
521
+ // - an operator key (`name_CONTAINS`) was dropped, silently widening
522
+ // the match to the remaining keys;
523
+ // - a typo (`usernme`) became a phantom property on the new node.
524
+ // Pre-1.7.4 any key set was accepted; 1.7.4 required at least one
525
+ // unique key but still let the others through.
514
526
  const mergeProps = [];
515
527
  const mergeKeyNames = [];
516
528
  for (const [key, value] of Object.entries(where)) {
517
- if (value === undefined || value === null)
518
- continue;
519
- // Skip relationship suffixes and operators
520
- if (key === 'AND' || key === 'OR' || key === 'NOT')
521
- continue;
522
- if (key.includes('_') && !nodeDef.properties.has(key))
529
+ if (value === undefined)
523
530
  continue;
524
- (0, validation_1.assertSafePropertyName)(key, 'merge key');
525
531
  const propDef = nodeDef.properties.get(key);
532
+ if (!propDef ||
533
+ propDef.isCypher ||
534
+ !(propDef.isUnique || propDef.isGenerated))
535
+ throw new errors_1.OGMError(`upsert "where" accepts only @id / @unique fields of ${nodeDef.typeName}; "${key}" is not one. ` +
536
+ `MERGE matches on exactly these keys — put other fields in "create" / "update", ` +
537
+ `or use findFirst() + create() / update() for a non-unique match.`);
538
+ if (value === null)
539
+ throw new errors_1.OGMError(`upsert "where.${key}" is null; a MERGE key must have a value.`);
540
+ (0, validation_1.assertSafePropertyName)(key, 'merge key');
526
541
  const paramName = `merge_${key}`;
527
542
  const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
528
543
  mergeProps.push(`${(0, validation_1.escapeIdentifier)(key)}: ${valueExpr}`);
@@ -530,30 +545,8 @@ class MutationCompiler {
530
545
  params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
531
546
  }
532
547
  if (mergeProps.length === 0)
533
- throw new errors_1.OGMError('upsert requires at least one scalar property in "where" for MERGE key');
534
- // Validate that the MERGE key set contains at least one `@unique` or
535
- // `@id` property. Pre-1.7.4 we accepted any key set, so:
536
- // - A typo in `where` (e.g. `usernme`) became a phantom MERGE
537
- // property — Neo4j happily created a node with that mis-named
538
- // attribute on first use.
539
- // - A non-unique `where` (e.g. `where: { country: 'AR' }`) matched
540
- // multiple existing nodes; MERGE then fans out across all of
541
- // them, applying ON MATCH SET to every match. Almost always a
542
- // bug, never the developer's intent.
543
- // Requiring at least one unique/id key forces MERGE to target at
544
- // most one row.
545
- const hasUniqueOrId = mergeKeyNames.some((name) => {
546
- const prop = nodeDef.properties.get(name);
547
- // `isGenerated` is the parser's flag for the `@id` directive;
548
- // `isUnique` is the flag for `@unique`. Either is sufficient
549
- // to guarantee the MERGE pattern targets at most one row.
550
- return prop !== undefined && (prop.isUnique || prop.isGenerated);
551
- });
552
- if (!hasUniqueOrId)
553
- throw new errors_1.OGMError(`upsert "where" must contain at least one property marked with @id or @unique. ` +
554
- `Got [${mergeKeyNames.map((n) => `"${n}"`).join(', ')}] — none of these are unique on ${nodeDef.typeName}. ` +
555
- `Without a unique key, MERGE can fan out across multiple existing nodes (applying ON MATCH SET to each) ` +
556
- `or create phantom properties on typo. If you really want a non-unique merge, use create() + update().`);
548
+ throw new errors_1.OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
549
+ `an @id or @unique field of ${nodeDef.typeName}.`);
557
550
  lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
558
551
  // ON CREATE SET — all properties from create input (scalar only)
559
552
  const createSets = [];
@@ -566,6 +559,7 @@ class MutationCompiler {
566
559
  throw upsertRelationshipError(nodeDef, key, 'create');
567
560
  }
568
561
  (0, validation_1.assertSafePropertyName)(key, 'create property');
562
+ assertKnownField(nodeDef, key, 'create');
569
563
  const propDef = nodeDef.properties.get(key);
570
564
  const paramName = `onCreate_${key}`;
571
565
  const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
@@ -616,6 +610,7 @@ class MutationCompiler {
616
610
  throw upsertRelationshipError(nodeDef, key, 'update');
617
611
  }
618
612
  (0, validation_1.assertSafePropertyName)(key, 'update property');
613
+ assertKnownField(nodeDef, key, 'update');
619
614
  const propDef = nodeDef.properties.get(key);
620
615
  const paramName = `onMatch_${key}`;
621
616
  const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
@@ -689,16 +684,13 @@ class MutationCompiler {
689
684
  for (const key of Object.keys(item)) {
690
685
  if (item[key] === undefined || scalarKeys.includes(key))
691
686
  continue;
692
- if (nodeDef.relationships.has(key)) {
693
- // Only the first item's relationship fields have ever been
694
- // rejected; in later items they were (and still are) skipped
695
- // unwritten. Rejecting them everywhere would make calls that
696
- // succeed today throw — tracked as a separate, breaking change.
697
- if (index > 0)
698
- continue;
699
- throw new errors_1.OGMError(`createMany does not support relationship fields. Found: "${key}". Use create() for nested operations.`);
700
- }
687
+ // v2.5.0 — rejected in EVERY item. Pre-2.5.0 only the first item's
688
+ // relationship fields were; in later items the nested input was
689
+ // skipped unwritten.
690
+ if (nodeDef.relationships.has(key))
691
+ throw new errors_1.OGMError(`createMany does not support relationship fields. Found: "${key}" in data[${index}]. Use create() for nested operations.`);
701
692
  (0, validation_1.assertSafePropertyName)(key, 'createMany property');
693
+ assertKnownField(nodeDef, key, 'createMany');
702
694
  scalarKeys.push(key);
703
695
  }
704
696
  // Identify @id (generated) fields that are NOT provided in data
@@ -830,6 +822,7 @@ class MutationCompiler {
830
822
  if (value === undefined)
831
823
  continue;
832
824
  (0, validation_1.assertSafePropertyName)(key, 'create property');
825
+ assertKnownField(nodeDef, key, 'create');
833
826
  const propDef = nodeDef.properties.get(key);
834
827
  const paramName = `${prefix}_${key}`;
835
828
  const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
@@ -1674,6 +1667,7 @@ class MutationCompiler {
1674
1667
  if (val === undefined)
1675
1668
  continue;
1676
1669
  (0, validation_1.assertSafePropertyName)(prop, 'nested update property');
1670
+ assertKnownField(targetNodeDef, prop, 'nested update');
1677
1671
  const nodePropDef = targetNodeDef.properties.get(prop);
1678
1672
  const paramName = `${itemPrefix}_set_${prop}`;
1679
1673
  setClauses.push(`${updateVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, nodePropDef)}`);
@@ -1977,6 +1971,18 @@ function unwrapAbstractCreateNode(nodeSpec, target, fieldName) {
1977
1971
  `e.g. { node: { ${target.members[0] ?? 'Member'}: { … } } }. Allowed: ${target.members.map((m) => `"${m}"`).join(', ')}.`);
1978
1972
  return [keys[0], nodeSpec[keys[0]]];
1979
1973
  }
1974
+ /**
1975
+ * Reject a write key that is neither a property nor a relationship of the
1976
+ * type (v2.5.0). Pre-2.5.0 only its spelling was checked, so a typo
1977
+ * (`nmae`) was stored as a stray property and the intended field was left
1978
+ * untouched — at every depth (nested creates and updates included).
1979
+ */
1980
+ function assertKnownField(nodeDef, key, input) {
1981
+ if (nodeDef.properties.has(key) || nodeDef.relationships.has(key))
1982
+ return;
1983
+ throw new errors_1.OGMError(`Unknown field "${key}" in the ${input} input of ${nodeDef.typeName}: it is neither a property nor a ` +
1984
+ `relationship of the type. Declare it in the schema, or fix the key.`);
1985
+ }
1980
1986
  /**
1981
1987
  * MERGE writes scalars only. Pre-2.4.8 a relationship field in an upsert's
1982
1988
  * `create` / `update` was skipped, so its nested connect / create never