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
|
@@ -4,6 +4,7 @@ import { NO_ROOT_POLICY } from '../policy/types';
|
|
|
4
4
|
import { buildRelPattern, getTargetLabelString, resolveTargetDef, } from '../schema/utils';
|
|
5
5
|
import { assertSafeIdentifier, assertSafeLabel, assertSafePropertyName, escapeIdentifier, isPlainObject, mergeParams, } from '../utils/validation';
|
|
6
6
|
import { coerceWriteValue, resolveDefaultWriteValue, wrapListItemExpr, wrapWhereParam, wrapWriteExpr, } from '../utils/write-coercion';
|
|
7
|
+
import { lenientStrictMode } from '../utils/strict-mode';
|
|
7
8
|
import { WhereCompiler } from './where.compiler';
|
|
8
9
|
/** The root node `n` of an update — the pre-2.4.8 names, unprefixed. */
|
|
9
10
|
const TOP_SCOPE = {
|
|
@@ -60,6 +61,7 @@ export class MutationCompiler {
|
|
|
60
61
|
this.narrowedRelDefs = new WeakSet();
|
|
61
62
|
this.logger = options?.logger;
|
|
62
63
|
this.policyWhereCompiler = options?.whereCompiler;
|
|
64
|
+
this.strictMode = options?.strictMode ?? lenientStrictMode();
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
67
|
* Resolve and compile the TARGET type's `read` policy predicate for a
|
|
@@ -110,9 +112,10 @@ export class MutationCompiler {
|
|
|
110
112
|
reason: 'no-permissive-matched',
|
|
111
113
|
});
|
|
112
114
|
}
|
|
113
|
-
// Abstract (interface / union) targets get a
|
|
114
|
-
// members' own clauses —
|
|
115
|
-
//
|
|
115
|
+
// Abstract (interface / union) targets get a conjunction over their
|
|
116
|
+
// concrete members' own clauses — every member label the node carries
|
|
117
|
+
// must pass (v2.5.4; pre-2.5.4 a first-match CASE) — pre-2.3.0 only
|
|
118
|
+
// the abstract type's own policies (usually none) were applied.
|
|
116
119
|
const clause = this.getPolicyWhereCompiler().compileTargetPolicyClause(targetNodeDef.typeName, targetVar, op, policyContext, counter);
|
|
117
120
|
if (clause.preludes.length > 0)
|
|
118
121
|
throw new OGMError(`Policy on "${targetNodeDef.typeName}" requires @cypher field ` +
|
|
@@ -277,6 +280,68 @@ export class MutationCompiler {
|
|
|
277
280
|
throw new OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
|
|
278
281
|
return targetNodeDef;
|
|
279
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* v2.6.0 — keys of a connect / disconnect item that the compiler does not
|
|
285
|
+
* read. Pre-2.6.0 (and still by default) they are ignored, so a typo such
|
|
286
|
+
* as `{ wehre: … }` compiles to an item with NO filter: a connect links
|
|
287
|
+
* every target node, a disconnect unlinks every related one. Default: one
|
|
288
|
+
* warning per OGM; strict: `OGMError`, like an unknown write field.
|
|
289
|
+
*/
|
|
290
|
+
checkLinkItemKeys(item, kind, relDef) {
|
|
291
|
+
const allowed = kind === 'connect' ? CONNECT_ITEM_KEYS : DISCONNECT_ITEM_KEYS;
|
|
292
|
+
const unknown = Object.keys(item).filter((k) => item[k] !== undefined && !allowed.has(k));
|
|
293
|
+
if (unknown.length === 0)
|
|
294
|
+
return;
|
|
295
|
+
const list = unknown.map((k) => `"${k}"`).join(', ');
|
|
296
|
+
const allowedList = [...allowed].map((k) => `"${k}"`).join(', ');
|
|
297
|
+
if (this.strictMode.strict)
|
|
298
|
+
throw new OGMError(`Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}". Allowed: ${allowedList}.`);
|
|
299
|
+
const unfiltered = item.where == null;
|
|
300
|
+
this.strictMode.warn(`${kind}-unknown-key`, `${relDef.fieldName}\u0000${unknown.join(',')}`, `Unknown key(s) ${list} in a ${kind} item on "${relDef.fieldName}" ` +
|
|
301
|
+
`${unknown.length > 1 ? 'are' : 'is'} ignored` +
|
|
302
|
+
(unfiltered
|
|
303
|
+
? `; with no "where", the item ${kind === 'connect'
|
|
304
|
+
? `connects EVERY "${relDef.target}" node`
|
|
305
|
+
: `unlinks EVERY related "${relDef.target}" node`}.`
|
|
306
|
+
: '.') +
|
|
307
|
+
` Allowed: ${allowedList}.`, `throw an OGMError for unknown ${kind} item keys`);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* v2.6.0 — `labels` on a MERGE path (upsert, createMany with
|
|
311
|
+
* skipDuplicates) are SET after the MERGE, so they are also added to an
|
|
312
|
+
* EXISTING node it matched. Default: unchanged, one warning; strict: the
|
|
313
|
+
* labels go in `ON CREATE SET`, onto newly created nodes only.
|
|
314
|
+
*/
|
|
315
|
+
labelsOnMatchedNodes(nodeDef, op) {
|
|
316
|
+
const call = op === 'upsert' ? 'upsert' : 'createMany with skipDuplicates';
|
|
317
|
+
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 ` +
|
|
318
|
+
`it matched is relabelled too.`, 'apply "labels" only to the nodes it creates');
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* v2.6.0 — `edge` keys that are not properties of the relationship's
|
|
322
|
+
* `@relationshipProperties` type. Pre-2.6.0 (and still by default) each
|
|
323
|
+
* is written to the relationship as a stray property. Default: one
|
|
324
|
+
* warning per OGM; strict: `OGMError` (`Unknown field …`).
|
|
325
|
+
*/
|
|
326
|
+
checkEdgeKeys(relDef, edge, input) {
|
|
327
|
+
if (!isPlainObject(edge))
|
|
328
|
+
return;
|
|
329
|
+
const declared = relDef.properties
|
|
330
|
+
? this.schema.relationshipProperties.get(relDef.properties)?.properties
|
|
331
|
+
: undefined;
|
|
332
|
+
for (const key of Object.keys(edge)) {
|
|
333
|
+
if (edge[key] === undefined || declared?.has(key))
|
|
334
|
+
continue;
|
|
335
|
+
const owner = relDef.properties
|
|
336
|
+
? `the relationship properties type "${relDef.properties}"`
|
|
337
|
+
: `the relationship, which declares no @relationshipProperties`;
|
|
338
|
+
if (this.strictMode.strict)
|
|
339
|
+
throw new OGMError(`Unknown field "${key}" in the edge input (${input}) of "${relDef.fieldName}": it is not a property of ` +
|
|
340
|
+
`${owner}. Declare it in the schema, or fix the key.`);
|
|
341
|
+
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}; ` +
|
|
342
|
+
`it is written to the relationship as a stray property.`, `throw an OGMError for unknown edge fields`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
280
345
|
/**
|
|
281
346
|
* Generate CREATE Cypher for one or more nodes.
|
|
282
347
|
* Handles scalar properties, nested relationship creates, and connects.
|
|
@@ -570,6 +635,19 @@ export class MutationCompiler {
|
|
|
570
635
|
if (mergeProps.length === 0)
|
|
571
636
|
throw new OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
|
|
572
637
|
`an @id or @unique field of ${nodeDef.typeName}.`);
|
|
638
|
+
// v2.6.0 — MERGE matches ALL its keys together, so `{ id, email }`
|
|
639
|
+
// misses a node that has the email under another id and creates a
|
|
640
|
+
// duplicate beside it (or fails on the unique constraint). Default:
|
|
641
|
+
// compiled as before, with one warning; strict: exactly one key.
|
|
642
|
+
if (mergeKeyNames.length > 1) {
|
|
643
|
+
const keys = mergeKeyNames.map((k) => `"${k}"`).join(', ');
|
|
644
|
+
if (this.strictMode.strict)
|
|
645
|
+
throw new OGMError(`upsert "where" must name exactly one @id / @unique field of ${nodeDef.typeName}; got ${keys}. ` +
|
|
646
|
+
`MERGE matches all of them together, so a node that has one but not the others is not found ` +
|
|
647
|
+
`and a duplicate is created. Pass a single unique key.`);
|
|
648
|
+
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 ` +
|
|
649
|
+
`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"');
|
|
650
|
+
}
|
|
573
651
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
|
|
574
652
|
// ON CREATE SET — all properties from create input (scalar only)
|
|
575
653
|
const createSets = [];
|
|
@@ -620,6 +698,11 @@ export class MutationCompiler {
|
|
|
620
698
|
continue;
|
|
621
699
|
createSets.push(`n.${escapeIdentifier(propDef.name)} = randomUUID()`);
|
|
622
700
|
}
|
|
701
|
+
// v2.6.0 — strict: runtime labels go on the node only when MERGE creates
|
|
702
|
+
// it (see `labelsOnMatchedNodes`).
|
|
703
|
+
const labelsOnCreate = this.strictMode.strict && labels && labels.length > 0;
|
|
704
|
+
if (labelsOnCreate)
|
|
705
|
+
createSets.push(`n:${labels.map((l) => assertSafeLabel(l)).join(':')}`);
|
|
623
706
|
if (createSets.length > 0)
|
|
624
707
|
lines.push(`ON CREATE SET ${createSets.join(', ')}`);
|
|
625
708
|
// ON MATCH SET — all properties from update input (scalar only)
|
|
@@ -643,9 +726,10 @@ export class MutationCompiler {
|
|
|
643
726
|
if (updateSets.length > 0)
|
|
644
727
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
645
728
|
// Extra labels
|
|
646
|
-
if (labels && labels.length > 0) {
|
|
729
|
+
if (labels && labels.length > 0 && !labelsOnCreate) {
|
|
647
730
|
const validatedLabels = labels.map((l) => assertSafeLabel(l)).join(':');
|
|
648
731
|
lines.push(`SET n:${validatedLabels}`);
|
|
732
|
+
this.labelsOnMatchedNodes(nodeDef, 'upsert');
|
|
649
733
|
}
|
|
650
734
|
lines.push('RETURN n');
|
|
651
735
|
return { cypher: lines.join('\n'), params };
|
|
@@ -795,6 +879,9 @@ export class MutationCompiler {
|
|
|
795
879
|
onCreateSets.push(`n.${escapeIdentifier(propDef.name)} = ${wrapWriteExpr(`$${paramName}`, bound, propDef)}`);
|
|
796
880
|
params[paramName] = bound;
|
|
797
881
|
}
|
|
882
|
+
// v2.6.0 — strict: labels only on the nodes MERGE creates.
|
|
883
|
+
if (this.strictMode.strict && labels && labels.length > 0)
|
|
884
|
+
onCreateSets.push(`n:${labels.map((l) => assertSafeLabel(l)).join(':')}`);
|
|
798
885
|
if (onCreateSets.length > 0)
|
|
799
886
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
800
887
|
}
|
|
@@ -814,9 +901,13 @@ export class MutationCompiler {
|
|
|
814
901
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
815
902
|
}
|
|
816
903
|
// Extra labels
|
|
817
|
-
if (labels &&
|
|
904
|
+
if (labels &&
|
|
905
|
+
labels.length > 0 &&
|
|
906
|
+
!(skipDuplicates && this.strictMode.strict)) {
|
|
818
907
|
const validatedLabels = labels.map((l) => assertSafeLabel(l)).join(':');
|
|
819
908
|
lines.push(`SET n:${validatedLabels}`);
|
|
909
|
+
if (skipDuplicates)
|
|
910
|
+
this.labelsOnMatchedNodes(nodeDef, 'createMany');
|
|
820
911
|
}
|
|
821
912
|
lines.push('RETURN count(n) AS count');
|
|
822
913
|
return { cypher: lines.join('\n'), params };
|
|
@@ -966,6 +1057,7 @@ export class MutationCompiler {
|
|
|
966
1057
|
mergeParams(params, propParams);
|
|
967
1058
|
lines.push(`CREATE ${relPattern}`);
|
|
968
1059
|
// Set edge properties if present
|
|
1060
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
969
1061
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
970
1062
|
const relVar = `r_edge_${startCounter + counter - 1}`;
|
|
971
1063
|
// Re-match the relationship to set edge props
|
|
@@ -1104,6 +1196,11 @@ export class MutationCompiler {
|
|
|
1104
1196
|
scope,
|
|
1105
1197
|
}));
|
|
1106
1198
|
else {
|
|
1199
|
+
// Every item is a plain object here (fast-path eligibility).
|
|
1200
|
+
for (const item of spec) {
|
|
1201
|
+
this.checkLinkItemKeys(item, 'connect', relDef);
|
|
1202
|
+
this.checkEdgeKeys(relDef, item.edge, 'connect');
|
|
1203
|
+
}
|
|
1107
1204
|
const targetVar = `${varPrefix}target`;
|
|
1108
1205
|
const relVar = `${varPrefix}r`;
|
|
1109
1206
|
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
@@ -1230,6 +1327,8 @@ export class MutationCompiler {
|
|
|
1230
1327
|
// compiling it would MATCH, and link, every target node.
|
|
1231
1328
|
if (!isPlainObject(connectSpec))
|
|
1232
1329
|
throw new OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
|
|
1330
|
+
this.checkLinkItemKeys(connectSpec, 'connect', relDef);
|
|
1331
|
+
this.checkEdgeKeys(relDef, connectSpec.edge, 'connect');
|
|
1233
1332
|
const lines = [];
|
|
1234
1333
|
const whereSpec = connectSpec.where;
|
|
1235
1334
|
const edgeInput = connectSpec.edge;
|
|
@@ -1371,21 +1470,25 @@ export class MutationCompiler {
|
|
|
1371
1470
|
const predicate = targetNodeDef
|
|
1372
1471
|
? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
|
|
1373
1472
|
: undefined;
|
|
1473
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1374
1474
|
const pattern = buildRelPattern({
|
|
1375
1475
|
sourceVar,
|
|
1376
1476
|
relDef,
|
|
1377
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1477
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1378
1478
|
edgeVar: relVar,
|
|
1379
|
-
targetLabelRaw:
|
|
1479
|
+
targetLabelRaw: labelRaw,
|
|
1380
1480
|
});
|
|
1381
1481
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1382
|
-
|
|
1383
|
-
|
|
1482
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1483
|
+
if (blanketConditions.length > 0)
|
|
1484
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1384
1485
|
lines.push(`DELETE ${relVar}`);
|
|
1385
1486
|
}
|
|
1386
1487
|
else {
|
|
1387
1488
|
// Specific disconnect with conditions
|
|
1388
1489
|
const disconnectSpec = item;
|
|
1490
|
+
if (isPlainObject(disconnectSpec))
|
|
1491
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1389
1492
|
const whereSpec = disconnectSpec.where;
|
|
1390
1493
|
const targetVar = `${varPrefix}target_${tag}_${si}`;
|
|
1391
1494
|
const pattern = buildRelPattern({
|
|
@@ -1424,6 +1527,33 @@ export class MutationCompiler {
|
|
|
1424
1527
|
this.narrowedRelDefs.add(narrowed);
|
|
1425
1528
|
return narrowed;
|
|
1426
1529
|
}
|
|
1530
|
+
/**
|
|
1531
|
+
* v2.6.0 — how a blanket disconnect (`null` / `{}`) restricts its target.
|
|
1532
|
+
*
|
|
1533
|
+
* Default (v2.5.3): only a relationship narrowed to a union / interface
|
|
1534
|
+
* member keeps that member's label; otherwise the target is anonymous,
|
|
1535
|
+
* so with two fields on one relationship type and direction
|
|
1536
|
+
* (`pets: [Pet]`, `toys: [Toy]`, both `HAS`) a blanket disconnect of one
|
|
1537
|
+
* also unlinks the other's nodes (warned once at OGM construction).
|
|
1538
|
+
*
|
|
1539
|
+
* Strict: the field's own target type — its label, or for an abstract
|
|
1540
|
+
* target a predicate over its members' labels on `targetVar`.
|
|
1541
|
+
*/
|
|
1542
|
+
blanketRestriction(relDef, targetNodeDef, targetVar) {
|
|
1543
|
+
const narrowed = this.blanketTargetLabel(relDef);
|
|
1544
|
+
if (narrowed || !this.strictMode.strict || !targetNodeDef)
|
|
1545
|
+
return { labelRaw: narrowed, labelPredicate: null };
|
|
1546
|
+
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1547
|
+
return {
|
|
1548
|
+
labelRaw: undefined,
|
|
1549
|
+
labelPredicate: this.targetMatch(targetNodeDef, targetVar)
|
|
1550
|
+
.labelPredicate,
|
|
1551
|
+
};
|
|
1552
|
+
return {
|
|
1553
|
+
labelRaw: this.getCachedLabelString(targetNodeDef),
|
|
1554
|
+
labelPredicate: null,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1427
1557
|
/** The label a blanket disconnect over `relDef` must keep (see above). */
|
|
1428
1558
|
blanketTargetLabel(relDef) {
|
|
1429
1559
|
if (!this.narrowedRelDefs.has(relDef))
|
|
@@ -1514,6 +1644,8 @@ export class MutationCompiler {
|
|
|
1514
1644
|
: [item.disconnect];
|
|
1515
1645
|
for (let di = 0; di < disconnectItems.length; di++) {
|
|
1516
1646
|
const disconnectSpec = disconnectItems[di];
|
|
1647
|
+
if (isPlainObject(disconnectSpec))
|
|
1648
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1517
1649
|
const whereSpec = disconnectSpec.where;
|
|
1518
1650
|
// A nested `disconnect` needs the target bound, so an item that
|
|
1519
1651
|
// carries one is never the anonymous blanket form.
|
|
@@ -1530,18 +1662,20 @@ export class MutationCompiler {
|
|
|
1530
1662
|
// disconnect: only unlink targets the caller can read.
|
|
1531
1663
|
const blanketTarget = `${sourceVar}_disc${varCounter}`;
|
|
1532
1664
|
const predicate = this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter);
|
|
1533
|
-
|
|
1665
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1666
|
+
if (predicate || labelPredicate)
|
|
1534
1667
|
varCounter++;
|
|
1535
1668
|
const pattern = buildRelPattern({
|
|
1536
1669
|
sourceVar,
|
|
1537
1670
|
relDef,
|
|
1538
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1671
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1539
1672
|
edgeVar: relVar,
|
|
1540
|
-
targetLabelRaw:
|
|
1673
|
+
targetLabelRaw: labelRaw,
|
|
1541
1674
|
});
|
|
1542
1675
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1543
|
-
|
|
1544
|
-
|
|
1676
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1677
|
+
if (blanketConditions.length > 0)
|
|
1678
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1545
1679
|
lines.push(`DELETE ${relVar}`);
|
|
1546
1680
|
}
|
|
1547
1681
|
else {
|
|
@@ -1649,6 +1783,7 @@ export class MutationCompiler {
|
|
|
1649
1783
|
// create() accepts. Pre-2.4.5 this path never read
|
|
1650
1784
|
// `createSpec.edge`, so they were silently dropped.
|
|
1651
1785
|
const edgeInput = createSpec.edge;
|
|
1786
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
1652
1787
|
const edgeEntries = Object.entries(edgeInput ?? {}).filter(([, v]) => v !== undefined);
|
|
1653
1788
|
const edgeVar = edgeEntries.length ? `${createVar}_e` : undefined;
|
|
1654
1789
|
const relPattern = buildRelPattern({
|
|
@@ -1719,6 +1854,7 @@ export class MutationCompiler {
|
|
|
1719
1854
|
// Pre-compute SET clauses for edge (relationship) properties
|
|
1720
1855
|
const edgeClauses = [];
|
|
1721
1856
|
const edgeParams = {};
|
|
1857
|
+
this.checkEdgeKeys(relDef, edgeUpdate, 'nested update');
|
|
1722
1858
|
if (edgeUpdate)
|
|
1723
1859
|
for (const [prop, val] of Object.entries(edgeUpdate)) {
|
|
1724
1860
|
if (val === undefined)
|
|
@@ -1866,6 +2002,21 @@ MutationCompiler.OPERATOR_SUFFIXES = [
|
|
|
1866
2002
|
* path to validate that every item shares the same shape; mismatched
|
|
1867
2003
|
* shapes silently drop keys from the compiled WHERE / SET.
|
|
1868
2004
|
*/
|
|
2005
|
+
/**
|
|
2006
|
+
* v2.6.0 — the keys a connect / disconnect item is compiled from. `overwrite`
|
|
2007
|
+
* is offered by the generated `ConnectFieldInput` and accepted as before
|
|
2008
|
+
* (MERGE + SET always overwrites the edge properties it is given).
|
|
2009
|
+
*/
|
|
2010
|
+
const CONNECT_ITEM_KEYS = new Set([
|
|
2011
|
+
'where',
|
|
2012
|
+
'edge',
|
|
2013
|
+
'overwrite',
|
|
2014
|
+
'connect',
|
|
2015
|
+
]);
|
|
2016
|
+
const DISCONNECT_ITEM_KEYS = new Set([
|
|
2017
|
+
'where',
|
|
2018
|
+
'disconnect',
|
|
2019
|
+
]);
|
|
1869
2020
|
/** Keys that make a nested-mutation `where` connection-shaped. */
|
|
1870
2021
|
const CONNECTION_WHERE_KEYS = new Set([
|
|
1871
2022
|
'node',
|