grafeo-ogm 2.5.4 → 2.6.1
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 +68 -0
- package/README.md +126 -1
- 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 +65 -2
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +249 -15
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +65 -1
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +521 -25
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts +4 -0
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +23 -0
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/interface-model.d.ts.map +1 -1
- package/dist/cjs/interface-model.js +8 -1
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts +33 -0
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +90 -11
- 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 +36 -2
- 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 +65 -2
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +249 -15
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +65 -1
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +521 -25
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts +4 -0
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +23 -0
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/interface-model.d.ts.map +1 -1
- package/dist/esm/interface-model.js +8 -1
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts +33 -0
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +91 -12
- 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 +36 -2
- 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 +2 -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
|
|
@@ -285,6 +287,68 @@ class MutationCompiler {
|
|
|
285
287
|
throw new errors_1.OGMError(`Cannot resolve the target type "${relDef.target}" of relationship "${relDef.fieldName}".`);
|
|
286
288
|
return targetNodeDef;
|
|
287
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
|
+
}
|
|
288
352
|
/**
|
|
289
353
|
* Generate CREATE Cypher for one or more nodes.
|
|
290
354
|
* Handles scalar properties, nested relationship creates, and connects.
|
|
@@ -539,8 +603,14 @@ class MutationCompiler {
|
|
|
539
603
|
/**
|
|
540
604
|
* Generate MERGE (upsert) Cypher with ON CREATE SET / ON MATCH SET.
|
|
541
605
|
* Scalar properties only — nested relationship ops are not supported.
|
|
606
|
+
*
|
|
607
|
+
* v2.6.1 — `updateGuard` is the root type's `update` row filter on `n`
|
|
608
|
+
* (compiled by the caller with `preserveVars: ['__matched']`). When it
|
|
609
|
+
* is non-empty, an EXISTING node is updated only when it passes the
|
|
610
|
+
* guard, exactly like a direct `update`; one that fails it is left
|
|
611
|
+
* untouched and no row is returned. See `buildGuardedMerge`.
|
|
542
612
|
*/
|
|
543
|
-
compileMerge(where, create, update, nodeDef, labels) {
|
|
613
|
+
compileMerge(where, create, update, nodeDef, labels, updateGuard) {
|
|
544
614
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
545
615
|
const lines = [];
|
|
546
616
|
const params = {};
|
|
@@ -578,6 +648,19 @@ class MutationCompiler {
|
|
|
578
648
|
if (mergeProps.length === 0)
|
|
579
649
|
throw new errors_1.OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
|
|
580
650
|
`an @id or @unique field of ${nodeDef.typeName}.`);
|
|
651
|
+
// v2.6.0 — MERGE matches ALL its keys together, so `{ id, email }`
|
|
652
|
+
// misses a node that has the email under another id and creates a
|
|
653
|
+
// duplicate beside it (or fails on the unique constraint). Default:
|
|
654
|
+
// compiled as before, with one warning; strict: exactly one key.
|
|
655
|
+
if (mergeKeyNames.length > 1) {
|
|
656
|
+
const keys = mergeKeyNames.map((k) => `"${k}"`).join(', ');
|
|
657
|
+
if (this.strictMode.strict)
|
|
658
|
+
throw new errors_1.OGMError(`upsert "where" must name exactly one @id / @unique field of ${nodeDef.typeName}; got ${keys}. ` +
|
|
659
|
+
`MERGE matches all of them together, so a node that has one but not the others is not found ` +
|
|
660
|
+
`and a duplicate is created. Pass a single unique key.`);
|
|
661
|
+
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 ` +
|
|
662
|
+
`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"');
|
|
663
|
+
}
|
|
581
664
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
|
|
582
665
|
// ON CREATE SET — all properties from create input (scalar only)
|
|
583
666
|
const createSets = [];
|
|
@@ -628,6 +711,11 @@ class MutationCompiler {
|
|
|
628
711
|
continue;
|
|
629
712
|
createSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = randomUUID()`);
|
|
630
713
|
}
|
|
714
|
+
// v2.6.0 — strict: runtime labels go on the node only when MERGE creates
|
|
715
|
+
// it (see `labelsOnMatchedNodes`).
|
|
716
|
+
const labelsOnCreate = this.strictMode.strict && labels && labels.length > 0;
|
|
717
|
+
if (labelsOnCreate)
|
|
718
|
+
createSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
|
|
631
719
|
if (createSets.length > 0)
|
|
632
720
|
lines.push(`ON CREATE SET ${createSets.join(', ')}`);
|
|
633
721
|
// ON MATCH SET — all properties from update input (scalar only)
|
|
@@ -648,12 +736,19 @@ class MutationCompiler {
|
|
|
648
736
|
updateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${valueExpr}`);
|
|
649
737
|
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
650
738
|
}
|
|
651
|
-
if (
|
|
739
|
+
if (updateGuard !== undefined && updateGuard.cypher !== '') {
|
|
740
|
+
const guarded = buildGuardedMerge(labelStr, mergeProps, updateGuard, updateSets, params);
|
|
741
|
+
lines.unshift(...guarded.before);
|
|
742
|
+
lines.push(...guarded.after);
|
|
743
|
+
}
|
|
744
|
+
else if (updateSets.length > 0)
|
|
652
745
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
653
|
-
// Extra labels
|
|
654
|
-
|
|
746
|
+
// Extra labels. v2.6.1 — with an update guard, the rows that reach this
|
|
747
|
+
// SET are the created node and an existing node that passed the guard.
|
|
748
|
+
if (labels && labels.length > 0 && !labelsOnCreate) {
|
|
655
749
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
656
750
|
lines.push(`SET n:${validatedLabels}`);
|
|
751
|
+
this.labelsOnMatchedNodes(nodeDef, 'upsert');
|
|
657
752
|
}
|
|
658
753
|
lines.push('RETURN n');
|
|
659
754
|
return { cypher: lines.join('\n'), params };
|
|
@@ -699,7 +794,15 @@ class MutationCompiler {
|
|
|
699
794
|
* Scalar properties only — no nested relationship operations.
|
|
700
795
|
* Returns count of created nodes.
|
|
701
796
|
*/
|
|
702
|
-
compileCreateMany(data, nodeDef, skipDuplicates, labels
|
|
797
|
+
compileCreateMany(data, nodeDef, skipDuplicates, labels,
|
|
798
|
+
/**
|
|
799
|
+
* v2.6.1 — the root type's `update` row filter on `n`. With
|
|
800
|
+
* `skipDuplicates` and default-mode `labels`, an EXISTING node the
|
|
801
|
+
* MERGE matched is relabelled only when it passes this guard (a label
|
|
802
|
+
* change is an update, as `setLabels` enforces); created nodes get the
|
|
803
|
+
* labels in `ON CREATE SET`.
|
|
804
|
+
*/
|
|
805
|
+
updateGuard) {
|
|
703
806
|
if (data.length === 0)
|
|
704
807
|
return { cypher: 'RETURN 0 AS count', params: {} };
|
|
705
808
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
@@ -772,6 +875,12 @@ class MutationCompiler {
|
|
|
772
875
|
});
|
|
773
876
|
params.items = sanitizedItems;
|
|
774
877
|
lines.push('UNWIND $items AS item');
|
|
878
|
+
const guardedLabels = Boolean(skipDuplicates) &&
|
|
879
|
+
!this.strictMode.strict &&
|
|
880
|
+
labels !== undefined &&
|
|
881
|
+
labels.length > 0 &&
|
|
882
|
+
updateGuard !== undefined &&
|
|
883
|
+
updateGuard.cypher !== '';
|
|
775
884
|
if (skipDuplicates) {
|
|
776
885
|
// MERGE key: the unique/id fields EVERY item supplies — an item
|
|
777
886
|
// missing one would MERGE on null, which Neo4j rejects.
|
|
@@ -803,6 +912,13 @@ class MutationCompiler {
|
|
|
803
912
|
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
|
|
804
913
|
params[paramName] = bound;
|
|
805
914
|
}
|
|
915
|
+
// v2.6.0 — strict: labels only on the nodes MERGE creates.
|
|
916
|
+
// v2.6.1 — also with an update guard: the matched nodes are
|
|
917
|
+
// relabelled below, only where the guard passes.
|
|
918
|
+
if ((this.strictMode.strict || guardedLabels) &&
|
|
919
|
+
labels &&
|
|
920
|
+
labels.length > 0)
|
|
921
|
+
onCreateSets.push(`n:${labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':')}`);
|
|
806
922
|
if (onCreateSets.length > 0)
|
|
807
923
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
808
924
|
}
|
|
@@ -822,9 +938,26 @@ class MutationCompiler {
|
|
|
822
938
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
823
939
|
}
|
|
824
940
|
// Extra labels
|
|
825
|
-
if (
|
|
941
|
+
if (guardedLabels) {
|
|
942
|
+
// v2.6.1 — matched nodes are relabelled only where the update guard
|
|
943
|
+
// passes. Pre-2.6.1 `SET n:<labels>` ran on every node the MERGE
|
|
944
|
+
// returned, so an existing node the context could not update was
|
|
945
|
+
// relabelled. FOREACH keeps one row per item (invariant 2).
|
|
946
|
+
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
947
|
+
(0, validation_1.mergeParams)(params, updateGuard.params);
|
|
948
|
+
lines.push('WITH n');
|
|
949
|
+
if (updateGuard.preludes && updateGuard.preludes.length > 0)
|
|
950
|
+
lines.push(...updateGuard.preludes);
|
|
951
|
+
lines.push(`FOREACH (__l IN CASE WHEN ${updateGuard.cypher} THEN [1] ELSE [] END | SET n:${validatedLabels})`);
|
|
952
|
+
this.labelsOnMatchedNodes(nodeDef, 'createMany');
|
|
953
|
+
}
|
|
954
|
+
else if (labels &&
|
|
955
|
+
labels.length > 0 &&
|
|
956
|
+
!(skipDuplicates && this.strictMode.strict)) {
|
|
826
957
|
const validatedLabels = labels.map((l) => (0, validation_1.assertSafeLabel)(l)).join(':');
|
|
827
958
|
lines.push(`SET n:${validatedLabels}`);
|
|
959
|
+
if (skipDuplicates)
|
|
960
|
+
this.labelsOnMatchedNodes(nodeDef, 'createMany');
|
|
828
961
|
}
|
|
829
962
|
lines.push('RETURN count(n) AS count');
|
|
830
963
|
return { cypher: lines.join('\n'), params };
|
|
@@ -974,6 +1107,7 @@ class MutationCompiler {
|
|
|
974
1107
|
(0, validation_1.mergeParams)(params, propParams);
|
|
975
1108
|
lines.push(`CREATE ${relPattern}`);
|
|
976
1109
|
// Set edge properties if present
|
|
1110
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
977
1111
|
if (edgeInput && Object.keys(edgeInput).length > 0) {
|
|
978
1112
|
const relVar = `r_edge_${startCounter + counter - 1}`;
|
|
979
1113
|
// Re-match the relationship to set edge props
|
|
@@ -1112,6 +1246,11 @@ class MutationCompiler {
|
|
|
1112
1246
|
scope,
|
|
1113
1247
|
}));
|
|
1114
1248
|
else {
|
|
1249
|
+
// Every item is a plain object here (fast-path eligibility).
|
|
1250
|
+
for (const item of spec) {
|
|
1251
|
+
this.checkLinkItemKeys(item, 'connect', relDef);
|
|
1252
|
+
this.checkEdgeKeys(relDef, item.edge, 'connect');
|
|
1253
|
+
}
|
|
1115
1254
|
const targetVar = `${varPrefix}target`;
|
|
1116
1255
|
const relVar = `${varPrefix}r`;
|
|
1117
1256
|
const targetLabelStr = this.getCachedLabelString(targetNodeDef);
|
|
@@ -1238,6 +1377,8 @@ class MutationCompiler {
|
|
|
1238
1377
|
// compiling it would MATCH, and link, every target node.
|
|
1239
1378
|
if (!(0, validation_1.isPlainObject)(connectSpec))
|
|
1240
1379
|
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}.`);
|
|
1380
|
+
this.checkLinkItemKeys(connectSpec, 'connect', relDef);
|
|
1381
|
+
this.checkEdgeKeys(relDef, connectSpec.edge, 'connect');
|
|
1241
1382
|
const lines = [];
|
|
1242
1383
|
const whereSpec = connectSpec.where;
|
|
1243
1384
|
const edgeInput = connectSpec.edge;
|
|
@@ -1379,21 +1520,25 @@ class MutationCompiler {
|
|
|
1379
1520
|
const predicate = targetNodeDef
|
|
1380
1521
|
? this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter)
|
|
1381
1522
|
: undefined;
|
|
1523
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1382
1524
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
1383
1525
|
sourceVar,
|
|
1384
1526
|
relDef,
|
|
1385
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1527
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1386
1528
|
edgeVar: relVar,
|
|
1387
|
-
targetLabelRaw:
|
|
1529
|
+
targetLabelRaw: labelRaw,
|
|
1388
1530
|
});
|
|
1389
1531
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1390
|
-
|
|
1391
|
-
|
|
1532
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1533
|
+
if (blanketConditions.length > 0)
|
|
1534
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1392
1535
|
lines.push(`DELETE ${relVar}`);
|
|
1393
1536
|
}
|
|
1394
1537
|
else {
|
|
1395
1538
|
// Specific disconnect with conditions
|
|
1396
1539
|
const disconnectSpec = item;
|
|
1540
|
+
if ((0, validation_1.isPlainObject)(disconnectSpec))
|
|
1541
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1397
1542
|
const whereSpec = disconnectSpec.where;
|
|
1398
1543
|
const targetVar = `${varPrefix}target_${tag}_${si}`;
|
|
1399
1544
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
@@ -1432,6 +1577,33 @@ class MutationCompiler {
|
|
|
1432
1577
|
this.narrowedRelDefs.add(narrowed);
|
|
1433
1578
|
return narrowed;
|
|
1434
1579
|
}
|
|
1580
|
+
/**
|
|
1581
|
+
* v2.6.0 — how a blanket disconnect (`null` / `{}`) restricts its target.
|
|
1582
|
+
*
|
|
1583
|
+
* Default (v2.5.3): only a relationship narrowed to a union / interface
|
|
1584
|
+
* member keeps that member's label; otherwise the target is anonymous,
|
|
1585
|
+
* so with two fields on one relationship type and direction
|
|
1586
|
+
* (`pets: [Pet]`, `toys: [Toy]`, both `HAS`) a blanket disconnect of one
|
|
1587
|
+
* also unlinks the other's nodes (warned once at OGM construction).
|
|
1588
|
+
*
|
|
1589
|
+
* Strict: the field's own target type — its label, or for an abstract
|
|
1590
|
+
* target a predicate over its members' labels on `targetVar`.
|
|
1591
|
+
*/
|
|
1592
|
+
blanketRestriction(relDef, targetNodeDef, targetVar) {
|
|
1593
|
+
const narrowed = this.blanketTargetLabel(relDef);
|
|
1594
|
+
if (narrowed || !this.strictMode.strict || !targetNodeDef)
|
|
1595
|
+
return { labelRaw: narrowed, labelPredicate: null };
|
|
1596
|
+
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1597
|
+
return {
|
|
1598
|
+
labelRaw: undefined,
|
|
1599
|
+
labelPredicate: this.targetMatch(targetNodeDef, targetVar)
|
|
1600
|
+
.labelPredicate,
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
labelRaw: this.getCachedLabelString(targetNodeDef),
|
|
1604
|
+
labelPredicate: null,
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1435
1607
|
/** The label a blanket disconnect over `relDef` must keep (see above). */
|
|
1436
1608
|
blanketTargetLabel(relDef) {
|
|
1437
1609
|
if (!this.narrowedRelDefs.has(relDef))
|
|
@@ -1522,6 +1694,8 @@ class MutationCompiler {
|
|
|
1522
1694
|
: [item.disconnect];
|
|
1523
1695
|
for (let di = 0; di < disconnectItems.length; di++) {
|
|
1524
1696
|
const disconnectSpec = disconnectItems[di];
|
|
1697
|
+
if ((0, validation_1.isPlainObject)(disconnectSpec))
|
|
1698
|
+
this.checkLinkItemKeys(disconnectSpec, 'disconnect', relDef);
|
|
1525
1699
|
const whereSpec = disconnectSpec.where;
|
|
1526
1700
|
// A nested `disconnect` needs the target bound, so an item that
|
|
1527
1701
|
// carries one is never the anonymous blanket form.
|
|
@@ -1538,18 +1712,20 @@ class MutationCompiler {
|
|
|
1538
1712
|
// disconnect: only unlink targets the caller can read.
|
|
1539
1713
|
const blanketTarget = `${sourceVar}_disc${varCounter}`;
|
|
1540
1714
|
const predicate = this.buildTargetPolicyPredicate(targetNodeDef, blanketTarget, params, policyContext, paramCounter);
|
|
1541
|
-
|
|
1715
|
+
const { labelRaw, labelPredicate } = this.blanketRestriction(relDef, targetNodeDef, blanketTarget);
|
|
1716
|
+
if (predicate || labelPredicate)
|
|
1542
1717
|
varCounter++;
|
|
1543
1718
|
const pattern = (0, utils_1.buildRelPattern)({
|
|
1544
1719
|
sourceVar,
|
|
1545
1720
|
relDef,
|
|
1546
|
-
targetVar: predicate ? blanketTarget : '',
|
|
1721
|
+
targetVar: predicate || labelPredicate ? blanketTarget : '',
|
|
1547
1722
|
edgeVar: relVar,
|
|
1548
|
-
targetLabelRaw:
|
|
1723
|
+
targetLabelRaw: labelRaw,
|
|
1549
1724
|
});
|
|
1550
1725
|
lines.push(`OPTIONAL MATCH ${pattern}`);
|
|
1551
|
-
|
|
1552
|
-
|
|
1726
|
+
const blanketConditions = [labelPredicate, predicate].filter(Boolean);
|
|
1727
|
+
if (blanketConditions.length > 0)
|
|
1728
|
+
lines.push(`WHERE ${blanketConditions.join(' AND ')}`);
|
|
1553
1729
|
lines.push(`DELETE ${relVar}`);
|
|
1554
1730
|
}
|
|
1555
1731
|
else {
|
|
@@ -1657,6 +1833,7 @@ class MutationCompiler {
|
|
|
1657
1833
|
// create() accepts. Pre-2.4.5 this path never read
|
|
1658
1834
|
// `createSpec.edge`, so they were silently dropped.
|
|
1659
1835
|
const edgeInput = createSpec.edge;
|
|
1836
|
+
this.checkEdgeKeys(relDef, edgeInput, 'nested create');
|
|
1660
1837
|
const edgeEntries = Object.entries(edgeInput ?? {}).filter(([, v]) => v !== undefined);
|
|
1661
1838
|
const edgeVar = edgeEntries.length ? `${createVar}_e` : undefined;
|
|
1662
1839
|
const relPattern = (0, utils_1.buildRelPattern)({
|
|
@@ -1727,6 +1904,7 @@ class MutationCompiler {
|
|
|
1727
1904
|
// Pre-compute SET clauses for edge (relationship) properties
|
|
1728
1905
|
const edgeClauses = [];
|
|
1729
1906
|
const edgeParams = {};
|
|
1907
|
+
this.checkEdgeKeys(relDef, edgeUpdate, 'nested update');
|
|
1730
1908
|
if (edgeUpdate)
|
|
1731
1909
|
for (const [prop, val] of Object.entries(edgeUpdate)) {
|
|
1732
1910
|
if (val === undefined)
|
|
@@ -1875,6 +2053,21 @@ MutationCompiler.OPERATOR_SUFFIXES = [
|
|
|
1875
2053
|
* path to validate that every item shares the same shape; mismatched
|
|
1876
2054
|
* shapes silently drop keys from the compiled WHERE / SET.
|
|
1877
2055
|
*/
|
|
2056
|
+
/**
|
|
2057
|
+
* v2.6.0 — the keys a connect / disconnect item is compiled from. `overwrite`
|
|
2058
|
+
* is offered by the generated `ConnectFieldInput` and accepted as before
|
|
2059
|
+
* (MERGE + SET always overwrites the edge properties it is given).
|
|
2060
|
+
*/
|
|
2061
|
+
const CONNECT_ITEM_KEYS = new Set([
|
|
2062
|
+
'where',
|
|
2063
|
+
'edge',
|
|
2064
|
+
'overwrite',
|
|
2065
|
+
'connect',
|
|
2066
|
+
]);
|
|
2067
|
+
const DISCONNECT_ITEM_KEYS = new Set([
|
|
2068
|
+
'where',
|
|
2069
|
+
'disconnect',
|
|
2070
|
+
]);
|
|
1878
2071
|
/** Keys that make a nested-mutation `where` connection-shaped. */
|
|
1879
2072
|
const CONNECTION_WHERE_KEYS = new Set([
|
|
1880
2073
|
'node',
|
|
@@ -2047,6 +2240,47 @@ function upsertRelationshipError(nodeDef, key, branch) {
|
|
|
2047
2240
|
return new errors_1.OGMError(`upsert does not support relationship fields: "${key}" in "${branch}" (${nodeDef.typeName}). ` +
|
|
2048
2241
|
`Use create() / update() for nested relationship operations.`);
|
|
2049
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* v2.6.1 — the lines around an upsert MERGE that apply the root type's
|
|
2245
|
+
* `update` row filter to the node it MATCHES, as a direct `update` does:
|
|
2246
|
+
*
|
|
2247
|
+
* OPTIONAL MATCH (__existing:L { key }) ┐ before
|
|
2248
|
+
* WITH count(__existing) > 0 AS __matched ┘
|
|
2249
|
+
* MERGE (n:L { key })
|
|
2250
|
+
* ON CREATE SET …
|
|
2251
|
+
* WITH n, __matched ┐ after
|
|
2252
|
+
* [@cypher preludes of the guard] │
|
|
2253
|
+
* WHERE NOT __matched OR (<guard>) │
|
|
2254
|
+
* FOREACH (__u IN CASE WHEN __matched THEN [1] ELSE [] END | SET …)
|
|
2255
|
+
*
|
|
2256
|
+
* A created node passes untouched by the update sets; an existing node is
|
|
2257
|
+
* updated only when it passes the guard; one that fails it is neither
|
|
2258
|
+
* updated nor returned (and the MERGE created nothing). Pre-2.6.1 the
|
|
2259
|
+
* update went in `ON MATCH SET` with no row filter, so any context the
|
|
2260
|
+
* WITH CHECK admitted rewrote any existing node — even its owner field.
|
|
2261
|
+
*
|
|
2262
|
+
* `count()` keeps one row before the MERGE whether 0, 1 or several nodes
|
|
2263
|
+
* match (invariant 2); FOREACH needs no subquery after the write
|
|
2264
|
+
* (invariant 1). Under a race, a node created between the OPTIONAL MATCH
|
|
2265
|
+
* and the MERGE is matched with `__matched = false`: its properties are
|
|
2266
|
+
* not updated (default-mode runtime `labels`, SET after this, still are).
|
|
2267
|
+
*/
|
|
2268
|
+
function buildGuardedMerge(labelStr, mergeProps, guard, updateSets, params) {
|
|
2269
|
+
(0, validation_1.mergeParams)(params, guard.params);
|
|
2270
|
+
const after = ['WITH n, __matched'];
|
|
2271
|
+
if (guard.preludes && guard.preludes.length > 0)
|
|
2272
|
+
after.push(...guard.preludes);
|
|
2273
|
+
after.push(`WHERE NOT __matched OR (${guard.cypher})`);
|
|
2274
|
+
if (updateSets.length > 0)
|
|
2275
|
+
after.push(`FOREACH (__u IN CASE WHEN __matched THEN [1] ELSE [] END | SET ${updateSets.join(', ')})`);
|
|
2276
|
+
return {
|
|
2277
|
+
before: [
|
|
2278
|
+
`OPTIONAL MATCH (__existing:${labelStr} { ${mergeProps.join(', ')} })`,
|
|
2279
|
+
'WITH count(__existing) > 0 AS __matched',
|
|
2280
|
+
],
|
|
2281
|
+
after,
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2050
2284
|
/** `null` / `{}`: the whole-relationship (blanket) form, never per member. */
|
|
2051
2285
|
function isBlanketSpec(spec) {
|
|
2052
2286
|
if (spec == null)
|