grafeo-ogm 2.4.8 → 2.5.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 +72 -0
- package/README.md +106 -27
- package/assets/brand/grafeo-mark-on-dark.svg +1 -0
- package/assets/brand/grafeo-mark-on-light.svg +1 -0
- package/dist/cjs/cli/commands/db-push.d.ts.map +1 -1
- package/dist/cjs/cli/commands/db-push.js +17 -5
- package/dist/cjs/cli/commands/db-push.js.map +1 -1
- package/dist/cjs/cli/push-planner.d.ts +6 -0
- package/dist/cjs/cli/push-planner.d.ts.map +1 -1
- package/dist/cjs/cli/push-planner.js +24 -5
- package/dist/cjs/cli/push-planner.js.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +92 -51
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/selection.compiler.d.ts +16 -0
- package/dist/cjs/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/selection.compiler.js +158 -18
- package/dist/cjs/compilers/selection.compiler.js.map +1 -1
- package/dist/cjs/generator/type-emitters/where-emitter.js +16 -0
- package/dist/cjs/generator/type-emitters/where-emitter.js.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 +21 -13
- package/dist/cjs/interface-model.js.map +1 -1
- package/dist/cjs/model.d.ts.map +1 -1
- package/dist/cjs/model.js +6 -0
- package/dist/cjs/model.js.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 +192 -11
- package/dist/cjs/testing/cypher-assert.js.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.d.ts +8 -4
- package/dist/cjs/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/cjs/testing/neo4j-record-factory.js +18 -7
- package/dist/cjs/testing/neo4j-record-factory.js.map +1 -1
- package/dist/esm/cli/commands/db-push.d.ts.map +1 -1
- package/dist/esm/cli/commands/db-push.js +17 -5
- package/dist/esm/cli/commands/db-push.js.map +1 -1
- package/dist/esm/cli/push-planner.d.ts +6 -0
- package/dist/esm/cli/push-planner.d.ts.map +1 -1
- package/dist/esm/cli/push-planner.js +24 -5
- package/dist/esm/cli/push-planner.js.map +1 -1
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +92 -51
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/selection.compiler.d.ts +16 -0
- package/dist/esm/compilers/selection.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/selection.compiler.js +160 -20
- package/dist/esm/compilers/selection.compiler.js.map +1 -1
- package/dist/esm/generator/type-emitters/where-emitter.js +16 -0
- package/dist/esm/generator/type-emitters/where-emitter.js.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 +21 -13
- package/dist/esm/interface-model.js.map +1 -1
- package/dist/esm/model.d.ts.map +1 -1
- package/dist/esm/model.js +6 -0
- package/dist/esm/model.js.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 +192 -11
- package/dist/esm/testing/cypher-assert.js.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.d.ts +8 -4
- package/dist/esm/testing/neo4j-record-factory.d.ts.map +1 -1
- package/dist/esm/testing/neo4j-record-factory.js +18 -7
- package/dist/esm/testing/neo4j-record-factory.js.map +1 -1
- package/package.json +1 -1
- package/assets/logo.jpg +0 -0
|
@@ -20,7 +20,15 @@ const MAX_NESTED_LINK_DEPTH = 5;
|
|
|
20
20
|
* bound as `targetVar`: that node becomes the source, everything in scope
|
|
21
21
|
* stays carried, and its own names are prefixed by it.
|
|
22
22
|
*/
|
|
23
|
-
function nestedScope(carryVars, targetVar, depth
|
|
23
|
+
function nestedScope(carryVars, targetVar, depth,
|
|
24
|
+
/**
|
|
25
|
+
* A token unique along the whole write path (v2.5.1). Pre-2.5.1 the
|
|
26
|
+
* prefix was `targetVar` alone, which sibling connects share (`target`)
|
|
27
|
+
* and which per-member union / interface dispatch restarts (`n_conn0`),
|
|
28
|
+
* so their nested params collided and the last writer won: links went
|
|
29
|
+
* to the wrong node and edge values were overwritten.
|
|
30
|
+
*/
|
|
31
|
+
uniqueToken) {
|
|
24
32
|
if (depth >= MAX_NESTED_LINK_DEPTH)
|
|
25
33
|
throw new OGMError(`Nested connect / disconnect deeper than ${MAX_NESTED_LINK_DEPTH} levels is not supported.`);
|
|
26
34
|
const vars = [...new Set([...carryVars, targetVar])];
|
|
@@ -28,7 +36,7 @@ function nestedScope(carryVars, targetVar, depth) {
|
|
|
28
36
|
sourceVar: targetVar,
|
|
29
37
|
carryVars: vars,
|
|
30
38
|
withClause: `WITH ${vars.join(', ')}`,
|
|
31
|
-
varPrefix: `${
|
|
39
|
+
varPrefix: `${uniqueToken}_`,
|
|
32
40
|
depth: depth + 1,
|
|
33
41
|
};
|
|
34
42
|
}
|
|
@@ -168,6 +176,9 @@ export class MutationCompiler {
|
|
|
168
176
|
*/
|
|
169
177
|
isConnectFastPathEligible(items, targetNodeDef) {
|
|
170
178
|
for (const item of items) {
|
|
179
|
+
// A non-object item takes the per-item path, which rejects it.
|
|
180
|
+
if (!isPlainObject(item))
|
|
181
|
+
return false;
|
|
171
182
|
// A nested `connect` is per item (v2.4.8): the UNWIND cannot run it.
|
|
172
183
|
if (item.connect != null)
|
|
173
184
|
return false;
|
|
@@ -276,8 +287,10 @@ export class MutationCompiler {
|
|
|
276
287
|
const input = inputs[i];
|
|
277
288
|
const prefix = `create${i}`;
|
|
278
289
|
const nodeVar = i === 0 ? 'n' : `n_${i}`;
|
|
279
|
-
|
|
290
|
+
// Labels first: an unsafe label must fail as such, before any
|
|
291
|
+
// input-key check (v2.5.0 rejects unknown keys).
|
|
280
292
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
293
|
+
const { propString, propParams } = this.buildCreateProperties(input, nodeDef, prefix);
|
|
281
294
|
lines.push(`CREATE (${nodeVar}:${labelStr} { ${propString} })`);
|
|
282
295
|
mergeParams(params, propParams);
|
|
283
296
|
createdVars.push(nodeVar);
|
|
@@ -352,6 +365,7 @@ export class MutationCompiler {
|
|
|
352
365
|
if (value === undefined)
|
|
353
366
|
continue;
|
|
354
367
|
assertSafePropertyName(key, 'update property');
|
|
368
|
+
assertKnownField(nodeDef, key, 'update');
|
|
355
369
|
const propDef = nodeDef.properties.get(key);
|
|
356
370
|
const paramName = `update_${key}`;
|
|
357
371
|
const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
|
|
@@ -480,6 +494,11 @@ export class MutationCompiler {
|
|
|
480
494
|
schema: this.schema,
|
|
481
495
|
});
|
|
482
496
|
const conditions = [
|
|
497
|
+
// v2.5.1 — an interface / union target binds no label in the
|
|
498
|
+
// pattern, so restrict the match to its member types. Without it a
|
|
499
|
+
// nested or cascade delete also DETACH DELETEd any other node on the
|
|
500
|
+
// same relationship type (a Toy deleted through `pets`).
|
|
501
|
+
this.targetMatch(targetNodeDef, targetVar).labelPredicate,
|
|
483
502
|
this.compileTargetWhere(item.where, targetVar, targetNodeDef, args.params, args.policyContext, args.paramCounter),
|
|
484
503
|
this.buildTargetPolicyPredicate(targetNodeDef, targetVar, args.params, args.policyContext, args.paramCounter, 'delete'),
|
|
485
504
|
].filter((c) => Boolean(c));
|
|
@@ -503,19 +522,31 @@ export class MutationCompiler {
|
|
|
503
522
|
const labelStr = this.getCachedLabelString(nodeDef);
|
|
504
523
|
const lines = [];
|
|
505
524
|
const params = {};
|
|
506
|
-
//
|
|
525
|
+
// MERGE key: the `@id` / `@unique` fields of `where`, and nothing else
|
|
526
|
+
// (v2.5.0). Any other key throws:
|
|
527
|
+
// - a plain field (`{ email, name }`) became part of the MERGE
|
|
528
|
+
// pattern, so an existing node with that email but another name did
|
|
529
|
+
// not match — MERGE created a DUPLICATE beside it;
|
|
530
|
+
// - an operator key (`name_CONTAINS`) was dropped, silently widening
|
|
531
|
+
// the match to the remaining keys;
|
|
532
|
+
// - a typo (`usernme`) became a phantom property on the new node.
|
|
533
|
+
// Pre-1.7.4 any key set was accepted; 1.7.4 required at least one
|
|
534
|
+
// unique key but still let the others through.
|
|
507
535
|
const mergeProps = [];
|
|
508
536
|
const mergeKeyNames = [];
|
|
509
537
|
for (const [key, value] of Object.entries(where)) {
|
|
510
|
-
if (value === undefined
|
|
511
|
-
continue;
|
|
512
|
-
// Skip relationship suffixes and operators
|
|
513
|
-
if (key === 'AND' || key === 'OR' || key === 'NOT')
|
|
514
|
-
continue;
|
|
515
|
-
if (key.includes('_') && !nodeDef.properties.has(key))
|
|
538
|
+
if (value === undefined)
|
|
516
539
|
continue;
|
|
517
|
-
assertSafePropertyName(key, 'merge key');
|
|
518
540
|
const propDef = nodeDef.properties.get(key);
|
|
541
|
+
if (!propDef ||
|
|
542
|
+
propDef.isCypher ||
|
|
543
|
+
!(propDef.isUnique || propDef.isGenerated))
|
|
544
|
+
throw new OGMError(`upsert "where" accepts only @id / @unique fields of ${nodeDef.typeName}; "${key}" is not one. ` +
|
|
545
|
+
`MERGE matches on exactly these keys — put other fields in "create" / "update", ` +
|
|
546
|
+
`or use findFirst() + create() / update() for a non-unique match.`);
|
|
547
|
+
if (value === null)
|
|
548
|
+
throw new OGMError(`upsert "where.${key}" is null; a MERGE key must have a value.`);
|
|
549
|
+
assertSafePropertyName(key, 'merge key');
|
|
519
550
|
const paramName = `merge_${key}`;
|
|
520
551
|
const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
|
|
521
552
|
mergeProps.push(`${escapeIdentifier(key)}: ${valueExpr}`);
|
|
@@ -523,30 +554,8 @@ export class MutationCompiler {
|
|
|
523
554
|
params[paramName] = coerceWriteValue(value, propDef);
|
|
524
555
|
}
|
|
525
556
|
if (mergeProps.length === 0)
|
|
526
|
-
throw new OGMError(
|
|
527
|
-
|
|
528
|
-
// `@id` property. Pre-1.7.4 we accepted any key set, so:
|
|
529
|
-
// - A typo in `where` (e.g. `usernme`) became a phantom MERGE
|
|
530
|
-
// property — Neo4j happily created a node with that mis-named
|
|
531
|
-
// attribute on first use.
|
|
532
|
-
// - A non-unique `where` (e.g. `where: { country: 'AR' }`) matched
|
|
533
|
-
// multiple existing nodes; MERGE then fans out across all of
|
|
534
|
-
// them, applying ON MATCH SET to every match. Almost always a
|
|
535
|
-
// bug, never the developer's intent.
|
|
536
|
-
// Requiring at least one unique/id key forces MERGE to target at
|
|
537
|
-
// most one row.
|
|
538
|
-
const hasUniqueOrId = mergeKeyNames.some((name) => {
|
|
539
|
-
const prop = nodeDef.properties.get(name);
|
|
540
|
-
// `isGenerated` is the parser's flag for the `@id` directive;
|
|
541
|
-
// `isUnique` is the flag for `@unique`. Either is sufficient
|
|
542
|
-
// to guarantee the MERGE pattern targets at most one row.
|
|
543
|
-
return prop !== undefined && (prop.isUnique || prop.isGenerated);
|
|
544
|
-
});
|
|
545
|
-
if (!hasUniqueOrId)
|
|
546
|
-
throw new OGMError(`upsert "where" must contain at least one property marked with @id or @unique. ` +
|
|
547
|
-
`Got [${mergeKeyNames.map((n) => `"${n}"`).join(', ')}] — none of these are unique on ${nodeDef.typeName}. ` +
|
|
548
|
-
`Without a unique key, MERGE can fan out across multiple existing nodes (applying ON MATCH SET to each) ` +
|
|
549
|
-
`or create phantom properties on typo. If you really want a non-unique merge, use create() + update().`);
|
|
557
|
+
throw new OGMError(`upsert requires at least one scalar property in "where" for MERGE key: ` +
|
|
558
|
+
`an @id or @unique field of ${nodeDef.typeName}.`);
|
|
550
559
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps.join(', ')} })`);
|
|
551
560
|
// ON CREATE SET — all properties from create input (scalar only)
|
|
552
561
|
const createSets = [];
|
|
@@ -559,6 +568,7 @@ export class MutationCompiler {
|
|
|
559
568
|
throw upsertRelationshipError(nodeDef, key, 'create');
|
|
560
569
|
}
|
|
561
570
|
assertSafePropertyName(key, 'create property');
|
|
571
|
+
assertKnownField(nodeDef, key, 'create');
|
|
562
572
|
const propDef = nodeDef.properties.get(key);
|
|
563
573
|
const paramName = `onCreate_${key}`;
|
|
564
574
|
const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
|
|
@@ -609,6 +619,7 @@ export class MutationCompiler {
|
|
|
609
619
|
throw upsertRelationshipError(nodeDef, key, 'update');
|
|
610
620
|
}
|
|
611
621
|
assertSafePropertyName(key, 'update property');
|
|
622
|
+
assertKnownField(nodeDef, key, 'update');
|
|
612
623
|
const propDef = nodeDef.properties.get(key);
|
|
613
624
|
const paramName = `onMatch_${key}`;
|
|
614
625
|
const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
|
|
@@ -682,16 +693,13 @@ export class MutationCompiler {
|
|
|
682
693
|
for (const key of Object.keys(item)) {
|
|
683
694
|
if (item[key] === undefined || scalarKeys.includes(key))
|
|
684
695
|
continue;
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (index > 0)
|
|
691
|
-
continue;
|
|
692
|
-
throw new OGMError(`createMany does not support relationship fields. Found: "${key}". Use create() for nested operations.`);
|
|
693
|
-
}
|
|
696
|
+
// v2.5.0 — rejected in EVERY item. Pre-2.5.0 only the first item's
|
|
697
|
+
// relationship fields were; in later items the nested input was
|
|
698
|
+
// skipped unwritten.
|
|
699
|
+
if (nodeDef.relationships.has(key))
|
|
700
|
+
throw new OGMError(`createMany does not support relationship fields. Found: "${key}" in data[${index}]. Use create() for nested operations.`);
|
|
694
701
|
assertSafePropertyName(key, 'createMany property');
|
|
702
|
+
assertKnownField(nodeDef, key, 'createMany');
|
|
695
703
|
scalarKeys.push(key);
|
|
696
704
|
}
|
|
697
705
|
// Identify @id (generated) fields that are NOT provided in data
|
|
@@ -823,6 +831,7 @@ export class MutationCompiler {
|
|
|
823
831
|
if (value === undefined)
|
|
824
832
|
continue;
|
|
825
833
|
assertSafePropertyName(key, 'create property');
|
|
834
|
+
assertKnownField(nodeDef, key, 'create');
|
|
826
835
|
const propDef = nodeDef.properties.get(key);
|
|
827
836
|
const paramName = `${prefix}_${key}`;
|
|
828
837
|
const valueExpr = wrapWriteExpr(`$${paramName}`, value, propDef);
|
|
@@ -1022,6 +1031,11 @@ export class MutationCompiler {
|
|
|
1022
1031
|
const relDef = nodeDef.relationships.get(fieldName);
|
|
1023
1032
|
if (!relDef)
|
|
1024
1033
|
continue;
|
|
1034
|
+
// v2.5.1 — `{ rel: null }` (the generated input is `InputMaybe`)
|
|
1035
|
+
// connects nothing. Since 2.4.8 it reached the item compiler with no
|
|
1036
|
+
// `where`, so the MATCH had no filter and EVERY target was linked.
|
|
1037
|
+
if (spec == null)
|
|
1038
|
+
continue;
|
|
1025
1039
|
const abstract = abstractTarget(relDef.target, this.schema);
|
|
1026
1040
|
const memberEntries = abstract && !isBlanketSpec(spec)
|
|
1027
1041
|
? perMemberEntries(spec, abstract, fieldName)
|
|
@@ -1193,9 +1207,14 @@ export class MutationCompiler {
|
|
|
1193
1207
|
*/
|
|
1194
1208
|
buildConnectItem(a) {
|
|
1195
1209
|
const { connectSpec, relDef, targetNodeDef, sourceVar, connectVar } = a;
|
|
1210
|
+
// v2.5.1 — every connect item reaches here (top-level, array, nested in
|
|
1211
|
+
// create / update / connect). A null or non-object item has no filter:
|
|
1212
|
+
// compiling it would MATCH, and link, every target node.
|
|
1213
|
+
if (!isPlainObject(connectSpec))
|
|
1214
|
+
throw new OGMError(`A connect item on "${relDef.fieldName}" must be an object such as { where: { node: { … } } }; got ${connectSpec === null ? 'null' : typeof connectSpec}.`);
|
|
1196
1215
|
const lines = [];
|
|
1197
|
-
const whereSpec = connectSpec
|
|
1198
|
-
const edgeInput = connectSpec
|
|
1216
|
+
const whereSpec = connectSpec.where;
|
|
1217
|
+
const edgeInput = connectSpec.edge;
|
|
1199
1218
|
lines.push(a.withClause);
|
|
1200
1219
|
lines.push('CALL {');
|
|
1201
1220
|
lines.push(a.withClause);
|
|
@@ -1241,12 +1260,12 @@ export class MutationCompiler {
|
|
|
1241
1260
|
// A `connect` nested in the item links the CONNECTED node onward
|
|
1242
1261
|
// (`connect: { posts: [{ where, connect: { tags: [...] } }] }`), with
|
|
1243
1262
|
// the same policy predicates at its level. Pre-2.4.8 it was ignored.
|
|
1244
|
-
const nested = connectSpec
|
|
1263
|
+
const nested = connectSpec.connect;
|
|
1245
1264
|
if (nested != null) {
|
|
1246
1265
|
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1247
1266
|
throw new OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
|
|
1248
1267
|
`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);
|
|
1268
|
+
const inner = nestedScope(a.scope.carryVars, connectVar, a.scope.depth, a.edgeParamPrefix);
|
|
1250
1269
|
for (const nestedConnect of Array.isArray(nested) ? nested : [nested]) {
|
|
1251
1270
|
if (!isPlainObject(nestedConnect))
|
|
1252
1271
|
throw new OGMError(`A nested "connect" inside a connect on "${relDef.fieldName}" must be an object keyed by relationship.`);
|
|
@@ -1289,13 +1308,15 @@ export class MutationCompiler {
|
|
|
1289
1308
|
* { tags: [...] } }] }`), with the same policy predicates at its level.
|
|
1290
1309
|
* Pre-2.4.8 it was ignored. `''`-free: no input, no lines.
|
|
1291
1310
|
*/
|
|
1292
|
-
buildNestedDisconnect(nested, relDef, targetNodeDef, targetVar, carryVars, params, policyContext, paramCounter, depth = 0
|
|
1311
|
+
buildNestedDisconnect(nested, relDef, targetNodeDef, targetVar, carryVars, params, policyContext, paramCounter, depth = 0,
|
|
1312
|
+
/** Unique along the write path; see `nestedScope`. */
|
|
1313
|
+
uniqueToken = targetVar) {
|
|
1293
1314
|
if (nested == null)
|
|
1294
1315
|
return [];
|
|
1295
1316
|
if (abstractTarget(targetNodeDef.typeName, this.schema))
|
|
1296
1317
|
throw new OGMError(`A nested "disconnect" inside a disconnect on "${relDef.fieldName}" (abstract type "${targetNodeDef.typeName}") ` +
|
|
1297
1318
|
`must be keyed by the implementing type, e.g. { ${relDef.fieldName}: { Member: [{ where, disconnect }] } }.`);
|
|
1298
|
-
const inner = nestedScope(carryVars, targetVar, depth);
|
|
1319
|
+
const inner = nestedScope(carryVars, targetVar, depth, uniqueToken);
|
|
1299
1320
|
const lines = [];
|
|
1300
1321
|
for (const nestedDisconnect of Array.isArray(nested) ? nested : [nested]) {
|
|
1301
1322
|
if (!isPlainObject(nestedDisconnect))
|
|
@@ -1537,7 +1558,10 @@ export class MutationCompiler {
|
|
|
1537
1558
|
if (nestedDiscConditions.length > 0)
|
|
1538
1559
|
lines.push(`WHERE ${nestedDiscConditions.join(' AND ')}`);
|
|
1539
1560
|
lines.push(`DELETE ${relVar}`);
|
|
1540
|
-
lines.push(...this.buildNestedDisconnect(nestedDisconnect, relDef, targetNodeDef, discTarget, allVars, params, policyContext, paramCounter
|
|
1561
|
+
lines.push(...this.buildNestedDisconnect(nestedDisconnect, relDef, targetNodeDef, discTarget, allVars, params, policyContext, paramCounter, 0,
|
|
1562
|
+
// `discTarget` restarts per member under union dispatch;
|
|
1563
|
+
// `itemPrefix` is unique along the path.
|
|
1564
|
+
`${itemPrefix}_disc${di}`));
|
|
1541
1565
|
}
|
|
1542
1566
|
lines.push(`RETURN count(*) AS _${relVar}`);
|
|
1543
1567
|
lines.push('}');
|
|
@@ -1667,6 +1691,7 @@ export class MutationCompiler {
|
|
|
1667
1691
|
if (val === undefined)
|
|
1668
1692
|
continue;
|
|
1669
1693
|
assertSafePropertyName(prop, 'nested update property');
|
|
1694
|
+
assertKnownField(targetNodeDef, prop, 'nested update');
|
|
1670
1695
|
const nodePropDef = targetNodeDef.properties.get(prop);
|
|
1671
1696
|
const paramName = `${itemPrefix}_set_${prop}`;
|
|
1672
1697
|
setClauses.push(`${updateVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${paramName}`, val, nodePropDef)}`);
|
|
@@ -1723,6 +1748,10 @@ export class MutationCompiler {
|
|
|
1723
1748
|
// `update` policy, exactly like a direct update on that type
|
|
1724
1749
|
// (pre-2.3.0 it enforced no target policy at all).
|
|
1725
1750
|
const updateConditions = [
|
|
1751
|
+
// v2.5.1 — restrict an interface / union target to its member
|
|
1752
|
+
// types; without it the nested update also SET properties on
|
|
1753
|
+
// other nodes on the same relationship type.
|
|
1754
|
+
this.targetMatch(targetNodeDef, updateVar).labelPredicate,
|
|
1726
1755
|
this.compileTargetWhere(updateWhere, updateVar, targetNodeDef, params, policyContext, paramCounter),
|
|
1727
1756
|
this.buildTargetPolicyPredicate(targetNodeDef, updateVar, params, policyContext, paramCounter, 'update'),
|
|
1728
1757
|
].filter((c) => Boolean(c));
|
|
@@ -1969,6 +1998,18 @@ export function unwrapAbstractCreateNode(nodeSpec, target, fieldName) {
|
|
|
1969
1998
|
`e.g. { node: { ${target.members[0] ?? 'Member'}: { … } } }. Allowed: ${target.members.map((m) => `"${m}"`).join(', ')}.`);
|
|
1970
1999
|
return [keys[0], nodeSpec[keys[0]]];
|
|
1971
2000
|
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Reject a write key that is neither a property nor a relationship of the
|
|
2003
|
+
* type (v2.5.0). Pre-2.5.0 only its spelling was checked, so a typo
|
|
2004
|
+
* (`nmae`) was stored as a stray property and the intended field was left
|
|
2005
|
+
* untouched — at every depth (nested creates and updates included).
|
|
2006
|
+
*/
|
|
2007
|
+
function assertKnownField(nodeDef, key, input) {
|
|
2008
|
+
if (nodeDef.properties.has(key) || nodeDef.relationships.has(key))
|
|
2009
|
+
return;
|
|
2010
|
+
throw new OGMError(`Unknown field "${key}" in the ${input} input of ${nodeDef.typeName}: it is neither a property nor a ` +
|
|
2011
|
+
`relationship of the type. Declare it in the schema, or fix the key.`);
|
|
2012
|
+
}
|
|
1972
2013
|
/**
|
|
1973
2014
|
* MERGE writes scalars only. Pre-2.4.8 a relationship field in an upsert's
|
|
1974
2015
|
* `create` / `update` was skipped, so its nested connect / create never
|