grafeo-ogm 2.5.3 → 2.6.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 +66 -0
- package/README.md +123 -0
- package/dist/cjs/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/cjs/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/fulltext.compiler.js +35 -3
- package/dist/cjs/compilers/fulltext.compiler.js.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts +39 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +165 -14
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +79 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +549 -28
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +21 -7
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +47 -13
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts +14 -10
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +25 -14
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +7 -0
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +20 -4
- package/dist/cjs/model.js.map +1 -1
- package/dist/cjs/ogm.d.ts +14 -0
- package/dist/cjs/ogm.d.ts.map +1 -1
- package/dist/cjs/ogm.js +37 -3
- package/dist/cjs/ogm.js.map +1 -1
- package/dist/cjs/utils/strict-mode.d.ts +39 -0
- package/dist/cjs/utils/strict-mode.d.ts.map +1 -0
- package/dist/cjs/utils/strict-mode.js +59 -0
- package/dist/cjs/utils/strict-mode.js.map +1 -0
- package/dist/esm/compilers/fulltext.compiler.d.ts +17 -1
- package/dist/esm/compilers/fulltext.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/fulltext.compiler.js +35 -3
- package/dist/esm/compilers/fulltext.compiler.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts +39 -0
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +165 -14
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +79 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +549 -28
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +21 -7
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +47 -13
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts +14 -10
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +25 -14
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +7 -0
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +20 -4
- package/dist/esm/model.js.map +1 -1
- package/dist/esm/ogm.d.ts +14 -0
- package/dist/esm/ogm.d.ts.map +1 -1
- package/dist/esm/ogm.js +37 -3
- package/dist/esm/ogm.js.map +1 -1
- package/dist/esm/utils/strict-mode.d.ts +39 -0
- package/dist/esm/utils/strict-mode.d.ts.map +1 -0
- package/dist/esm/utils/strict-mode.js +54 -0
- package/dist/esm/utils/strict-mode.js.map +1 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ const types_1 = require("../policy/types");
|
|
|
11
11
|
const utils_1 = require("../schema/utils");
|
|
12
12
|
const validation_1 = require("../utils/validation");
|
|
13
13
|
const write_coercion_1 = require("../utils/write-coercion");
|
|
14
|
+
const strict_mode_1 = require("../utils/strict-mode");
|
|
14
15
|
const where_compiler_1 = require("./where.compiler");
|
|
15
16
|
/** The root node `n` of an update — the pre-2.4.8 names, unprefixed. */
|
|
16
17
|
const TOP_SCOPE = {
|
|
@@ -67,6 +68,7 @@ class MutationCompiler {
|
|
|
67
68
|
this.narrowedRelDefs = new WeakSet();
|
|
68
69
|
this.logger = options?.logger;
|
|
69
70
|
this.policyWhereCompiler = options?.whereCompiler;
|
|
71
|
+
this.strictMode = options?.strictMode ?? (0, strict_mode_1.lenientStrictMode)();
|
|
70
72
|
}
|
|
71
73
|
/**
|
|
72
74
|
* Resolve and compile the TARGET type's `read` policy predicate for a
|
|
@@ -117,9 +119,10 @@ class MutationCompiler {
|
|
|
117
119
|
reason: 'no-permissive-matched',
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
|
-
// Abstract (interface / union) targets get a
|
|
121
|
-
// members' own clauses —
|
|
122
|
-
//
|
|
122
|
+
// Abstract (interface / union) targets get a conjunction over their
|
|
123
|
+
// concrete members' own clauses — every member label the node carries
|
|
124
|
+
// must pass (v2.5.4; pre-2.5.4 a first-match CASE) — pre-2.3.0 only
|
|
125
|
+
// the abstract type's own policies (usually none) were applied.
|
|
123
126
|
const clause = this.getPolicyWhereCompiler().compileTargetPolicyClause(targetNodeDef.typeName, targetVar, op, policyContext, counter);
|
|
124
127
|
if (clause.preludes.length > 0)
|
|
125
128
|
throw new errors_1.OGMError(`Policy on "${targetNodeDef.typeName}" requires @cypher field ` +
|
|
@@ -284,6 +287,68 @@ class MutationCompiler {
|
|
|
284
287
|
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
|
|
285
288
|
return targetNodeDef;
|
|
286
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* v2.6.0 — keys of a connect / disconnect item that the compiler does not
|
|
292
|
+
* read. Pre-2.6.0 (and still by default) they are ignored, so a typo such
|
|
293
|
+
* as `{ wehre: … }` compiles to an item with NO filter: a connect links
|
|
294
|
+
* every target node, a disconnect unlinks every related one. Default: one
|
|
295
|
+
* warning per OGM; strict: `OGMError`, like an unknown write field.
|
|
296
|
+
*/
|
|
297
|
+
checkLinkItemKeys(item, kind, relDef) {
|
|
298
|
+
const allowed = kind === 'connect' ? CONNECT_ITEM_KEYS : DISCONNECT_ITEM_KEYS;
|
|
299
|
+
const unknown = Object.keys(item).filter((k) => item[k] !== undefined && !allowed.has(k));
|
|
300
|
+
if (unknown.length === 0)
|
|
301
|
+
return;
|
|
302
|
+
const list = unknown.map((k) => `"${k}"`).join(', ');
|
|
303
|
+
const allowedList = [...allowed].map((k) => `"${k}"`).join(', ');
|
|
304
|
+
if (this.strictMode.strict)
|
|
305
|
+
throw new errors_1.OGMError(`Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}". Allowed: ${allowedList}.`);
|
|
306
|
+
const unfiltered = item.where == null;
|
|
307
|
+
this.strictMode.warn(`${kind}-unknown-key`, `${relDef.fieldName}\u0000${unknown.join(',')}`, `Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}" ` +
|
|
308
|
+
`${unknown.length > 1 ? 'are' : 'is'} ignored` +
|
|
309
|
+
(unfiltered
|
|
310
|
+
? `; with no "where", the item ${kind === 'connect'
|
|
311
|
+
? `connects EVERY "${relDef.target}" node`
|
|
312
|
+
: `unlinks EVERY related "${relDef.target}" node`}.`
|
|
313
|
+
: '.') +
|
|
314
|
+
` Allowed: ${allowedList}.`, `throw an OGMError for unknown ${kind} item keys`);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* v2.6.0 — `labels` on a MERGE path (upsert, createMany with
|
|
318
|
+
* skipDuplicates) are SET after the MERGE, so they are also added to an
|
|
319
|
+
* EXISTING node it matched. Default: unchanged, one warning; strict: the
|
|
320
|
+
* labels go in `ON CREATE SET`, onto newly created nodes only.
|
|
321
|
+
*/
|
|
322
|
+
labelsOnMatchedNodes(nodeDef, op) {
|
|
323
|
+
const call = op === 'upsert' ? 'upsert' : 'createMany with skipDuplicates';
|
|
324
|
+
this.strictMode.warn('labels-on-matched-nodes', `${nodeDef.typeName}\u0000${op}`, `"labels" on ${call} (${nodeDef.typeName}) are added to every node the MERGE returns, so an existing node ` +
|
|
325
|
+
`it matched is relabelled too.`, 'apply "labels" only to the nodes it creates');
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* v2.6.0 — `edge` keys that are not properties of the relationship's
|
|
329
|
+
* `@relationshipProperties` type. Pre-2.6.0 (and still by default) each
|
|
330
|
+
* is written to the relationship as a stray property. Default: one
|
|
331
|
+
* warning per OGM; strict: `OGMError` (`Unknown field …`).
|
|
332
|
+
*/
|
|
333
|
+
checkEdgeKeys(relDef, edge, input) {
|
|
334
|
+
if (!(0, validation_1.isPlainObject)(edge))
|
|
335
|
+
return;
|
|
336
|
+
const declared = relDef.properties
|
|
337
|
+
? this.schema.relationshipProperties.get(relDef.properties)?.properties
|
|
338
|
+
: undefined;
|
|
339
|
+
for (const key of Object.keys(edge)) {
|
|
340
|
+
if (edge[key] === undefined || declared?.has(key))
|
|
341
|
+
continue;
|
|
342
|
+
const owner = relDef.properties
|
|
343
|
+
? `the relationship properties type "${relDef.properties}"`
|
|
344
|
+
: `the relationship, which declares no @relationshipProperties`;
|
|
345
|
+
if (this.strictMode.strict)
|
|
346
|
+
throw new errors_1.OGMError(`Unknown field "${key}" in the edge input (${input}) of "${relDef.fieldName}": it is not a property of ` +
|
|
347
|
+
`${owner}. Declare it in the schema, or fix the key.`);
|
|
348
|
+
this.strictMode.warn('edge-unknown-key', `${relDef.fieldName}\u0000${key}`, `Unknown field "${key}" in the edge input of "${relDef.fieldName}" is not a property of ${owner}; ` +
|
|
349
|
+
`it is written to the relationship as a stray property.`, `throw an OGMError for unknown edge fields`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
287
352
|
/**
|
|
288
353
|
* Generate CREATE Cypher for one or more nodes.
|
|
289
354
|
* Handles scalar properties, nested relationship creates, and connects.
|
|
@@ -577,6 +642,19 @@ class MutationCompiler {
|
|
|
577
642
|
if (mergeProps.length === 0)
|
|
578
643
|
throw new errors_1.OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
|
|
579
644
|
`an @id or @unique field of ${nodeDef.typeName}.`);
|
|
645
|
+
// v2.6.0 — MERGE matches ALL its keys together, so `{ id, email }`
|
|
646
|
+
// misses a node that has the email under another id and creates a
|
|
647
|
+
// duplicate beside it (or fails on the unique constraint). Default:
|
|
648
|
+
// compiled as before, with one warning; strict: exactly one key.
|
|
649
|
+
if (mergeKeyNames.length > 1) {
|
|
650
|
+
const keys = mergeKeyNames.map((k) => `"${k}"`).join(', ');
|
|
651
|
+
if (this.strictMode.strict)
|
|
652
|
+
throw new errors_1.OGMError(`upsert "where" must name exactly one @id / @unique field of ${nodeDef.typeName}; got ${keys}. ` +
|
|
653
|
+
`MERGE matches all of them together, so a node that has one but not the others is not found ` +
|
|
654
|
+
`and a duplicate is created. Pass a single unique key.`);
|
|
655
|
+
this.strictMode.warn('upsert-multiple-unique-keys', `${nodeDef.typeName}\u0000${mergeKeyNames.join(',')}`, `upsert on ${nodeDef.typeName} with several unique keys in "where" (${keys}) MERGEs on all of them ` +
|
|
656
|
+
`together: a node that has one but not the others is not matched and a duplicate is created.`, 'require exactly one unique key in an upsert "where"');
|
|
657
|
+
}
|
|
580
658
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
|
|
581
659
|
// ON CREATE SET — all properties from create input (scalar only)
|
|
582
660
|
const createSets = [];
|
|
@@ -627,6 +705,11 @@ class MutationCompiler {
|
|
|
627
705
|
continue;
|
|
628
706
|
createSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = randomUUID()`);
|
|
629
707
|
}
|
|
708
|
+
// v2.6.0 — strict: runtime labels go on the node only when MERGE creates
|
|
709
|
+
// it (see `labelsOnMatchedNodes`).
|
|
710
|
+
const labelsOnCreate = this.strictMode.strict && labels && labels.length > 0;
|
|
711
|
+
if (labelsOnCreate)
|
|
712
|
+
createSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
|
|
630
713
|
if (createSets.length > 0)
|
|
631
714
|
lines.push(`ON CREATE SET ${createSets.join(', ')}`);
|
|
632
715
|
// ON MATCH SET — all properties from update input (scalar only)
|
|
@@ -650,9 +733,10 @@ class MutationCompiler {
|
|
|
650
733
|
if (updateSets.length > 0)
|
|
651
734
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
652
735
|
// Extra labels
|
|
653
|
-
if (labels && labels.length > 0) {
|
|
736
|
+
if (labels && labels.length > 0 && !labelsOnCreate) {
|
|
654
737
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
655
738
|
lines.push(`SET n:${validatedLabels}`);
|
|
739
|
+
this.labelsOnMatchedNodes(nodeDef, 'upsert');
|
|
656
740
|
}
|
|
657
741
|
lines.push('RETURN n');
|
|
658
742
|
return { cypher: lines.join('\n'), params };
|
|
@@ -802,6 +886,9 @@ class MutationCompiler {
|
|
|
802
886
|
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
|
|
803
887
|
params[paramName] = bound;
|
|
804
888
|
}
|
|
889
|
+
// v2.6.0 — strict: labels only on the nodes MERGE creates.
|
|
890
|
+
if (this.strictMode.strict && labels && labels.length > 0)
|
|
891
|
+
onCreateSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
|
|
805
892
|
if (onCreateSets.length > 0)
|
|
806
893
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
807
894
|
}
|
|
@@ -821,9 +908,13 @@ class MutationCompiler {
|
|
|
821
908
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
822
909
|
}
|
|
823
910
|
// Extra labels
|
|
824
|
-
if (labels &&
|
|
911
|
+
if (labels &&
|
|
912
|
+
labels.length > 0 &&
|
|
913
|
+
!(skipDuplicates && this.strictMode.strict)) {
|
|
825
914
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
826
915
|
lines.push(`SET n:${validatedLabels}`);
|
|
916
|
+
if (skipDuplicates)
|
|
917
|
+
this.labelsOnMatchedNodes(nodeDef, 'createMany');
|
|
827
918
|
}
|
|
828
919
|
lines.push('RETURN count(n) AS count');
|
|
829
920
|
return { cypher: lines.join('\n'), params };
|
|
@@ -973,6 +1064,7 @@ class MutationCompiler {
|
|
|
973
1064
|
(0, validation_1.mergeParams)(params, propParams);
|
|
974
1065
|
lines.push(`CREATE ${relPattern}`);
|
|
975
1066
|
// Set edge properties if present
|
|
1067
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
976
1068
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
977
1069
|
const relVar = `r_edge_${startCounter + counter - 1}`;
|
|
978
1070
|
// Re-match the relationship to set edge props
|
|
@@ -1111,6 +1203,11 @@ class MutationCompiler {
|
|
|
1111
1203
|
scope,
|
|
1112
1204
|
}));
|
|
1113
1205
|
else {
|
|
1206
|
+
// Every item is a plain object here (fast-path eligibility).
|
|
1207
|
+
for (const item of spec) {
|
|
1208
|
+
this.checkLinkItemKeys(item, 'connect', relDef);
|
|
1209
|
+
this.checkEdgeKeys(relDef, item.edge, 'connect');
|
|
1210
|
+
}
|
|
1114
1211
|
const targetVar = `${varPrefix}target`;
|
|
1115
1212
|
const relVar = `${varPrefix}r`;
|
|
1116
1213
|
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
@@ -1237,6 +1334,8 @@ class MutationCompiler {
|
|
|
1237
1334
|
// compiling it would MATCH, and link, every target node.
|
|
1238
1335
|
if (!(0, validation_1.isPlainObject)(connectSpec))
|
|
1239
1336
|
throw new errors_1.OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
|
|
1337
|
+
this.checkLinkItemKeys(connectSpec, 'connect', relDef);
|
|
1338
|
+
this.checkEdgeKeys(relDef, connectSpec.edge, 'connect');
|
|
1240
1339
|
const lines = [];
|
|
1241
1340
|
const whereSpec = connectSpec.where;
|
|
1242
1341
|
const edgeInput = connectSpec.edge;
|
|
@@ -1378,21 +1477,25 @@ class MutationCompiler {
|
|
|
1378
1477
|
const predicate = targetNodeDef
|
|
1379
1478
|
? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
|
|
1380
1479
|
: undefined;
|
|
1480
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1381
1481
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
1382
1482
|
sourceVar,
|
|
1383
1483
|
relDef,
|
|
1384
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1484
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1385
1485
|
edgeVar: relVar,
|
|
1386
|
-
targetLabelRaw:
|
|
1486
|
+
targetLabelRaw: labelRaw,
|
|
1387
1487
|
});
|
|
1388
1488
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1389
|
-
|
|
1390
|
-
|
|
1489
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1490
|
+
if (blanketConditions.length > 0)
|
|
1491
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1391
1492
|
lines.push(`DELETE ${relVar}`);
|
|
1392
1493
|
}
|
|
1393
1494
|
else {
|
|
1394
1495
|
// Specific disconnect with conditions
|
|
1395
1496
|
const disconnectSpec = item;
|
|
1497
|
+
if ((0, validation_1.isPlainObject)(disconnectSpec))
|
|
1498
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1396
1499
|
const whereSpec = disconnectSpec.where;
|
|
1397
1500
|
const targetVar = `${varPrefix}target_${tag}_${si}`;
|
|
1398
1501
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
@@ -1431,6 +1534,33 @@ class MutationCompiler {
|
|
|
1431
1534
|
this.narrowedRelDefs.add(narrowed);
|
|
1432
1535
|
return narrowed;
|
|
1433
1536
|
}
|
|
1537
|
+
/**
|
|
1538
|
+
* v2.6.0 — how a blanket disconnect (`null` / `{}`) restricts its target.
|
|
1539
|
+
*
|
|
1540
|
+
* Default (v2.5.3): only a relationship narrowed to a union / interface
|
|
1541
|
+
* member keeps that member's label; otherwise the target is anonymous,
|
|
1542
|
+
* so with two fields on one relationship type and direction
|
|
1543
|
+
* (`pets: [Pet]`, `toys: [Toy]`, both `HAS`) a blanket disconnect of one
|
|
1544
|
+
* also unlinks the other's nodes (warned once at OGM construction).
|
|
1545
|
+
*
|
|
1546
|
+
* Strict: the field's own target type — its label, or for an abstract
|
|
1547
|
+
* target a predicate over its members' labels on `targetVar`.
|
|
1548
|
+
*/
|
|
1549
|
+
blanketRestriction(relDef, targetNodeDef, targetVar) {
|
|
1550
|
+
const narrowed = this.blanketTargetLabel(relDef);
|
|
1551
|
+
if (narrowed || !this.strictMode.strict || !targetNodeDef)
|
|
1552
|
+
return { labelRaw: narrowed, labelPredicate: null };
|
|
1553
|
+
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1554
|
+
return {
|
|
1555
|
+
labelRaw: undefined,
|
|
1556
|
+
labelPredicate: this.targetMatch(targetNodeDef, targetVar)
|
|
1557
|
+
.labelPredicate,
|
|
1558
|
+
};
|
|
1559
|
+
return {
|
|
1560
|
+
labelRaw: this.getCachedLabelString(targetNodeDef),
|
|
1561
|
+
labelPredicate: null,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1434
1564
|
/** The label a blanket disconnect over `relDef` must keep (see above). */
|
|
1435
1565
|
blanketTargetLabel(relDef) {
|
|
1436
1566
|
if (!this.narrowedRelDefs.has(relDef))
|
|
@@ -1521,6 +1651,8 @@ class MutationCompiler {
|
|
|
1521
1651
|
: [item.disconnect];
|
|
1522
1652
|
for (let di = 0; di < disconnectItems.length; di++) {
|
|
1523
1653
|
const disconnectSpec = disconnectItems[di];
|
|
1654
|
+
if ((0, validation_1.isPlainObject)(disconnectSpec))
|
|
1655
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1524
1656
|
const whereSpec = disconnectSpec.where;
|
|
1525
1657
|
// A nested `disconnect` needs the target bound, so an item that
|
|
1526
1658
|
// carries one is never the anonymous blanket form.
|
|
@@ -1537,18 +1669,20 @@ class MutationCompiler {
|
|
|
1537
1669
|
// disconnect: only unlink targets the caller can read.
|
|
1538
1670
|
const blanketTarget = `${sourceVar}_disc${varCounter}`;
|
|
1539
1671
|
const predicate = this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter);
|
|
1540
|
-
|
|
1672
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1673
|
+
if (predicate || labelPredicate)
|
|
1541
1674
|
varCounter++;
|
|
1542
1675
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
1543
1676
|
sourceVar,
|
|
1544
1677
|
relDef,
|
|
1545
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1678
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1546
1679
|
edgeVar: relVar,
|
|
1547
|
-
targetLabelRaw:
|
|
1680
|
+
targetLabelRaw: labelRaw,
|
|
1548
1681
|
});
|
|
1549
1682
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1550
|
-
|
|
1551
|
-
|
|
1683
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1684
|
+
if (blanketConditions.length > 0)
|
|
1685
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1552
1686
|
lines.push(`DELETE ${relVar}`);
|
|
1553
1687
|
}
|
|
1554
1688
|
else {
|
|
@@ -1656,6 +1790,7 @@ class MutationCompiler {
|
|
|
1656
1790
|
// create() accepts. Pre-2.4.5 this path never read
|
|
1657
1791
|
// `createSpec.edge`, so they were silently dropped.
|
|
1658
1792
|
const edgeInput = createSpec.edge;
|
|
1793
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
1659
1794
|
const edgeEntries = Object.entries(edgeInput ?? {}).filter(([, v]) => v !== undefined);
|
|
1660
1795
|
const edgeVar = edgeEntries.length ? `${createVar}_e` : undefined;
|
|
1661
1796
|
const relPattern = (0, utils_1.buildRelPattern)({
|
|
@@ -1726,6 +1861,7 @@ class MutationCompiler {
|
|
|
1726
1861
|
// Pre-compute SET clauses for edge (relationship) properties
|
|
1727
1862
|
const edgeClauses = [];
|
|
1728
1863
|
const edgeParams = {};
|
|
1864
|
+
this.checkEdgeKeys(relDef, edgeUpdate, 'nested update');
|
|
1729
1865
|
if (edgeUpdate)
|
|
1730
1866
|
for (const [prop, val] of Object.entries(edgeUpdate)) {
|
|
1731
1867
|
if (val === undefined)
|
|
@@ -1874,6 +2010,21 @@ MutationCompiler.OPERATOR_SUFFIXES = [
|
|
|
1874
2010
|
* path to validate that every item shares the same shape; mismatched
|
|
1875
2011
|
* shapes silently drop keys from the compiled WHERE / SET.
|
|
1876
2012
|
*/
|
|
2013
|
+
/**
|
|
2014
|
+
* v2.6.0 — the keys a connect / disconnect item is compiled from. `overwrite`
|
|
2015
|
+
* is offered by the generated `ConnectFieldInput` and accepted as before
|
|
2016
|
+
* (MERGE + SET always overwrites the edge properties it is given).
|
|
2017
|
+
*/
|
|
2018
|
+
const CONNECT_ITEM_KEYS = new Set([
|
|
2019
|
+
'where',
|
|
2020
|
+
'edge',
|
|
2021
|
+
'overwrite',
|
|
2022
|
+
'connect',
|
|
2023
|
+
]);
|
|
2024
|
+
const DISCONNECT_ITEM_KEYS = new Set([
|
|
2025
|
+
'where',
|
|
2026
|
+
'disconnect',
|
|
2027
|
+
]);
|
|
1877
2028
|
/** Keys that make a nested-mutation `where` connection-shaped. */
|
|
1878
2029
|
const CONNECTION_WHERE_KEYS = new Set([
|
|
1879
2030
|
'node',
|