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.
- package/CHANGELOG.md +58 -0
- package/README.md +20 -6
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +48 -42
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +16 -0
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +158 -18
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/generator/type-emitters/where-emitter.js +16 -0
- package/dist/cjs/generator/type-emitters/where-emitter.js.map +1 -1
- package/dist/cjs/interface-model.d.ts +12 -0
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +21 -13
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +6 -0
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/subgraph/subgraph-operations.d.ts.map +1 -1
- package/dist/cjs/subgraph/subgraph-operations.js +71 -6
- package/dist/cjs/subgraph/subgraph-operations.js.map +1 -1
- package/dist/cjs/subgraph/types.d.ts +14 -0
- package/dist/cjs/subgraph/types.d.ts.map +1 -1
- package/dist/cjs/testing/cypher-assert.d.ts +19 -1
- package/dist/cjs/testing/cypher-assert.d.ts.map +1 -1
- package/dist/cjs/testing/cypher-assert.js +192 -11
- package/dist/cjs/testing/cypher-assert.js.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.d.ts +8 -4
- package/dist/cjs/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.js +18 -7
- package/dist/cjs/testing/neo4j-record-factory.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +48 -42
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +16 -0
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +160 -20
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/generator/type-emitters/where-emitter.js +16 -0
- package/dist/esm/generator/type-emitters/where-emitter.js.map +1 -1
- package/dist/esm/interface-model.d.ts +12 -0
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +21 -13
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +6 -0
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/subgraph/subgraph-operations.d.ts.map +1 -1
- package/dist/esm/subgraph/subgraph-operations.js +71 -6
- package/dist/esm/subgraph/subgraph-operations.js.map +1 -1
- package/dist/esm/subgraph/types.d.ts +14 -0
- package/dist/esm/subgraph/types.d.ts.map +1 -1
- package/dist/esm/testing/cypher-assert.d.ts +19 -1
- package/dist/esm/testing/cypher-assert.d.ts.map +1 -1
- package/dist/esm/testing/cypher-assert.js +192 -11
- package/dist/esm/testing/cypher-assert.js.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.d.ts +8 -4
- package/dist/esm/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.js +18 -7
- package/dist/esm/testing/neo4j-record-factory.js.map +1 -1
- 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
|
|
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
|
|
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
|
-
'
|
|
1087
|
+
'chapter-node-id',
|
|
1075
1088
|
{
|
|
1076
|
-
|
|
1077
|
-
|
|
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":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAGvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,cAAc,EAGd,cAAc,EACf,MAAM,iBAAiB,CAAC;AAqBzB,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAgDD;;GAEG;AACH,qBAAa,gBAAgB;IAmBzB,OAAO,CAAC,MAAM;IAlBhB;;;OAGG;IACH,OAAO,CAAC,UAAU,CAA6B;IAE/C,OAAO,CAAC,MAAM,CAAC,CAAY;IAE3B;;;;;;OAMG;IACH,OAAO,CAAC,mBAAmB,CAAC,CAAgB;gBAGlC,MAAM,EAAE,cAAc,EAC9B,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE;IAMjE;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,OAAO,CAAC,0BAA0B;IAuDlC,OAAO,CAAC,sBAAsB;IAU9B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,kBAAkB;IA+B1B;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,yBAAyB;IA6BjC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAQxB,8EAA8E;IAC9E,WAAW,IAAI,IAAI;IAInB,OAAO,CAAC,oBAAoB;IAQ5B;;;;;;OAMG;IACH,OAAO,CAAC,WAAW;IAqBnB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IASxB;;;OAGG;IACH,aAAa,CACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EACjC,OAAO,EAAE,cAAc,EACvB,MAAM,CAAC,EAAE,MAAM,EAAE;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,mBAAmB;IACnC;;;OAGG;IACH,YAAY,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAC/B,cAAc;
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
534
|
-
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|