grafeo-ogm 2.4.7 → 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 +126 -0
- package/README.md +95 -35
- package/dist/cjs/cli/commands/db-push.d.ts.map +1 -1
- package/dist/cjs/cli/commands/db-push.js +74 -8
- package/dist/cjs/cli/commands/db-push.js.map +1 -1
- package/dist/cjs/cli/commands/db-seed.d.ts.map +1 -1
- package/dist/cjs/cli/commands/db-seed.js +10 -1
- package/dist/cjs/cli/commands/db-seed.js.map +1 -1
- package/dist/cjs/cli/introspect.d.ts +2 -2
- package/dist/cjs/cli/introspect.d.ts.map +1 -1
- package/dist/cjs/cli/introspect.js +44 -2
- package/dist/cjs/cli/introspect.js.map +1 -1
- package/dist/cjs/cli/push-planner.d.ts +57 -2
- package/dist/cjs/cli/push-planner.d.ts.map +1 -1
- package/dist/cjs/cli/push-planner.js +161 -3
- package/dist/cjs/cli/push-planner.js.map +1 -1
- package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/fulltext.compiler.js +5 -1
- package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +107 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +695 -422
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/select-normalizer.d.ts.map +1 -1
- package/dist/cjs/compilers/select-normalizer.js +12 -1
- package/dist/cjs/compilers/select-normalizer.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +56 -0
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +271 -29
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +36 -15
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/execution/executor.d.ts +34 -9
- package/dist/cjs/execution/executor.d.ts.map +1 -1
- package/dist/cjs/execution/executor.js +42 -5
- package/dist/cjs/execution/executor.js.map +1 -1
- package/dist/cjs/execution/result-mapper.d.ts.map +1 -1
- package/dist/cjs/execution/result-mapper.js +4 -2
- package/dist/cjs/execution/result-mapper.js.map +1 -1
- package/dist/cjs/generator/generate-types.d.ts.map +1 -1
- package/dist/cjs/generator/generate-types.js +76 -0
- package/dist/cjs/generator/generate-types.js.map +1 -1
- package/dist/cjs/generator/type-emitters/aggregation-emitter.d.ts.map +1 -1
- package/dist/cjs/generator/type-emitters/aggregation-emitter.js +19 -7
- package/dist/cjs/generator/type-emitters/aggregation-emitter.js.map +1 -1
- package/dist/cjs/generator/type-emitters/input-emitter.js +13 -4
- package/dist/cjs/generator/type-emitters/input-emitter.js.map +1 -1
- package/dist/cjs/generator/type-emitters/model-map-emitter.d.ts +1 -0
- package/dist/cjs/generator/type-emitters/model-map-emitter.d.ts.map +1 -1
- package/dist/cjs/generator/type-emitters/model-map-emitter.js +5 -20
- package/dist/cjs/generator/type-emitters/model-map-emitter.js.map +1 -1
- package/dist/cjs/generator/type-emitters/node-type-emitter.d.ts +3 -0
- package/dist/cjs/generator/type-emitters/node-type-emitter.d.ts.map +1 -1
- package/dist/cjs/generator/type-emitters/node-type-emitter.js +6 -11
- package/dist/cjs/generator/type-emitters/node-type-emitter.js.map +1 -1
- package/dist/cjs/generator/type-emitters/select-fields-emitter.js +21 -2
- package/dist/cjs/generator/type-emitters/select-fields-emitter.js.map +1 -1
- package/dist/cjs/generator/type-emitters/where-emitter.js +19 -3
- package/dist/cjs/generator/type-emitters/where-emitter.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.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 +27 -13
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +12 -0
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts +22 -2
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +42 -5
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/policy/nested-writes.d.ts.map +1 -1
- package/dist/cjs/policy/nested-writes.js +40 -20
- package/dist/cjs/policy/nested-writes.js.map +1 -1
- package/dist/cjs/schema/parser.d.ts.map +1 -1
- package/dist/cjs/schema/parser.js +94 -16
- package/dist/cjs/schema/parser.js.map +1 -1
- package/dist/cjs/schema/types.d.ts +1 -1
- package/dist/cjs/schema/types.d.ts.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 +217 -13
- package/dist/cjs/testing/cypher-assert.js.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.d.ts +29 -2
- package/dist/cjs/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.js +93 -3
- package/dist/cjs/testing/neo4j-record-factory.js.map +1 -1
- package/dist/cjs/utils/cypher-field-projection.d.ts +31 -3
- package/dist/cjs/utils/cypher-field-projection.d.ts.map +1 -1
- package/dist/cjs/utils/cypher-field-projection.js +59 -7
- package/dist/cjs/utils/cypher-field-projection.js.map +1 -1
- package/dist/cjs/utils/write-coercion.d.ts +18 -0
- package/dist/cjs/utils/write-coercion.d.ts.map +1 -1
- package/dist/cjs/utils/write-coercion.js +74 -1
- package/dist/cjs/utils/write-coercion.js.map +1 -1
- package/dist/esm/cli/commands/db-push.d.ts.map +1 -1
- package/dist/esm/cli/commands/db-push.js +74 -8
- package/dist/esm/cli/commands/db-push.js.map +1 -1
- package/dist/esm/cli/commands/db-seed.d.ts.map +1 -1
- package/dist/esm/cli/commands/db-seed.js +10 -1
- package/dist/esm/cli/commands/db-seed.js.map +1 -1
- package/dist/esm/cli/introspect.d.ts +2 -2
- package/dist/esm/cli/introspect.d.ts.map +1 -1
- package/dist/esm/cli/introspect.js +44 -2
- package/dist/esm/cli/introspect.js.map +1 -1
- package/dist/esm/cli/push-planner.d.ts +57 -2
- package/dist/esm/cli/push-planner.d.ts.map +1 -1
- package/dist/esm/cli/push-planner.js +162 -4
- package/dist/esm/cli/push-planner.js.map +1 -1
- package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/fulltext.compiler.js +5 -1
- package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +107 -1
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +692 -422
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/select-normalizer.d.ts.map +1 -1
- package/dist/esm/compilers/select-normalizer.js +12 -1
- package/dist/esm/compilers/select-normalizer.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +56 -0
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +273 -31
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +37 -16
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/execution/executor.d.ts +34 -9
- package/dist/esm/execution/executor.d.ts.map +1 -1
- package/dist/esm/execution/executor.js +41 -5
- package/dist/esm/execution/executor.js.map +1 -1
- package/dist/esm/execution/result-mapper.d.ts.map +1 -1
- package/dist/esm/execution/result-mapper.js +4 -2
- package/dist/esm/execution/result-mapper.js.map +1 -1
- package/dist/esm/generator/generate-types.d.ts.map +1 -1
- package/dist/esm/generator/generate-types.js +76 -0
- package/dist/esm/generator/generate-types.js.map +1 -1
- package/dist/esm/generator/type-emitters/aggregation-emitter.d.ts.map +1 -1
- package/dist/esm/generator/type-emitters/aggregation-emitter.js +19 -7
- package/dist/esm/generator/type-emitters/aggregation-emitter.js.map +1 -1
- package/dist/esm/generator/type-emitters/input-emitter.js +13 -4
- package/dist/esm/generator/type-emitters/input-emitter.js.map +1 -1
- package/dist/esm/generator/type-emitters/model-map-emitter.d.ts +1 -0
- package/dist/esm/generator/type-emitters/model-map-emitter.d.ts.map +1 -1
- package/dist/esm/generator/type-emitters/model-map-emitter.js +5 -20
- package/dist/esm/generator/type-emitters/model-map-emitter.js.map +1 -1
- package/dist/esm/generator/type-emitters/node-type-emitter.d.ts +3 -0
- package/dist/esm/generator/type-emitters/node-type-emitter.d.ts.map +1 -1
- package/dist/esm/generator/type-emitters/node-type-emitter.js +6 -11
- package/dist/esm/generator/type-emitters/node-type-emitter.js.map +1 -1
- package/dist/esm/generator/type-emitters/select-fields-emitter.js +21 -2
- package/dist/esm/generator/type-emitters/select-fields-emitter.js.map +1 -1
- package/dist/esm/generator/type-emitters/where-emitter.js +19 -3
- package/dist/esm/generator/type-emitters/where-emitter.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.d.ts.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 +27 -13
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +12 -0
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts +22 -2
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +43 -6
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/policy/nested-writes.d.ts.map +1 -1
- package/dist/esm/policy/nested-writes.js +40 -20
- package/dist/esm/policy/nested-writes.js.map +1 -1
- package/dist/esm/schema/parser.d.ts.map +1 -1
- package/dist/esm/schema/parser.js +94 -16
- package/dist/esm/schema/parser.js.map +1 -1
- package/dist/esm/schema/types.d.ts +1 -1
- package/dist/esm/schema/types.d.ts.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 +217 -13
- package/dist/esm/testing/cypher-assert.js.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.d.ts +29 -2
- package/dist/esm/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.js +93 -3
- package/dist/esm/testing/neo4j-record-factory.js.map +1 -1
- package/dist/esm/utils/cypher-field-projection.d.ts +31 -3
- package/dist/esm/utils/cypher-field-projection.d.ts.map +1 -1
- package/dist/esm/utils/cypher-field-projection.js +58 -7
- package/dist/esm/utils/cypher-field-projection.js.map +1 -1
- package/dist/esm/utils/write-coercion.d.ts +18 -0
- package/dist/esm/utils/write-coercion.d.ts.map +1 -1
- package/dist/esm/utils/write-coercion.js +73 -1
- package/dist/esm/utils/write-coercion.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MutationCompiler = void 0;
|
|
4
4
|
exports.connectionWhereToNodeWhere = connectionWhereToNodeWhere;
|
|
5
|
+
exports.abstractTarget = abstractTarget;
|
|
6
|
+
exports.perMemberEntries = perMemberEntries;
|
|
7
|
+
exports.unwrapAbstractCreateNode = unwrapAbstractCreateNode;
|
|
5
8
|
const errors_1 = require("../errors");
|
|
6
9
|
const errors_2 = require("../policy/errors");
|
|
7
10
|
const types_1 = require("../policy/types");
|
|
@@ -9,6 +12,33 @@ const utils_1 = require("../schema/utils");
|
|
|
9
12
|
const validation_1 = require("../utils/validation");
|
|
10
13
|
const write_coercion_1 = require("../utils/write-coercion");
|
|
11
14
|
const where_compiler_1 = require("./where.compiler");
|
|
15
|
+
/** The root node `n` of an update — the pre-2.4.8 names, unprefixed. */
|
|
16
|
+
const TOP_SCOPE = {
|
|
17
|
+
sourceVar: 'n',
|
|
18
|
+
carryVars: ['n'],
|
|
19
|
+
withClause: 'WITH n',
|
|
20
|
+
varPrefix: '',
|
|
21
|
+
depth: 0,
|
|
22
|
+
};
|
|
23
|
+
/** Max depth of connects nested in connects (disconnects likewise). */
|
|
24
|
+
const MAX_NESTED_LINK_DEPTH = 5;
|
|
25
|
+
/**
|
|
26
|
+
* The scope of a connect / disconnect nested in an item whose target is
|
|
27
|
+
* bound as `targetVar`: that node becomes the source, everything in scope
|
|
28
|
+
* stays carried, and its own names are prefixed by it.
|
|
29
|
+
*/
|
|
30
|
+
function nestedScope(carryVars, targetVar, depth) {
|
|
31
|
+
if (depth >= MAX_NESTED_LINK_DEPTH)
|
|
32
|
+
throw new errors_1.OGMError(`Nested connect / disconnect deeper than ${MAX_NESTED_LINK_DEPTH} levels is not supported.`);
|
|
33
|
+
const vars = [...new Set([...carryVars, targetVar])];
|
|
34
|
+
return {
|
|
35
|
+
sourceVar: targetVar,
|
|
36
|
+
carryVars: vars,
|
|
37
|
+
withClause: `WITH ${vars.join(', ')}`,
|
|
38
|
+
varPrefix: `${targetVar}_`,
|
|
39
|
+
depth: depth + 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
12
42
|
/**
|
|
13
43
|
* Compiles create, update, delete, and label mutations into Cypher + params.
|
|
14
44
|
*/
|
|
@@ -20,6 +50,13 @@ class MutationCompiler {
|
|
|
20
50
|
* Bounded by the number of node types in the schema (~50-100 entries).
|
|
21
51
|
*/
|
|
22
52
|
this.labelCache = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Relationships narrowed from an abstract target to one member by the
|
|
55
|
+
* per-member dispatch. A BLANKET disconnect on one adds the member's
|
|
56
|
+
* label: the relationship type alone also reaches the other members, so
|
|
57
|
+
* pre-2.4.8 `{ Ebook: [{ disconnect: [{}] }] }` unlinked every member.
|
|
58
|
+
*/
|
|
59
|
+
this.narrowedRelDefs = new WeakSet();
|
|
23
60
|
this.logger = options?.logger;
|
|
24
61
|
this.policyWhereCompiler = options?.whereCompiler;
|
|
25
62
|
}
|
|
@@ -138,6 +175,9 @@ class MutationCompiler {
|
|
|
138
175
|
*/
|
|
139
176
|
isConnectFastPathEligible(items, targetNodeDef) {
|
|
140
177
|
for (const item of items) {
|
|
178
|
+
// A nested `connect` is per item (v2.4.8): the UNWIND cannot run it.
|
|
179
|
+
if (item.connect != null)
|
|
180
|
+
return false;
|
|
141
181
|
const where = item.where;
|
|
142
182
|
if (where === undefined)
|
|
143
183
|
continue;
|
|
@@ -189,6 +229,36 @@ class MutationCompiler {
|
|
|
189
229
|
this.labelCache.set(nodeDef.typeName, labelStr);
|
|
190
230
|
return labelStr;
|
|
191
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* The MATCH that binds a connect TARGET by type. A node type keeps its
|
|
234
|
+
* label pattern; an interface / union target matches its MEMBERS' labels
|
|
235
|
+
* through a WHERE predicate (v2.4.8). Pre-2.4.8 it matched the abstract
|
|
236
|
+
* type's own label, which implementer and member nodes do not carry, so
|
|
237
|
+
* the connect linked nothing.
|
|
238
|
+
*/
|
|
239
|
+
targetMatch(targetNodeDef, targetVar) {
|
|
240
|
+
const abstract = abstractTarget(targetNodeDef.typeName, this.schema);
|
|
241
|
+
if (!abstract)
|
|
242
|
+
return {
|
|
243
|
+
match: `MATCH (${targetVar}:${this.getCachedLabelString(targetNodeDef)})`,
|
|
244
|
+
labelPredicate: null,
|
|
245
|
+
};
|
|
246
|
+
const alternatives = abstract.members.map((m) => `${targetVar}:${this.getCachedLabelString(this.schema.nodes.get(m))}`);
|
|
247
|
+
return {
|
|
248
|
+
match: `MATCH (${targetVar})`,
|
|
249
|
+
labelPredicate: alternatives.length > 0 ? `(${alternatives.join(' OR ')})` : 'false',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Resolve a relationship's target for a nested write. Throws when the
|
|
254
|
+
* target is unknown — pre-2.4.8 the nested op was silently skipped.
|
|
255
|
+
*/
|
|
256
|
+
requireTargetDef(relDef) {
|
|
257
|
+
const targetNodeDef = (0, utils_1.resolveTargetDef)(relDef.target, this.schema);
|
|
258
|
+
if (!targetNodeDef)
|
|
259
|
+
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
|
|
260
|
+
return targetNodeDef;
|
|
261
|
+
}
|
|
192
262
|
/**
|
|
193
263
|
* Generate CREATE Cypher for one or more nodes.
|
|
194
264
|
* Handles scalar properties, nested relationship creates, and connects.
|
|
@@ -213,8 +283,10 @@ class MutationCompiler {
|
|
|
213
283
|
const input = inputs[i];
|
|
214
284
|
const prefix = `create${i}`;
|
|
215
285
|
const nodeVar = i === 0 ? 'n' : `n_${i}`;
|
|
216
|
-
|
|
286
|
+
// Labels first: an unsafe label must fail as such, before any
|
|
287
|
+
// input-key check (v2.5.0 rejects unknown keys).
|
|
217
288
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
289
|
+
const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
|
|
218
290
|
lines.push(`CREATE (${nodeVar}:${labelStr} { ${propString} })`);
|
|
219
291
|
(0, validation_1.mergeParams)(params, propParams);
|
|
220
292
|
createdVars.push(nodeVar);
|
|
@@ -289,6 +361,7 @@ class MutationCompiler {
|
|
|
289
361
|
if (value === undefined)
|
|
290
362
|
continue;
|
|
291
363
|
(0, validation_1.assertSafePropertyName)(key, 'update property');
|
|
364
|
+
assertKnownField(nodeDef, key, 'update');
|
|
292
365
|
const propDef = nodeDef.properties.get(key);
|
|
293
366
|
const paramName = `update_${key}`;
|
|
294
367
|
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
@@ -440,19 +513,31 @@ class MutationCompiler {
|
|
|
440
513
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
441
514
|
const lines = [];
|
|
442
515
|
const params = {};
|
|
443
|
-
//
|
|
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.
|
|
444
526
|
const mergeProps = [];
|
|
445
527
|
const mergeKeyNames = [];
|
|
446
528
|
for (const [key, value] of Object.entries(where)) {
|
|
447
|
-
if (value === undefined
|
|
448
|
-
continue;
|
|
449
|
-
// Skip relationship suffixes and operators
|
|
450
|
-
if (key === 'AND' || key === 'OR' || key === 'NOT')
|
|
451
|
-
continue;
|
|
452
|
-
if (key.includes('_') && !nodeDef.properties.has(key))
|
|
529
|
+
if (value === undefined)
|
|
453
530
|
continue;
|
|
454
|
-
(0, validation_1.assertSafePropertyName)(key, 'merge key');
|
|
455
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');
|
|
456
541
|
const paramName = `merge_${key}`;
|
|
457
542
|
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
458
543
|
mergeProps.push(`${(0, validation_1.escapeIdentifier)(key)}: ${valueExpr}`);
|
|
@@ -460,39 +545,21 @@ class MutationCompiler {
|
|
|
460
545
|
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
461
546
|
}
|
|
462
547
|
if (mergeProps.length === 0)
|
|
463
|
-
throw new errors_1.OGMError(
|
|
464
|
-
|
|
465
|
-
// `@id` property. Pre-1.7.4 we accepted any key set, so:
|
|
466
|
-
// - A typo in `where` (e.g. `usernme`) became a phantom MERGE
|
|
467
|
-
// property — Neo4j happily created a node with that mis-named
|
|
468
|
-
// attribute on first use.
|
|
469
|
-
// - A non-unique `where` (e.g. `where: { country: 'AR' }`) matched
|
|
470
|
-
// multiple existing nodes; MERGE then fans out across all of
|
|
471
|
-
// them, applying ON MATCH SET to every match. Almost always a
|
|
472
|
-
// bug, never the developer's intent.
|
|
473
|
-
// Requiring at least one unique/id key forces MERGE to target at
|
|
474
|
-
// most one row.
|
|
475
|
-
const hasUniqueOrId = mergeKeyNames.some((name) => {
|
|
476
|
-
const prop = nodeDef.properties.get(name);
|
|
477
|
-
// `isGenerated` is the parser's flag for the `@id` directive;
|
|
478
|
-
// `isUnique` is the flag for `@unique`. Either is sufficient
|
|
479
|
-
// to guarantee the MERGE pattern targets at most one row.
|
|
480
|
-
return prop !== undefined && (prop.isUnique || prop.isGenerated);
|
|
481
|
-
});
|
|
482
|
-
if (!hasUniqueOrId)
|
|
483
|
-
throw new errors_1.OGMError(`upsert "where" must contain at least one property marked with @id or @unique. ` +
|
|
484
|
-
`Got [${mergeKeyNames.map((n) => `"${n}"`).join(', ')}] — none of these are unique on ${nodeDef.typeName}. ` +
|
|
485
|
-
`Without a unique key, MERGE can fan out across multiple existing nodes (applying ON MATCH SET to each) ` +
|
|
486
|
-
`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}.`);
|
|
487
550
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
|
|
488
551
|
// ON CREATE SET — all properties from create input (scalar only)
|
|
489
552
|
const createSets = [];
|
|
490
553
|
for (const [key, value] of Object.entries(create)) {
|
|
491
554
|
if (value === undefined)
|
|
492
555
|
continue;
|
|
493
|
-
if (nodeDef.relationships.has(key))
|
|
494
|
-
|
|
556
|
+
if (nodeDef.relationships.has(key)) {
|
|
557
|
+
if (value === null)
|
|
558
|
+
continue;
|
|
559
|
+
throw upsertRelationshipError(nodeDef, key, 'create');
|
|
560
|
+
}
|
|
495
561
|
(0, validation_1.assertSafePropertyName)(key, 'create property');
|
|
562
|
+
assertKnownField(nodeDef, key, 'create');
|
|
496
563
|
const propDef = nodeDef.properties.get(key);
|
|
497
564
|
const paramName = `onCreate_${key}`;
|
|
498
565
|
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
@@ -537,9 +604,13 @@ class MutationCompiler {
|
|
|
537
604
|
for (const [key, value] of Object.entries(update)) {
|
|
538
605
|
if (value === undefined)
|
|
539
606
|
continue;
|
|
540
|
-
if (nodeDef.relationships.has(key))
|
|
541
|
-
|
|
607
|
+
if (nodeDef.relationships.has(key)) {
|
|
608
|
+
if (value === null)
|
|
609
|
+
continue;
|
|
610
|
+
throw upsertRelationshipError(nodeDef, key, 'update');
|
|
611
|
+
}
|
|
542
612
|
(0, validation_1.assertSafePropertyName)(key, 'update property');
|
|
613
|
+
assertKnownField(nodeDef, key, 'update');
|
|
543
614
|
const propDef = nodeDef.properties.get(key);
|
|
544
615
|
const paramName = `onMatch_${key}`;
|
|
545
616
|
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
@@ -613,16 +684,13 @@ class MutationCompiler {
|
|
|
613
684
|
for (const key of Object.keys(item)) {
|
|
614
685
|
if (item[key] === undefined || scalarKeys.includes(key))
|
|
615
686
|
continue;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (index > 0)
|
|
622
|
-
continue;
|
|
623
|
-
throw new errors_1.OGMError(`createMany does not support relationship fields. Found: "${key}". Use create() for nested operations.`);
|
|
624
|
-
}
|
|
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.`);
|
|
625
692
|
(0, validation_1.assertSafePropertyName)(key, 'createMany property');
|
|
693
|
+
assertKnownField(nodeDef, key, 'createMany');
|
|
626
694
|
scalarKeys.push(key);
|
|
627
695
|
}
|
|
628
696
|
// Identify @id (generated) fields that are NOT provided in data
|
|
@@ -754,6 +822,7 @@ class MutationCompiler {
|
|
|
754
822
|
if (value === undefined)
|
|
755
823
|
continue;
|
|
756
824
|
(0, validation_1.assertSafePropertyName)(key, 'create property');
|
|
825
|
+
assertKnownField(nodeDef, key, 'create');
|
|
757
826
|
const propDef = nodeDef.properties.get(key);
|
|
758
827
|
const paramName = `${prefix}_${key}`;
|
|
759
828
|
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
@@ -789,19 +858,20 @@ class MutationCompiler {
|
|
|
789
858
|
const relDef = nodeDef.relationships.get(key);
|
|
790
859
|
if (!relDef)
|
|
791
860
|
continue;
|
|
861
|
+
if (value == null)
|
|
862
|
+
continue;
|
|
792
863
|
const relInput = value;
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
864
|
+
// Union / interface targets: per-member keys (e.g. items: { Ebook:
|
|
865
|
+
// { create: [...] } }) dispatch to the concrete member; an interface
|
|
866
|
+
// also takes the flat `{ connect, create }` shape (v2.4.8 — pre-2.4.8
|
|
867
|
+
// interface targets were skipped entirely, so nothing was written).
|
|
868
|
+
const abstract = abstractTarget(relDef.target, this.schema);
|
|
869
|
+
const memberEntries = abstract
|
|
870
|
+
? perMemberEntries(relInput, abstract, key)
|
|
871
|
+
: null;
|
|
872
|
+
if (memberEntries) {
|
|
873
|
+
for (const [memberKey, memberValue] of memberEntries) {
|
|
802
874
|
const memberNodeDef = this.schema.nodes.get(memberKey);
|
|
803
|
-
if (!memberNodeDef)
|
|
804
|
-
continue;
|
|
805
875
|
const memberInput = memberValue;
|
|
806
876
|
// Process creates/connects for this union member
|
|
807
877
|
const memberLines = this.buildCreateRelationshipsForTarget(memberInput, memberNodeDef, relDef, nodeVar, `${prefix}_${key}_${memberKey}`, params, allVars, withClause, nestedCounter, policyContext, paramCounter);
|
|
@@ -810,9 +880,7 @@ class MutationCompiler {
|
|
|
810
880
|
}
|
|
811
881
|
continue;
|
|
812
882
|
}
|
|
813
|
-
const targetNodeDef = this.
|
|
814
|
-
if (!targetNodeDef)
|
|
815
|
-
continue;
|
|
883
|
+
const targetNodeDef = this.requireTargetDef(relDef);
|
|
816
884
|
const resultLines = this.buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, `${prefix}_${key}`, params, allVars, withClause, nestedCounter, policyContext, paramCounter);
|
|
817
885
|
nestedCounter += resultLines.counterUsed;
|
|
818
886
|
lines.push(...resultLines.lines);
|
|
@@ -826,7 +894,8 @@ class MutationCompiler {
|
|
|
826
894
|
buildCreateRelationshipsForTarget(relInput, targetNodeDef, relDef, nodeVar, prefix, params, allVars, withClause, startCounter, policyContext, paramCounter) {
|
|
827
895
|
const lines = [];
|
|
828
896
|
let counter = 0;
|
|
829
|
-
|
|
897
|
+
// A flat interface input (v2.4.8): each create names its implementer.
|
|
898
|
+
const abstract = abstractTarget(targetNodeDef.typeName, this.schema);
|
|
830
899
|
// --- create ---
|
|
831
900
|
if (relInput.create) {
|
|
832
901
|
const createItems = Array.isArray(relInput.create)
|
|
@@ -834,15 +903,22 @@ class MutationCompiler {
|
|
|
834
903
|
: [relInput.create];
|
|
835
904
|
for (let ci = 0; ci < createItems.length; ci++) {
|
|
836
905
|
const createSpec = createItems[ci];
|
|
837
|
-
|
|
906
|
+
let nodeSpec = (createSpec.node ?? createSpec);
|
|
907
|
+
let createDef = targetNodeDef;
|
|
908
|
+
if (abstract) {
|
|
909
|
+
const [member, memberSpec] = unwrapAbstractCreateNode(nodeSpec, abstract, relDef.fieldName);
|
|
910
|
+
createDef = this.schema.nodes.get(member);
|
|
911
|
+
nodeSpec = memberSpec;
|
|
912
|
+
}
|
|
913
|
+
const targetLabelStr = this.getCachedLabelString(createDef);
|
|
838
914
|
const createVar = `${nodeVar}_c${startCounter + counter}`;
|
|
839
915
|
counter++;
|
|
840
|
-
const { propString, propParams } = this.buildCreateProperties(nodeSpec,
|
|
916
|
+
const { propString, propParams } = this.buildCreateProperties(nodeSpec, createDef, `${prefix}_create${ci}`);
|
|
841
917
|
// Handle nested relationship creates within the created node
|
|
842
|
-
const nestedRelLines = this.buildCreateRelationships(nodeSpec,
|
|
918
|
+
const nestedRelLines = this.buildCreateRelationships(nodeSpec, createDef, createVar, `${prefix}_create${ci}`, params, allVars, policyContext, paramCounter);
|
|
843
919
|
const propsClause = propString.length > 0
|
|
844
920
|
? ` { ${propString} }`
|
|
845
|
-
: ` { ${this.buildGeneratedIdClause(
|
|
921
|
+
: ` { ${this.buildGeneratedIdClause(createDef)} }`;
|
|
846
922
|
const relPattern = (0, utils_1.buildRelPattern)({
|
|
847
923
|
sourceVar: nodeVar,
|
|
848
924
|
relDef,
|
|
@@ -872,6 +948,9 @@ class MutationCompiler {
|
|
|
872
948
|
targetVar: createVar,
|
|
873
949
|
edgeVar: relVar,
|
|
874
950
|
targetLabel: 'auto',
|
|
951
|
+
// Abstract targets: no label — the created node never carries
|
|
952
|
+
// the union / interface label (v2.4.8).
|
|
953
|
+
schema: this.schema,
|
|
875
954
|
});
|
|
876
955
|
lines.push(`WITH ${[...allVars, createVar].join(', ')}`);
|
|
877
956
|
lines.push(`MATCH ${relArrow}`);
|
|
@@ -901,339 +980,426 @@ class MutationCompiler {
|
|
|
901
980
|
? relInput.connect
|
|
902
981
|
: [relInput.connect];
|
|
903
982
|
for (let ci = 0; ci < connectItems.length; ci++) {
|
|
904
|
-
const connectSpec = connectItems[ci];
|
|
905
|
-
const whereSpec = connectSpec.where;
|
|
906
|
-
const edgeInput = connectSpec.edge;
|
|
907
983
|
// Wrap in CALL subquery so a failed MATCH doesn't kill the outer pipeline
|
|
908
|
-
|
|
909
|
-
lines.push(`CALL {`);
|
|
910
|
-
lines.push(withClause);
|
|
911
|
-
const connectVar = `${nodeVar}_cn${startCounter + counter}`;
|
|
984
|
+
const index = startCounter + counter;
|
|
912
985
|
counter++;
|
|
913
|
-
lines.push(
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
? `(${nodeVar})-[${relVar}:${escapedRelType}]->(${connectVar})`
|
|
928
|
-
: `(${nodeVar})<-[${relVar}:${escapedRelType}]-(${connectVar})`;
|
|
929
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
930
|
-
const setItems = [];
|
|
931
|
-
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
932
|
-
if (val === undefined)
|
|
933
|
-
continue;
|
|
934
|
-
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
935
|
-
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
936
|
-
const paramName = `${prefix}_conn${ci}_edge_${prop}`;
|
|
937
|
-
setItems.push(`r_conn_${startCounter + counter - 1}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
938
|
-
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
939
|
-
}
|
|
940
|
-
if (setItems.length > 0)
|
|
941
|
-
lines.push(`SET ${setItems.join(', ')}`);
|
|
942
|
-
}
|
|
943
|
-
else {
|
|
944
|
-
const relPattern = (0, utils_1.buildRelPattern)({
|
|
986
|
+
lines.push(...this.buildConnectItem({
|
|
987
|
+
connectSpec: connectItems[ci],
|
|
988
|
+
relDef,
|
|
989
|
+
targetNodeDef,
|
|
990
|
+
sourceVar: nodeVar,
|
|
991
|
+
withClause,
|
|
992
|
+
connectVar: `${nodeVar}_cn${index}`,
|
|
993
|
+
relVar: `r_conn_${index}`,
|
|
994
|
+
edgeParamPrefix: `${prefix}_conn${ci}`,
|
|
995
|
+
returnAlias: `_ccn_${prefix}_${ci}`,
|
|
996
|
+
params,
|
|
997
|
+
policyContext,
|
|
998
|
+
paramCounter,
|
|
999
|
+
scope: {
|
|
945
1000
|
sourceVar: nodeVar,
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
lines.push(`}`);
|
|
1001
|
+
carryVars: allVars,
|
|
1002
|
+
withClause,
|
|
1003
|
+
varPrefix: '',
|
|
1004
|
+
depth: 0,
|
|
1005
|
+
},
|
|
1006
|
+
}));
|
|
953
1007
|
}
|
|
954
1008
|
}
|
|
955
1009
|
return { lines, counterUsed: counter };
|
|
956
1010
|
}
|
|
957
|
-
|
|
1011
|
+
/**
|
|
1012
|
+
* Top-level `connect`. A union / interface relationship keyed by member
|
|
1013
|
+
* (`{ media: { Book: [...] } }`, the shape codegen emits) connects each
|
|
1014
|
+
* member separately; a flat interface input matches the implementers by
|
|
1015
|
+
* their own labels (v2.4.8 — pre-2.4.8 both matched the abstract type's
|
|
1016
|
+
* label, which no node carries, and the per-member shape was never
|
|
1017
|
+
* unwrapped, so the MATCH carried NO filter at all).
|
|
1018
|
+
*/
|
|
1019
|
+
buildConnects(connect, nodeDef, params, policyContext, paramCounter, scope = TOP_SCOPE) {
|
|
958
1020
|
const lines = [];
|
|
959
1021
|
for (const [fieldName, spec] of Object.entries(connect)) {
|
|
960
1022
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
961
1023
|
if (!relDef)
|
|
962
1024
|
continue;
|
|
963
|
-
const
|
|
964
|
-
|
|
1025
|
+
const abstract = abstractTarget(relDef.target, this.schema);
|
|
1026
|
+
const memberEntries = abstract && !isBlanketSpec(spec)
|
|
1027
|
+
? perMemberEntries(spec, abstract, fieldName)
|
|
1028
|
+
: null;
|
|
1029
|
+
if (memberEntries) {
|
|
1030
|
+
for (const [member, memberSpec] of memberEntries)
|
|
1031
|
+
lines.push(...this.buildRelConnects(memberSpec, this.narrowToMember(relDef, member), this.schema.nodes.get(member), `${fieldName}_${member}`, params, policyContext, paramCounter, scope));
|
|
965
1032
|
continue;
|
|
966
|
-
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
967
|
-
// Array connect
|
|
968
|
-
if (Array.isArray(spec)) {
|
|
969
|
-
if (spec.length === 0)
|
|
970
|
-
continue; // Empty array — nothing to connect
|
|
971
|
-
// The UNWIND fast path compiles filters against per-row values and
|
|
972
|
-
// is only exact for plain scalar, non-null filters (see
|
|
973
|
-
// isConnectFastPathEligible). Everything else — null values,
|
|
974
|
-
// logical keys, traversals — takes one CALL subquery per item,
|
|
975
|
-
// compiled through WhereCompiler.
|
|
976
|
-
const useFastPath = this.isConnectFastPathEligible(spec, targetNodeDef);
|
|
977
|
-
if (!useFastPath)
|
|
978
|
-
for (let ci = 0; ci < spec.length; ci++) {
|
|
979
|
-
const connectSpec = spec[ci];
|
|
980
|
-
const whereSpec = connectSpec.where;
|
|
981
|
-
const edgeInput = connectSpec.edge;
|
|
982
|
-
lines.push('WITH n');
|
|
983
|
-
lines.push(`CALL {`);
|
|
984
|
-
lines.push('WITH n');
|
|
985
|
-
const connectVar = `target_${fieldName}_${ci}`;
|
|
986
|
-
lines.push(`MATCH (${connectVar}:${targetLabelStr})`);
|
|
987
|
-
const connectConditions = [
|
|
988
|
-
this.compileTargetWhere(whereSpec, connectVar, targetNodeDef, params, policyContext, paramCounter),
|
|
989
|
-
this.buildTargetPolicyPredicate(targetNodeDef, connectVar, params, policyContext, paramCounter),
|
|
990
|
-
].filter((c) => Boolean(c));
|
|
991
|
-
if (connectConditions.length > 0)
|
|
992
|
-
lines.push(`WHERE ${connectConditions.join(' AND ')}`);
|
|
993
|
-
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
994
|
-
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
995
|
-
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
996
|
-
const relVar = `r_conn_${fieldName}_${ci}`;
|
|
997
|
-
const mergePattern = relDef.direction === 'OUT'
|
|
998
|
-
? `(n)-[${relVar}:${escapedRelType}]->(${connectVar})`
|
|
999
|
-
: `(n)<-[${relVar}:${escapedRelType}]-(${connectVar})`;
|
|
1000
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
1001
|
-
const setItems = [];
|
|
1002
|
-
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
1003
|
-
if (val === undefined)
|
|
1004
|
-
continue;
|
|
1005
|
-
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
1006
|
-
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1007
|
-
const paramName = `connect_${fieldName}_${ci}_edge_${prop}`;
|
|
1008
|
-
setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1009
|
-
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1010
|
-
}
|
|
1011
|
-
if (setItems.length > 0)
|
|
1012
|
-
lines.push(`SET ${setItems.join(', ')}`);
|
|
1013
|
-
}
|
|
1014
|
-
else {
|
|
1015
|
-
const mergePattern = (0, utils_1.buildRelPattern)({
|
|
1016
|
-
sourceVar: 'n',
|
|
1017
|
-
relDef,
|
|
1018
|
-
targetVar: connectVar,
|
|
1019
|
-
});
|
|
1020
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
1021
|
-
}
|
|
1022
|
-
lines.push(`RETURN count(*) AS _ccn_${fieldName}_${ci}`);
|
|
1023
|
-
lines.push(`}`);
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
// Scalar-only WHERE — use efficient UNWIND approach
|
|
1027
|
-
const paramName = `connect_${fieldName}`;
|
|
1028
|
-
// Strip undefined values from edge objects — Neo4j driver drops undefined,
|
|
1029
|
-
// causing param mismatches when the Cypher references nested edge properties.
|
|
1030
|
-
params[paramName] = spec.map((item) => {
|
|
1031
|
-
const edge = item.edge;
|
|
1032
|
-
if (!edge)
|
|
1033
|
-
return item;
|
|
1034
|
-
const cleaned = {};
|
|
1035
|
-
for (const [k, v] of Object.entries(edge))
|
|
1036
|
-
if (v !== undefined)
|
|
1037
|
-
cleaned[k] = (0, write_coercion_1.coerceWriteValue)(v, this.getEdgePropDef(relDef, k));
|
|
1038
|
-
return { ...item, edge: cleaned };
|
|
1039
|
-
});
|
|
1040
|
-
// Wrap in a CALL subquery: UNWIND + MATCH yields one row per
|
|
1041
|
-
// matched target (zero when nothing matches), which would
|
|
1042
|
-
// multiply — or drop — the updated node for every later clause.
|
|
1043
|
-
// Connecting to no target is a no-op, as in @neo4j/graphql.
|
|
1044
|
-
lines.push('WITH n');
|
|
1045
|
-
lines.push('CALL {');
|
|
1046
|
-
lines.push('WITH n');
|
|
1047
|
-
lines.push(`UNWIND $${paramName} AS connItem`);
|
|
1048
|
-
lines.push(`MATCH (target:${targetLabelStr})`);
|
|
1049
|
-
// Build WHERE from the first item's structure to determine the path.
|
|
1050
|
-
// Pre-1.7.4 we silently used `firstItem`'s keys for ALL items —
|
|
1051
|
-
// if `spec[1]` had different filter keys (e.g. an extra
|
|
1052
|
-
// `tenantId`), those keys were silently dropped from the WHERE.
|
|
1053
|
-
// Validate homogeneity now and throw if items diverge so the
|
|
1054
|
-
// caller knows to split into separate calls or use the per-item
|
|
1055
|
-
// CALL fallback.
|
|
1056
|
-
const firstItem = spec[0];
|
|
1057
|
-
const firstItemSig = computeConnectItemSignature(firstItem);
|
|
1058
|
-
for (let idx = 1; idx < spec.length; idx++) {
|
|
1059
|
-
const sig = computeConnectItemSignature(spec[idx]);
|
|
1060
|
-
if (sig !== firstItemSig)
|
|
1061
|
-
throw new errors_1.OGMError(`connect array items have divergent shapes — item[0] has keys "${firstItemSig}" ` +
|
|
1062
|
-
`but item[${idx}] has keys "${sig}". The UNWIND fast path requires every item ` +
|
|
1063
|
-
`to share the same WHERE / edge key set; otherwise the additional keys are ` +
|
|
1064
|
-
`silently dropped. Split into separate connect calls, or normalise the items ` +
|
|
1065
|
-
`to share the same shape.`);
|
|
1066
|
-
}
|
|
1067
|
-
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem', targetNodeDef);
|
|
1068
|
-
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter);
|
|
1069
|
-
const unwindConditions = policyPredicate
|
|
1070
|
-
? [...whereConditions, policyPredicate]
|
|
1071
|
-
: whereConditions;
|
|
1072
|
-
if (unwindConditions.length > 0)
|
|
1073
|
-
lines.push(`WHERE ${unwindConditions.join(' AND ')}`);
|
|
1074
|
-
const mergePattern = (0, utils_1.buildRelPattern)({
|
|
1075
|
-
sourceVar: 'n',
|
|
1076
|
-
relDef,
|
|
1077
|
-
targetVar: 'target',
|
|
1078
|
-
});
|
|
1079
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
1080
|
-
// Handle edge properties from first item structure
|
|
1081
|
-
const edgeProps = this.extractEdgeProperties(firstItem);
|
|
1082
|
-
if (edgeProps.length > 0) {
|
|
1083
|
-
// Use bare target var (no label) since target is already bound by MATCH
|
|
1084
|
-
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
1085
|
-
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
1086
|
-
const mergeWithVar = relDef.direction === 'OUT'
|
|
1087
|
-
? `(n)-[r:${escapedRelType}]->(target)`
|
|
1088
|
-
: `(n)<-[r:${escapedRelType}]-(target)`;
|
|
1089
|
-
// Replace MERGE line with one that has a variable
|
|
1090
|
-
lines[lines.length - 1] = `MERGE ${mergeWithVar}`;
|
|
1091
|
-
// r.prop is relationship property (escape), connItem.edge.prop is parameter map (no escape)
|
|
1092
|
-
const setItems = edgeProps.map((p) => `r.${(0, validation_1.escapeIdentifier)(p)} = ${(0, write_coercion_1.wrapListItemExpr)(`connItem.edge.${p}`, spec.map((it) => it.edge?.[p]), this.getEdgePropDef(relDef, p))}`);
|
|
1093
|
-
lines.push(`SET ${setItems.join(', ')}`);
|
|
1094
|
-
}
|
|
1095
|
-
lines.push(`RETURN count(*) AS _conn_${fieldName}`);
|
|
1096
|
-
lines.push('}');
|
|
1097
|
-
}
|
|
1098
1033
|
}
|
|
1034
|
+
lines.push(...this.buildRelConnects(spec, relDef, this.requireTargetDef(relDef), fieldName, params, policyContext, paramCounter, scope));
|
|
1035
|
+
}
|
|
1036
|
+
return lines;
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* One relationship's top-level connect. `tag` names its variables,
|
|
1040
|
+
* params and aliases: the field name, or `<field>_<Member>` for an
|
|
1041
|
+
* abstract relationship given per member, so two members never collide.
|
|
1042
|
+
*/
|
|
1043
|
+
buildRelConnects(spec, relDef, targetNodeDef, tag, params, policyContext, paramCounter, scope) {
|
|
1044
|
+
const lines = [];
|
|
1045
|
+
const { sourceVar, withClause, varPrefix } = scope;
|
|
1046
|
+
// Array connect
|
|
1047
|
+
if (Array.isArray(spec)) {
|
|
1048
|
+
if (spec.length === 0)
|
|
1049
|
+
return lines; // Empty array — nothing to connect
|
|
1050
|
+
// The UNWIND fast path compiles filters against per-row values and
|
|
1051
|
+
// is only exact for plain scalar, non-null filters (see
|
|
1052
|
+
// isConnectFastPathEligible). Everything else — null values,
|
|
1053
|
+
// logical keys, traversals, abstract targets — takes one CALL
|
|
1054
|
+
// subquery per item, compiled through WhereCompiler.
|
|
1055
|
+
const useFastPath = !abstractTarget(targetNodeDef.typeName, this.schema) &&
|
|
1056
|
+
this.isConnectFastPathEligible(spec, targetNodeDef);
|
|
1057
|
+
if (!useFastPath)
|
|
1058
|
+
for (let ci = 0; ci < spec.length; ci++)
|
|
1059
|
+
lines.push(...this.buildConnectItem({
|
|
1060
|
+
connectSpec: spec[ci],
|
|
1061
|
+
relDef,
|
|
1062
|
+
targetNodeDef,
|
|
1063
|
+
sourceVar,
|
|
1064
|
+
withClause,
|
|
1065
|
+
connectVar: `${varPrefix}target_${tag}_${ci}`,
|
|
1066
|
+
relVar: `${varPrefix}r_conn_${tag}_${ci}`,
|
|
1067
|
+
edgeParamPrefix: `${varPrefix}connect_${tag}_${ci}`,
|
|
1068
|
+
returnAlias: `_ccn_${varPrefix}${tag}_${ci}`,
|
|
1069
|
+
params,
|
|
1070
|
+
policyContext,
|
|
1071
|
+
paramCounter,
|
|
1072
|
+
scope,
|
|
1073
|
+
}));
|
|
1099
1074
|
else {
|
|
1100
|
-
|
|
1101
|
-
const
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1075
|
+
const targetVar = `${varPrefix}target`;
|
|
1076
|
+
const relVar = `${varPrefix}r`;
|
|
1077
|
+
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
1078
|
+
// Scalar-only WHERE — use efficient UNWIND approach
|
|
1079
|
+
const paramName = `${varPrefix}connect_${tag}`;
|
|
1080
|
+
// Strip undefined values from edge objects — Neo4j driver drops undefined,
|
|
1081
|
+
// causing param mismatches when the Cypher references nested edge properties.
|
|
1082
|
+
params[paramName] = spec.map((raw) => {
|
|
1083
|
+
// The WHERE reads `connItem.where.node.<key>`, so a bare-property
|
|
1084
|
+
// where (`{ where: { title } }`, which the fast path accepts) is
|
|
1085
|
+
// rebound under `node`. Pre-2.4.8 it was sent as-is: every lookup
|
|
1086
|
+
// was null, so the array connect silently linked nothing.
|
|
1087
|
+
const where = raw.where;
|
|
1088
|
+
const item = (0, validation_1.isPlainObject)(where) &&
|
|
1089
|
+
!Object.keys(where).some((k) => CONNECTION_WHERE_KEYS.has(k))
|
|
1090
|
+
? { ...raw, where: { node: where } }
|
|
1091
|
+
: raw;
|
|
1092
|
+
const edge = item.edge;
|
|
1093
|
+
if (!edge)
|
|
1094
|
+
return item;
|
|
1095
|
+
const cleaned = {};
|
|
1096
|
+
for (const [k, v] of Object.entries(edge))
|
|
1097
|
+
if (v !== undefined)
|
|
1098
|
+
cleaned[k] = (0, write_coercion_1.coerceWriteValue)(v, this.getEdgePropDef(relDef, k));
|
|
1099
|
+
return { ...item, edge: cleaned };
|
|
1100
|
+
});
|
|
1101
|
+
// Wrap in a CALL subquery: UNWIND + MATCH yields one row per
|
|
1102
|
+
// matched target (zero when nothing matches), which would
|
|
1103
|
+
// multiply — or drop — the updated node for every later clause.
|
|
1104
|
+
// Connecting to no target is a no-op, as in @neo4j/graphql.
|
|
1105
|
+
lines.push(withClause);
|
|
1106
1106
|
lines.push('CALL {');
|
|
1107
|
-
lines.push(
|
|
1108
|
-
lines.push(`
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
]
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1107
|
+
lines.push(withClause);
|
|
1108
|
+
lines.push(`UNWIND $${paramName} AS connItem`);
|
|
1109
|
+
lines.push(`MATCH (${targetVar}:${targetLabelStr})`);
|
|
1110
|
+
// Build WHERE from the first item's structure to determine the path.
|
|
1111
|
+
// Pre-1.7.4 we silently used `firstItem`'s keys for ALL items —
|
|
1112
|
+
// if `spec[1]` had different filter keys (e.g. an extra
|
|
1113
|
+
// `tenantId`), those keys were silently dropped from the WHERE.
|
|
1114
|
+
// Validate homogeneity now and throw if items diverge so the
|
|
1115
|
+
// caller knows to split into separate calls or use the per-item
|
|
1116
|
+
// CALL fallback.
|
|
1117
|
+
const firstItem = spec[0];
|
|
1118
|
+
const firstItemSig = computeConnectItemSignature(firstItem);
|
|
1119
|
+
for (let idx = 1; idx < spec.length; idx++) {
|
|
1120
|
+
const sig = computeConnectItemSignature(spec[idx]);
|
|
1121
|
+
if (sig !== firstItemSig)
|
|
1122
|
+
throw new errors_1.OGMError(`connect array items have divergent shapes — item[0] has keys "${firstItemSig}" ` +
|
|
1123
|
+
`but item[${idx}] has keys "${sig}". The UNWIND fast path requires every item ` +
|
|
1124
|
+
`to share the same WHERE / edge key set; otherwise the additional keys are ` +
|
|
1125
|
+
`silently dropped. Split into separate connect calls, or normalise the items ` +
|
|
1126
|
+
`to share the same shape.`);
|
|
1127
|
+
}
|
|
1128
|
+
const whereConditions = this.extractConnectWhereConditions(firstItem, targetVar, 'connItem', targetNodeDef);
|
|
1129
|
+
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter);
|
|
1130
|
+
const unwindConditions = policyPredicate
|
|
1131
|
+
? [...whereConditions, policyPredicate]
|
|
1132
|
+
: whereConditions;
|
|
1133
|
+
if (unwindConditions.length > 0)
|
|
1134
|
+
lines.push(`WHERE ${unwindConditions.join(' AND ')}`);
|
|
1135
|
+
const mergePattern = (0, utils_1.buildRelPattern)({
|
|
1136
|
+
sourceVar,
|
|
1137
|
+
relDef,
|
|
1138
|
+
targetVar,
|
|
1139
|
+
});
|
|
1140
|
+
lines.push(`MERGE ${mergePattern}`);
|
|
1141
|
+
// Handle edge properties from first item structure
|
|
1142
|
+
const edgeProps = this.extractEdgeProperties(firstItem);
|
|
1143
|
+
if (edgeProps.length > 0) {
|
|
1116
1144
|
// Use bare target var (no label) since target is already bound by MATCH
|
|
1117
1145
|
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
1118
1146
|
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
1119
1147
|
const mergeWithVar = relDef.direction === 'OUT'
|
|
1120
|
-
? `(
|
|
1121
|
-
: `(
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1128
|
-
const paramName = `connect_${fieldName}_edge_${prop}`;
|
|
1129
|
-
setItems.push(`r.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1130
|
-
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1131
|
-
}
|
|
1132
|
-
if (setItems.length > 0)
|
|
1133
|
-
lines.push(`SET ${setItems.join(', ')}`);
|
|
1148
|
+
? `(${sourceVar})-[${relVar}:${escapedRelType}]->(${targetVar})`
|
|
1149
|
+
: `(${sourceVar})<-[${relVar}:${escapedRelType}]-(${targetVar})`;
|
|
1150
|
+
// Replace MERGE line with one that has a variable
|
|
1151
|
+
lines[lines.length - 1] = `MERGE ${mergeWithVar}`;
|
|
1152
|
+
// r.prop is relationship property (escape), connItem.edge.prop is parameter map (no escape)
|
|
1153
|
+
const setItems = edgeProps.map((p) => `${relVar}.${(0, validation_1.escapeIdentifier)(p)} = ${(0, write_coercion_1.wrapListItemExpr)(`connItem.edge.${p}`, spec.map((it) => it.edge?.[p]), this.getEdgePropDef(relDef, p))}`);
|
|
1154
|
+
lines.push(`SET ${setItems.join(', ')}`);
|
|
1134
1155
|
}
|
|
1135
|
-
|
|
1136
|
-
const mergePattern = (0, utils_1.buildRelPattern)({
|
|
1137
|
-
sourceVar: 'n',
|
|
1138
|
-
relDef,
|
|
1139
|
-
targetVar: 'target',
|
|
1140
|
-
});
|
|
1141
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
1142
|
-
}
|
|
1143
|
-
lines.push(`RETURN count(*) AS _conn_${fieldName}`);
|
|
1156
|
+
lines.push(`RETURN count(*) AS _conn_${varPrefix}${tag}`);
|
|
1144
1157
|
lines.push('}');
|
|
1145
1158
|
}
|
|
1159
|
+
return lines;
|
|
1160
|
+
}
|
|
1161
|
+
// Single connect — same row-preserving isolation as the UNWIND path.
|
|
1162
|
+
return this.buildConnectItem({
|
|
1163
|
+
connectSpec: spec,
|
|
1164
|
+
relDef,
|
|
1165
|
+
targetNodeDef,
|
|
1166
|
+
sourceVar,
|
|
1167
|
+
withClause,
|
|
1168
|
+
connectVar: `${varPrefix}target`,
|
|
1169
|
+
relVar: `${varPrefix}r`,
|
|
1170
|
+
edgeParamPrefix: `${varPrefix}connect_${tag}`,
|
|
1171
|
+
returnAlias: `_conn_${varPrefix}${tag}`,
|
|
1172
|
+
params,
|
|
1173
|
+
policyContext,
|
|
1174
|
+
paramCounter,
|
|
1175
|
+
scope,
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* One connect item, isolated in a CALL subquery so a MATCH that finds
|
|
1180
|
+
* nothing (or many) never changes the outer row count:
|
|
1181
|
+
*
|
|
1182
|
+
* <withClause>
|
|
1183
|
+
* CALL {
|
|
1184
|
+
* <withClause>
|
|
1185
|
+
* MATCH (cv[:Label])
|
|
1186
|
+
* WHERE <member labels> AND <where> AND <target `read` policy>
|
|
1187
|
+
* MERGE (src)-[rv:REL]->(cv) [SET rv.<edge> = …]
|
|
1188
|
+
* RETURN count(*) AS <alias>
|
|
1189
|
+
* }
|
|
1190
|
+
*
|
|
1191
|
+
* Shared by every connect site (top-level, nested in create, nested in
|
|
1192
|
+
* update), which differ only in the names they pass.
|
|
1193
|
+
*/
|
|
1194
|
+
buildConnectItem(a) {
|
|
1195
|
+
const { connectSpec, relDef, targetNodeDef, sourceVar, connectVar } = a;
|
|
1196
|
+
const lines = [];
|
|
1197
|
+
const whereSpec = connectSpec?.where;
|
|
1198
|
+
const edgeInput = connectSpec?.edge;
|
|
1199
|
+
lines.push(a.withClause);
|
|
1200
|
+
lines.push('CALL {');
|
|
1201
|
+
lines.push(a.withClause);
|
|
1202
|
+
const { match, labelPredicate } = this.targetMatch(targetNodeDef, connectVar);
|
|
1203
|
+
lines.push(match);
|
|
1204
|
+
const conditions = [
|
|
1205
|
+
labelPredicate,
|
|
1206
|
+
this.compileTargetWhere(whereSpec, connectVar, targetNodeDef, a.params, a.policyContext, a.paramCounter),
|
|
1207
|
+
this.buildTargetPolicyPredicate(targetNodeDef, connectVar, a.params, a.policyContext, a.paramCounter),
|
|
1208
|
+
].filter((c) => Boolean(c));
|
|
1209
|
+
if (conditions.length > 0)
|
|
1210
|
+
lines.push(`WHERE ${conditions.join(' AND ')}`);
|
|
1211
|
+
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
1212
|
+
// Use bare target var (no label) since connectVar is already bound by MATCH.
|
|
1213
|
+
// Including labels on a bound variable in MERGE causes Neo4j to reject it.
|
|
1214
|
+
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
1215
|
+
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
1216
|
+
const mergePattern = relDef.direction === 'OUT'
|
|
1217
|
+
? `(${sourceVar})-[${a.relVar}:${escapedRelType}]->(${connectVar})`
|
|
1218
|
+
: `(${sourceVar})<-[${a.relVar}:${escapedRelType}]-(${connectVar})`;
|
|
1219
|
+
lines.push(`MERGE ${mergePattern}`);
|
|
1220
|
+
const setItems = [];
|
|
1221
|
+
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
1222
|
+
if (val === undefined)
|
|
1223
|
+
continue;
|
|
1224
|
+
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
1225
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1226
|
+
const paramName = `${a.edgeParamPrefix}_edge_${prop}`;
|
|
1227
|
+
setItems.push(`${a.relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1228
|
+
a.params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1229
|
+
}
|
|
1230
|
+
if (setItems.length > 0)
|
|
1231
|
+
lines.push(`SET ${setItems.join(', ')}`);
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
const relPattern = (0, utils_1.buildRelPattern)({
|
|
1235
|
+
sourceVar,
|
|
1236
|
+
relDef,
|
|
1237
|
+
targetVar: connectVar,
|
|
1238
|
+
});
|
|
1239
|
+
lines.push(`MERGE ${relPattern}`);
|
|
1240
|
+
}
|
|
1241
|
+
// A `connect` nested in the item links the CONNECTED node onward
|
|
1242
|
+
// (`connect: { posts: [{ where, connect: { tags: [...] } }] }`), with
|
|
1243
|
+
// the same policy predicates at its level. Pre-2.4.8 it was ignored.
|
|
1244
|
+
const nested = connectSpec?.connect;
|
|
1245
|
+
if (nested != null) {
|
|
1246
|
+
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1247
|
+
throw new errors_1.OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
|
|
1248
|
+
`must be keyed by the implementing type, e.g. { ${relDef.fieldName}: { Member: [{ where, connect }] } }.`);
|
|
1249
|
+
const inner = nestedScope(a.scope.carryVars, connectVar, a.scope.depth);
|
|
1250
|
+
for (const nestedConnect of Array.isArray(nested) ? nested : [nested]) {
|
|
1251
|
+
if (!(0, validation_1.isPlainObject)(nestedConnect))
|
|
1252
|
+
throw new errors_1.OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" must be an object keyed by relationship.`);
|
|
1253
|
+
lines.push(...this.buildConnects(nestedConnect, targetNodeDef, a.params, a.policyContext, a.paramCounter, inner));
|
|
1254
|
+
}
|
|
1146
1255
|
}
|
|
1256
|
+
lines.push(`RETURN count(*) AS ${a.returnAlias}`);
|
|
1257
|
+
lines.push('}');
|
|
1147
1258
|
return lines;
|
|
1148
1259
|
}
|
|
1149
|
-
|
|
1260
|
+
/**
|
|
1261
|
+
* Top-level `disconnect`. Abstract relationships follow the connect
|
|
1262
|
+
* rules (see buildConnects): per member, or flat on an interface,
|
|
1263
|
+
* matching implementers by their own labels (v2.4.8 — pre-2.4.8 the
|
|
1264
|
+
* target pattern used the interface / union label, so nothing matched).
|
|
1265
|
+
*/
|
|
1266
|
+
buildDisconnects(disconnect, nodeDef, params, policyContext, paramCounter, scope = TOP_SCOPE) {
|
|
1150
1267
|
const lines = [];
|
|
1151
1268
|
for (const [fieldName, spec] of Object.entries(disconnect)) {
|
|
1152
1269
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
1153
1270
|
if (!relDef)
|
|
1154
1271
|
continue;
|
|
1155
|
-
const
|
|
1156
|
-
//
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
lines.push('WITH n');
|
|
1165
|
-
lines.push('CALL {');
|
|
1166
|
-
lines.push('WITH n');
|
|
1167
|
-
if (item == null ||
|
|
1168
|
-
(typeof item === 'object' && Object.keys(item).length === 0)) {
|
|
1169
|
-
// Blanket disconnect — remove every relationship of this type to
|
|
1170
|
-
// a target the caller can READ. Like the `where` branch below, a
|
|
1171
|
-
// policy-bound context only unlinks nodes it can see; pre-2.5.0
|
|
1172
|
-
// the blanket form bound an anonymous `()` and also severed links
|
|
1173
|
-
// to policy-hidden nodes. Without policies there is no predicate
|
|
1174
|
-
// and the Cypher is unchanged.
|
|
1175
|
-
const blanketTarget = `target_${fieldName}_${si}`;
|
|
1176
|
-
const predicate = targetNodeDef
|
|
1177
|
-
? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
|
|
1178
|
-
: undefined;
|
|
1179
|
-
const pattern = (0, utils_1.buildRelPattern)({
|
|
1180
|
-
sourceVar: 'n',
|
|
1181
|
-
relDef,
|
|
1182
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1183
|
-
edgeVar: relVar,
|
|
1184
|
-
});
|
|
1185
|
-
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1186
|
-
if (predicate)
|
|
1187
|
-
lines.push(`WHERE ${predicate}`);
|
|
1188
|
-
lines.push(`DELETE ${relVar}`);
|
|
1189
|
-
}
|
|
1190
|
-
else {
|
|
1191
|
-
// Specific disconnect with conditions
|
|
1192
|
-
const disconnectSpec = item;
|
|
1193
|
-
const whereSpec = disconnectSpec.where;
|
|
1194
|
-
const targetVar = `target_${fieldName}_${si}`;
|
|
1195
|
-
const pattern = (0, utils_1.buildRelPattern)({
|
|
1196
|
-
sourceVar: 'n',
|
|
1197
|
-
relDef,
|
|
1198
|
-
targetVar,
|
|
1199
|
-
edgeVar: relVar,
|
|
1200
|
-
targetLabel: 'auto',
|
|
1201
|
-
});
|
|
1202
|
-
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1203
|
-
if (!targetNodeDef)
|
|
1204
|
-
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${fieldName}".`);
|
|
1205
|
-
const disconnectConditions = [
|
|
1206
|
-
this.compileTargetWhere(whereSpec, targetVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1207
|
-
this.buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter),
|
|
1208
|
-
].filter((c) => Boolean(c));
|
|
1209
|
-
if (disconnectConditions.length > 0)
|
|
1210
|
-
lines.push(`WHERE ${disconnectConditions.join(' AND ')}`);
|
|
1211
|
-
lines.push(`DELETE ${relVar}`);
|
|
1212
|
-
}
|
|
1213
|
-
lines.push(`RETURN count(*) AS _${relVar}`);
|
|
1214
|
-
lines.push('}');
|
|
1272
|
+
const abstract = abstractTarget(relDef.target, this.schema);
|
|
1273
|
+
// `null` / `{}` stays a blanket disconnect of the whole relationship.
|
|
1274
|
+
const memberEntries = abstract && !isBlanketSpec(spec)
|
|
1275
|
+
? perMemberEntries(spec, abstract, fieldName)
|
|
1276
|
+
: null;
|
|
1277
|
+
if (memberEntries) {
|
|
1278
|
+
for (const [member, memberSpec] of memberEntries)
|
|
1279
|
+
lines.push(...this.buildRelDisconnects(memberSpec, this.narrowToMember(relDef, member), this.schema.nodes.get(member), `${fieldName}_${member}`, params, policyContext, paramCounter, scope));
|
|
1280
|
+
continue;
|
|
1215
1281
|
}
|
|
1282
|
+
lines.push(...this.buildRelDisconnects(spec, relDef, (0, utils_1.resolveTargetDef)(relDef.target, this.schema) ?? undefined, fieldName, params, policyContext, paramCounter, scope));
|
|
1216
1283
|
}
|
|
1217
1284
|
return lines;
|
|
1218
1285
|
}
|
|
1219
1286
|
/**
|
|
1220
|
-
*
|
|
1287
|
+
* A `disconnect` nested in a disconnect item unlinks the disconnected
|
|
1288
|
+
* node's OWN relationships (`disconnect: { posts: [{ where, disconnect:
|
|
1289
|
+
* { tags: [...] } }] }`), with the same policy predicates at its level.
|
|
1290
|
+
* Pre-2.4.8 it was ignored. `''`-free: no input, no lines.
|
|
1291
|
+
*/
|
|
1292
|
+
buildNestedDisconnect(nested, relDef, targetNodeDef, targetVar, carryVars, params, policyContext, paramCounter, depth = 0) {
|
|
1293
|
+
if (nested == null)
|
|
1294
|
+
return [];
|
|
1295
|
+
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1296
|
+
throw new errors_1.OGMError(`A nested "disconnect" inside a disconnect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
|
|
1297
|
+
`must be keyed by the implementing type, e.g. { ${relDef.fieldName}: { Member: [{ where, disconnect }] } }.`);
|
|
1298
|
+
const inner = nestedScope(carryVars, targetVar, depth);
|
|
1299
|
+
const lines = [];
|
|
1300
|
+
for (const nestedDisconnect of Array.isArray(nested) ? nested : [nested]) {
|
|
1301
|
+
if (!(0, validation_1.isPlainObject)(nestedDisconnect))
|
|
1302
|
+
throw new errors_1.OGMError(`A nested "disconnect" inside a disconnect on "${relDef.fieldName}" must be an object keyed by relationship.`);
|
|
1303
|
+
lines.push(...this.buildDisconnects(nestedDisconnect, targetNodeDef, params, policyContext, paramCounter, inner));
|
|
1304
|
+
}
|
|
1305
|
+
return lines;
|
|
1306
|
+
}
|
|
1307
|
+
/** One relationship's disconnect items; `tag` as in buildRelConnects. */
|
|
1308
|
+
buildRelDisconnects(spec, relDef, targetNodeDef, tag, params, policyContext, paramCounter, scope) {
|
|
1309
|
+
const lines = [];
|
|
1310
|
+
const { sourceVar, withClause, varPrefix } = scope;
|
|
1311
|
+
// Normalize spec to an array of disconnect items
|
|
1312
|
+
const specItems = Array.isArray(spec) ? spec : [spec];
|
|
1313
|
+
for (let si = 0; si < specItems.length; si++) {
|
|
1314
|
+
const item = specItems[si];
|
|
1315
|
+
const relVar = `${varPrefix}r_${tag}_${si}`;
|
|
1316
|
+
// Wrap in a CALL subquery: the OPTIONAL MATCH yields one row per
|
|
1317
|
+
// matched relationship, which would multiply every later clause.
|
|
1318
|
+
// `count(*)` collapses it back to exactly one row per `n`.
|
|
1319
|
+
lines.push(withClause);
|
|
1320
|
+
lines.push('CALL {');
|
|
1321
|
+
lines.push(withClause);
|
|
1322
|
+
if (item == null ||
|
|
1323
|
+
(typeof item === 'object' && Object.keys(item).length === 0)) {
|
|
1324
|
+
// Blanket disconnect — remove every relationship of this type to
|
|
1325
|
+
// a target the caller can READ. Like the `where` branch below, a
|
|
1326
|
+
// policy-bound context only unlinks nodes it can see; pre-2.4.4
|
|
1327
|
+
// the blanket form bound an anonymous `()` and also severed links
|
|
1328
|
+
// to policy-hidden nodes. Without policies there is no predicate
|
|
1329
|
+
// and the Cypher is unchanged.
|
|
1330
|
+
const blanketTarget = `${varPrefix}target_${tag}_${si}`;
|
|
1331
|
+
const predicate = targetNodeDef
|
|
1332
|
+
? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
|
|
1333
|
+
: undefined;
|
|
1334
|
+
const pattern = (0, utils_1.buildRelPattern)({
|
|
1335
|
+
sourceVar,
|
|
1336
|
+
relDef,
|
|
1337
|
+
targetVar: predicate ? blanketTarget : '',
|
|
1338
|
+
edgeVar: relVar,
|
|
1339
|
+
targetLabelRaw: this.blanketTargetLabel(relDef),
|
|
1340
|
+
});
|
|
1341
|
+
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1342
|
+
if (predicate)
|
|
1343
|
+
lines.push(`WHERE ${predicate}`);
|
|
1344
|
+
lines.push(`DELETE ${relVar}`);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
// Specific disconnect with conditions
|
|
1348
|
+
const disconnectSpec = item;
|
|
1349
|
+
const whereSpec = disconnectSpec.where;
|
|
1350
|
+
const targetVar = `${varPrefix}target_${tag}_${si}`;
|
|
1351
|
+
const pattern = (0, utils_1.buildRelPattern)({
|
|
1352
|
+
sourceVar,
|
|
1353
|
+
relDef,
|
|
1354
|
+
targetVar,
|
|
1355
|
+
edgeVar: relVar,
|
|
1356
|
+
targetLabel: 'auto',
|
|
1357
|
+
// An abstract target binds no label (its nodes carry their
|
|
1358
|
+
// members' labels); the member predicate below narrows it.
|
|
1359
|
+
schema: this.schema,
|
|
1360
|
+
});
|
|
1361
|
+
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1362
|
+
if (!targetNodeDef)
|
|
1363
|
+
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
|
|
1364
|
+
const labelPredicate = abstractTarget(targetNodeDef.typeName, this.schema)
|
|
1365
|
+
? this.targetMatch(targetNodeDef, targetVar).labelPredicate
|
|
1366
|
+
: null;
|
|
1367
|
+
const disconnectConditions = [
|
|
1368
|
+
labelPredicate,
|
|
1369
|
+
this.compileTargetWhere(whereSpec, targetVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1370
|
+
this.buildTargetPolicyPredicate(targetNodeDef, targetVar, params, policyContext, paramCounter),
|
|
1371
|
+
].filter((c) => Boolean(c));
|
|
1372
|
+
if (disconnectConditions.length > 0)
|
|
1373
|
+
lines.push(`WHERE ${disconnectConditions.join(' AND ')}`);
|
|
1374
|
+
lines.push(`DELETE ${relVar}`);
|
|
1375
|
+
lines.push(...this.buildNestedDisconnect(disconnectSpec.disconnect, relDef, targetNodeDef, targetVar, scope.carryVars, params, policyContext, paramCounter, scope.depth));
|
|
1376
|
+
}
|
|
1377
|
+
lines.push(`RETURN count(*) AS _${relVar}`);
|
|
1378
|
+
lines.push('}');
|
|
1379
|
+
}
|
|
1380
|
+
return lines;
|
|
1381
|
+
}
|
|
1382
|
+
narrowToMember(relDef, member) {
|
|
1383
|
+
const narrowed = { ...relDef, target: member };
|
|
1384
|
+
this.narrowedRelDefs.add(narrowed);
|
|
1385
|
+
return narrowed;
|
|
1386
|
+
}
|
|
1387
|
+
/** The label a blanket disconnect over `relDef` must keep (see above). */
|
|
1388
|
+
blanketTargetLabel(relDef) {
|
|
1389
|
+
if (!this.narrowedRelDefs.has(relDef))
|
|
1390
|
+
return undefined;
|
|
1391
|
+
return this.getCachedLabelString(this.schema.nodes.get(relDef.target));
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Dispatch update operations for a union- or interface-typed
|
|
1395
|
+
* relationship given per member (interfaces since v2.4.8).
|
|
1221
1396
|
* Each member key in the input is recursed into buildUpdateRelationships
|
|
1222
1397
|
* with a narrowed relDef pointing at the concrete member type.
|
|
1223
1398
|
*/
|
|
1224
1399
|
dispatchUnionUpdateOps(key, memberEntries, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars, policyContext, paramCounter) {
|
|
1225
|
-
const unionMembers = this.schema.unions.get(relDef.target);
|
|
1226
1400
|
const lines = [];
|
|
1227
|
-
for (const [memberKey, memberValue] of
|
|
1228
|
-
|
|
1229
|
-
continue;
|
|
1230
|
-
const memberNodeDef = this.schema.nodes.get(memberKey);
|
|
1231
|
-
if (!memberNodeDef || memberValue == null)
|
|
1232
|
-
continue;
|
|
1233
|
-
const memberRelDef = {
|
|
1234
|
-
...relDef,
|
|
1235
|
-
target: memberKey,
|
|
1236
|
-
};
|
|
1401
|
+
for (const [memberKey, memberValue] of memberEntries) {
|
|
1402
|
+
const memberRelDef = this.narrowToMember(relDef, memberKey);
|
|
1237
1403
|
const memberItems = Array.isArray(memberValue)
|
|
1238
1404
|
? memberValue
|
|
1239
1405
|
: [memberValue];
|
|
@@ -1262,18 +1428,21 @@ class MutationCompiler {
|
|
|
1262
1428
|
const relDef = nodeDef.relationships.get(key);
|
|
1263
1429
|
if (!relDef || value == null)
|
|
1264
1430
|
continue;
|
|
1265
|
-
// Union targets
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1431
|
+
// Union / interface targets given per member — dispatch each member
|
|
1432
|
+
// recursively. A flat interface input is handled below against the
|
|
1433
|
+
// interface (v2.4.8 — pre-2.4.8 it was skipped, writing nothing).
|
|
1434
|
+
const abstract = abstractTarget(relDef.target, this.schema);
|
|
1435
|
+
const memberEntries = abstract
|
|
1436
|
+
? perMemberEntries(value, abstract, key)
|
|
1437
|
+
: null;
|
|
1438
|
+
if (memberEntries) {
|
|
1439
|
+
lines.push(...this.dispatchUnionUpdateOps(key, memberEntries, relDef, nodeDef, sourceVar, prefix, params, depth, ancestorVars, policyContext, paramCounter));
|
|
1270
1440
|
continue;
|
|
1271
1441
|
}
|
|
1272
|
-
const targetNodeDef = this.
|
|
1273
|
-
if (!targetNodeDef)
|
|
1274
|
-
continue;
|
|
1275
|
-
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
1442
|
+
const targetNodeDef = this.requireTargetDef(relDef);
|
|
1276
1443
|
const relPrefix = `${prefix}_${key}`;
|
|
1444
|
+
// Past the dispatch, `abstract` is set only for a flat INTERFACE
|
|
1445
|
+
// input (a flat union input threw in perMemberEntries).
|
|
1277
1446
|
// Value can be an array (e.g., chapters: [{ create: ... }, { update: ... }])
|
|
1278
1447
|
// or an object (e.g., author: { connect: ... })
|
|
1279
1448
|
const items = Array.isArray(value)
|
|
@@ -1306,13 +1475,17 @@ class MutationCompiler {
|
|
|
1306
1475
|
for (let di = 0; di < disconnectItems.length; di++) {
|
|
1307
1476
|
const disconnectSpec = disconnectItems[di];
|
|
1308
1477
|
const whereSpec = disconnectSpec.where;
|
|
1478
|
+
// A nested `disconnect` needs the target bound, so an item that
|
|
1479
|
+
// carries one is never the anonymous blanket form.
|
|
1480
|
+
const nestedDisconnect = disconnectSpec.disconnect;
|
|
1309
1481
|
const relVar = `r_disc_${key}_${i}_${di}`;
|
|
1310
1482
|
// Same isolation as the top-level disconnect: keep the
|
|
1311
1483
|
// OPTIONAL MATCH fan-out from multiplying later nested ops.
|
|
1312
1484
|
lines.push(withClause);
|
|
1313
1485
|
lines.push('CALL {');
|
|
1314
1486
|
lines.push(withClause);
|
|
1315
|
-
if (!whereSpec || Object.keys(whereSpec).length === 0)
|
|
1487
|
+
if ((!whereSpec || Object.keys(whereSpec).length === 0) &&
|
|
1488
|
+
nestedDisconnect == null) {
|
|
1316
1489
|
// Blanket — same read-policy gate as the top-level blanket
|
|
1317
1490
|
// disconnect: only unlink targets the caller can read.
|
|
1318
1491
|
const blanketTarget = `${sourceVar}_disc${varCounter}`;
|
|
@@ -1324,6 +1497,7 @@ class MutationCompiler {
|
|
|
1324
1497
|
relDef,
|
|
1325
1498
|
targetVar: predicate ? blanketTarget : '',
|
|
1326
1499
|
edgeVar: relVar,
|
|
1500
|
+
targetLabelRaw: this.blanketTargetLabel(relDef),
|
|
1327
1501
|
});
|
|
1328
1502
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1329
1503
|
if (predicate)
|
|
@@ -1339,6 +1513,9 @@ class MutationCompiler {
|
|
|
1339
1513
|
targetVar: discTarget,
|
|
1340
1514
|
edgeVar: relVar,
|
|
1341
1515
|
targetLabel: 'auto',
|
|
1516
|
+
// An interface target matches its implementers, which do
|
|
1517
|
+
// not carry the interface label (v2.4.8).
|
|
1518
|
+
schema: this.schema,
|
|
1342
1519
|
});
|
|
1343
1520
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1344
1521
|
// Pre-1.8.1 this called `buildNodeWhereConditions` with
|
|
@@ -1351,12 +1528,16 @@ class MutationCompiler {
|
|
|
1351
1528
|
// path (`buildDisconnects`). Restores backwards-compat with
|
|
1352
1529
|
// @neo4j/graphql-ogm's nested update.disconnect[].where shape.
|
|
1353
1530
|
const nestedDiscConditions = [
|
|
1531
|
+
abstract
|
|
1532
|
+
? this.targetMatch(targetNodeDef, discTarget).labelPredicate
|
|
1533
|
+
: null,
|
|
1354
1534
|
this.compileTargetWhere(whereSpec, discTarget, targetNodeDef, params, policyContext, paramCounter),
|
|
1355
1535
|
this.buildTargetPolicyPredicate(targetNodeDef, discTarget, params, policyContext, paramCounter),
|
|
1356
1536
|
].filter((c) => Boolean(c));
|
|
1357
1537
|
if (nestedDiscConditions.length > 0)
|
|
1358
1538
|
lines.push(`WHERE ${nestedDiscConditions.join(' AND ')}`);
|
|
1359
1539
|
lines.push(`DELETE ${relVar}`);
|
|
1540
|
+
lines.push(...this.buildNestedDisconnect(nestedDisconnect, relDef, targetNodeDef, discTarget, allVars, params, policyContext, paramCounter));
|
|
1360
1541
|
}
|
|
1361
1542
|
lines.push(`RETURN count(*) AS _${relVar}`);
|
|
1362
1543
|
lines.push('}');
|
|
@@ -1368,61 +1549,30 @@ class MutationCompiler {
|
|
|
1368
1549
|
? item.connect
|
|
1369
1550
|
: [item.connect];
|
|
1370
1551
|
for (let ci = 0; ci < connectItems.length; ci++) {
|
|
1371
|
-
const connectSpec = connectItems[ci];
|
|
1372
|
-
const whereSpec = connectSpec.where;
|
|
1373
|
-
const edgeInput = connectSpec.edge;
|
|
1374
1552
|
// Wrap in CALL subquery so a failed MATCH doesn't kill the outer pipeline
|
|
1375
|
-
lines.push(withClause);
|
|
1376
|
-
lines.push(`CALL {`);
|
|
1377
|
-
lines.push(withClause);
|
|
1378
1553
|
const connectVar = `${sourceVar}_conn${varCounter}`;
|
|
1379
1554
|
varCounter++;
|
|
1380
|
-
lines.push(
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
1395
|
-
// Use bare target var (no label) since connectVar is already bound by MATCH
|
|
1396
|
-
const relVar = `r_conn_${key}_${ci}`;
|
|
1397
|
-
(0, validation_1.assertSafeIdentifier)(relDef.type, 'relationship type');
|
|
1398
|
-
const escapedRelType = (0, validation_1.escapeIdentifier)(relDef.type);
|
|
1399
|
-
const mergePattern = relDef.direction === 'OUT'
|
|
1400
|
-
? `(${sourceVar})-[${relVar}:${escapedRelType}]->(${connectVar})`
|
|
1401
|
-
: `(${sourceVar})<-[${relVar}:${escapedRelType}]-(${connectVar})`;
|
|
1402
|
-
lines.push(`MERGE ${mergePattern}`);
|
|
1403
|
-
const setItems = [];
|
|
1404
|
-
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
1405
|
-
if (val === undefined)
|
|
1406
|
-
continue;
|
|
1407
|
-
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
1408
|
-
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1409
|
-
const paramName = `${itemPrefix}_conn${ci}_edge_${prop}`;
|
|
1410
|
-
setItems.push(`r_conn_${key}_${ci}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1411
|
-
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1412
|
-
}
|
|
1413
|
-
if (setItems.length > 0)
|
|
1414
|
-
lines.push(`SET ${setItems.join(', ')}`);
|
|
1415
|
-
}
|
|
1416
|
-
else {
|
|
1417
|
-
const relPattern = (0, utils_1.buildRelPattern)({
|
|
1555
|
+
lines.push(...this.buildConnectItem({
|
|
1556
|
+
connectSpec: connectItems[ci],
|
|
1557
|
+
relDef,
|
|
1558
|
+
targetNodeDef,
|
|
1559
|
+
sourceVar,
|
|
1560
|
+
withClause,
|
|
1561
|
+
connectVar,
|
|
1562
|
+
relVar: `r_conn_${key}_${ci}`,
|
|
1563
|
+
edgeParamPrefix: `${itemPrefix}_conn${ci}`,
|
|
1564
|
+
returnAlias: `_cn_${key}_${ci}`,
|
|
1565
|
+
params,
|
|
1566
|
+
policyContext,
|
|
1567
|
+
paramCounter,
|
|
1568
|
+
scope: {
|
|
1418
1569
|
sourceVar,
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
lines.push(`}`);
|
|
1570
|
+
carryVars: allVars,
|
|
1571
|
+
withClause,
|
|
1572
|
+
varPrefix: '',
|
|
1573
|
+
depth: 0,
|
|
1574
|
+
},
|
|
1575
|
+
}));
|
|
1426
1576
|
}
|
|
1427
1577
|
}
|
|
1428
1578
|
// --- create ---
|
|
@@ -1435,15 +1585,23 @@ class MutationCompiler {
|
|
|
1435
1585
|
: [item.create];
|
|
1436
1586
|
for (let ci = 0; ci < createItems.length; ci++) {
|
|
1437
1587
|
const createSpec = createItems[ci];
|
|
1438
|
-
|
|
1588
|
+
let nodeSpec = (createSpec.node ?? createSpec);
|
|
1589
|
+
// Flat interface input: the create names its implementer.
|
|
1590
|
+
let createDef = targetNodeDef;
|
|
1591
|
+
if (abstract) {
|
|
1592
|
+
const [member, memberSpec] = unwrapAbstractCreateNode(nodeSpec, abstract, key);
|
|
1593
|
+
createDef = this.schema.nodes.get(member);
|
|
1594
|
+
nodeSpec = memberSpec;
|
|
1595
|
+
}
|
|
1596
|
+
const targetLabelStr = this.getCachedLabelString(createDef);
|
|
1439
1597
|
const createVar = `${sourceVar}_cr${varCounter}`;
|
|
1440
1598
|
varCounter++;
|
|
1441
|
-
const { propString, propParams } = this.buildCreateProperties(nodeSpec,
|
|
1599
|
+
const { propString, propParams } = this.buildCreateProperties(nodeSpec, createDef, `${itemPrefix}_create${ci}`);
|
|
1442
1600
|
// Handle nested relationship creates within the created node
|
|
1443
|
-
const nestedRelLines = this.buildCreateRelationships(nodeSpec,
|
|
1601
|
+
const nestedRelLines = this.buildCreateRelationships(nodeSpec, createDef, createVar, `${itemPrefix}_create${ci}`, params, allVars, policyContext, paramCounter);
|
|
1444
1602
|
const propsClause = propString.length > 0
|
|
1445
1603
|
? ` { ${propString} }`
|
|
1446
|
-
: ` { ${this.buildGeneratedIdClause(
|
|
1604
|
+
: ` { ${this.buildGeneratedIdClause(createDef)} }`;
|
|
1447
1605
|
// `edge` properties on the new relationship — the same input
|
|
1448
1606
|
// create() accepts. Pre-2.4.5 this path never read
|
|
1449
1607
|
// `createSpec.edge`, so they were silently dropped.
|
|
@@ -1492,6 +1650,11 @@ class MutationCompiler {
|
|
|
1492
1650
|
const nodeUpdate = (updateSpec.node ??
|
|
1493
1651
|
(edgeUpdate ? {} : updateSpec));
|
|
1494
1652
|
const updateWhere = item.where;
|
|
1653
|
+
// A flat interface update writes the interface's own fields; the
|
|
1654
|
+
// per-implementer `_on` form is not supported (v2.4.8).
|
|
1655
|
+
if (abstract && (0, validation_1.isPlainObject)(nodeUpdate) && '_on' in nodeUpdate)
|
|
1656
|
+
throw new errors_1.OGMError(`"_on" in a nested update of "${key}" is not supported. Key the update by implementing type instead ` +
|
|
1657
|
+
`(e.g. { ${key}: { ${abstract.members[0] ?? 'Member'}: [{ where, update }] } }).`);
|
|
1495
1658
|
const updateVar = `${sourceVar}_u${varCounter}`;
|
|
1496
1659
|
const relVar = `r_${key}_${i}`;
|
|
1497
1660
|
varCounter++;
|
|
@@ -1504,6 +1667,7 @@ class MutationCompiler {
|
|
|
1504
1667
|
if (val === undefined)
|
|
1505
1668
|
continue;
|
|
1506
1669
|
(0, validation_1.assertSafePropertyName)(prop, 'nested update property');
|
|
1670
|
+
assertKnownField(targetNodeDef, prop, 'nested update');
|
|
1507
1671
|
const nodePropDef = targetNodeDef.properties.get(prop);
|
|
1508
1672
|
const paramName = `${itemPrefix}_set_${prop}`;
|
|
1509
1673
|
setClauses.push(`${updateVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, nodePropDef)}`);
|
|
@@ -1542,6 +1706,7 @@ class MutationCompiler {
|
|
|
1542
1706
|
targetVar: updateVar,
|
|
1543
1707
|
edgeVar: relVar,
|
|
1544
1708
|
targetLabel: 'auto',
|
|
1709
|
+
schema: this.schema,
|
|
1545
1710
|
});
|
|
1546
1711
|
lines.push(`MATCH ${relArrow}`);
|
|
1547
1712
|
// WHERE on the target.
|
|
@@ -1725,6 +1890,114 @@ function connectionWhereToNodeWhere(spec) {
|
|
|
1725
1890
|
}
|
|
1726
1891
|
return parts.length === 1 ? parts[0] : { AND: parts };
|
|
1727
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* The concrete members of an interface / union relationship target, or
|
|
1895
|
+
* `null` for a node type. Implementer nodes carry their OWN labels, not
|
|
1896
|
+
* the interface's, so every write that has to find one matches by these
|
|
1897
|
+
* members' labels (v2.4.8).
|
|
1898
|
+
*/
|
|
1899
|
+
function abstractTarget(target, schema) {
|
|
1900
|
+
if (schema.nodes.has(target))
|
|
1901
|
+
return null;
|
|
1902
|
+
const iface = schema.interfaces?.get(target);
|
|
1903
|
+
if (iface)
|
|
1904
|
+
return {
|
|
1905
|
+
kind: 'interface',
|
|
1906
|
+
name: target,
|
|
1907
|
+
members: iface.implementedBy.filter((m) => schema.nodes.has(m)),
|
|
1908
|
+
};
|
|
1909
|
+
const union = schema.unions?.get(target);
|
|
1910
|
+
if (union)
|
|
1911
|
+
return {
|
|
1912
|
+
kind: 'union',
|
|
1913
|
+
name: target,
|
|
1914
|
+
members: union.filter((m) => schema.nodes.has(m)),
|
|
1915
|
+
};
|
|
1916
|
+
return null;
|
|
1917
|
+
}
|
|
1918
|
+
/**
|
|
1919
|
+
* Classify the nested-write value of an abstract relationship (v2.4.8):
|
|
1920
|
+
*
|
|
1921
|
+
* - PER-MEMBER — keyed by member type (`{ Dog: { … } }`), the shape
|
|
1922
|
+
* codegen emits for union AND interface targets. Returns the defined
|
|
1923
|
+
* `[member, value]` entries.
|
|
1924
|
+
* - FLAT — the operation keys themselves (`{ connect, create, … }`, or an
|
|
1925
|
+
* array of them), the @neo4j/graphql interface shape. Returns `null`.
|
|
1926
|
+
* Only interfaces accept it: a union has no common fields to filter or
|
|
1927
|
+
* write, so a flat union input throws.
|
|
1928
|
+
*
|
|
1929
|
+
* Pre-2.4.8 a key that was not a member was skipped, so a flat or typo'd
|
|
1930
|
+
* input on an abstract relationship compiled to no write at all. Mixing
|
|
1931
|
+
* member keys with other keys now throws instead.
|
|
1932
|
+
*/
|
|
1933
|
+
function perMemberEntries(value, target, fieldName) {
|
|
1934
|
+
if (Array.isArray(value) || !(0, validation_1.isPlainObject)(value)) {
|
|
1935
|
+
if (target.kind === 'union')
|
|
1936
|
+
throw new errors_1.OGMError(`Relationship "${fieldName}" targets the union "${target.name}": key its nested input by member type ` +
|
|
1937
|
+
`(${target.members.map((m) => `"${m}"`).join(', ')}).`);
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined);
|
|
1941
|
+
const memberKeys = keys.filter((k) => target.members.includes(k));
|
|
1942
|
+
if (memberKeys.length === 0 && target.kind === 'interface')
|
|
1943
|
+
return null;
|
|
1944
|
+
if (memberKeys.length !== keys.length) {
|
|
1945
|
+
const other = keys.filter((k) => !target.members.includes(k));
|
|
1946
|
+
throw new errors_1.OGMError(`Unknown key(s) ${other.map((k) => `"${k}"`).join(', ')} in the nested input of "${fieldName}" ` +
|
|
1947
|
+
`(${target.kind} "${target.name}"). ` +
|
|
1948
|
+
(target.kind === 'union'
|
|
1949
|
+
? `Key it by member type: ${target.members.map((m) => `"${m}"`).join(', ')}.`
|
|
1950
|
+
: `Use either member keys (${target.members.map((m) => `"${m}"`).join(', ')}) or the operation keys, not both.`));
|
|
1951
|
+
}
|
|
1952
|
+
return memberKeys
|
|
1953
|
+
.filter((k) => value[k] !== null)
|
|
1954
|
+
.map((k) => [k, value[k]]);
|
|
1955
|
+
}
|
|
1956
|
+
/**
|
|
1957
|
+
* Unwrap a FLAT interface nested-create node spec — `{ Dog: { … } }`,
|
|
1958
|
+
* exactly one implementer key — into the implementer and its own input
|
|
1959
|
+
* (v2.4.8). Anything else throws: without a member key the compiler cannot
|
|
1960
|
+
* know which labels and properties the new node gets.
|
|
1961
|
+
*/
|
|
1962
|
+
function unwrapAbstractCreateNode(nodeSpec, target, fieldName) {
|
|
1963
|
+
const keys = (0, validation_1.isPlainObject)(nodeSpec)
|
|
1964
|
+
? Object.keys(nodeSpec).filter((k) => nodeSpec[k] !== undefined)
|
|
1965
|
+
: [];
|
|
1966
|
+
if (!(0, validation_1.isPlainObject)(nodeSpec) ||
|
|
1967
|
+
keys.length !== 1 ||
|
|
1968
|
+
!target.members.includes(keys[0]) ||
|
|
1969
|
+
!(0, validation_1.isPlainObject)(nodeSpec[keys[0]]))
|
|
1970
|
+
throw new errors_1.OGMError(`A nested create on "${fieldName}" (${target.kind} "${target.name}") must name exactly one implementing type, ` +
|
|
1971
|
+
`e.g. { node: { ${target.members[0] ?? 'Member'}: { … } } }. Allowed: ${target.members.map((m) => `"${m}"`).join(', ')}.`);
|
|
1972
|
+
return [keys[0], nodeSpec[keys[0]]];
|
|
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
|
+
}
|
|
1986
|
+
/**
|
|
1987
|
+
* MERGE writes scalars only. Pre-2.4.8 a relationship field in an upsert's
|
|
1988
|
+
* `create` / `update` was skipped, so its nested connect / create never
|
|
1989
|
+
* ran and nothing said so.
|
|
1990
|
+
*/
|
|
1991
|
+
function upsertRelationshipError(nodeDef, key, branch) {
|
|
1992
|
+
return new errors_1.OGMError(`upsert does not support relationship fields: "${key}" in "${branch}" (${nodeDef.typeName}). ` +
|
|
1993
|
+
`Use create() / update() for nested relationship operations.`);
|
|
1994
|
+
}
|
|
1995
|
+
/** `null` / `{}`: the whole-relationship (blanket) form, never per member. */
|
|
1996
|
+
function isBlanketSpec(spec) {
|
|
1997
|
+
if (spec == null)
|
|
1998
|
+
return true;
|
|
1999
|
+
return ((0, validation_1.isPlainObject)(spec) && Object.keys(spec).every((k) => spec[k] === undefined));
|
|
2000
|
+
}
|
|
1728
2001
|
/** A nested delete item's own `delete` asks for a deeper cascade. */
|
|
1729
2002
|
function hasNestedCascade(value) {
|
|
1730
2003
|
if (value === undefined || value === null)
|