grafeo-ogm 2.2.0 → 2.3.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 +83 -0
- package/README.md +49 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +94 -31
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +381 -343
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +33 -39
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +124 -1
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +371 -102
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts +20 -10
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +47 -104
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +4 -29
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +46 -129
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/policy/nested-writes.d.ts +35 -0
- package/dist/cjs/policy/nested-writes.d.ts.map +1 -0
- package/dist/cjs/policy/nested-writes.js +199 -0
- package/dist/cjs/policy/nested-writes.js.map +1 -0
- package/dist/cjs/policy/types.d.ts +18 -0
- package/dist/cjs/policy/types.d.ts.map +1 -1
- package/dist/cjs/policy/types.js +24 -0
- package/dist/cjs/policy/types.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +94 -31
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +381 -344
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +33 -39
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +124 -1
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +371 -104
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts +20 -10
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +47 -104
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +4 -29
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +47 -130
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/policy/nested-writes.d.ts +35 -0
- package/dist/esm/policy/nested-writes.d.ts.map +1 -0
- package/dist/esm/policy/nested-writes.js +193 -0
- package/dist/esm/policy/nested-writes.js.map +1 -0
- package/dist/esm/policy/types.d.ts +18 -0
- package/dist/esm/policy/types.d.ts.map +1 -1
- package/dist/esm/policy/types.js +23 -0
- package/dist/esm/policy/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MutationCompiler = void 0;
|
|
4
|
+
exports.connectionWhereToNodeWhere = connectionWhereToNodeWhere;
|
|
4
5
|
const errors_1 = require("../errors");
|
|
6
|
+
const errors_2 = require("../policy/errors");
|
|
7
|
+
const types_1 = require("../policy/types");
|
|
5
8
|
const utils_1 = require("../schema/utils");
|
|
6
9
|
const validation_1 = require("../utils/validation");
|
|
7
10
|
const write_coercion_1 = require("../utils/write-coercion");
|
|
8
11
|
const where_compiler_1 = require("./where.compiler");
|
|
9
|
-
/**
|
|
10
|
-
* Relationship filter suffixes that require EXISTS subquery patterns.
|
|
11
|
-
* These match related nodes via relationship traversal rather than scalar property comparison.
|
|
12
|
-
*/
|
|
13
|
-
const RELATIONSHIP_SUFFIXES = ['_SOME', '_NONE', '_ALL', '_SINGLE'];
|
|
14
12
|
/**
|
|
15
13
|
* Compiles create, update, delete, and label mutations into Cypher + params.
|
|
16
14
|
*/
|
|
@@ -22,8 +20,6 @@ class MutationCompiler {
|
|
|
22
20
|
* Bounded by the number of node types in the schema (~50-100 entries).
|
|
23
21
|
*/
|
|
24
22
|
this.labelCache = new Map();
|
|
25
|
-
/** Counter for unique relationship filter variables within a single WHERE clause. */
|
|
26
|
-
this.relFilterCounter = 0;
|
|
27
23
|
this.logger = options?.logger;
|
|
28
24
|
this.policyWhereCompiler = options?.whereCompiler;
|
|
29
25
|
}
|
|
@@ -51,34 +47,126 @@ class MutationCompiler {
|
|
|
51
47
|
* `connect_<field>_<prop>`) or the selection's params (allocated later
|
|
52
48
|
* from the same counter).
|
|
53
49
|
*/
|
|
54
|
-
buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter,
|
|
51
|
+
/**
|
|
52
|
+
* Operation whose policy gates the target (v2.3.0): `read` for
|
|
53
|
+
* connect/disconnect (you must see a node to link it), `update` for a
|
|
54
|
+
* nested update, `delete` for nested/cascade deletes — exactly what the
|
|
55
|
+
* equivalent DIRECT write on the target type enforces.
|
|
56
|
+
*/
|
|
57
|
+
op = 'read') {
|
|
58
|
+
if (!policyContext)
|
|
59
59
|
return null;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
60
|
+
// v2.3.0 — a missing counter must never mean a missing policy.
|
|
61
|
+
const counter = paramCounter ?? counterAfter(params);
|
|
62
|
+
// Mirror the direct write's compile-time default-deny: a direct
|
|
63
|
+
// update/delete with `onDeny: 'throw'` and no applicable permissive
|
|
64
|
+
// throws before running, so the nested equivalent must too. (Connect/
|
|
65
|
+
// disconnect keep their pre-2.3.0 `read` semantics: compile to false.)
|
|
66
|
+
if (op !== 'read' && policyContext.defaults.onDeny === 'throw') {
|
|
67
|
+
const resolved = policyContext.resolveForType(targetNodeDef.typeName, op);
|
|
68
|
+
if (resolved && !resolved.overridden && resolved.permissives.length === 0)
|
|
69
|
+
throw new errors_2.PolicyDeniedError({
|
|
70
|
+
typeName: targetNodeDef.typeName,
|
|
71
|
+
operation: op,
|
|
72
|
+
reason: 'no-permissive-matched',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Abstract (interface / union) targets get a CASE over their concrete
|
|
76
|
+
// members' own clauses — pre-2.3.0 only the abstract type's own
|
|
77
|
+
// policies (usually none) were applied.
|
|
78
|
+
const clause = this.getPolicyWhereCompiler().compileTargetPolicyClause(targetNodeDef.typeName, targetVar, op, policyContext, counter);
|
|
79
|
+
if (clause.preludes.length > 0)
|
|
69
80
|
throw new errors_1.OGMError(`Policy on "${targetNodeDef.typeName}" requires @cypher field ` +
|
|
70
|
-
`projection, which is not supported inside
|
|
81
|
+
`projection, which is not supported inside nested mutation ` +
|
|
71
82
|
`target filters. Refactor the policy to use stored properties.`);
|
|
72
|
-
if (!
|
|
83
|
+
if (!clause.cypher)
|
|
73
84
|
return null;
|
|
74
|
-
(0, validation_1.mergeParams)(params,
|
|
75
|
-
return
|
|
85
|
+
(0, validation_1.mergeParams)(params, clause.params);
|
|
86
|
+
return clause.cypher;
|
|
76
87
|
}
|
|
77
88
|
getPolicyWhereCompiler() {
|
|
89
|
+
// Inherit the logger: since v2.3.0 this compiler also compiles every
|
|
90
|
+
// nested-write `where`, and its empty-AND/OR warnings must surface.
|
|
78
91
|
if (!this.policyWhereCompiler)
|
|
79
|
-
this.policyWhereCompiler = new where_compiler_1.WhereCompiler(this.schema
|
|
92
|
+
this.policyWhereCompiler = new where_compiler_1.WhereCompiler(this.schema, {
|
|
93
|
+
logger: this.logger,
|
|
94
|
+
});
|
|
80
95
|
return this.policyWhereCompiler;
|
|
81
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Compile a nested-write `where` (connect, disconnect, nested update,
|
|
99
|
+
* nested/cascade delete) against `targetVar` through `WhereCompiler` —
|
|
100
|
+
* the single where implementation (v2.3.0: the mutation compiler's
|
|
101
|
+
* parallel builder was removed after repeatedly drifting from it).
|
|
102
|
+
* Operators, null semantics, abstract relationship targets and —
|
|
103
|
+
* crucially — traversal-policy enforcement are identical to a direct
|
|
104
|
+
* `where`: a relationship filter inside it AND-stitches the traversed
|
|
105
|
+
* type's `read` policy. The target's OWN operation policy is added
|
|
106
|
+
* separately by `buildTargetPolicyPredicate`, so the root here is bound
|
|
107
|
+
* as `NO_ROOT_POLICY`.
|
|
108
|
+
*
|
|
109
|
+
* Returns `''` for an absent or empty where.
|
|
110
|
+
*/
|
|
111
|
+
compileTargetWhere(whereSpec, targetVar, targetNodeDef, params, policyContext, paramCounter) {
|
|
112
|
+
if (!whereSpec || Object.keys(whereSpec).length === 0)
|
|
113
|
+
return '';
|
|
114
|
+
const counter = paramCounter ?? counterAfter(params);
|
|
115
|
+
const nodeWhere = connectionWhereToNodeWhere(whereSpec);
|
|
116
|
+
const traversalBundle = policyContext
|
|
117
|
+
? { ...policyContext, operation: 'read', resolved: types_1.NO_ROOT_POLICY }
|
|
118
|
+
: undefined;
|
|
119
|
+
const compiled = this.getPolicyWhereCompiler().compile(nodeWhere, targetVar, targetNodeDef, counter, traversalBundle ? { policyContext: traversalBundle } : undefined);
|
|
120
|
+
if (compiled.preludes && compiled.preludes.length > 0)
|
|
121
|
+
throw new errors_1.OGMError(`Filtering a nested mutation target of type "${targetNodeDef.typeName}" ` +
|
|
122
|
+
`by an @cypher field is not supported. Use stored properties in ` +
|
|
123
|
+
`nested connect/disconnect/update/delete filters.`);
|
|
124
|
+
(0, validation_1.mergeParams)(params, compiled.params);
|
|
125
|
+
return compiled.cypher;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The bulk-connect UNWIND fast path compiles each where key against the
|
|
129
|
+
* ROW's own value (`connItem.where.node.<key>`), which `WhereCompiler`
|
|
130
|
+
* cannot express. It is therefore restricted to the one shape where
|
|
131
|
+
* that is exact: every item's where is `{ node: { … } }` or bare
|
|
132
|
+
* properties, holding only declared, stored scalar properties of the
|
|
133
|
+
* target (registered operators allowed) with NON-NULL values — no
|
|
134
|
+
* `AND`/`OR`/`NOT`/`node_NOT`, no relationship traversal, no `@cypher`
|
|
135
|
+
* field. Every predicate it emits is then a scalar comparison against a
|
|
136
|
+
* concrete row value, so it can only narrow matches, never fail open.
|
|
137
|
+
* Anything else takes the per-item path through `WhereCompiler`.
|
|
138
|
+
*/
|
|
139
|
+
isConnectFastPathEligible(items, targetNodeDef) {
|
|
140
|
+
for (const item of items) {
|
|
141
|
+
const where = item.where;
|
|
142
|
+
if (where === undefined)
|
|
143
|
+
continue;
|
|
144
|
+
if (!(0, validation_1.isPlainObject)(where))
|
|
145
|
+
return false;
|
|
146
|
+
const keys = Object.keys(where).filter((k) => where[k] !== undefined);
|
|
147
|
+
let nodeWhere;
|
|
148
|
+
if (keys.some((k) => CONNECTION_WHERE_KEYS.has(k))) {
|
|
149
|
+
if (keys.length !== 1 || keys[0] !== 'node')
|
|
150
|
+
return false;
|
|
151
|
+
if (!(0, validation_1.isPlainObject)(where.node))
|
|
152
|
+
return false;
|
|
153
|
+
nodeWhere = where.node;
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
nodeWhere = where;
|
|
157
|
+
for (const [key, val] of Object.entries(nodeWhere)) {
|
|
158
|
+
if (val === undefined || val === null)
|
|
159
|
+
return false;
|
|
160
|
+
if ((0, validation_1.isPlainObject)(val))
|
|
161
|
+
return false;
|
|
162
|
+
const { baseProp } = this.parseOperatorSuffix(key);
|
|
163
|
+
const propDef = targetNodeDef.properties.get(baseProp);
|
|
164
|
+
if (!propDef || propDef.isCypher)
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
82
170
|
/**
|
|
83
171
|
* Surface a logical operator that contributed zero effective conditions.
|
|
84
172
|
* Mirrors WhereCompiler.warnEmptyLogical — fires only on the anomalous
|
|
@@ -105,8 +193,19 @@ class MutationCompiler {
|
|
|
105
193
|
* Generate CREATE Cypher for one or more nodes.
|
|
106
194
|
* Handles scalar properties, nested relationship creates, and connects.
|
|
107
195
|
*/
|
|
108
|
-
compileCreate(inputs, nodeDef, labels
|
|
109
|
-
|
|
196
|
+
compileCreate(inputs, nodeDef, labels,
|
|
197
|
+
/**
|
|
198
|
+
* The caller's `create` bundle (v2.3.0). Its `resolveForType` gates
|
|
199
|
+
* every connect target in the nested input by the TARGET type's `read`
|
|
200
|
+
* policy — pre-2.3.0 the create path threaded no policy at all.
|
|
201
|
+
*/
|
|
202
|
+
policyContext,
|
|
203
|
+
/**
|
|
204
|
+
* Shared `param<N>` counter; continue it into the RETURN selection so
|
|
205
|
+
* nested-filter params never collide with the selection's.
|
|
206
|
+
*/
|
|
207
|
+
paramCounter) {
|
|
208
|
+
const counter = paramCounter ?? { count: 0 };
|
|
110
209
|
const lines = [];
|
|
111
210
|
const params = {};
|
|
112
211
|
const createdVars = [];
|
|
@@ -123,10 +222,7 @@ class MutationCompiler {
|
|
|
123
222
|
// Pass all previously created node vars as ancestors so WITH clauses
|
|
124
223
|
// don't drop them from scope (needed for RETURN n at the end)
|
|
125
224
|
const relLines = this.buildCreateRelationships(input, nodeDef, nodeVar, prefix, params, createdVars.slice(0, -1), // ancestors = all vars before current
|
|
126
|
-
|
|
127
|
-
// nested connect enforcement is out of scope for this fix, so
|
|
128
|
-
// the emitted create Cypher stays byte-identical.
|
|
129
|
-
undefined, undefined);
|
|
225
|
+
policyContext, counter);
|
|
130
226
|
lines.push(...relLines);
|
|
131
227
|
}
|
|
132
228
|
// Add extra labels to all created nodes
|
|
@@ -156,10 +252,12 @@ class MutationCompiler {
|
|
|
156
252
|
* disconnect `where` params or the selection's params.
|
|
157
253
|
*/
|
|
158
254
|
paramCounter) {
|
|
159
|
-
this.relFilterCounter = 0;
|
|
160
255
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
161
256
|
const lines = [];
|
|
162
257
|
const params = { ...whereResult.params };
|
|
258
|
+
// Continue after the root WHERE's params when the caller threads no
|
|
259
|
+
// counter, so nested filters can never collide with them.
|
|
260
|
+
const counter = paramCounter ?? counterAfter(whereResult.params);
|
|
163
261
|
// Apply runtime labels to MATCH pattern (same as find())
|
|
164
262
|
if (labels && labels.length > 0) {
|
|
165
263
|
const extraLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
@@ -192,18 +290,18 @@ class MutationCompiler {
|
|
|
192
290
|
if (setClauses.length > 0)
|
|
193
291
|
lines.push(`SET ${setClauses.join(', ')}`);
|
|
194
292
|
// Process nested relationship operations within update body
|
|
195
|
-
const relLines = this.buildUpdateRelationships(update, nodeDef, 'n', 'update', params, 0, [], policyContext,
|
|
293
|
+
const relLines = this.buildUpdateRelationships(update, nodeDef, 'n', 'update', params, 0, [], policyContext, counter);
|
|
196
294
|
lines.push(...relLines);
|
|
197
295
|
}
|
|
198
296
|
// Disconnect relationships (top-level) — must run before connects
|
|
199
297
|
// so that blanket disconnects don't remove newly-connected relationships.
|
|
200
298
|
if (disconnect) {
|
|
201
|
-
const disconnectLines = this.buildDisconnects(disconnect, nodeDef, params, policyContext,
|
|
299
|
+
const disconnectLines = this.buildDisconnects(disconnect, nodeDef, params, policyContext, counter);
|
|
202
300
|
lines.push(...disconnectLines);
|
|
203
301
|
}
|
|
204
302
|
// Connect relationships (top-level)
|
|
205
303
|
if (connect) {
|
|
206
|
-
const connectLines = this.buildConnects(connect, nodeDef, params, policyContext,
|
|
304
|
+
const connectLines = this.buildConnects(connect, nodeDef, params, policyContext, counter);
|
|
207
305
|
lines.push(...connectLines);
|
|
208
306
|
}
|
|
209
307
|
lines.push(returnMode === 'count' ? 'RETURN count(n) AS count' : 'RETURN n');
|
|
@@ -211,40 +309,121 @@ class MutationCompiler {
|
|
|
211
309
|
}
|
|
212
310
|
/**
|
|
213
311
|
* Generate DELETE Cypher with optional cascade.
|
|
312
|
+
*
|
|
313
|
+
* v2.3.0 — each cascaded relationship honours its per-item `where` and
|
|
314
|
+
* is gated by the TARGET type's `delete` policy (`buildNestedDelete`).
|
|
315
|
+
* Pre-2.3.0 the cascade iterated only the relationship KEYS: the
|
|
316
|
+
* documented `{ where }` was ignored, so every related node — shared
|
|
317
|
+
* ones included — was deleted, with no policy check.
|
|
214
318
|
*/
|
|
215
|
-
compileDelete(nodeDef, whereResult, deleteInput
|
|
319
|
+
compileDelete(nodeDef, whereResult, deleteInput,
|
|
320
|
+
/** The caller's `delete` bundle; gates each cascade target. */
|
|
321
|
+
policyContext,
|
|
322
|
+
/** Shared `param<N>` counter, continued from the root WHERE. */
|
|
323
|
+
paramCounter) {
|
|
216
324
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
217
325
|
const lines = [];
|
|
218
326
|
const params = { ...whereResult.params };
|
|
327
|
+
const counter = paramCounter ?? counterAfter(whereResult.params);
|
|
219
328
|
lines.push(`MATCH (n:${labelStr})`);
|
|
220
329
|
if (whereResult.preludes && whereResult.preludes.length > 0)
|
|
221
330
|
lines.push(...whereResult.preludes);
|
|
222
331
|
if (whereResult.cypher)
|
|
223
332
|
lines.push(`WHERE ${whereResult.cypher}`);
|
|
224
333
|
if (deleteInput && Object.keys(deleteInput).length > 0) {
|
|
225
|
-
const deleteVars = [];
|
|
226
334
|
let varCounter = 0;
|
|
227
|
-
for (const [fieldName] of Object.entries(deleteInput)) {
|
|
335
|
+
for (const [fieldName, spec] of Object.entries(deleteInput)) {
|
|
336
|
+
if (spec === undefined)
|
|
337
|
+
continue;
|
|
228
338
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
229
339
|
if (!relDef)
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
340
|
+
throw new errors_1.OGMError(`Unknown relationship "${fieldName}" in the delete input of ${nodeDef.typeName}.`);
|
|
341
|
+
const targetNodeDef = (0, utils_1.resolveTargetDef)(relDef.target, this.schema);
|
|
342
|
+
if (!targetNodeDef)
|
|
343
|
+
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${fieldName}".`);
|
|
344
|
+
lines.push(...this.buildNestedDelete({
|
|
345
|
+
spec,
|
|
235
346
|
relDef,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
347
|
+
sourceVar: 'n',
|
|
348
|
+
withClause: 'WITH n',
|
|
349
|
+
targetNodeDef,
|
|
350
|
+
nextVar: () => `cascade_${varCounter++}`,
|
|
351
|
+
tag: fieldName,
|
|
352
|
+
params,
|
|
353
|
+
policyContext,
|
|
354
|
+
paramCounter: counter,
|
|
355
|
+
}));
|
|
241
356
|
}
|
|
242
|
-
lines.push(`DETACH DELETE ${[...deleteVars, 'n'].join(', ')}`);
|
|
243
357
|
}
|
|
244
|
-
|
|
245
|
-
lines.push('DETACH DELETE n');
|
|
358
|
+
lines.push('DETACH DELETE n');
|
|
246
359
|
return { cypher: lines.join('\n'), params };
|
|
247
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Nested / cascade delete of ONE relationship's targets (v2.3.0), shared
|
|
363
|
+
* by `compileDelete` (`Model.delete`'s `delete` input) and delete-inside-
|
|
364
|
+
* `update`. One subquery per spec item:
|
|
365
|
+
*
|
|
366
|
+
* <withClause>
|
|
367
|
+
* CALL {
|
|
368
|
+
* <withClause>
|
|
369
|
+
* MATCH (src)-[:REL]->(t)
|
|
370
|
+
* WHERE <item.where via WhereCompiler> AND <target `delete` policy>
|
|
371
|
+
* DETACH DELETE t
|
|
372
|
+
* RETURN count(*) AS _del_<tag>_<i>
|
|
373
|
+
* }
|
|
374
|
+
*
|
|
375
|
+
* `spec` is one item or an array (singular vs list relationships);
|
|
376
|
+
* `null` means "no cascade" (GraphQL InputMaybe). `{}` / no `where`
|
|
377
|
+
* deletes every related node the target's `delete` policy permits.
|
|
378
|
+
* A multi-level cascade (`delete` inside an item) and unknown item keys
|
|
379
|
+
* are rejected instead of silently ignored.
|
|
380
|
+
*/
|
|
381
|
+
buildNestedDelete(args) {
|
|
382
|
+
const { spec, relDef, sourceVar, withClause, targetNodeDef } = args;
|
|
383
|
+
if (spec === null || spec === undefined)
|
|
384
|
+
return [];
|
|
385
|
+
// Legacy shorthand: `{ rel: true }` = delete every related node (the
|
|
386
|
+
// pre-2.3.0 cascade ignored values, so callers relied on it). Any other
|
|
387
|
+
// non-object value — `false` included, which used to delete everything
|
|
388
|
+
// too — is rejected below rather than silently reinterpreted.
|
|
389
|
+
const items = spec === true ? [{}] : Array.isArray(spec) ? spec : [spec];
|
|
390
|
+
const lines = [];
|
|
391
|
+
items.forEach((item, idx) => {
|
|
392
|
+
if (!(0, validation_1.isPlainObject)(item))
|
|
393
|
+
throw new errors_1.OGMError(`Each nested delete of "${relDef.fieldName}" must be an object ({ where?, delete? }).`);
|
|
394
|
+
for (const key of Object.keys(item)) {
|
|
395
|
+
if (item[key] === undefined)
|
|
396
|
+
continue;
|
|
397
|
+
if (key !== 'where' && key !== 'delete')
|
|
398
|
+
throw new errors_1.OGMError(`Unknown key "${key}" in a nested delete of "${relDef.fieldName}". Allowed: "where", "delete".`);
|
|
399
|
+
}
|
|
400
|
+
if (hasNestedCascade(item.delete))
|
|
401
|
+
throw new errors_1.OGMError(`Multi-level cascade delete is not supported (a "delete" inside the nested delete of "${relDef.fieldName}"). ` +
|
|
402
|
+
`Delete the deeper nodes in a separate call.`);
|
|
403
|
+
const targetVar = args.nextVar();
|
|
404
|
+
const pattern = (0, utils_1.buildRelPattern)({
|
|
405
|
+
sourceVar,
|
|
406
|
+
relDef,
|
|
407
|
+
targetVar,
|
|
408
|
+
targetLabel: 'auto',
|
|
409
|
+
schema: this.schema,
|
|
410
|
+
});
|
|
411
|
+
const conditions = [
|
|
412
|
+
this.compileTargetWhere(item.where, targetVar, targetNodeDef, args.params, args.policyContext, args.paramCounter),
|
|
413
|
+
this.buildTargetPolicyPredicate(targetNodeDef, targetVar, args.params, args.policyContext, args.paramCounter, 'delete'),
|
|
414
|
+
].filter((c) => Boolean(c));
|
|
415
|
+
lines.push(withClause);
|
|
416
|
+
lines.push('CALL {');
|
|
417
|
+
lines.push(withClause);
|
|
418
|
+
lines.push(`MATCH ${pattern}`);
|
|
419
|
+
if (conditions.length > 0)
|
|
420
|
+
lines.push(`WHERE ${conditions.join(' AND ')}`);
|
|
421
|
+
lines.push(`DETACH DELETE ${targetVar}`);
|
|
422
|
+
lines.push(`RETURN count(*) AS _del_${args.tag}_${idx}`);
|
|
423
|
+
lines.push('}');
|
|
424
|
+
});
|
|
425
|
+
return lines;
|
|
426
|
+
}
|
|
248
427
|
/**
|
|
249
428
|
* Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET.
|
|
250
429
|
* Scalar properties only — nested relationship ops are not supported.
|
|
@@ -677,11 +856,10 @@ class MutationCompiler {
|
|
|
677
856
|
const connectVar = `${nodeVar}_cn${startCounter + counter}`;
|
|
678
857
|
counter++;
|
|
679
858
|
lines.push(`MATCH (${connectVar}:${targetLabelStr})`);
|
|
680
|
-
const
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
: conditions;
|
|
859
|
+
const connectConditions = [
|
|
860
|
+
this.compileTargetWhere(whereSpec, connectVar, targetNodeDef, params, policyContext, paramCounter),
|
|
861
|
+
this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter),
|
|
862
|
+
].filter((c) => Boolean(c));
|
|
685
863
|
if (connectConditions.length > 0)
|
|
686
864
|
lines.push(`WHERE ${connectConditions.join(' AND ')}`);
|
|
687
865
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
@@ -735,13 +913,13 @@ class MutationCompiler {
|
|
|
735
913
|
if (Array.isArray(spec)) {
|
|
736
914
|
if (spec.length === 0)
|
|
737
915
|
continue; // Empty array — nothing to connect
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
916
|
+
// The UNWIND fast path compiles filters against per-row values and
|
|
917
|
+
// is only exact for plain scalar, non-null filters (see
|
|
918
|
+
// isConnectFastPathEligible). Everything else — null values,
|
|
919
|
+
// logical keys, traversals — takes one CALL subquery per item,
|
|
920
|
+
// compiled through WhereCompiler.
|
|
921
|
+
const useFastPath = this.isConnectFastPathEligible(spec, targetNodeDef);
|
|
922
|
+
if (!useFastPath)
|
|
745
923
|
for (let ci = 0; ci < spec.length; ci++) {
|
|
746
924
|
const connectSpec = spec[ci];
|
|
747
925
|
const whereSpec = connectSpec.where;
|
|
@@ -751,11 +929,10 @@ class MutationCompiler {
|
|
|
751
929
|
lines.push('WITH n');
|
|
752
930
|
const connectVar = `target_${fieldName}_${ci}`;
|
|
753
931
|
lines.push(`MATCH (${connectVar}:${targetLabelStr})`);
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
: conditions;
|
|
932
|
+
const connectConditions = [
|
|
933
|
+
this.compileTargetWhere(whereSpec, connectVar, targetNodeDef, params, policyContext, paramCounter),
|
|
934
|
+
this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter),
|
|
935
|
+
].filter((c) => Boolean(c));
|
|
759
936
|
if (connectConditions.length > 0)
|
|
760
937
|
lines.push(`WHERE ${connectConditions.join(' AND ')}`);
|
|
761
938
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
@@ -863,11 +1040,10 @@ class MutationCompiler {
|
|
|
863
1040
|
const edgeInput = connectSpec.edge;
|
|
864
1041
|
lines.push('WITH n');
|
|
865
1042
|
lines.push(`MATCH (target:${targetLabelStr})`);
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
: matchConditions;
|
|
1043
|
+
const singleConnectConditions = [
|
|
1044
|
+
this.compileTargetWhere(whereSpec, 'target', targetNodeDef, params, policyContext, paramCounter),
|
|
1045
|
+
this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter),
|
|
1046
|
+
].filter((c) => Boolean(c));
|
|
871
1047
|
if (singleConnectConditions.length > 0)
|
|
872
1048
|
lines.push(`WHERE ${singleConnectConditions.join(' AND ')}`);
|
|
873
1049
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
@@ -940,13 +1116,12 @@ class MutationCompiler {
|
|
|
940
1116
|
targetLabel: 'auto',
|
|
941
1117
|
});
|
|
942
1118
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
: conditions;
|
|
1119
|
+
if (!targetNodeDef)
|
|
1120
|
+
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${fieldName}".`);
|
|
1121
|
+
const disconnectConditions = [
|
|
1122
|
+
this.compileTargetWhere(whereSpec, targetVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1123
|
+
this.buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter),
|
|
1124
|
+
].filter((c) => Boolean(c));
|
|
950
1125
|
if (disconnectConditions.length > 0)
|
|
951
1126
|
lines.push(`WHERE ${disconnectConditions.join(' AND ')}`);
|
|
952
1127
|
lines.push(`DELETE ${relVar}`);
|
|
@@ -1022,47 +1197,21 @@ class MutationCompiler {
|
|
|
1022
1197
|
const item = items[i];
|
|
1023
1198
|
const itemPrefix = `${relPrefix}_${i}`;
|
|
1024
1199
|
// --- delete (detach delete target nodes) ---
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
if (item.delete)
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
const pattern = (0, utils_1.buildRelPattern)({
|
|
1041
|
-
sourceVar,
|
|
1042
|
-
relDef,
|
|
1043
|
-
targetVar: delTarget,
|
|
1044
|
-
edgeVar: delRel,
|
|
1045
|
-
targetLabel: 'auto',
|
|
1046
|
-
});
|
|
1047
|
-
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1048
|
-
if (whereSpec && Object.keys(whereSpec).length > 0) {
|
|
1049
|
-
const nodeWhere = (whereSpec.node ?? whereSpec);
|
|
1050
|
-
// Use the same WHERE-condition builder as the disconnect /
|
|
1051
|
-
// connect paths so operator suffixes (_GT/_CONTAINS/_IN/etc.),
|
|
1052
|
-
// `NOT`, and relationship sub-filters work consistently. The
|
|
1053
|
-
// pre-1.7.2 inline implementation only emitted `prop = $param`
|
|
1054
|
-
// for every key, silently turning operator-suffixed keys
|
|
1055
|
-
// (`name_CONTAINS`) into property lookups against
|
|
1056
|
-
// non-existent fields → zero rows deleted.
|
|
1057
|
-
const conditions = this.buildNodeWhereConditions(nodeWhere, delTarget, `${itemPrefix}_del${di}`, params, targetNodeDef);
|
|
1058
|
-
if (conditions.length > 0)
|
|
1059
|
-
lines.push(`WHERE ${conditions.join(' AND ')}`);
|
|
1060
|
-
}
|
|
1061
|
-
lines.push(`DETACH DELETE ${delTarget}`);
|
|
1062
|
-
lines.push(`RETURN count(*) AS _del_${key}_${i}_${di}`);
|
|
1063
|
-
lines.push(`}`);
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1200
|
+
// Shared with `compileDelete`'s cascade: where + target `delete`
|
|
1201
|
+
// policy, one CALL subquery per spec item (v2.3.0).
|
|
1202
|
+
if (item.delete)
|
|
1203
|
+
lines.push(...this.buildNestedDelete({
|
|
1204
|
+
spec: item.delete,
|
|
1205
|
+
relDef,
|
|
1206
|
+
sourceVar,
|
|
1207
|
+
withClause,
|
|
1208
|
+
targetNodeDef,
|
|
1209
|
+
nextVar: () => `${sourceVar}_del${varCounter++}`,
|
|
1210
|
+
tag: `${key}_${i}`,
|
|
1211
|
+
params,
|
|
1212
|
+
policyContext,
|
|
1213
|
+
paramCounter,
|
|
1214
|
+
}));
|
|
1066
1215
|
// --- create ---
|
|
1067
1216
|
if (item.create) {
|
|
1068
1217
|
const createItems = Array.isArray(item.create)
|
|
@@ -1143,11 +1292,10 @@ class MutationCompiler {
|
|
|
1143
1292
|
// keys are handled identically to the top-level disconnect
|
|
1144
1293
|
// path (`buildDisconnects`). Restores backwards-compat with
|
|
1145
1294
|
// @neo4j/graphql-ogm's nested update.disconnect[].where shape.
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
: conditions;
|
|
1295
|
+
const nestedDiscConditions = [
|
|
1296
|
+
this.compileTargetWhere(whereSpec, discTarget, targetNodeDef, params, policyContext, paramCounter),
|
|
1297
|
+
this.buildTargetPolicyPredicate(targetNodeDef, discTarget, params, policyContext, paramCounter),
|
|
1298
|
+
].filter((c) => Boolean(c));
|
|
1151
1299
|
if (nestedDiscConditions.length > 0)
|
|
1152
1300
|
lines.push(`WHERE ${nestedDiscConditions.join(' AND ')}`);
|
|
1153
1301
|
lines.push(`DELETE ${relVar}`);
|
|
@@ -1177,11 +1325,10 @@ class MutationCompiler {
|
|
|
1177
1325
|
// connect handle the same shapes as nested disconnect and
|
|
1178
1326
|
// top-level paths. Empty/undefined `whereSpec` short-circuits
|
|
1179
1327
|
// to `[]` inside the helper, so the no-where case still works.
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
: conditions;
|
|
1328
|
+
const nestedConnConditions = [
|
|
1329
|
+
this.compileTargetWhere(whereSpec, connectVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1330
|
+
this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter),
|
|
1331
|
+
].filter((c) => Boolean(c));
|
|
1185
1332
|
if (nestedConnConditions.length > 0)
|
|
1186
1333
|
lines.push(`WHERE ${nestedConnConditions.join(' AND ')}`);
|
|
1187
1334
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
@@ -1288,11 +1435,15 @@ class MutationCompiler {
|
|
|
1288
1435
|
// compiler; this was the missed third sibling. The param
|
|
1289
1436
|
// prefix is chosen so the legacy plain-equality case keeps
|
|
1290
1437
|
// byte-identical param names (`${itemPrefix}_where_${prop}`).
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1438
|
+
// v2.3.0 — the nested update is row-filtered by the TARGET's
|
|
1439
|
+
// `update` policy, exactly like a direct update on that type
|
|
1440
|
+
// (pre-2.3.0 it enforced no target policy at all).
|
|
1441
|
+
const updateConditions = [
|
|
1442
|
+
this.compileTargetWhere(updateWhere, updateVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1443
|
+
this.buildTargetPolicyPredicate(targetNodeDef, updateVar, params, policyContext, paramCounter, 'update'),
|
|
1444
|
+
].filter((c) => Boolean(c));
|
|
1445
|
+
if (updateConditions.length > 0)
|
|
1446
|
+
lines.push(`WHERE ${updateConditions.join(' AND ')}`);
|
|
1296
1447
|
if (setClauses.length > 0)
|
|
1297
1448
|
lines.push(`SET ${setClauses.join(', ')}`);
|
|
1298
1449
|
(0, validation_1.mergeParams)(params, setParams);
|
|
@@ -1320,180 +1471,6 @@ class MutationCompiler {
|
|
|
1320
1471
|
// No suffix — simple equality
|
|
1321
1472
|
return { baseProp: key, template: '%v = $%p' };
|
|
1322
1473
|
}
|
|
1323
|
-
/**
|
|
1324
|
-
* Build WHERE conditions for connect/disconnect operations.
|
|
1325
|
-
* Handles operator suffixes (_IN, _NOT, _NOT_IN, _CONTAINS, etc.),
|
|
1326
|
-
* NOT blocks, and relationship filters (_SOME, _NONE, _ALL, _SINGLE).
|
|
1327
|
-
*
|
|
1328
|
-
* @param targetNodeDef - Optional node definition for the target node.
|
|
1329
|
-
* Required to resolve relationship filters like `configurations_SOME`.
|
|
1330
|
-
* When omitted, relationship filters are silently skipped.
|
|
1331
|
-
*/
|
|
1332
|
-
buildNodeWhereConditions(nodeWhere, targetVar, prefix, params, targetNodeDef) {
|
|
1333
|
-
const conditions = [];
|
|
1334
|
-
for (const [prop, val] of Object.entries(nodeWhere))
|
|
1335
|
-
if (prop === 'NOT') {
|
|
1336
|
-
const notSpec = val;
|
|
1337
|
-
for (const [notProp, notVal] of Object.entries(notSpec)) {
|
|
1338
|
-
const { baseProp, template } = this.parseOperatorSuffix(notProp);
|
|
1339
|
-
(0, validation_1.assertSafePropertyName)(baseProp, 'where property');
|
|
1340
|
-
const paramName = `${prefix}_NOT_${notProp}`;
|
|
1341
|
-
const escapedProp = (0, validation_1.escapeIdentifier)(baseProp);
|
|
1342
|
-
const paramRef = (0, write_coercion_1.wrapWhereParam)(`$${paramName}`, notVal, targetNodeDef?.properties.get(baseProp), notProp.slice(baseProp.length));
|
|
1343
|
-
let expr;
|
|
1344
|
-
if (template === '%v = $%p')
|
|
1345
|
-
// Simple equality → negate to <>
|
|
1346
|
-
expr = `${targetVar}.${escapedProp} <> ${paramRef}`;
|
|
1347
|
-
else {
|
|
1348
|
-
// Operator suffix — wrap with NOT if not already negated
|
|
1349
|
-
const resolved = template
|
|
1350
|
-
.replace('%v', `${targetVar}.${escapedProp}`)
|
|
1351
|
-
.replace('$%p', paramRef);
|
|
1352
|
-
expr = resolved.startsWith('NOT ') ? resolved : `NOT ${resolved}`;
|
|
1353
|
-
}
|
|
1354
|
-
conditions.push(expr);
|
|
1355
|
-
params[paramName] = notVal;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
else {
|
|
1359
|
-
// Check for relationship filter suffixes (_SOME, _NONE, _ALL, _SINGLE)
|
|
1360
|
-
const relFilter = this.tryBuildRelationshipFilter(prop, val, targetVar, prefix, params, targetNodeDef);
|
|
1361
|
-
if (relFilter) {
|
|
1362
|
-
conditions.push(relFilter);
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
const { baseProp, template } = this.parseOperatorSuffix(prop);
|
|
1366
|
-
(0, validation_1.assertSafePropertyName)(baseProp, 'where property');
|
|
1367
|
-
const paramName = `${prefix}_${prop}`;
|
|
1368
|
-
const paramRef = (0, write_coercion_1.wrapWhereParam)(`$${paramName}`, val, targetNodeDef?.properties.get(baseProp), prop.slice(baseProp.length));
|
|
1369
|
-
conditions.push(template
|
|
1370
|
-
.replace('%v', `${targetVar}.${(0, validation_1.escapeIdentifier)(baseProp)}`)
|
|
1371
|
-
.replace('$%p', paramRef));
|
|
1372
|
-
params[paramName] = val;
|
|
1373
|
-
}
|
|
1374
|
-
return conditions;
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* Build WHERE conditions from a connection-level WHERE spec.
|
|
1378
|
-
* Handles `node`, `NOT`, `AND`, `OR` at the connection level.
|
|
1379
|
-
*
|
|
1380
|
-
* Connection WHERE format: `{ node: {...}, NOT: { node: {...} }, AND: [...], OR: [...] }`
|
|
1381
|
-
*/
|
|
1382
|
-
buildConnectionWhereConditions(whereSpec, targetVar, prefix, params, targetNodeDef) {
|
|
1383
|
-
if (!whereSpec || Object.keys(whereSpec).length === 0)
|
|
1384
|
-
return [];
|
|
1385
|
-
const conditions = [];
|
|
1386
|
-
// Direct node conditions
|
|
1387
|
-
if (whereSpec.node) {
|
|
1388
|
-
const nodeConditions = this.buildNodeWhereConditions(whereSpec.node, targetVar, prefix, params, targetNodeDef);
|
|
1389
|
-
conditions.push(...nodeConditions);
|
|
1390
|
-
}
|
|
1391
|
-
// NOT wrapper — negate the inner connection conditions
|
|
1392
|
-
if (whereSpec.NOT) {
|
|
1393
|
-
const notSpec = whereSpec.NOT;
|
|
1394
|
-
const innerConditions = this.buildConnectionWhereConditions(notSpec, targetVar, `${prefix}_NOT`, params, targetNodeDef);
|
|
1395
|
-
if (innerConditions.length > 0)
|
|
1396
|
-
conditions.push(`NOT (${innerConditions.join(' AND ')})`);
|
|
1397
|
-
}
|
|
1398
|
-
// AND — all sub-conditions must hold
|
|
1399
|
-
if (Array.isArray(whereSpec.AND)) {
|
|
1400
|
-
const before = conditions.length;
|
|
1401
|
-
for (let i = 0; i < whereSpec.AND.length; i++) {
|
|
1402
|
-
const sub = this.buildConnectionWhereConditions(whereSpec.AND[i], targetVar, `${prefix}_AND${i}`, params, targetNodeDef);
|
|
1403
|
-
conditions.push(...sub);
|
|
1404
|
-
}
|
|
1405
|
-
if (conditions.length === before)
|
|
1406
|
-
this.warnEmptyLogical('AND', 'mutation connection where');
|
|
1407
|
-
}
|
|
1408
|
-
// OR — any sub-condition must hold
|
|
1409
|
-
if (Array.isArray(whereSpec.OR)) {
|
|
1410
|
-
const orClauses = [];
|
|
1411
|
-
for (let i = 0; i < whereSpec.OR.length; i++) {
|
|
1412
|
-
const sub = this.buildConnectionWhereConditions(whereSpec.OR[i], targetVar, `${prefix}_OR${i}`, params, targetNodeDef);
|
|
1413
|
-
if (sub.length > 0)
|
|
1414
|
-
orClauses.push(`(${sub.join(' AND ')})`);
|
|
1415
|
-
}
|
|
1416
|
-
if (orClauses.length > 0)
|
|
1417
|
-
conditions.push(`(${orClauses.join(' OR ')})`);
|
|
1418
|
-
// Prisma semantics: OR with zero effective disjuncts matches NOTHING.
|
|
1419
|
-
// Previously the operator vanished, so `disconnect: [{ where:
|
|
1420
|
-
// { OR: [] } }]` detached EVERY related node instead of none.
|
|
1421
|
-
else {
|
|
1422
|
-
this.warnEmptyLogical('OR', 'mutation connection where');
|
|
1423
|
-
conditions.push('false');
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
// Fallback: if no recognized keys, treat the whole spec as node conditions
|
|
1427
|
-
// (backward compatibility for simple `{ id: '...' }` style)
|
|
1428
|
-
if (!whereSpec.node && !whereSpec.NOT && !whereSpec.AND && !whereSpec.OR) {
|
|
1429
|
-
// Reject unsupported connection WHERE keys (e.g., "edge")
|
|
1430
|
-
if (whereSpec.edge !== undefined)
|
|
1431
|
-
throw new errors_1.OGMError(`Connection WHERE with "edge" filters is not supported in mutations. ` +
|
|
1432
|
-
`Only "node", "NOT", "AND", "OR" keys or direct property conditions are allowed.`);
|
|
1433
|
-
const fallbackConditions = this.buildNodeWhereConditions(whereSpec, targetVar, prefix, params, targetNodeDef);
|
|
1434
|
-
conditions.push(...fallbackConditions);
|
|
1435
|
-
}
|
|
1436
|
-
return conditions;
|
|
1437
|
-
}
|
|
1438
|
-
/**
|
|
1439
|
-
* Try to compile a relationship filter condition (_SOME, _NONE, _ALL, _SINGLE).
|
|
1440
|
-
* Returns the Cypher expression string or null if the key is not a relationship filter.
|
|
1441
|
-
*/
|
|
1442
|
-
tryBuildRelationshipFilter(key, value, nodeVar, prefix, params, targetNodeDef) {
|
|
1443
|
-
if (!targetNodeDef || typeof value !== 'object' || value === null)
|
|
1444
|
-
return null;
|
|
1445
|
-
// Detect suffix and extract field name
|
|
1446
|
-
let suffix = null;
|
|
1447
|
-
let fieldName = '';
|
|
1448
|
-
for (const s of RELATIONSHIP_SUFFIXES)
|
|
1449
|
-
if (key.endsWith(s)) {
|
|
1450
|
-
suffix = s;
|
|
1451
|
-
fieldName = key.slice(0, -s.length);
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
1454
|
-
// If no suffix, check if bare key is a relationship (defaults to _SOME)
|
|
1455
|
-
if (!suffix) {
|
|
1456
|
-
const relDef = targetNodeDef.relationships.get(key);
|
|
1457
|
-
if (!relDef)
|
|
1458
|
-
return null;
|
|
1459
|
-
suffix = '_SOME';
|
|
1460
|
-
fieldName = key;
|
|
1461
|
-
}
|
|
1462
|
-
const relDef = targetNodeDef.relationships.get(fieldName);
|
|
1463
|
-
if (!relDef)
|
|
1464
|
-
return null;
|
|
1465
|
-
const relTargetDef = (0, utils_1.resolveTargetDef)(relDef.target, this.schema);
|
|
1466
|
-
if (!relTargetDef)
|
|
1467
|
-
return null;
|
|
1468
|
-
const relVar = `rf${this.relFilterCounter++}`;
|
|
1469
|
-
const innerWhere = value;
|
|
1470
|
-
// Build the MATCH pattern
|
|
1471
|
-
const edgePart = `[:${(0, validation_1.escapeIdentifier)(relDef.type)}]`;
|
|
1472
|
-
const escapedTarget = (0, validation_1.escapeIdentifier)(relDef.target);
|
|
1473
|
-
const pattern = relDef.direction === 'IN'
|
|
1474
|
-
? `(${nodeVar})<-${edgePart}-(${relVar}:${escapedTarget})`
|
|
1475
|
-
: `(${nodeVar})-${edgePart}->(${relVar}:${escapedTarget})`;
|
|
1476
|
-
// Recursively compile inner conditions
|
|
1477
|
-
const innerConditions = this.buildNodeWhereConditions(innerWhere, relVar, `${prefix}_${fieldName}`, params, relTargetDef);
|
|
1478
|
-
const innerClause = innerConditions.length > 0
|
|
1479
|
-
? ` WHERE ${innerConditions.join(' AND ')}`
|
|
1480
|
-
: '';
|
|
1481
|
-
switch (suffix) {
|
|
1482
|
-
case '_SOME':
|
|
1483
|
-
return `EXISTS { MATCH ${pattern}${innerClause} }`;
|
|
1484
|
-
case '_NONE':
|
|
1485
|
-
return `NOT EXISTS { MATCH ${pattern}${innerClause} }`;
|
|
1486
|
-
case '_ALL':
|
|
1487
|
-
// Double negation: all match ≡ none fail to match
|
|
1488
|
-
if (!innerClause)
|
|
1489
|
-
return '';
|
|
1490
|
-
return `NOT EXISTS { MATCH ${pattern} WHERE NOT (${innerConditions.join(' AND ')}) }`;
|
|
1491
|
-
case '_SINGLE':
|
|
1492
|
-
return `size([${relVar} IN [(${pattern}${innerClause} | ${relVar})] | ${relVar}]) = 1`;
|
|
1493
|
-
default:
|
|
1494
|
-
return null;
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
1474
|
buildGeneratedIdClause(nodeDef) {
|
|
1498
1475
|
const parts = [];
|
|
1499
1476
|
for (const [, propDef] of nodeDef.properties)
|
|
@@ -1501,38 +1478,6 @@ class MutationCompiler {
|
|
|
1501
1478
|
parts.push(`${(0, validation_1.escapeIdentifier)(propDef.name)}: randomUUID()`);
|
|
1502
1479
|
return parts.join(', ');
|
|
1503
1480
|
}
|
|
1504
|
-
/**
|
|
1505
|
-
* Check if any item in an array connect spec has relationship filters in its WHERE clause.
|
|
1506
|
-
* Relationship filters require EXISTS subqueries which can't use UNWIND param references.
|
|
1507
|
-
*/
|
|
1508
|
-
arrayConnectHasRelationshipFilters(items, targetNodeDef) {
|
|
1509
|
-
for (const item of items) {
|
|
1510
|
-
const whereSpec = item.where;
|
|
1511
|
-
if (!whereSpec)
|
|
1512
|
-
continue;
|
|
1513
|
-
const nodeWhere = (whereSpec.node ?? whereSpec);
|
|
1514
|
-
for (const key of Object.keys(nodeWhere)) {
|
|
1515
|
-
// Strip operator suffixes to get the base field name
|
|
1516
|
-
let fieldName = key;
|
|
1517
|
-
for (const s of RELATIONSHIP_SUFFIXES)
|
|
1518
|
-
if (key.endsWith(s)) {
|
|
1519
|
-
fieldName = key.slice(0, -s.length);
|
|
1520
|
-
break;
|
|
1521
|
-
}
|
|
1522
|
-
// If the base field name is a relationship on the target, it's a rel filter
|
|
1523
|
-
if (targetNodeDef.relationships.has(fieldName))
|
|
1524
|
-
return true;
|
|
1525
|
-
// Also check if the value is an object (nested filter on a relationship)
|
|
1526
|
-
const val = nodeWhere[key];
|
|
1527
|
-
if (typeof val === 'object' &&
|
|
1528
|
-
val !== null &&
|
|
1529
|
-
!Array.isArray(val) &&
|
|
1530
|
-
targetNodeDef.relationships.has(key))
|
|
1531
|
-
return true;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
return false;
|
|
1535
|
-
}
|
|
1536
1481
|
extractConnectWhereConditions(item, targetVar, itemVar, targetNodeDef) {
|
|
1537
1482
|
const conditions = [];
|
|
1538
1483
|
const whereSpec = item.where;
|
|
@@ -1590,6 +1535,99 @@ MutationCompiler.OPERATOR_SUFFIXES = [
|
|
|
1590
1535
|
* path to validate that every item shares the same shape; mismatched
|
|
1591
1536
|
* shapes silently drop keys from the compiled WHERE / SET.
|
|
1592
1537
|
*/
|
|
1538
|
+
/** Keys that make a nested-mutation `where` connection-shaped. */
|
|
1539
|
+
const CONNECTION_WHERE_KEYS = new Set([
|
|
1540
|
+
'node',
|
|
1541
|
+
'node_NOT',
|
|
1542
|
+
'NOT',
|
|
1543
|
+
'AND',
|
|
1544
|
+
'OR',
|
|
1545
|
+
'edge',
|
|
1546
|
+
'edge_NOT',
|
|
1547
|
+
]);
|
|
1548
|
+
/**
|
|
1549
|
+
* Map a nested-mutation connection where onto an equivalent NODE where on
|
|
1550
|
+
* the target, for `WhereCompiler` (v2.3.0). Mutation `where` never supports
|
|
1551
|
+
* edge filters, so the two shapes are interchangeable:
|
|
1552
|
+
*
|
|
1553
|
+
* { node: A } → A
|
|
1554
|
+
* { node_NOT: A } → { NOT: A }
|
|
1555
|
+
* { NOT: C } → { NOT: map(C) }
|
|
1556
|
+
* { AND: [C…] } → { AND: [map(C)…] } (likewise OR; `OR: []`
|
|
1557
|
+
* still matches nothing)
|
|
1558
|
+
* bare properties → themselves (legacy `{ id: 'x' }` shape)
|
|
1559
|
+
*
|
|
1560
|
+
* `edge`/`edge_NOT` throw (unsupported in mutations). In a connection-
|
|
1561
|
+
* shaped where, any other key throws too — the removed parallel builder
|
|
1562
|
+
* silently IGNORED such keys (`{ id: 'x', NOT: {…} }` dropped `id`),
|
|
1563
|
+
* widening the match.
|
|
1564
|
+
*/
|
|
1565
|
+
function connectionWhereToNodeWhere(spec) {
|
|
1566
|
+
if (!(0, validation_1.isPlainObject)(spec))
|
|
1567
|
+
throw new errors_1.OGMError('A nested mutation where must be an object.');
|
|
1568
|
+
const keys = Object.keys(spec).filter((k) => spec[k] !== undefined);
|
|
1569
|
+
const edgeKey = keys.find((k) => k === 'edge' || k === 'edge_NOT');
|
|
1570
|
+
if (edgeKey)
|
|
1571
|
+
throw new errors_1.OGMError(`Connection WHERE with "${edgeKey}" filters is not supported in mutations. ` +
|
|
1572
|
+
`Only "node", "node_NOT", "NOT", "AND", "OR" keys or direct property conditions are allowed.`);
|
|
1573
|
+
if (!keys.some((k) => CONNECTION_WHERE_KEYS.has(k)))
|
|
1574
|
+
return spec;
|
|
1575
|
+
const parts = [];
|
|
1576
|
+
for (const key of keys) {
|
|
1577
|
+
const value = spec[key];
|
|
1578
|
+
if (value === null)
|
|
1579
|
+
continue;
|
|
1580
|
+
switch (key) {
|
|
1581
|
+
case 'node':
|
|
1582
|
+
parts.push(value);
|
|
1583
|
+
break;
|
|
1584
|
+
case 'node_NOT':
|
|
1585
|
+
parts.push({ NOT: value });
|
|
1586
|
+
break;
|
|
1587
|
+
case 'NOT':
|
|
1588
|
+
parts.push({
|
|
1589
|
+
NOT: connectionWhereToNodeWhere(value),
|
|
1590
|
+
});
|
|
1591
|
+
break;
|
|
1592
|
+
case 'AND':
|
|
1593
|
+
case 'OR':
|
|
1594
|
+
if (!Array.isArray(value))
|
|
1595
|
+
throw new errors_1.OGMError(`"${key}" in a nested mutation where must be an array.`);
|
|
1596
|
+
parts.push({
|
|
1597
|
+
[key]: value.map((item) => connectionWhereToNodeWhere(item)),
|
|
1598
|
+
});
|
|
1599
|
+
break;
|
|
1600
|
+
default:
|
|
1601
|
+
throw new errors_1.OGMError(`Unknown key "${key}" in a connection-shaped nested mutation where. ` +
|
|
1602
|
+
`Allowed: "node", "node_NOT", "NOT", "AND", "OR" — put property ` +
|
|
1603
|
+
`conditions inside "node".`);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
return parts.length === 1 ? parts[0] : { AND: parts };
|
|
1607
|
+
}
|
|
1608
|
+
/** A nested delete item's own `delete` asks for a deeper cascade. */
|
|
1609
|
+
function hasNestedCascade(value) {
|
|
1610
|
+
if (value === undefined || value === null)
|
|
1611
|
+
return false;
|
|
1612
|
+
if (Array.isArray(value))
|
|
1613
|
+
return value.length > 0;
|
|
1614
|
+
return (0, validation_1.isPlainObject)(value) ? Object.keys(value).length > 0 : true;
|
|
1615
|
+
}
|
|
1616
|
+
/**
|
|
1617
|
+
* A `param<N>` counter that continues after every `param<N>` already in
|
|
1618
|
+
* `params`, for callers that did not thread their own counter (direct
|
|
1619
|
+
* `MutationCompiler` use): nested-write filters compiled with it can never
|
|
1620
|
+
* collide with the root WHERE's params.
|
|
1621
|
+
*/
|
|
1622
|
+
function counterAfter(params) {
|
|
1623
|
+
let max = -1;
|
|
1624
|
+
for (const key of Object.keys(params)) {
|
|
1625
|
+
const match = /^param(\d+)$/.exec(key);
|
|
1626
|
+
if (match)
|
|
1627
|
+
max = Math.max(max, Number(match[1]));
|
|
1628
|
+
}
|
|
1629
|
+
return { count: max + 1 };
|
|
1630
|
+
}
|
|
1593
1631
|
function computeConnectItemSignature(item) {
|
|
1594
1632
|
const where = item.where;
|
|
1595
1633
|
const nodeWhere = (where?.node ?? where ?? {});
|