grafeo-ogm 2.0.0 โ 2.1.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 +105 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts +7 -0
- package/dist/cjs/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/mutation.compiler.js +130 -36
- package/dist/cjs/compilers/mutation.compiler.js.map +1 -1
- package/dist/cjs/compilers/where.compiler.d.ts.map +1 -1
- package/dist/cjs/compilers/where.compiler.js +13 -4
- package/dist/cjs/compilers/where.compiler.js.map +1 -1
- package/dist/cjs/generator/type-emitters/utility-types.js +1 -1
- package/dist/cjs/utils/write-coercion.d.ts +62 -0
- package/dist/cjs/utils/write-coercion.d.ts.map +1 -0
- package/dist/cjs/utils/write-coercion.js +283 -0
- package/dist/cjs/utils/write-coercion.js.map +1 -0
- package/dist/esm/compilers/mutation.compiler.d.ts +7 -0
- package/dist/esm/compilers/mutation.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/mutation.compiler.js +130 -36
- package/dist/esm/compilers/mutation.compiler.js.map +1 -1
- package/dist/esm/compilers/where.compiler.d.ts.map +1 -1
- package/dist/esm/compilers/where.compiler.js +13 -4
- package/dist/esm/compilers/where.compiler.js.map +1 -1
- package/dist/esm/generator/type-emitters/utility-types.js +1 -1
- package/dist/esm/utils/write-coercion.d.ts +62 -0
- package/dist/esm/utils/write-coercion.d.ts.map +1 -0
- package/dist/esm/utils/write-coercion.js +276 -0
- package/dist/esm/utils/write-coercion.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,110 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.1 (2026-08-13) โ ๐ Point fields round-trip
|
|
4
|
+
|
|
5
|
+
> **The last member of the 2.1.0 type-faithfulness family.** Reads flatten a native Point into a plain `{ x, y[, z], srid }` object โ but writing that object back bound it as a map property value, which Neo4j rejects at runtime ("Property values can only be of primitive types or arrays thereof"). A `Point` read through the OGM could not be written back through the OGM.
|
|
6
|
+
>
|
|
7
|
+
> **Pure patch:** nothing that previously worked changes behavior. Plain point writes previously failed loudly at the database; they now succeed as the README's scalar table always promised.
|
|
8
|
+
|
|
9
|
+
### ๐ Plain point objects are wrapped in `point()`
|
|
10
|
+
|
|
11
|
+
`Point` / `CartesianPoint` writes now wrap the parameter in Cypher's `point()` constructor whenever the value is a plain point-shaped object โ cartesian (`x`/`y`) or geographic (`longitude`/`latitude`). `point()` accepts the read shape back verbatim, `srid` included, so the read โ write round-trip is exact:
|
|
12
|
+
|
|
13
|
+
```cypher
|
|
14
|
+
CREATE (n:`Event` { `location`: point($create0_location) })
|
|
15
|
+
// list fields wrap element-wise:
|
|
16
|
+
`route`: [wc_t IN $create0_route | point(wc_t)]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
WHERE equality and `_IN`/`_NOT_IN` params wrap the same way, so filters match natively-stored points instead of silently comparing a map against a POINT (`NULL` โ row dropped). Driver `Point` instances still bind raw.
|
|
20
|
+
|
|
21
|
+
Unlike the temporal constructors, `point()` rejects an already-native POINT argument โ so a list or batch column mixing plain maps with driver `Point` instances throws a clear `OGMError` at compile time, where it previously died as an opaque database error.
|
|
22
|
+
|
|
23
|
+
### ๐ง Internal
|
|
24
|
+
|
|
25
|
+
The write-constructor helpers are no longer temporal-specific and were renamed accordingly (`wrapWriteExpr`, `wrapListItemExpr`, `wrapWhereParam`); each constructor family now carries an explicit list rule (`arrayMode: 'any'` for temporals โ `datetime(datetime)` is legal Cypher โ vs `'all'` for points). 2.1.0 temporal behavior is byte-identical.
|
|
26
|
+
|
|
27
|
+
### Tests
|
|
28
|
+
|
|
29
|
+
- 7 new tests in `tests/type-faithful-writes.spec.ts` covering create/update wrapping, geographic + `CartesianPoint` input, driver-instance passthrough, list + mixed-list handling, WHERE `=`/`_IN`, and `createMany` columns.
|
|
30
|
+
|
|
31
|
+
## 2.1.0 (2026-08-13) โ ๐งญ Writes now store what the schema declares
|
|
32
|
+
|
|
33
|
+
> **Every write path now binds values as the Neo4j type the schema promises.** Until now the compilers bound raw JS values and left the rest to the driver โ and the driver packs every JS `number` as a Bolt FLOAT64 and every string as a String. So `Int`/`BigInt` fields landed as **Doubles** ([#5](https://github.com/neomodular/grafeo-ogm/issues/5)), the six temporal types landed as **Strings**, and `BigInt`'s documented string input landed as a **String** โ while reads converted every native value back to a JS primitive, so the corruption was invisible until something type-sensitive touched the property. `@default` rode along: parsed, documented in the README, and applied nowhere.
|
|
34
|
+
>
|
|
35
|
+
> **This is a minor version because behavior changes on inputs that previously "worked":** `position: 1.5` and `plays: 'abc'` now throw instead of silently storing the wrong type, schemas that declare `@default` now actually receive defaults on create, and the generated `BigInt` scalar changes shape. If your inputs already match the documented contract you can upgrade and regenerate directly โ but read the **migration note** below if you have data written through the OGM before 2.1.0.
|
|
36
|
+
|
|
37
|
+
### ๐ `Int` / `BigInt` writes are bound as Neo4j Integer ([#5](https://github.com/neomodular/grafeo-ogm/issues/5))
|
|
38
|
+
|
|
39
|
+
The driver packs every JS `number` as FLOAT64 โ `disableLosslessIntegers` only affects reads โ so every OGM-written `Int` property landed as a Double. Once a field held a mixed Long/Double population (OGM writes vs seed/raw-Cypher writes), Long-typed consumers crashed:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
Failed to invoke function `apoc.coll.sortMulti`:
|
|
43
|
+
java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Long
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Every mutation binding site โ create, update, upsert (MERGE key, `ON CREATE SET`, `ON MATCH SET`), `createMany` items, and all edge-property paths through connect and nested updates (**14 sites** in total; the report named 5) โ now binds `Int`/`BigInt` values through `neo4j.int()`. Numbers, bigints, integer strings, and pre-built `Integer` instances are all accepted; `null` clears as before.
|
|
47
|
+
|
|
48
|
+
**Rejected, not truncated:** a non-integral number (`1.5`) or a non-integer string (`'abc'`) for an integer field now throws `OGMError`. Coercing would silently truncate; binding raw would silently store the wrong type โ both are the defect class 2.0.0 exists to reject.
|
|
49
|
+
|
|
50
|
+
### ๐ Temporal writes are stored as native temporal types
|
|
51
|
+
|
|
52
|
+
`DateTime`, `Date`, `Time`, `LocalTime`, `LocalDateTime`, and `Duration` fields were stored as **String** properties โ the write path had no temporal conversion at all, while the README's scalar table promised native storage and reads converted native temporals to ISO strings. Against natively-typed rows (seed, APOC, raw Cypher), `WHERE n.createdAt > $param` with a string param is a cross-type comparison: Neo4j evaluates it to `NULL` and **silently drops the row**.
|
|
53
|
+
|
|
54
|
+
ISO-string writes are now wrapped in the field's Cypher constructor โ the parameter stays a string and Neo4j does the parsing, which preserves nanosecond precision and bracket timezones (`2024-01-15T10:30:00[America/New_York]`) that a JS `Date` round-trip would destroy:
|
|
55
|
+
|
|
56
|
+
```cypher
|
|
57
|
+
CREATE (n:`Event` { `startsAt`: datetime($create0_startsAt) })
|
|
58
|
+
SET n.`day` = date($update_day)
|
|
59
|
+
// list fields wrap element-wise:
|
|
60
|
+
`slots`: [wc_t IN $create0_slots | datetime(wc_t)]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The same wrapping applies to WHERE params for comparison-shaped operators (`=`, `_NOT`, `_GT`/`_GTE`/`_LT`/`_LTE`, `_IN`/`_NOT_IN`) across the main where compiler and the connect/disconnect where paths, so filters keep matching the natively-stored values. String operators (`_CONTAINS`, `_MATCHES`, โฆ) are untouched. Driver temporal objects still bind raw โ only strings are wrapped. Batched paths (`createMany`, array connect) share one Cypher expression per field, so a column mixing ISO strings with driver objects throws instead of guessing.
|
|
64
|
+
|
|
65
|
+
### ๐ `BigInt` honours its generated contract in both directions
|
|
66
|
+
|
|
67
|
+
The generated scalar declared `BigInt: { input: string; output: string }` โ but a string input sailed through to a **String property**, and reads returned `number` (or `bigint` above 2โตยณ), never `string`. Integer strings now coerce through `neo4j.int()` (for `Int` fields too), and the scalar tells the truth:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
BigInt: { input: string | number | bigint; output: number | bigint };
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Regenerate your types to pick this up. If you annotated variables with the old `string` output type, those annotations were already wrong at runtime โ the regen turns them into compile errors you can fix.
|
|
74
|
+
|
|
75
|
+
### ๐ `@default` is now applied on create
|
|
76
|
+
|
|
77
|
+
`@default` was parsed into schema metadata and consumed by **nothing** โ a documented silent no-op. It now applies wherever a node is created:
|
|
78
|
+
|
|
79
|
+
- `create` (nested relationship creates included)
|
|
80
|
+
- `upsert` โ `ON CREATE SET` branch only, never `ON MATCH`, and never for MERGE-key fields
|
|
81
|
+
- `createMany` โ as a whole-column default when **no** item supplies the field (per-item gaps inside a provided column stay `null`: every batch shares one Cypher text, and `coalesce()` would also override explicit nulls)
|
|
82
|
+
|
|
83
|
+
Defaults convert by declared type โ `@default(value: "5")` on an `Int` binds an Integer, `Boolean` binds `true`/`false`, temporal defaults get their constructor wrapper. **A provided value always wins, and an explicit `null` stays `null`** โ the user asked for null; a default overriding it would betray intent.
|
|
84
|
+
|
|
85
|
+
### โ ๏ธ Migrating data written before 2.1.0
|
|
86
|
+
|
|
87
|
+
Properties written through the OGM before this release carry the old storage types. New writes and filters use native types, so mixed populations should be migrated once, per affected field:
|
|
88
|
+
|
|
89
|
+
```cypher
|
|
90
|
+
// String-stored temporals โ native (repeat per temporal field/label)
|
|
91
|
+
MATCH (n:Event) WHERE valueType(n.startsAt) STARTS WITH 'STRING'
|
|
92
|
+
SET n.startsAt = datetime(n.startsAt);
|
|
93
|
+
|
|
94
|
+
// Double-stored Ints โ Long (only needed for Long-sensitive consumers,
|
|
95
|
+
// e.g. apoc.coll.sortMulti โ Cypher numeric comparisons already match)
|
|
96
|
+
MATCH (n:ShelfRow) WHERE valueType(n.position) = 'FLOAT'
|
|
97
|
+
SET n.position = toInteger(n.position);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Until the temporal migration runs, WHERE filters on temporal fields will not match old String-stored rows (they compare as cross-type โ `NULL`).
|
|
101
|
+
|
|
102
|
+
### Tests
|
|
103
|
+
|
|
104
|
+
- `tests/int-write-coercion.spec.ts` โ 14 tests pinning Integer coercion across every mutation path.
|
|
105
|
+
- `tests/type-faithful-writes.spec.ts` โ 24 tests pinning temporal wrapping (writes + WHERE), integer-string coercion, and `@default` semantics.
|
|
106
|
+
- 12 existing assertions that pinned the old behavior (raw-number edge params, unwrapped temporal comparisons, `DateTime` strings passed verbatim) updated to the corrected contract.
|
|
107
|
+
|
|
3
108
|
## 2.0.0 (2026-08-04) โ ๐ฅ Inputs that were silently truncated are now rejected
|
|
4
109
|
|
|
5
110
|
> **A major release with one theme: grafeo used to read the first key of certain input objects and throw the rest away without a word.** `sort` entries, fulltext leaves, and fulltext logical operators all did it. Every one produced a *successful* query answering a different question than the caller asked โ no exception, no log, nothing in the Cypher to suggest anything was missing.
|
|
@@ -120,6 +120,13 @@ export declare class MutationCompiler {
|
|
|
120
120
|
* Returns count of created nodes.
|
|
121
121
|
*/
|
|
122
122
|
compileCreateMany(data: Record<string, unknown>[], nodeDef: NodeDefinition, skipDuplicates?: boolean, labels?: string[]): MutationResult;
|
|
123
|
+
/**
|
|
124
|
+
* Resolve the PropertyDefinition of an edge (relationship) property so
|
|
125
|
+
* Int/BigInt edge writes get the same Integer coercion as node writes.
|
|
126
|
+
* Returns undefined when the relationship declares no properties type
|
|
127
|
+
* (the coercion then passes the value through untouched).
|
|
128
|
+
*/
|
|
129
|
+
private getEdgePropDef;
|
|
123
130
|
private buildCreateProperties;
|
|
124
131
|
private buildCreateRelationships;
|
|
125
132
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,cAAc,
|
|
1
|
+
{"version":3,"file":"mutation.compiler.d.ts","sourceRoot":"","sources":["../../../src/compilers/mutation.compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,6 +4,7 @@ exports.MutationCompiler = void 0;
|
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
const utils_1 = require("../schema/utils");
|
|
6
6
|
const validation_1 = require("../utils/validation");
|
|
7
|
+
const write_coercion_1 = require("../utils/write-coercion");
|
|
7
8
|
const where_compiler_1 = require("./where.compiler");
|
|
8
9
|
/**
|
|
9
10
|
* Relationship filter suffixes that require EXISTS subquery patterns.
|
|
@@ -182,9 +183,11 @@ class MutationCompiler {
|
|
|
182
183
|
if (value === undefined)
|
|
183
184
|
continue;
|
|
184
185
|
(0, validation_1.assertSafePropertyName)(key, 'update property');
|
|
186
|
+
const propDef = nodeDef.properties.get(key);
|
|
185
187
|
const paramName = `update_${key}`;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
189
|
+
setClauses.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${valueExpr}`);
|
|
190
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
188
191
|
}
|
|
189
192
|
if (setClauses.length > 0)
|
|
190
193
|
lines.push(`SET ${setClauses.join(', ')}`);
|
|
@@ -262,10 +265,12 @@ class MutationCompiler {
|
|
|
262
265
|
if (key.includes('_') && !nodeDef.properties.has(key))
|
|
263
266
|
continue;
|
|
264
267
|
(0, validation_1.assertSafePropertyName)(key, 'merge key');
|
|
268
|
+
const propDef = nodeDef.properties.get(key);
|
|
265
269
|
const paramName = `merge_${key}`;
|
|
266
|
-
|
|
270
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
271
|
+
mergeProps.push(`${(0, validation_1.escapeIdentifier)(key)}: ${valueExpr}`);
|
|
267
272
|
mergeKeyNames.push(key);
|
|
268
|
-
params[paramName] = value;
|
|
273
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
269
274
|
}
|
|
270
275
|
if (mergeProps.length === 0)
|
|
271
276
|
throw new errors_1.OGMError('upsert requires at least one scalar property in "where" for MERGE key');
|
|
@@ -301,9 +306,29 @@ class MutationCompiler {
|
|
|
301
306
|
if (nodeDef.relationships.has(key))
|
|
302
307
|
continue;
|
|
303
308
|
(0, validation_1.assertSafePropertyName)(key, 'create property');
|
|
309
|
+
const propDef = nodeDef.properties.get(key);
|
|
304
310
|
const paramName = `onCreate_${key}`;
|
|
305
|
-
|
|
306
|
-
|
|
311
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
312
|
+
createSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${valueExpr}`);
|
|
313
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
314
|
+
}
|
|
315
|
+
// `@default` values apply to the ON CREATE branch โ a defaulted
|
|
316
|
+
// property not supplied in `create` and not already fixed by the
|
|
317
|
+
// MERGE key gets its default, mirroring create().
|
|
318
|
+
for (const [, propDef] of nodeDef.properties) {
|
|
319
|
+
if (propDef.defaultValue === undefined)
|
|
320
|
+
continue;
|
|
321
|
+
if (propDef.isGenerated || propDef.isCypher)
|
|
322
|
+
continue;
|
|
323
|
+
if (create[propDef.name] !== undefined)
|
|
324
|
+
continue;
|
|
325
|
+
if (mergeKeyNames.includes(propDef.name))
|
|
326
|
+
continue;
|
|
327
|
+
const paramName = `onCreate_${propDef.name}`;
|
|
328
|
+
const bound = (0, write_coercion_1.resolveDefaultWriteValue)(propDef);
|
|
329
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef);
|
|
330
|
+
createSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${valueExpr}`);
|
|
331
|
+
params[paramName] = bound;
|
|
307
332
|
}
|
|
308
333
|
if (createSets.length > 0)
|
|
309
334
|
lines.push(`ON CREATE SET ${createSets.join(', ')}`);
|
|
@@ -315,9 +340,11 @@ class MutationCompiler {
|
|
|
315
340
|
if (nodeDef.relationships.has(key))
|
|
316
341
|
continue;
|
|
317
342
|
(0, validation_1.assertSafePropertyName)(key, 'update property');
|
|
343
|
+
const propDef = nodeDef.properties.get(key);
|
|
318
344
|
const paramName = `onMatch_${key}`;
|
|
319
|
-
|
|
320
|
-
|
|
345
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
346
|
+
updateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${valueExpr}`);
|
|
347
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
321
348
|
}
|
|
322
349
|
if (updateSets.length > 0)
|
|
323
350
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
@@ -389,12 +416,24 @@ class MutationCompiler {
|
|
|
389
416
|
for (const [, propDef] of nodeDef.properties)
|
|
390
417
|
if (propDef.isGenerated && !scalarKeys.includes(propDef.name))
|
|
391
418
|
generatedFields.push(propDef.name);
|
|
419
|
+
// Whole-column `@default`s: applied only when NO item supplies the
|
|
420
|
+
// field. Per-item gaps inside a provided column stay null โ one
|
|
421
|
+
// createMany batch shares a single Cypher text, and a per-item
|
|
422
|
+
// default would need coalesce(), which would also override explicit
|
|
423
|
+
// nulls.
|
|
424
|
+
const defaultedProps = [];
|
|
425
|
+
for (const [, propDef] of nodeDef.properties)
|
|
426
|
+
if (propDef.defaultValue !== undefined &&
|
|
427
|
+
!propDef.isGenerated &&
|
|
428
|
+
!propDef.isCypher &&
|
|
429
|
+
!scalarKeys.includes(propDef.name))
|
|
430
|
+
defaultedProps.push(propDef);
|
|
392
431
|
// Build sanitized items (strip undefined values)
|
|
393
432
|
const sanitizedItems = data.map((item) => {
|
|
394
433
|
const sanitized = {};
|
|
395
434
|
for (const key of scalarKeys)
|
|
396
435
|
if (item[key] !== undefined)
|
|
397
|
-
sanitized[key] = item[key];
|
|
436
|
+
sanitized[key] = (0, write_coercion_1.coerceWriteValue)(item[key], nodeDef.properties.get(key));
|
|
398
437
|
return sanitized;
|
|
399
438
|
});
|
|
400
439
|
params.items = sanitizedItems;
|
|
@@ -411,16 +450,22 @@ class MutationCompiler {
|
|
|
411
450
|
throw new errors_1.OGMError('createMany with skipDuplicates requires at least one @id or @unique field in data');
|
|
412
451
|
// MERGE on unique key(s)
|
|
413
452
|
const mergeProps = mergeKeys
|
|
414
|
-
.map((k) => `${(0, validation_1.escapeIdentifier)(k)}: item.${(0, validation_1.escapeIdentifier)(k)}`)
|
|
453
|
+
.map((k) => `${(0, validation_1.escapeIdentifier)(k)}: ${(0, write_coercion_1.wrapListItemExpr)(`item.${(0, validation_1.escapeIdentifier)(k)}`, data.map((d) => d[k]), nodeDef.properties.get(k))}`)
|
|
415
454
|
.join(', ');
|
|
416
455
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps} })`);
|
|
417
456
|
// ON CREATE SET โ remaining fields + generated IDs
|
|
418
457
|
const onCreateSets = [];
|
|
419
458
|
for (const key of scalarKeys)
|
|
420
459
|
if (!mergeKeys.includes(key))
|
|
421
|
-
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = item.${(0, validation_1.escapeIdentifier)(key)}`);
|
|
460
|
+
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = ${(0, write_coercion_1.wrapListItemExpr)(`item.${(0, validation_1.escapeIdentifier)(key)}`, data.map((d) => d[key]), nodeDef.properties.get(key))}`);
|
|
422
461
|
for (const field of generatedFields)
|
|
423
462
|
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(field)} = randomUUID()`);
|
|
463
|
+
for (const propDef of defaultedProps) {
|
|
464
|
+
const paramName = `default_${propDef.name}`;
|
|
465
|
+
const bound = (0, write_coercion_1.resolveDefaultWriteValue)(propDef);
|
|
466
|
+
onCreateSets.push(`n.${(0, validation_1.escapeIdentifier)(propDef.name)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
|
|
467
|
+
params[paramName] = bound;
|
|
468
|
+
}
|
|
424
469
|
if (onCreateSets.length > 0)
|
|
425
470
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
426
471
|
}
|
|
@@ -430,7 +475,13 @@ class MutationCompiler {
|
|
|
430
475
|
for (const field of generatedFields)
|
|
431
476
|
propParts.push(`${(0, validation_1.escapeIdentifier)(field)}: randomUUID()`);
|
|
432
477
|
for (const key of scalarKeys)
|
|
433
|
-
propParts.push(`${(0, validation_1.escapeIdentifier)(key)}: item.${(0, validation_1.escapeIdentifier)(key)}`);
|
|
478
|
+
propParts.push(`${(0, validation_1.escapeIdentifier)(key)}: ${(0, write_coercion_1.wrapListItemExpr)(`item.${(0, validation_1.escapeIdentifier)(key)}`, data.map((d) => d[key]), nodeDef.properties.get(key))}`);
|
|
479
|
+
for (const propDef of defaultedProps) {
|
|
480
|
+
const paramName = `default_${propDef.name}`;
|
|
481
|
+
const bound = (0, write_coercion_1.resolveDefaultWriteValue)(propDef);
|
|
482
|
+
propParts.push(`${(0, validation_1.escapeIdentifier)(propDef.name)}: ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef)}`);
|
|
483
|
+
params[paramName] = bound;
|
|
484
|
+
}
|
|
434
485
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
435
486
|
}
|
|
436
487
|
// Extra labels
|
|
@@ -442,6 +493,19 @@ class MutationCompiler {
|
|
|
442
493
|
return { cypher: lines.join('\n'), params };
|
|
443
494
|
}
|
|
444
495
|
// โโโ Private helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
496
|
+
/**
|
|
497
|
+
* Resolve the PropertyDefinition of an edge (relationship) property so
|
|
498
|
+
* Int/BigInt edge writes get the same Integer coercion as node writes.
|
|
499
|
+
* Returns undefined when the relationship declares no properties type
|
|
500
|
+
* (the coercion then passes the value through untouched).
|
|
501
|
+
*/
|
|
502
|
+
getEdgePropDef(relDef, propName) {
|
|
503
|
+
if (!relDef.properties)
|
|
504
|
+
return undefined;
|
|
505
|
+
return this.schema.relationshipProperties
|
|
506
|
+
.get(relDef.properties)
|
|
507
|
+
?.properties.get(propName);
|
|
508
|
+
}
|
|
445
509
|
buildCreateProperties(input, nodeDef, prefix) {
|
|
446
510
|
const parts = [];
|
|
447
511
|
const propParams = {};
|
|
@@ -456,9 +520,28 @@ class MutationCompiler {
|
|
|
456
520
|
if (value === undefined)
|
|
457
521
|
continue;
|
|
458
522
|
(0, validation_1.assertSafePropertyName)(key, 'create property');
|
|
523
|
+
const propDef = nodeDef.properties.get(key);
|
|
459
524
|
const paramName = `${prefix}_${key}`;
|
|
460
|
-
|
|
461
|
-
|
|
525
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, value, propDef);
|
|
526
|
+
parts.push(`${(0, validation_1.escapeIdentifier)(key)}: ${valueExpr}`);
|
|
527
|
+
propParams[paramName] = (0, write_coercion_1.coerceWriteValue)(value, propDef);
|
|
528
|
+
}
|
|
529
|
+
// `@default` values โ applied for any property with a default that the
|
|
530
|
+
// input leaves undefined. Explicit null is respected as null: the user
|
|
531
|
+
// asked for null, a default would override intent. Generated (@id) and
|
|
532
|
+
// @cypher (computed, not stored) properties never take defaults.
|
|
533
|
+
for (const [, propDef] of nodeDef.properties) {
|
|
534
|
+
if (propDef.defaultValue === undefined)
|
|
535
|
+
continue;
|
|
536
|
+
if (propDef.isGenerated || propDef.isCypher)
|
|
537
|
+
continue;
|
|
538
|
+
if (input[propDef.name] !== undefined)
|
|
539
|
+
continue;
|
|
540
|
+
const paramName = `${prefix}_${propDef.name}`;
|
|
541
|
+
const bound = (0, write_coercion_1.resolveDefaultWriteValue)(propDef);
|
|
542
|
+
const valueExpr = (0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, bound, propDef);
|
|
543
|
+
parts.push(`${(0, validation_1.escapeIdentifier)(propDef.name)}: ${valueExpr}`);
|
|
544
|
+
propParams[paramName] = bound;
|
|
462
545
|
}
|
|
463
546
|
return { propString: parts.join(', '), propParams };
|
|
464
547
|
}
|
|
@@ -563,9 +646,10 @@ class MutationCompiler {
|
|
|
563
646
|
if (val === undefined)
|
|
564
647
|
continue;
|
|
565
648
|
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
649
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
566
650
|
const paramName = `${prefix}_create${ci}_edge_${prop}`;
|
|
567
|
-
setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
568
|
-
params[paramName] = val;
|
|
651
|
+
setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
652
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
569
653
|
}
|
|
570
654
|
if (setItems.length > 0)
|
|
571
655
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -615,9 +699,10 @@ class MutationCompiler {
|
|
|
615
699
|
if (val === undefined)
|
|
616
700
|
continue;
|
|
617
701
|
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
702
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
618
703
|
const paramName = `${prefix}_conn${ci}_edge_${prop}`;
|
|
619
|
-
setItems.push(`r_conn_${startCounter + counter - 1}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
620
|
-
params[paramName] = val;
|
|
704
|
+
setItems.push(`r_conn_${startCounter + counter - 1}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
705
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
621
706
|
}
|
|
622
707
|
if (setItems.length > 0)
|
|
623
708
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -686,9 +771,10 @@ class MutationCompiler {
|
|
|
686
771
|
if (val === undefined)
|
|
687
772
|
continue;
|
|
688
773
|
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
774
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
689
775
|
const paramName = `connect_${fieldName}_${ci}_edge_${prop}`;
|
|
690
|
-
setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
691
|
-
params[paramName] = val;
|
|
776
|
+
setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
777
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
692
778
|
}
|
|
693
779
|
if (setItems.length > 0)
|
|
694
780
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -716,7 +802,7 @@ class MutationCompiler {
|
|
|
716
802
|
const cleaned = {};
|
|
717
803
|
for (const [k, v] of Object.entries(edge))
|
|
718
804
|
if (v !== undefined)
|
|
719
|
-
cleaned[k] = v;
|
|
805
|
+
cleaned[k] = (0, write_coercion_1.coerceWriteValue)(v, this.getEdgePropDef(relDef, k));
|
|
720
806
|
return { ...item, edge: cleaned };
|
|
721
807
|
});
|
|
722
808
|
lines.push('WITH n');
|
|
@@ -740,7 +826,7 @@ class MutationCompiler {
|
|
|
740
826
|
`silently dropped. Split into separate connect calls, or normalise the items ` +
|
|
741
827
|
`to share the same shape.`);
|
|
742
828
|
}
|
|
743
|
-
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem');
|
|
829
|
+
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem', targetNodeDef);
|
|
744
830
|
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter);
|
|
745
831
|
const unwindConditions = policyPredicate
|
|
746
832
|
? [...whereConditions, policyPredicate]
|
|
@@ -765,7 +851,7 @@ class MutationCompiler {
|
|
|
765
851
|
// Replace MERGE line with one that has a variable
|
|
766
852
|
lines[lines.length - 1] = `MERGE ${mergeWithVar}`;
|
|
767
853
|
// r.prop is relationship property (escape), connItem.edge.prop is parameter map (no escape)
|
|
768
|
-
const setItems = edgeProps.map((p) => `r.${(0, validation_1.escapeIdentifier)(p)} = connItem.edge.${p}`);
|
|
854
|
+
const setItems = edgeProps.map((p) => `r.${(0, validation_1.escapeIdentifier)(p)} = ${(0, write_coercion_1.wrapListItemExpr)(`connItem.edge.${p}`, spec.map((it) => it.edge?.[p]), this.getEdgePropDef(relDef, p))}`);
|
|
769
855
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
770
856
|
}
|
|
771
857
|
}
|
|
@@ -796,9 +882,10 @@ class MutationCompiler {
|
|
|
796
882
|
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
797
883
|
if (val === undefined)
|
|
798
884
|
continue;
|
|
885
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
799
886
|
const paramName = `connect_${fieldName}_edge_${prop}`;
|
|
800
|
-
setItems.push(`r.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
801
|
-
params[paramName] = val;
|
|
887
|
+
setItems.push(`r.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
888
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
802
889
|
}
|
|
803
890
|
if (setItems.length > 0)
|
|
804
891
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -1111,9 +1198,10 @@ class MutationCompiler {
|
|
|
1111
1198
|
if (val === undefined)
|
|
1112
1199
|
continue;
|
|
1113
1200
|
(0, validation_1.assertSafePropertyName)(prop, 'edge property');
|
|
1201
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1114
1202
|
const paramName = `${itemPrefix}_conn${ci}_edge_${prop}`;
|
|
1115
|
-
setItems.push(`r_conn_${key}_${ci}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
1116
|
-
params[paramName] = val;
|
|
1203
|
+
setItems.push(`r_conn_${key}_${ci}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1204
|
+
params[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1117
1205
|
}
|
|
1118
1206
|
if (setItems.length > 0)
|
|
1119
1207
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -1149,9 +1237,10 @@ class MutationCompiler {
|
|
|
1149
1237
|
if (val === undefined)
|
|
1150
1238
|
continue;
|
|
1151
1239
|
(0, validation_1.assertSafePropertyName)(prop, 'nested update property');
|
|
1240
|
+
const nodePropDef = targetNodeDef.properties.get(prop);
|
|
1152
1241
|
const paramName = `${itemPrefix}_set_${prop}`;
|
|
1153
|
-
setClauses.push(`${updateVar}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
1154
|
-
setParams[paramName] = val;
|
|
1242
|
+
setClauses.push(`${updateVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, nodePropDef)}`);
|
|
1243
|
+
setParams[paramName] = (0, write_coercion_1.coerceWriteValue)(val, nodePropDef);
|
|
1155
1244
|
}
|
|
1156
1245
|
// Pre-compute SET clauses for edge (relationship) properties
|
|
1157
1246
|
const edgeClauses = [];
|
|
@@ -1161,9 +1250,10 @@ class MutationCompiler {
|
|
|
1161
1250
|
if (val === undefined)
|
|
1162
1251
|
continue;
|
|
1163
1252
|
(0, validation_1.assertSafePropertyName)(prop, 'edge update property');
|
|
1253
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1164
1254
|
const paramName = `${itemPrefix}_edge_${prop}`;
|
|
1165
|
-
edgeClauses.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} =
|
|
1166
|
-
edgeParams[paramName] = val;
|
|
1255
|
+
edgeClauses.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapWriteExpr)(`$${paramName}`, val, edgePropDef)}`);
|
|
1256
|
+
edgeParams[paramName] = (0, write_coercion_1.coerceWriteValue)(val, edgePropDef);
|
|
1167
1257
|
}
|
|
1168
1258
|
// Pre-compute nested relationship operations
|
|
1169
1259
|
const nestedParams = {};
|
|
@@ -1249,15 +1339,16 @@ class MutationCompiler {
|
|
|
1249
1339
|
(0, validation_1.assertSafePropertyName)(baseProp, 'where property');
|
|
1250
1340
|
const paramName = `${prefix}_NOT_${notProp}`;
|
|
1251
1341
|
const escapedProp = (0, validation_1.escapeIdentifier)(baseProp);
|
|
1342
|
+
const paramRef = (0, write_coercion_1.wrapWhereParam)(`$${paramName}`, notVal, targetNodeDef?.properties.get(baseProp), notProp.slice(baseProp.length));
|
|
1252
1343
|
let expr;
|
|
1253
1344
|
if (template === '%v = $%p')
|
|
1254
1345
|
// Simple equality โ negate to <>
|
|
1255
|
-
expr = `${targetVar}.${escapedProp} <>
|
|
1346
|
+
expr = `${targetVar}.${escapedProp} <> ${paramRef}`;
|
|
1256
1347
|
else {
|
|
1257
1348
|
// Operator suffix โ wrap with NOT if not already negated
|
|
1258
1349
|
const resolved = template
|
|
1259
1350
|
.replace('%v', `${targetVar}.${escapedProp}`)
|
|
1260
|
-
.replace('$%p',
|
|
1351
|
+
.replace('$%p', paramRef);
|
|
1261
1352
|
expr = resolved.startsWith('NOT ') ? resolved : `NOT ${resolved}`;
|
|
1262
1353
|
}
|
|
1263
1354
|
conditions.push(expr);
|
|
@@ -1274,9 +1365,10 @@ class MutationCompiler {
|
|
|
1274
1365
|
const { baseProp, template } = this.parseOperatorSuffix(prop);
|
|
1275
1366
|
(0, validation_1.assertSafePropertyName)(baseProp, 'where property');
|
|
1276
1367
|
const paramName = `${prefix}_${prop}`;
|
|
1368
|
+
const paramRef = (0, write_coercion_1.wrapWhereParam)(`$${paramName}`, val, targetNodeDef?.properties.get(baseProp), prop.slice(baseProp.length));
|
|
1277
1369
|
conditions.push(template
|
|
1278
1370
|
.replace('%v', `${targetVar}.${(0, validation_1.escapeIdentifier)(baseProp)}`)
|
|
1279
|
-
.replace('$%p',
|
|
1371
|
+
.replace('$%p', paramRef));
|
|
1280
1372
|
params[paramName] = val;
|
|
1281
1373
|
}
|
|
1282
1374
|
return conditions;
|
|
@@ -1441,7 +1533,7 @@ class MutationCompiler {
|
|
|
1441
1533
|
}
|
|
1442
1534
|
return false;
|
|
1443
1535
|
}
|
|
1444
|
-
extractConnectWhereConditions(item, targetVar, itemVar) {
|
|
1536
|
+
extractConnectWhereConditions(item, targetVar, itemVar, targetNodeDef) {
|
|
1445
1537
|
const conditions = [];
|
|
1446
1538
|
const whereSpec = item.where;
|
|
1447
1539
|
if (!whereSpec)
|
|
@@ -1452,7 +1544,9 @@ class MutationCompiler {
|
|
|
1452
1544
|
(0, validation_1.assertSafePropertyName)(baseProp, 'connect where property');
|
|
1453
1545
|
// Replace template placeholders with dynamic UNWIND references
|
|
1454
1546
|
// Note: itemVar map access is NOT escaped (parameter map keys, not Cypher identifiers)
|
|
1455
|
-
|
|
1547
|
+
// Temporal wrap keys off the FIRST item's value type โ the UNWIND
|
|
1548
|
+
// fast path already requires shape-homogeneous items.
|
|
1549
|
+
const valueRef = (0, write_coercion_1.wrapWhereParam)(`${itemVar}.where.node.${key}`, nodeWhere[key], targetNodeDef?.properties.get(baseProp), key.slice(baseProp.length));
|
|
1456
1550
|
conditions.push(template
|
|
1457
1551
|
.replace('%v', `${targetVar}.${(0, validation_1.escapeIdentifier)(baseProp)}`)
|
|
1458
1552
|
.replace('$%p', valueRef));
|