grafeo-ogm 1.13.0 → 1.14.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 +42 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts +54 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +134 -32
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +7 -0
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +9 -1
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +12 -3
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +6 -1
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/utils/validation.d.ts +7 -0
- package/dist/cjs/utils/validation.d.ts.map +1 -1
- package/dist/cjs/utils/validation.js +421 -0
- package/dist/cjs/utils/validation.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +54 -1
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +134 -32
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +8 -1
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +9 -1
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +12 -3
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +6 -1
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/utils/validation.d.ts +7 -0
- package/dist/esm/utils/validation.d.ts.map +1 -1
- package/dist/esm/utils/validation.js +420 -0
- package/dist/esm/utils/validation.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0 (2026-07-24) — 🛡️ Security release: NLS enforcement on interface reads & relationship writes, and a `_MATCHES` ReDoS guard
|
|
4
|
+
|
|
5
|
+
> **Three security fixes.** Two are object-level authorization gaps: grafeo's node-level-security (NLS) policies were threaded correctly through the concrete-model read and write paths, but two paths silently skipped the **target type's** policy — nested relationship projections on **interface** reads, and **connect/disconnect** relationship writes — so a caller whose own access was correctly scoped could still read, or link/unlink, related nodes their policy forbids. The third hardens the `_MATCHES` filter operator against server-side ReDoS. If you use `policies` with interface models or nested `connect`/`disconnect`, or expose `_MATCHES` to untrusted input, upgrade.
|
|
6
|
+
|
|
7
|
+
### 🛡️ Interface reads now apply the target type's `read` policy to nested projections
|
|
8
|
+
|
|
9
|
+
`InterfaceModel.find` (and its `findFirst` / `findUnique` / `*OrThrow` siblings, which all route through it) applied the interface's source-side policy and the WHERE-side traversal policy — but compiled the **RETURN projection** without the read `policyContext` that `Model.find` passes. Every nested relationship pattern comprehension was therefore emitted with **no target-type policy**, so an interface query could project fields of related, policy-protected node types (e.g. another tenant's `author { email }`) that the target type's `read` policy is meant to filter out. The leak was silent and spanned every row the interface query returned.
|
|
10
|
+
|
|
11
|
+
The fix threads the resolved read `policyContext` into the interface selection compile, exactly as the concrete-model path already does, so every nested projection AND-stitches the target type's read policy — including union and connection branches. **When no policy layer is configured the emitted Cypher is byte-identical** to before; the guard engages only when a policy actually resolves for the projected type.
|
|
12
|
+
|
|
13
|
+
### 🛡️ Nested `connect` / `disconnect` now enforce the target type's `read` policy
|
|
14
|
+
|
|
15
|
+
`MutationCompiler` was policy-unaware: the target `MATCH` emitted for a nested `connect` (MERGE) or `disconnect` selected the target node by the caller's `where` alone, with no policy. A caller operating under a bound `withContext(ctx)` — whose *source* node was correctly policy-filtered — could therefore link or unlink relationships to arbitrary target nodes their policy should forbid (cross-tenant / cross-owner relationship writes):
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// `Book` has a read policy scoped to the owner. Previously this linked a
|
|
19
|
+
// foreign Book anyway — the target MATCH carried no Book policy. Now it
|
|
20
|
+
// AND-stitches the Book read policy, so an unreadable target fails to
|
|
21
|
+
// match and the MERGE is a no-op.
|
|
22
|
+
ogm.withContext({ uid }).model('User').update({
|
|
23
|
+
where: { id: myId },
|
|
24
|
+
connect: { ownedBooks: { where: { id: someBookId } } },
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The fix threads the resolved policy context into `MutationCompiler` and AND-stitches the target type's `read` policy into every connect/disconnect target `MATCH` — all seven sites (single / array / `UNWIND` connect, specific disconnect, nested-in-update connect and disconnect, union targets), reached from both `update` and `updateMany`. `read` is the correct gate: row-level target visibility is expressed as the `read` policy, while `create`/`update` restrictives are WITH-CHECK predicates over a mutation-input bag that a referenced existing node has none of. **No-policy, bypass (`unsafe.bypassPolicies`), and no-target-policy paths stay byte-identical**; `create` and `upsert` (which never emits a relationship `MATCH`) are untouched. Policy predicates use the mutation's shared, monotonic parameter counter, so they never collide with the caller's `connect`/`disconnect` `where` params.
|
|
29
|
+
|
|
30
|
+
### 🛡️ `_MATCHES` is now guarded against catastrophic-backtracking regexes (ReDoS)
|
|
31
|
+
|
|
32
|
+
The `_MATCHES` filter operator forwards its value into a Neo4j `=~` predicate, which the server evaluates with Java's **backtracking** regex engine. An attacker able to influence a `where` filter could supply a catastrophic pattern (e.g. `(a+)+$`, `([a-z]+)+$`, `(a?)+$`) and pin shared Neo4j CPU — a server-side ReDoS ([CWE-1333](https://cwe.mitre.org/data/definitions/1333.html)). Parameterization stops injection but not this: the payload *is* the regex.
|
|
33
|
+
|
|
34
|
+
Every `_MATCHES` value is now validated at compile time by a **sound-by-construction** guard. Rather than blocklisting known-bad shapes (always incomplete), it accepts a pattern **only when it can prove the pattern is linear**, and refuses everything it cannot model — alternation, quantified groups, lookaround, backreferences, unsupported escapes. The check is single-pass and bounded, so it cannot itself become a DoS vector, and Java-flavor divergences are modeled soundly (`\v` is the vertical-whitespace class; `\0`/octal escapes are refused; only inline flags that don't change the analysis — `i`/`m`/`s`/`d` — are honored, so `(?x)` COMMENTS mode and `(?u)`/`(?U)` Unicode widening are refused).
|
|
35
|
+
|
|
36
|
+
- **Precision cost (intended, fail-closed).** Some linear-but-unprovable patterns are refused — notably **all alternation** (`(cat|dog)`) and **contains-style** `.*x.*` shapes. Reach for the dedicated, always-safe operators (`_CONTAINS` / `_STARTS_WITH` / `_ENDS_WITH` / `_IN`), simplify the pattern, or disable `_MATCHES` entirely via `features.filters.String.MATCHES` if you must run arbitrary regexes and accept the risk. The refusal error names these options.
|
|
37
|
+
- **Common patterns keep working** — anchors, character classes, single wildcards (`^foo.*bar$`, `[A-Za-z0-9_]+`, `\d{3}-\d{4}`), fenced quantifiers (`[a-z]+@[a-z]+\.[a-z]+`), and inline-flag prefixes (`(?i)…`).
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
Interface-read policy: two specs asserting the nested projection now carries the target `read` predicate for interface reads, and that the no-policy path stays byte-identical. Connect/disconnect policy: six specs covering connect and disconnect target MATCHes carrying the target `read` policy, the update-op-not-stitched case, byte-identical no-policy/bypass Cypher, and non-colliding parameter numbering. ReDoS guard: a 100+-case battery covering every known catastrophic family (nested quantifiers, nullable/overlapping alternation under repeat, custom-class and nested-group bases, unrolled adjacent optionals), fail-closed refusal of unmodeled constructs, acceptance of provably-linear patterns, and the three Java-flavor soundness regressions found and closed during adversarial review (`(?x)`, `\v`, `\0`). Full suite green (1629); lint and format clean.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
3
45
|
## 1.13.0 (2026-07-14) — ✨ `$cloneSubgraph` returns `nodesByLabel`
|
|
4
46
|
|
|
5
47
|
> **First community-requested feature** ([#1](https://github.com/neomodular/grafeo-ogm/issues/1)). After deep-cloning a content subgraph, a caller often needs to post-process only the nodes of a **specific label** (re-normalize, re-stamp, re-index). Until now `nodeMapping` held every cloned id across all labels, forcing a broad-`IN`-plus-label-filter dance on the caller. The clone already visits every node with its labels in hand — so now it just tells you.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { OGMLogger } from '../execution/executor';
|
|
2
|
+
import type { PolicyContextBundle } from '../policy/types';
|
|
2
3
|
import { NodeDefinition, SchemaMetadata } from '../schema/types';
|
|
4
|
+
import { WhereCompiler } from './where.compiler';
|
|
3
5
|
export interface MutationResult {
|
|
4
6
|
cypher: string;
|
|
5
7
|
params: Record<string, unknown>;
|
|
@@ -15,9 +17,44 @@ export declare class MutationCompiler {
|
|
|
15
17
|
*/
|
|
16
18
|
private labelCache;
|
|
17
19
|
private logger?;
|
|
20
|
+
/**
|
|
21
|
+
* WhereCompiler used ONLY to compile a relationship TARGET type's
|
|
22
|
+
* `read` policy predicate for connect/disconnect target MATCHes,
|
|
23
|
+
* mirroring the read paths (`WhereCompiler`/`SelectionCompiler`).
|
|
24
|
+
* Injected by the OGM so it shares the user's compiler options;
|
|
25
|
+
* lazily constructed with default options otherwise.
|
|
26
|
+
*/
|
|
27
|
+
private policyWhereCompiler?;
|
|
18
28
|
constructor(schema: SchemaMetadata, options?: {
|
|
19
29
|
logger?: OGMLogger;
|
|
30
|
+
whereCompiler?: WhereCompiler;
|
|
20
31
|
});
|
|
32
|
+
/**
|
|
33
|
+
* Resolve and compile the TARGET type's `read` policy predicate for a
|
|
34
|
+
* connect/disconnect target MATCH, mirroring the read path
|
|
35
|
+
* (`WhereCompiler.buildTargetBundle` + `WhereCompiler.compile`). You
|
|
36
|
+
* must be allowed to SEE a node to link/unlink it, so linking to an
|
|
37
|
+
* unreadable node is the IDOR this closes; the codebase models the
|
|
38
|
+
* row-level write gate for `update`/`delete` targets as `read`
|
|
39
|
+
* ReadRestrictive policies (see policy/types.ts), so `read` is both
|
|
40
|
+
* necessary and the semantically correct operation here.
|
|
41
|
+
*
|
|
42
|
+
* Returns `null` — leaving the emitted Cypher BYTE-IDENTICAL — when:
|
|
43
|
+
* - no policy context is threaded (no policy bound / bypass active), OR
|
|
44
|
+
* - no `paramCounter` is available to allocate policy params, OR
|
|
45
|
+
* - the target type has no policy applicable to `read`, OR
|
|
46
|
+
* - the resolved policy is `overridden` (compiles to nothing).
|
|
47
|
+
*
|
|
48
|
+
* When a target policy resolves, the compiled predicate is returned and
|
|
49
|
+
* its bound params are merged into `params`. Policy params use the
|
|
50
|
+
* shared `paramCounter` (`param<N>`), continuing the sequence started by
|
|
51
|
+
* the top-level WHERE, so they can never collide with the
|
|
52
|
+
* connect/disconnect `where` params (prefix-named, e.g.
|
|
53
|
+
* `connect_<field>_<prop>`) or the selection's params (allocated later
|
|
54
|
+
* from the same counter).
|
|
55
|
+
*/
|
|
56
|
+
private buildTargetPolicyPredicate;
|
|
57
|
+
private getPolicyWhereCompiler;
|
|
21
58
|
/**
|
|
22
59
|
* Surface a logical operator that contributed zero effective conditions.
|
|
23
60
|
* Mirrors WhereCompiler.warnEmptyLogical — fires only on the anomalous
|
|
@@ -39,7 +76,23 @@ export declare class MutationCompiler {
|
|
|
39
76
|
cypher: string;
|
|
40
77
|
params: Record<string, unknown>;
|
|
41
78
|
preludes?: string[];
|
|
42
|
-
}, labels?: string[], returnMode?: 'node' | 'count'
|
|
79
|
+
}, labels?: string[], returnMode?: 'node' | 'count',
|
|
80
|
+
/**
|
|
81
|
+
* The caller's resolved policy bundle for this mutation (the SOURCE
|
|
82
|
+
* type's `update` bundle). Its `resolveForType` is used to resolve
|
|
83
|
+
* each connect/disconnect TARGET type's `read` policy. `undefined`
|
|
84
|
+
* when no policy is bound or a bypass is active — in which case the
|
|
85
|
+
* emitted Cypher is byte-identical to before this enforcement.
|
|
86
|
+
*/
|
|
87
|
+
policyContext?: PolicyContextBundle,
|
|
88
|
+
/**
|
|
89
|
+
* Shared `param<N>` counter, continued from the top-level WHERE
|
|
90
|
+
* compile so target-policy params never collide with connect/
|
|
91
|
+
* disconnect `where` params or the selection's params.
|
|
92
|
+
*/
|
|
93
|
+
paramCounter?: {
|
|
94
|
+
count: number;
|
|
95
|
+
}): MutationResult;
|
|
43
96
|
/**
|
|
44
97
|
* Generate DELETE Cypher with optional cascade.
|
|
45
98
|
*/
|
|
@@ -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;AACvD,OAAO,EACL,cAAc,EAEd,cAAc,EACf,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ exports.MutationCompiler = void 0;
|
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
const utils_1 = require("../schema/utils");
|
|
6
6
|
const validation_1 = require("../utils/validation");
|
|
7
|
+
const where_compiler_1 = require("./where.compiler");
|
|
7
8
|
/**
|
|
8
9
|
* Relationship filter suffixes that require EXISTS subquery patterns.
|
|
9
10
|
* These match related nodes via relationship traversal rather than scalar property comparison.
|
|
@@ -23,6 +24,59 @@ class MutationCompiler {
|
|
|
23
24
|
/** Counter for unique relationship filter variables within a single WHERE clause. */
|
|
24
25
|
this.relFilterCounter = 0;
|
|
25
26
|
this.logger = options?.logger;
|
|
27
|
+
this.policyWhereCompiler = options?.whereCompiler;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve and compile the TARGET type's `read` policy predicate for a
|
|
31
|
+
* connect/disconnect target MATCH, mirroring the read path
|
|
32
|
+
* (`WhereCompiler.buildTargetBundle` + `WhereCompiler.compile`). You
|
|
33
|
+
* must be allowed to SEE a node to link/unlink it, so linking to an
|
|
34
|
+
* unreadable node is the IDOR this closes; the codebase models the
|
|
35
|
+
* row-level write gate for `update`/`delete` targets as `read`
|
|
36
|
+
* ReadRestrictive policies (see policy/types.ts), so `read` is both
|
|
37
|
+
* necessary and the semantically correct operation here.
|
|
38
|
+
*
|
|
39
|
+
* Returns `null` — leaving the emitted Cypher BYTE-IDENTICAL — when:
|
|
40
|
+
* - no policy context is threaded (no policy bound / bypass active), OR
|
|
41
|
+
* - no `paramCounter` is available to allocate policy params, OR
|
|
42
|
+
* - the target type has no policy applicable to `read`, OR
|
|
43
|
+
* - the resolved policy is `overridden` (compiles to nothing).
|
|
44
|
+
*
|
|
45
|
+
* When a target policy resolves, the compiled predicate is returned and
|
|
46
|
+
* its bound params are merged into `params`. Policy params use the
|
|
47
|
+
* shared `paramCounter` (`param<N>`), continuing the sequence started by
|
|
48
|
+
* the top-level WHERE, so they can never collide with the
|
|
49
|
+
* connect/disconnect `where` params (prefix-named, e.g.
|
|
50
|
+
* `connect_<field>_<prop>`) or the selection's params (allocated later
|
|
51
|
+
* from the same counter).
|
|
52
|
+
*/
|
|
53
|
+
buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter) {
|
|
54
|
+
if (!policyContext || !paramCounter)
|
|
55
|
+
return null;
|
|
56
|
+
const targetPolicy = policyContext.resolveForType(targetNodeDef.typeName, 'read');
|
|
57
|
+
if (!targetPolicy)
|
|
58
|
+
return null;
|
|
59
|
+
const targetBundle = {
|
|
60
|
+
ctx: policyContext.ctx,
|
|
61
|
+
operation: 'read',
|
|
62
|
+
resolved: targetPolicy,
|
|
63
|
+
resolveForType: policyContext.resolveForType,
|
|
64
|
+
defaults: policyContext.defaults,
|
|
65
|
+
};
|
|
66
|
+
const compiled = this.getPolicyWhereCompiler().compile(undefined, targetVar, targetNodeDef, paramCounter, { policyContext: targetBundle });
|
|
67
|
+
if (compiled.preludes && compiled.preludes.length > 0)
|
|
68
|
+
throw new errors_1.OGMError(`Policy on "${targetNodeDef.typeName}" requires @cypher field ` +
|
|
69
|
+
`projection, which is not supported inside connect/disconnect ` +
|
|
70
|
+
`target filters. Refactor the policy to use stored properties.`);
|
|
71
|
+
if (!compiled.cypher)
|
|
72
|
+
return null;
|
|
73
|
+
(0, validation_1.mergeParams)(params, compiled.params);
|
|
74
|
+
return compiled.cypher;
|
|
75
|
+
}
|
|
76
|
+
getPolicyWhereCompiler() {
|
|
77
|
+
if (!this.policyWhereCompiler)
|
|
78
|
+
this.policyWhereCompiler = new where_compiler_1.WhereCompiler(this.schema);
|
|
79
|
+
return this.policyWhereCompiler;
|
|
26
80
|
}
|
|
27
81
|
/**
|
|
28
82
|
* Surface a logical operator that contributed zero effective conditions.
|
|
@@ -67,7 +121,11 @@ class MutationCompiler {
|
|
|
67
121
|
// Process relationship operations (nested create / connect)
|
|
68
122
|
// Pass all previously created node vars as ancestors so WITH clauses
|
|
69
123
|
// don't drop them from scope (needed for RETURN n at the end)
|
|
70
|
-
const relLines = this.buildCreateRelationships(input, nodeDef, nodeVar, prefix, params, createdVars.slice(0, -1)
|
|
124
|
+
const relLines = this.buildCreateRelationships(input, nodeDef, nodeVar, prefix, params, createdVars.slice(0, -1), // ancestors = all vars before current
|
|
125
|
+
// No policy context is threaded on the create path: `create`'s
|
|
126
|
+
// nested connect enforcement is out of scope for this fix, so
|
|
127
|
+
// the emitted create Cypher stays byte-identical.
|
|
128
|
+
undefined, undefined);
|
|
71
129
|
lines.push(...relLines);
|
|
72
130
|
}
|
|
73
131
|
// Add extra labels to all created nodes
|
|
@@ -82,7 +140,21 @@ class MutationCompiler {
|
|
|
82
140
|
/**
|
|
83
141
|
* Generate UPDATE Cypher (SET properties + connect/disconnect).
|
|
84
142
|
*/
|
|
85
|
-
compileUpdate(_where, update, connect, disconnect, nodeDef, whereResult, labels, returnMode = 'node'
|
|
143
|
+
compileUpdate(_where, update, connect, disconnect, nodeDef, whereResult, labels, returnMode = 'node',
|
|
144
|
+
/**
|
|
145
|
+
* The caller's resolved policy bundle for this mutation (the SOURCE
|
|
146
|
+
* type's `update` bundle). Its `resolveForType` is used to resolve
|
|
147
|
+
* each connect/disconnect TARGET type's `read` policy. `undefined`
|
|
148
|
+
* when no policy is bound or a bypass is active — in which case the
|
|
149
|
+
* emitted Cypher is byte-identical to before this enforcement.
|
|
150
|
+
*/
|
|
151
|
+
policyContext,
|
|
152
|
+
/**
|
|
153
|
+
* Shared `param<N>` counter, continued from the top-level WHERE
|
|
154
|
+
* compile so target-policy params never collide with connect/
|
|
155
|
+
* disconnect `where` params or the selection's params.
|
|
156
|
+
*/
|
|
157
|
+
paramCounter) {
|
|
86
158
|
this.relFilterCounter = 0;
|
|
87
159
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
88
160
|
const lines = [];
|
|
@@ -117,18 +189,18 @@ class MutationCompiler {
|
|
|
117
189
|
if (setClauses.length > 0)
|
|
118
190
|
lines.push(`SET ${setClauses.join(', ')}`);
|
|
119
191
|
// Process nested relationship operations within update body
|
|
120
|
-
const relLines = this.buildUpdateRelationships(update, nodeDef, 'n', 'update', params);
|
|
192
|
+
const relLines = this.buildUpdateRelationships(update, nodeDef, 'n', 'update', params, 0, [], policyContext, paramCounter);
|
|
121
193
|
lines.push(...relLines);
|
|
122
194
|
}
|
|
123
195
|
// Disconnect relationships (top-level) — must run before connects
|
|
124
196
|
// so that blanket disconnects don't remove newly-connected relationships.
|
|
125
197
|
if (disconnect) {
|
|
126
|
-
const disconnectLines = this.buildDisconnects(disconnect, nodeDef, params);
|
|
198
|
+
const disconnectLines = this.buildDisconnects(disconnect, nodeDef, params, policyContext, paramCounter);
|
|
127
199
|
lines.push(...disconnectLines);
|
|
128
200
|
}
|
|
129
201
|
// Connect relationships (top-level)
|
|
130
202
|
if (connect) {
|
|
131
|
-
const connectLines = this.buildConnects(connect, nodeDef, params);
|
|
203
|
+
const connectLines = this.buildConnects(connect, nodeDef, params, policyContext, paramCounter);
|
|
132
204
|
lines.push(...connectLines);
|
|
133
205
|
}
|
|
134
206
|
lines.push(returnMode === 'count' ? 'RETURN count(n) AS count' : 'RETURN n');
|
|
@@ -390,7 +462,7 @@ class MutationCompiler {
|
|
|
390
462
|
}
|
|
391
463
|
return { propString: parts.join(', '), propParams };
|
|
392
464
|
}
|
|
393
|
-
buildCreateRelationships(input, nodeDef, nodeVar, prefix, params, ancestorVars = []) {
|
|
465
|
+
buildCreateRelationships(input, nodeDef, nodeVar, prefix, params, ancestorVars = [], policyContext, paramCounter) {
|
|
394
466
|
const lines = [];
|
|
395
467
|
let nestedCounter = 0;
|
|
396
468
|
// WITH clause must carry all ancestors + the node being created on
|
|
@@ -415,7 +487,7 @@ class MutationCompiler {
|
|
|
415
487
|
continue;
|
|
416
488
|
const memberInput = memberValue;
|
|
417
489
|
// Process creates/connects for this union member
|
|
418
|
-
const memberLines = this.buildCreateRelationshipsForTarget(memberInput, memberNodeDef, relDef, nodeVar, `${prefix}_${key}_${memberKey}`, params, allVars, withClause, nestedCounter);
|
|
490
|
+
const memberLines = this.buildCreateRelationshipsForTarget(memberInput, memberNodeDef, relDef, nodeVar, `${prefix}_${key}_${memberKey}`, params, allVars, withClause, nestedCounter, policyContext, paramCounter);
|
|
419
491
|
nestedCounter += memberLines.counterUsed;
|
|
420
492
|
lines.push(...memberLines.lines);
|
|
421
493
|
}
|
|
@@ -424,7 +496,7 @@ class MutationCompiler {
|
|
|
424
496
|
const targetNodeDef = this.schema.nodes.get(relDef.target);
|
|
425
497
|
if (!targetNodeDef)
|
|
426
498
|
continue;
|
|
427
|
-
const resultLines = this.buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, `${prefix}_${key}`, params, allVars, withClause, nestedCounter);
|
|
499
|
+
const resultLines = this.buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, `${prefix}_${key}`, params, allVars, withClause, nestedCounter, policyContext, paramCounter);
|
|
428
500
|
nestedCounter += resultLines.counterUsed;
|
|
429
501
|
lines.push(...resultLines.lines);
|
|
430
502
|
}
|
|
@@ -434,7 +506,7 @@ class MutationCompiler {
|
|
|
434
506
|
* Process create/connect operations for a specific target node type.
|
|
435
507
|
* Used by buildCreateRelationships for both union-member and non-union targets.
|
|
436
508
|
*/
|
|
437
|
-
buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, prefix, params, allVars, withClause, startCounter) {
|
|
509
|
+
buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, prefix, params, allVars, withClause, startCounter, policyContext, paramCounter) {
|
|
438
510
|
const lines = [];
|
|
439
511
|
let counter = 0;
|
|
440
512
|
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
@@ -450,7 +522,7 @@ class MutationCompiler {
|
|
|
450
522
|
counter++;
|
|
451
523
|
const { propString, propParams } = this.buildCreateProperties(nodeSpec, targetNodeDef, `${prefix}_create${ci}`);
|
|
452
524
|
// Handle nested relationship creates within the created node
|
|
453
|
-
const nestedRelLines = this.buildCreateRelationships(nodeSpec, targetNodeDef, createVar, `${prefix}_create${ci}`, params, allVars);
|
|
525
|
+
const nestedRelLines = this.buildCreateRelationships(nodeSpec, targetNodeDef, createVar, `${prefix}_create${ci}`, params, allVars, policyContext, paramCounter);
|
|
454
526
|
const propsClause = propString.length > 0
|
|
455
527
|
? ` { ${propString} }`
|
|
456
528
|
: ` { ${this.buildGeneratedIdClause(targetNodeDef)} }`;
|
|
@@ -522,8 +594,12 @@ class MutationCompiler {
|
|
|
522
594
|
counter++;
|
|
523
595
|
lines.push(`MATCH (${connectVar}:${targetLabelStr})`);
|
|
524
596
|
const conditions = this.buildConnectionWhereConditions(whereSpec, connectVar, `${prefix}_conn${ci}`, params, targetNodeDef);
|
|
525
|
-
|
|
526
|
-
|
|
597
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter);
|
|
598
|
+
const connectConditions = policyPredicate
|
|
599
|
+
? [...conditions, policyPredicate]
|
|
600
|
+
: conditions;
|
|
601
|
+
if (connectConditions.length > 0)
|
|
602
|
+
lines.push(`WHERE ${connectConditions.join(' AND ')}`);
|
|
527
603
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
528
604
|
// Use bare target var (no label) since connectVar is already bound by MATCH.
|
|
529
605
|
// Including labels on a bound variable in MERGE causes Neo4j to reject it.
|
|
@@ -560,7 +636,7 @@ class MutationCompiler {
|
|
|
560
636
|
}
|
|
561
637
|
return { lines, counterUsed: counter };
|
|
562
638
|
}
|
|
563
|
-
buildConnects(connect, nodeDef, params) {
|
|
639
|
+
buildConnects(connect, nodeDef, params, policyContext, paramCounter) {
|
|
564
640
|
const lines = [];
|
|
565
641
|
for (const [fieldName, spec] of Object.entries(connect)) {
|
|
566
642
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
@@ -591,8 +667,12 @@ class MutationCompiler {
|
|
|
591
667
|
const connectVar = `target_${fieldName}_${ci}`;
|
|
592
668
|
lines.push(`MATCH (${connectVar}:${targetLabelStr})`);
|
|
593
669
|
const conditions = this.buildConnectionWhereConditions(whereSpec, connectVar, `connect_${fieldName}_${ci}`, params, targetNodeDef);
|
|
594
|
-
|
|
595
|
-
|
|
670
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter);
|
|
671
|
+
const connectConditions = policyPredicate
|
|
672
|
+
? [...conditions, policyPredicate]
|
|
673
|
+
: conditions;
|
|
674
|
+
if (connectConditions.length > 0)
|
|
675
|
+
lines.push(`WHERE ${connectConditions.join(' AND ')}`);
|
|
596
676
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
597
677
|
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
598
678
|
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
@@ -661,8 +741,12 @@ class MutationCompiler {
|
|
|
661
741
|
`to share the same shape.`);
|
|
662
742
|
}
|
|
663
743
|
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem');
|
|
664
|
-
|
|
665
|
-
|
|
744
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter);
|
|
745
|
+
const unwindConditions = policyPredicate
|
|
746
|
+
? [...whereConditions, policyPredicate]
|
|
747
|
+
: whereConditions;
|
|
748
|
+
if (unwindConditions.length > 0)
|
|
749
|
+
lines.push(`WHERE ${unwindConditions.join(' AND ')}`);
|
|
666
750
|
const mergePattern = (0, utils_1.buildRelPattern)({
|
|
667
751
|
sourceVar: 'n',
|
|
668
752
|
relDef,
|
|
@@ -694,8 +778,12 @@ class MutationCompiler {
|
|
|
694
778
|
lines.push('WITH n');
|
|
695
779
|
lines.push(`MATCH (target:${targetLabelStr})`);
|
|
696
780
|
const matchConditions = this.buildConnectionWhereConditions(whereSpec, 'target', `connect_${fieldName}`, params, targetNodeDef);
|
|
697
|
-
|
|
698
|
-
|
|
781
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter);
|
|
782
|
+
const singleConnectConditions = policyPredicate
|
|
783
|
+
? [...matchConditions, policyPredicate]
|
|
784
|
+
: matchConditions;
|
|
785
|
+
if (singleConnectConditions.length > 0)
|
|
786
|
+
lines.push(`WHERE ${singleConnectConditions.join(' AND ')}`);
|
|
699
787
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
700
788
|
// Use bare target var (no label) since target is already bound by MATCH
|
|
701
789
|
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
@@ -727,7 +815,7 @@ class MutationCompiler {
|
|
|
727
815
|
}
|
|
728
816
|
return lines;
|
|
729
817
|
}
|
|
730
|
-
buildDisconnects(disconnect, nodeDef, params) {
|
|
818
|
+
buildDisconnects(disconnect, nodeDef, params, policyContext, paramCounter) {
|
|
731
819
|
const lines = [];
|
|
732
820
|
for (const [fieldName, spec] of Object.entries(disconnect)) {
|
|
733
821
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
@@ -766,8 +854,14 @@ class MutationCompiler {
|
|
|
766
854
|
});
|
|
767
855
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
768
856
|
const conditions = this.buildConnectionWhereConditions(whereSpec, targetVar, `disconnect_${fieldName}_${si}`, params, targetNodeDef);
|
|
769
|
-
|
|
770
|
-
|
|
857
|
+
const policyPredicate = targetNodeDef
|
|
858
|
+
? this.buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter)
|
|
859
|
+
: null;
|
|
860
|
+
const disconnectConditions = policyPredicate
|
|
861
|
+
? [...conditions, policyPredicate]
|
|
862
|
+
: conditions;
|
|
863
|
+
if (disconnectConditions.length > 0)
|
|
864
|
+
lines.push(`WHERE ${disconnectConditions.join(' AND ')}`);
|
|
771
865
|
lines.push(`DELETE ${relVar}`);
|
|
772
866
|
}
|
|
773
867
|
}
|
|
@@ -779,7 +873,7 @@ class MutationCompiler {
|
|
|
779
873
|
* Each member key in the input is recursed into buildUpdateRelationships
|
|
780
874
|
* with a narrowed relDef pointing at the concrete member type.
|
|
781
875
|
*/
|
|
782
|
-
dispatchUnionUpdateOps(key, memberEntries, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars) {
|
|
876
|
+
dispatchUnionUpdateOps(key, memberEntries, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars, policyContext, paramCounter) {
|
|
783
877
|
const unionMembers = this.schema.unions.get(relDef.target);
|
|
784
878
|
const lines = [];
|
|
785
879
|
for (const [memberKey, memberValue] of Object.entries(memberEntries)) {
|
|
@@ -799,7 +893,7 @@ class MutationCompiler {
|
|
|
799
893
|
...nodeDef,
|
|
800
894
|
relationships: new Map([[key, memberRelDef]]),
|
|
801
895
|
};
|
|
802
|
-
lines.push(...this.buildUpdateRelationships({ [key]: memberItems }, memberNodeDef2, sourceVar, `${prefix}_${key}_${memberKey}`, params, depth + 1, ancestorVars));
|
|
896
|
+
lines.push(...this.buildUpdateRelationships({ [key]: memberItems }, memberNodeDef2, sourceVar, `${prefix}_${key}_${memberKey}`, params, depth + 1, ancestorVars, policyContext, paramCounter));
|
|
803
897
|
}
|
|
804
898
|
return lines;
|
|
805
899
|
}
|
|
@@ -807,7 +901,7 @@ class MutationCompiler {
|
|
|
807
901
|
* Process nested relationship operations within an update input.
|
|
808
902
|
* Handles: create, connect, disconnect, update, and arrays of these.
|
|
809
903
|
*/
|
|
810
|
-
buildUpdateRelationships(update, nodeDef, sourceVar, prefix, params, depth = 0, ancestorVars = []) {
|
|
904
|
+
buildUpdateRelationships(update, nodeDef, sourceVar, prefix, params, depth = 0, ancestorVars = [], policyContext, paramCounter) {
|
|
811
905
|
if (depth > 5)
|
|
812
906
|
throw new errors_1.OGMError(`[neo4j-ogm] buildUpdateRelationships: max nesting depth (5) exceeded for prefix "${prefix}". ` +
|
|
813
907
|
'Nested relationship mutations beyond depth 5 are not supported.');
|
|
@@ -824,7 +918,7 @@ class MutationCompiler {
|
|
|
824
918
|
const isUnionTarget = !this.schema.nodes.has(relDef.target) &&
|
|
825
919
|
this.schema.unions?.has(relDef.target);
|
|
826
920
|
if (isUnionTarget) {
|
|
827
|
-
lines.push(...this.dispatchUnionUpdateOps(key, value, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars));
|
|
921
|
+
lines.push(...this.dispatchUnionUpdateOps(key, value, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars, policyContext, paramCounter));
|
|
828
922
|
continue;
|
|
829
923
|
}
|
|
830
924
|
const targetNodeDef = this.schema.nodes.get(relDef.target);
|
|
@@ -894,7 +988,7 @@ class MutationCompiler {
|
|
|
894
988
|
varCounter++;
|
|
895
989
|
const { propString, propParams } = this.buildCreateProperties(nodeSpec, targetNodeDef, `${itemPrefix}_create${ci}`);
|
|
896
990
|
// Handle nested relationship creates within the created node
|
|
897
|
-
const nestedRelLines = this.buildCreateRelationships(nodeSpec, targetNodeDef, createVar, `${itemPrefix}_create${ci}`, params, allVars);
|
|
991
|
+
const nestedRelLines = this.buildCreateRelationships(nodeSpec, targetNodeDef, createVar, `${itemPrefix}_create${ci}`, params, allVars, policyContext, paramCounter);
|
|
898
992
|
const propsClause = propString.length > 0
|
|
899
993
|
? ` { ${propString} }`
|
|
900
994
|
: ` { ${this.buildGeneratedIdClause(targetNodeDef)} }`;
|
|
@@ -963,8 +1057,12 @@ class MutationCompiler {
|
|
|
963
1057
|
// path (`buildDisconnects`). Restores backwards-compat with
|
|
964
1058
|
// @neo4j/graphql-ogm's nested update.disconnect[].where shape.
|
|
965
1059
|
const conditions = this.buildConnectionWhereConditions(whereSpec, discTarget, `${itemPrefix}_disc_${di}`, params, targetNodeDef);
|
|
966
|
-
|
|
967
|
-
|
|
1060
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, discTarget, params, policyContext, paramCounter);
|
|
1061
|
+
const nestedDiscConditions = policyPredicate
|
|
1062
|
+
? [...conditions, policyPredicate]
|
|
1063
|
+
: conditions;
|
|
1064
|
+
if (nestedDiscConditions.length > 0)
|
|
1065
|
+
lines.push(`WHERE ${nestedDiscConditions.join(' AND ')}`);
|
|
968
1066
|
lines.push(`DELETE ${relVar}`);
|
|
969
1067
|
}
|
|
970
1068
|
}
|
|
@@ -993,8 +1091,12 @@ class MutationCompiler {
|
|
|
993
1091
|
// top-level paths. Empty/undefined `whereSpec` short-circuits
|
|
994
1092
|
// to `[]` inside the helper, so the no-where case still works.
|
|
995
1093
|
const conditions = this.buildConnectionWhereConditions(whereSpec, connectVar, `${itemPrefix}_conn${ci}`, params, targetNodeDef);
|
|
996
|
-
|
|
997
|
-
|
|
1094
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter);
|
|
1095
|
+
const nestedConnConditions = policyPredicate
|
|
1096
|
+
? [...conditions, policyPredicate]
|
|
1097
|
+
: conditions;
|
|
1098
|
+
if (nestedConnConditions.length > 0)
|
|
1099
|
+
lines.push(`WHERE ${nestedConnConditions.join(' AND ')}`);
|
|
998
1100
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
999
1101
|
// Use bare target var (no label) since connectVar is already bound by MATCH
|
|
1000
1102
|
const relVar = `r_conn_${key}_${ci}`;
|
|
@@ -1065,7 +1167,7 @@ class MutationCompiler {
|
|
|
1065
1167
|
}
|
|
1066
1168
|
// Pre-compute nested relationship operations
|
|
1067
1169
|
const nestedParams = {};
|
|
1068
|
-
const nestedRelLines = this.buildUpdateRelationships(nodeUpdate, targetNodeDef, updateVar, itemPrefix, nestedParams, depth + 1, allVars);
|
|
1170
|
+
const nestedRelLines = this.buildUpdateRelationships(nodeUpdate, targetNodeDef, updateVar, itemPrefix, nestedParams, depth + 1, allVars, policyContext, paramCounter);
|
|
1069
1171
|
// Only emit the MATCH block if there's actual work to do
|
|
1070
1172
|
const hasWork = setClauses.length > 0 ||
|
|
1071
1173
|
edgeClauses.length > 0 ||
|