grafeo-ogm 2.0.0 → 2.1.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 +77 -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 +61 -0
- package/dist/cjs/utils/write-coercion.d.ts.map +1 -0
- package/dist/cjs/utils/write-coercion.js +213 -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 +61 -0
- package/dist/esm/utils/write-coercion.d.ts.map +1 -0
- package/dist/esm/utils/write-coercion.js +206 -0
- package/dist/esm/utils/write-coercion.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.coerceWriteValue = coerceWriteValue;
|
|
4
|
+
exports.wrapTemporalWriteExpr = wrapTemporalWriteExpr;
|
|
5
|
+
exports.wrapTemporalListItemExpr = wrapTemporalListItemExpr;
|
|
6
|
+
exports.wrapTemporalWhereParam = wrapTemporalWhereParam;
|
|
7
|
+
exports.resolveDefaultWriteValue = resolveDefaultWriteValue;
|
|
8
|
+
const neo4j_driver_1 = require("neo4j-driver");
|
|
9
|
+
const errors_1 = require("../errors");
|
|
10
|
+
/**
|
|
11
|
+
* GraphQL scalar types whose Neo4j storage type is INTEGER (Long).
|
|
12
|
+
*/
|
|
13
|
+
const INTEGER_SCALAR_TYPES = new Set(['Int', 'BigInt']);
|
|
14
|
+
/**
|
|
15
|
+
* Temporal scalar types → the Cypher constructor that parses their ISO
|
|
16
|
+
* string form into the native Neo4j temporal type. Writes wrap the
|
|
17
|
+
* *parameter* in the constructor instead of converting the value in JS,
|
|
18
|
+
* because Neo4j's own parsers handle everything JS `Date` cannot:
|
|
19
|
+
* nanosecond precision, bracket timezone names
|
|
20
|
+
* (`2024-01-15T10:30:00[America/New_York]`), and ISO-8601 durations.
|
|
21
|
+
*/
|
|
22
|
+
const TEMPORAL_CONSTRUCTORS = {
|
|
23
|
+
DateTime: 'datetime',
|
|
24
|
+
LocalDateTime: 'localdatetime',
|
|
25
|
+
Date: 'date',
|
|
26
|
+
Time: 'time',
|
|
27
|
+
LocalTime: 'localtime',
|
|
28
|
+
Duration: 'duration',
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Where-operator suffixes whose parameter is compared against the field
|
|
32
|
+
* value (`''` = plain equality) and therefore needs the same temporal
|
|
33
|
+
* constructor treatment as writes. String operators (`_CONTAINS`,
|
|
34
|
+
* `_STARTS_WITH`, `_MATCHES`, ...) are excluded — their params are
|
|
35
|
+
* strings by definition.
|
|
36
|
+
*/
|
|
37
|
+
const TEMPORAL_COMPARABLE_SUFFIXES = new Set([
|
|
38
|
+
'',
|
|
39
|
+
'_NOT',
|
|
40
|
+
'_GT',
|
|
41
|
+
'_GTE',
|
|
42
|
+
'_LT',
|
|
43
|
+
'_LTE',
|
|
44
|
+
'_IN',
|
|
45
|
+
'_NOT_IN',
|
|
46
|
+
]);
|
|
47
|
+
/**
|
|
48
|
+
* Coerce a JS write value bound for an `Int`/`BigInt`-typed schema field
|
|
49
|
+
* into a Neo4j Integer so the stored property is a Long, not a Double.
|
|
50
|
+
*
|
|
51
|
+
* The driver packs EVERY JS `number` as a Bolt FLOAT64 (packstream:
|
|
52
|
+
* `typeof x === 'number'` → `packFloat`) — `disableLosslessIntegers`
|
|
53
|
+
* only affects the read direction. Reads convert Integer → number
|
|
54
|
+
* (ResultMapper), so without write-side coercion the round-trip is
|
|
55
|
+
* asymmetric: every OGM-written Int property lands as a Double. Once a
|
|
56
|
+
* field holds a mixed Long/Double population, Long-typed consumers break
|
|
57
|
+
* (e.g. `apoc.coll.sortMulti` throws `java.lang.Double cannot be cast to
|
|
58
|
+
* class java.lang.Long`).
|
|
59
|
+
*
|
|
60
|
+
* Non-integral numbers and non-integer strings for Int/BigInt fields
|
|
61
|
+
* throw instead of truncating or storing a String property, matching the
|
|
62
|
+
* v1.14 contract of rejecting inputs that would be silently corrupted.
|
|
63
|
+
* Values the coercion doesn't own — null/undefined, driver Integer
|
|
64
|
+
* instances, fields of any other scalar type, or fields absent from the
|
|
65
|
+
* schema — pass through untouched.
|
|
66
|
+
*/
|
|
67
|
+
function coerceWriteValue(value, propDef) {
|
|
68
|
+
if (propDef === undefined || value === null || value === undefined)
|
|
69
|
+
return value;
|
|
70
|
+
if (!INTEGER_SCALAR_TYPES.has(propDef.type))
|
|
71
|
+
return value;
|
|
72
|
+
if (propDef.isArray) {
|
|
73
|
+
if (!Array.isArray(value))
|
|
74
|
+
return value;
|
|
75
|
+
return value.map((element) => coerceIntegerElement(element, propDef));
|
|
76
|
+
}
|
|
77
|
+
return coerceIntegerElement(value, propDef);
|
|
78
|
+
}
|
|
79
|
+
function coerceIntegerElement(value, propDef) {
|
|
80
|
+
if (value === null || value === undefined)
|
|
81
|
+
return value;
|
|
82
|
+
// `int()` accepts strings in every driver major — `toString()` keeps
|
|
83
|
+
// bigint inputs working regardless of the installed driver's overloads.
|
|
84
|
+
if (typeof value === 'bigint')
|
|
85
|
+
return (0, neo4j_driver_1.int)(value.toString());
|
|
86
|
+
if (typeof value === 'number') {
|
|
87
|
+
if (!Number.isInteger(value))
|
|
88
|
+
throw new errors_1.OGMError(`Expected an integer for ${propDef.type} field "${propDef.name}" but got ${value}. ` +
|
|
89
|
+
`Storing it would corrupt the property's integer type; pass a whole number ` +
|
|
90
|
+
`(or declare the field as Float).`);
|
|
91
|
+
return (0, neo4j_driver_1.int)(value);
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
// The generated BigInt scalar declares `input: string` (64-bit values
|
|
95
|
+
// don't fit a JS number), so integer strings are the CANONICAL input
|
|
96
|
+
// shape for BigInt fields — binding them raw would store a String
|
|
97
|
+
// property. Same reject-don't-corrupt rule as numbers for anything
|
|
98
|
+
// that isn't a plain base-10 integer literal.
|
|
99
|
+
if (!/^-?\d+$/.test(value))
|
|
100
|
+
throw new errors_1.OGMError(`Expected an integer for ${propDef.type} field "${propDef.name}" but got the string "${value}". ` +
|
|
101
|
+
`Pass a base-10 integer literal string, a whole number, or a bigint.`);
|
|
102
|
+
return (0, neo4j_driver_1.int)(value);
|
|
103
|
+
}
|
|
104
|
+
// Already a driver Integer — pass through unchanged.
|
|
105
|
+
if ((0, neo4j_driver_1.isInt)(value))
|
|
106
|
+
return value;
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Wrap a write RHS expression (`$param`, or an UNWIND item reference) in
|
|
111
|
+
* the field's temporal Cypher constructor, so ISO-string inputs are
|
|
112
|
+
* stored as native temporal properties instead of Strings. Reads convert
|
|
113
|
+
* native temporals → ISO strings (ResultMapper), so this is the write
|
|
114
|
+
* half of the same round-trip symmetry issue #5 fixed for integers.
|
|
115
|
+
*
|
|
116
|
+
* Only string values are wrapped — a driver temporal object already
|
|
117
|
+
* packs natively and binds raw. Temporal LIST fields wrap element-wise
|
|
118
|
+
* via a list comprehension when any element is a string.
|
|
119
|
+
*/
|
|
120
|
+
function wrapTemporalWriteExpr(expr, value, propDef) {
|
|
121
|
+
if (propDef === undefined)
|
|
122
|
+
return expr;
|
|
123
|
+
const ctor = TEMPORAL_CONSTRUCTORS[propDef.type];
|
|
124
|
+
if (ctor === undefined)
|
|
125
|
+
return expr;
|
|
126
|
+
if (propDef.isArray) {
|
|
127
|
+
if (!Array.isArray(value) || !value.some((el) => typeof el === 'string'))
|
|
128
|
+
return expr;
|
|
129
|
+
return `[wc_t IN ${expr} | ${ctor}(wc_t)]`;
|
|
130
|
+
}
|
|
131
|
+
if (typeof value !== 'string')
|
|
132
|
+
return expr;
|
|
133
|
+
return `${ctor}(${expr})`;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Temporal wrapping for UNWIND item references (`item.prop`,
|
|
137
|
+
* `connItem.edge.prop`), where ONE Cypher expression serves EVERY item in
|
|
138
|
+
* the batch: the wrap decision must hold for all items' values for the
|
|
139
|
+
* key. A mixed string/non-string column throws — a single expression
|
|
140
|
+
* cannot be correct for both representations.
|
|
141
|
+
*/
|
|
142
|
+
function wrapTemporalListItemExpr(expr, values, propDef) {
|
|
143
|
+
if (propDef === undefined)
|
|
144
|
+
return expr;
|
|
145
|
+
const ctor = TEMPORAL_CONSTRUCTORS[propDef.type];
|
|
146
|
+
if (ctor === undefined)
|
|
147
|
+
return expr;
|
|
148
|
+
const defined = values.filter((v) => v !== null && v !== undefined);
|
|
149
|
+
if (defined.length === 0)
|
|
150
|
+
return expr;
|
|
151
|
+
if (propDef.isArray) {
|
|
152
|
+
const anyString = defined.some((v) => Array.isArray(v) && v.some((el) => typeof el === 'string'));
|
|
153
|
+
return anyString ? `[wc_t IN ${expr} | ${ctor}(wc_t)]` : expr;
|
|
154
|
+
}
|
|
155
|
+
const stringCount = defined.filter((v) => typeof v === 'string').length;
|
|
156
|
+
if (stringCount === 0)
|
|
157
|
+
return expr;
|
|
158
|
+
if (stringCount !== defined.length)
|
|
159
|
+
throw new errors_1.OGMError(`Mixed input types for ${propDef.type} field "${propDef.name}": some items pass ISO ` +
|
|
160
|
+
`strings, others pass non-string values. Batched writes share one Cypher expression ` +
|
|
161
|
+
`per field, so every item must use the same representation.`);
|
|
162
|
+
return `${ctor}(${expr})`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Wrap a WHERE parameter reference for a temporal field. With properties
|
|
166
|
+
* stored as native temporals, comparing them to a raw string param is a
|
|
167
|
+
* cross-type comparison — Neo4j evaluates it to NULL and silently drops
|
|
168
|
+
* the row. `suffix` is the operator suffix (`''` for plain equality);
|
|
169
|
+
* only comparison-shaped operators wrap, and `_IN`/`_NOT_IN` wrap their
|
|
170
|
+
* list param element-wise.
|
|
171
|
+
*/
|
|
172
|
+
function wrapTemporalWhereParam(paramRef, value, propDef, suffix) {
|
|
173
|
+
if (propDef === undefined)
|
|
174
|
+
return paramRef;
|
|
175
|
+
const ctor = TEMPORAL_CONSTRUCTORS[propDef.type];
|
|
176
|
+
if (ctor === undefined)
|
|
177
|
+
return paramRef;
|
|
178
|
+
if (!TEMPORAL_COMPARABLE_SUFFIXES.has(suffix))
|
|
179
|
+
return paramRef;
|
|
180
|
+
if (suffix === '_IN' || suffix === '_NOT_IN') {
|
|
181
|
+
if (!Array.isArray(value) || !value.some((el) => typeof el === 'string'))
|
|
182
|
+
return paramRef;
|
|
183
|
+
return `[wc_t IN ${paramRef} | ${ctor}(wc_t)]`;
|
|
184
|
+
}
|
|
185
|
+
if (typeof value !== 'string')
|
|
186
|
+
return paramRef;
|
|
187
|
+
return `${ctor}(${paramRef})`;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Materialize a parsed `@default` into a driver-ready bound value. The
|
|
191
|
+
* parser stores every default as a string (`'true'`, `'5'`, `'WELCOME'`);
|
|
192
|
+
* conversion is by declared scalar type, and Int/BigInt defaults route
|
|
193
|
+
* through the same Integer coercion as user input. Temporal defaults stay
|
|
194
|
+
* ISO strings here — they get their constructor wrapper at the binding
|
|
195
|
+
* site like any other temporal write.
|
|
196
|
+
*/
|
|
197
|
+
function resolveDefaultWriteValue(propDef) {
|
|
198
|
+
const raw = propDef.defaultValue;
|
|
199
|
+
if (raw === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
switch (propDef.type) {
|
|
202
|
+
case 'Boolean':
|
|
203
|
+
return raw === 'true';
|
|
204
|
+
case 'Float':
|
|
205
|
+
return Number(raw);
|
|
206
|
+
case 'Int':
|
|
207
|
+
case 'BigInt':
|
|
208
|
+
return coerceWriteValue(raw, propDef);
|
|
209
|
+
default:
|
|
210
|
+
return raw;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=write-coercion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write-coercion.js","sourceRoot":"","sources":["../../../src/utils/write-coercion.ts"],"names":[],"mappings":"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"}
|
|
@@ -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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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OGMError } from '../errors';
|
|
2
2
|
import { buildRelPattern, getTargetLabelString, resolveTargetDef, } from '../schema/utils';
|
|
3
3
|
import { assertSafeIdentifier, assertSafeLabel, assertSafePropertyName, escapeIdentifier, mergeParams, } from '../utils/validation';
|
|
4
|
+
import { coerceWriteValue, resolveDefaultWriteValue, wrapTemporalListItemExpr, wrapTemporalWhereParam, wrapTemporalWriteExpr, } from '../utils/write-coercion';
|
|
4
5
|
import { WhereCompiler } from './where.compiler';
|
|
5
6
|
/**
|
|
6
7
|
* Relationship filter suffixes that require EXISTS subquery patterns.
|
|
@@ -179,9 +180,11 @@ export class MutationCompiler {
|
|
|
179
180
|
if (value === undefined)
|
|
180
181
|
continue;
|
|
181
182
|
assertSafePropertyName(key, 'update property');
|
|
183
|
+
const propDef = nodeDef.properties.get(key);
|
|
182
184
|
const paramName = `update_${key}`;
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, value, propDef);
|
|
186
|
+
setClauses.push(`n.${escapeIdentifier(key)} = ${valueExpr}`);
|
|
187
|
+
params[paramName] = coerceWriteValue(value, propDef);
|
|
185
188
|
}
|
|
186
189
|
if (setClauses.length > 0)
|
|
187
190
|
lines.push(`SET ${setClauses.join(', ')}`);
|
|
@@ -259,10 +262,12 @@ export class MutationCompiler {
|
|
|
259
262
|
if (key.includes('_') && !nodeDef.properties.has(key))
|
|
260
263
|
continue;
|
|
261
264
|
assertSafePropertyName(key, 'merge key');
|
|
265
|
+
const propDef = nodeDef.properties.get(key);
|
|
262
266
|
const paramName = `merge_${key}`;
|
|
263
|
-
|
|
267
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, value, propDef);
|
|
268
|
+
mergeProps.push(`${escapeIdentifier(key)}: ${valueExpr}`);
|
|
264
269
|
mergeKeyNames.push(key);
|
|
265
|
-
params[paramName] = value;
|
|
270
|
+
params[paramName] = coerceWriteValue(value, propDef);
|
|
266
271
|
}
|
|
267
272
|
if (mergeProps.length === 0)
|
|
268
273
|
throw new OGMError('upsert requires at least one scalar property in "where" for MERGE key');
|
|
@@ -298,9 +303,29 @@ export class MutationCompiler {
|
|
|
298
303
|
if (nodeDef.relationships.has(key))
|
|
299
304
|
continue;
|
|
300
305
|
assertSafePropertyName(key, 'create property');
|
|
306
|
+
const propDef = nodeDef.properties.get(key);
|
|
301
307
|
const paramName = `onCreate_${key}`;
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, value, propDef);
|
|
309
|
+
createSets.push(`n.${escapeIdentifier(key)} = ${valueExpr}`);
|
|
310
|
+
params[paramName] = coerceWriteValue(value, propDef);
|
|
311
|
+
}
|
|
312
|
+
// `@default` values apply to the ON CREATE branch — a defaulted
|
|
313
|
+
// property not supplied in `create` and not already fixed by the
|
|
314
|
+
// MERGE key gets its default, mirroring create().
|
|
315
|
+
for (const [, propDef] of nodeDef.properties) {
|
|
316
|
+
if (propDef.defaultValue === undefined)
|
|
317
|
+
continue;
|
|
318
|
+
if (propDef.isGenerated || propDef.isCypher)
|
|
319
|
+
continue;
|
|
320
|
+
if (create[propDef.name] !== undefined)
|
|
321
|
+
continue;
|
|
322
|
+
if (mergeKeyNames.includes(propDef.name))
|
|
323
|
+
continue;
|
|
324
|
+
const paramName = `onCreate_${propDef.name}`;
|
|
325
|
+
const bound = resolveDefaultWriteValue(propDef);
|
|
326
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, bound, propDef);
|
|
327
|
+
createSets.push(`n.${escapeIdentifier(propDef.name)} = ${valueExpr}`);
|
|
328
|
+
params[paramName] = bound;
|
|
304
329
|
}
|
|
305
330
|
if (createSets.length > 0)
|
|
306
331
|
lines.push(`ON CREATE SET ${createSets.join(', ')}`);
|
|
@@ -312,9 +337,11 @@ export class MutationCompiler {
|
|
|
312
337
|
if (nodeDef.relationships.has(key))
|
|
313
338
|
continue;
|
|
314
339
|
assertSafePropertyName(key, 'update property');
|
|
340
|
+
const propDef = nodeDef.properties.get(key);
|
|
315
341
|
const paramName = `onMatch_${key}`;
|
|
316
|
-
|
|
317
|
-
|
|
342
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, value, propDef);
|
|
343
|
+
updateSets.push(`n.${escapeIdentifier(key)} = ${valueExpr}`);
|
|
344
|
+
params[paramName] = coerceWriteValue(value, propDef);
|
|
318
345
|
}
|
|
319
346
|
if (updateSets.length > 0)
|
|
320
347
|
lines.push(`ON MATCH SET ${updateSets.join(', ')}`);
|
|
@@ -386,12 +413,24 @@ export class MutationCompiler {
|
|
|
386
413
|
for (const [, propDef] of nodeDef.properties)
|
|
387
414
|
if (propDef.isGenerated && !scalarKeys.includes(propDef.name))
|
|
388
415
|
generatedFields.push(propDef.name);
|
|
416
|
+
// Whole-column `@default`s: applied only when NO item supplies the
|
|
417
|
+
// field. Per-item gaps inside a provided column stay null — one
|
|
418
|
+
// createMany batch shares a single Cypher text, and a per-item
|
|
419
|
+
// default would need coalesce(), which would also override explicit
|
|
420
|
+
// nulls.
|
|
421
|
+
const defaultedProps = [];
|
|
422
|
+
for (const [, propDef] of nodeDef.properties)
|
|
423
|
+
if (propDef.defaultValue !== undefined &&
|
|
424
|
+
!propDef.isGenerated &&
|
|
425
|
+
!propDef.isCypher &&
|
|
426
|
+
!scalarKeys.includes(propDef.name))
|
|
427
|
+
defaultedProps.push(propDef);
|
|
389
428
|
// Build sanitized items (strip undefined values)
|
|
390
429
|
const sanitizedItems = data.map((item) => {
|
|
391
430
|
const sanitized = {};
|
|
392
431
|
for (const key of scalarKeys)
|
|
393
432
|
if (item[key] !== undefined)
|
|
394
|
-
sanitized[key] = item[key];
|
|
433
|
+
sanitized[key] = coerceWriteValue(item[key], nodeDef.properties.get(key));
|
|
395
434
|
return sanitized;
|
|
396
435
|
});
|
|
397
436
|
params.items = sanitizedItems;
|
|
@@ -408,16 +447,22 @@ export class MutationCompiler {
|
|
|
408
447
|
throw new OGMError('createMany with skipDuplicates requires at least one @id or @unique field in data');
|
|
409
448
|
// MERGE on unique key(s)
|
|
410
449
|
const mergeProps = mergeKeys
|
|
411
|
-
.map((k) => `${escapeIdentifier(k)}: item.${escapeIdentifier(k)}`)
|
|
450
|
+
.map((k) => `${escapeIdentifier(k)}: ${wrapTemporalListItemExpr(`item.${escapeIdentifier(k)}`, data.map((d) => d[k]), nodeDef.properties.get(k))}`)
|
|
412
451
|
.join(', ');
|
|
413
452
|
lines.push(`MERGE (n:${labelStr} { ${mergeProps} })`);
|
|
414
453
|
// ON CREATE SET — remaining fields + generated IDs
|
|
415
454
|
const onCreateSets = [];
|
|
416
455
|
for (const key of scalarKeys)
|
|
417
456
|
if (!mergeKeys.includes(key))
|
|
418
|
-
onCreateSets.push(`n.${escapeIdentifier(key)} = item.${escapeIdentifier(key)}`);
|
|
457
|
+
onCreateSets.push(`n.${escapeIdentifier(key)} = ${wrapTemporalListItemExpr(`item.${escapeIdentifier(key)}`, data.map((d) => d[key]), nodeDef.properties.get(key))}`);
|
|
419
458
|
for (const field of generatedFields)
|
|
420
459
|
onCreateSets.push(`n.${escapeIdentifier(field)} = randomUUID()`);
|
|
460
|
+
for (const propDef of defaultedProps) {
|
|
461
|
+
const paramName = `default_${propDef.name}`;
|
|
462
|
+
const bound = resolveDefaultWriteValue(propDef);
|
|
463
|
+
onCreateSets.push(`n.${escapeIdentifier(propDef.name)} = ${wrapTemporalWriteExpr(`$${paramName}`, bound, propDef)}`);
|
|
464
|
+
params[paramName] = bound;
|
|
465
|
+
}
|
|
421
466
|
if (onCreateSets.length > 0)
|
|
422
467
|
lines.push(`ON CREATE SET ${onCreateSets.join(', ')}`);
|
|
423
468
|
}
|
|
@@ -427,7 +472,13 @@ export class MutationCompiler {
|
|
|
427
472
|
for (const field of generatedFields)
|
|
428
473
|
propParts.push(`${escapeIdentifier(field)}: randomUUID()`);
|
|
429
474
|
for (const key of scalarKeys)
|
|
430
|
-
propParts.push(`${escapeIdentifier(key)}: item.${escapeIdentifier(key)}`);
|
|
475
|
+
propParts.push(`${escapeIdentifier(key)}: ${wrapTemporalListItemExpr(`item.${escapeIdentifier(key)}`, data.map((d) => d[key]), nodeDef.properties.get(key))}`);
|
|
476
|
+
for (const propDef of defaultedProps) {
|
|
477
|
+
const paramName = `default_${propDef.name}`;
|
|
478
|
+
const bound = resolveDefaultWriteValue(propDef);
|
|
479
|
+
propParts.push(`${escapeIdentifier(propDef.name)}: ${wrapTemporalWriteExpr(`$${paramName}`, bound, propDef)}`);
|
|
480
|
+
params[paramName] = bound;
|
|
481
|
+
}
|
|
431
482
|
lines.push(`CREATE (n:${labelStr} { ${propParts.join(', ')} })`);
|
|
432
483
|
}
|
|
433
484
|
// Extra labels
|
|
@@ -439,6 +490,19 @@ export class MutationCompiler {
|
|
|
439
490
|
return { cypher: lines.join('\n'), params };
|
|
440
491
|
}
|
|
441
492
|
// ─── Private helpers ──────────────────────────────────────────────
|
|
493
|
+
/**
|
|
494
|
+
* Resolve the PropertyDefinition of an edge (relationship) property so
|
|
495
|
+
* Int/BigInt edge writes get the same Integer coercion as node writes.
|
|
496
|
+
* Returns undefined when the relationship declares no properties type
|
|
497
|
+
* (the coercion then passes the value through untouched).
|
|
498
|
+
*/
|
|
499
|
+
getEdgePropDef(relDef, propName) {
|
|
500
|
+
if (!relDef.properties)
|
|
501
|
+
return undefined;
|
|
502
|
+
return this.schema.relationshipProperties
|
|
503
|
+
.get(relDef.properties)
|
|
504
|
+
?.properties.get(propName);
|
|
505
|
+
}
|
|
442
506
|
buildCreateProperties(input, nodeDef, prefix) {
|
|
443
507
|
const parts = [];
|
|
444
508
|
const propParams = {};
|
|
@@ -453,9 +517,28 @@ export class MutationCompiler {
|
|
|
453
517
|
if (value === undefined)
|
|
454
518
|
continue;
|
|
455
519
|
assertSafePropertyName(key, 'create property');
|
|
520
|
+
const propDef = nodeDef.properties.get(key);
|
|
456
521
|
const paramName = `${prefix}_${key}`;
|
|
457
|
-
|
|
458
|
-
|
|
522
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, value, propDef);
|
|
523
|
+
parts.push(`${escapeIdentifier(key)}: ${valueExpr}`);
|
|
524
|
+
propParams[paramName] = coerceWriteValue(value, propDef);
|
|
525
|
+
}
|
|
526
|
+
// `@default` values — applied for any property with a default that the
|
|
527
|
+
// input leaves undefined. Explicit null is respected as null: the user
|
|
528
|
+
// asked for null, a default would override intent. Generated (@id) and
|
|
529
|
+
// @cypher (computed, not stored) properties never take defaults.
|
|
530
|
+
for (const [, propDef] of nodeDef.properties) {
|
|
531
|
+
if (propDef.defaultValue === undefined)
|
|
532
|
+
continue;
|
|
533
|
+
if (propDef.isGenerated || propDef.isCypher)
|
|
534
|
+
continue;
|
|
535
|
+
if (input[propDef.name] !== undefined)
|
|
536
|
+
continue;
|
|
537
|
+
const paramName = `${prefix}_${propDef.name}`;
|
|
538
|
+
const bound = resolveDefaultWriteValue(propDef);
|
|
539
|
+
const valueExpr = wrapTemporalWriteExpr(`$${paramName}`, bound, propDef);
|
|
540
|
+
parts.push(`${escapeIdentifier(propDef.name)}: ${valueExpr}`);
|
|
541
|
+
propParams[paramName] = bound;
|
|
459
542
|
}
|
|
460
543
|
return { propString: parts.join(', '), propParams };
|
|
461
544
|
}
|
|
@@ -560,9 +643,10 @@ export class MutationCompiler {
|
|
|
560
643
|
if (val === undefined)
|
|
561
644
|
continue;
|
|
562
645
|
assertSafePropertyName(prop, 'edge property');
|
|
646
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
563
647
|
const paramName = `${prefix}_create${ci}_edge_${prop}`;
|
|
564
|
-
setItems.push(`${relVar}.${escapeIdentifier(prop)} =
|
|
565
|
-
params[paramName] = val;
|
|
648
|
+
setItems.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
649
|
+
params[paramName] = coerceWriteValue(val, edgePropDef);
|
|
566
650
|
}
|
|
567
651
|
if (setItems.length > 0)
|
|
568
652
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -612,9 +696,10 @@ export class MutationCompiler {
|
|
|
612
696
|
if (val === undefined)
|
|
613
697
|
continue;
|
|
614
698
|
assertSafePropertyName(prop, 'edge property');
|
|
699
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
615
700
|
const paramName = `${prefix}_conn${ci}_edge_${prop}`;
|
|
616
|
-
setItems.push(`r_conn_${startCounter + counter - 1}.${escapeIdentifier(prop)} =
|
|
617
|
-
params[paramName] = val;
|
|
701
|
+
setItems.push(`r_conn_${startCounter + counter - 1}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
702
|
+
params[paramName] = coerceWriteValue(val, edgePropDef);
|
|
618
703
|
}
|
|
619
704
|
if (setItems.length > 0)
|
|
620
705
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -683,9 +768,10 @@ export class MutationCompiler {
|
|
|
683
768
|
if (val === undefined)
|
|
684
769
|
continue;
|
|
685
770
|
assertSafePropertyName(prop, 'edge property');
|
|
771
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
686
772
|
const paramName = `connect_${fieldName}_${ci}_edge_${prop}`;
|
|
687
|
-
setItems.push(`${relVar}.${escapeIdentifier(prop)} =
|
|
688
|
-
params[paramName] = val;
|
|
773
|
+
setItems.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
774
|
+
params[paramName] = coerceWriteValue(val, edgePropDef);
|
|
689
775
|
}
|
|
690
776
|
if (setItems.length > 0)
|
|
691
777
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -713,7 +799,7 @@ export class MutationCompiler {
|
|
|
713
799
|
const cleaned = {};
|
|
714
800
|
for (const [k, v] of Object.entries(edge))
|
|
715
801
|
if (v !== undefined)
|
|
716
|
-
cleaned[k] = v;
|
|
802
|
+
cleaned[k] = coerceWriteValue(v, this.getEdgePropDef(relDef, k));
|
|
717
803
|
return { ...item, edge: cleaned };
|
|
718
804
|
});
|
|
719
805
|
lines.push('WITH n');
|
|
@@ -737,7 +823,7 @@ export class MutationCompiler {
|
|
|
737
823
|
`silently dropped. Split into separate connect calls, or normalise the items ` +
|
|
738
824
|
`to share the same shape.`);
|
|
739
825
|
}
|
|
740
|
-
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem');
|
|
826
|
+
const whereConditions = this.extractConnectWhereConditions(firstItem, 'target', 'connItem', targetNodeDef);
|
|
741
827
|
const policyPredicate = this.buildTargetPolicyPredicate(targetNodeDef, 'target', params, policyContext, paramCounter);
|
|
742
828
|
const unwindConditions = policyPredicate
|
|
743
829
|
? [...whereConditions, policyPredicate]
|
|
@@ -762,7 +848,7 @@ export class MutationCompiler {
|
|
|
762
848
|
// Replace MERGE line with one that has a variable
|
|
763
849
|
lines[lines.length - 1] = `MERGE ${mergeWithVar}`;
|
|
764
850
|
// r.prop is relationship property (escape), connItem.edge.prop is parameter map (no escape)
|
|
765
|
-
const setItems = edgeProps.map((p) => `r.${escapeIdentifier(p)} = connItem.edge.${p}`);
|
|
851
|
+
const setItems = edgeProps.map((p) => `r.${escapeIdentifier(p)} = ${wrapTemporalListItemExpr(`connItem.edge.${p}`, spec.map((it) => it.edge?.[p]), this.getEdgePropDef(relDef, p))}`);
|
|
766
852
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
767
853
|
}
|
|
768
854
|
}
|
|
@@ -793,9 +879,10 @@ export class MutationCompiler {
|
|
|
793
879
|
for (const [prop, val] of Object.entries(edgeInput)) {
|
|
794
880
|
if (val === undefined)
|
|
795
881
|
continue;
|
|
882
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
796
883
|
const paramName = `connect_${fieldName}_edge_${prop}`;
|
|
797
|
-
setItems.push(`r.${escapeIdentifier(prop)} =
|
|
798
|
-
params[paramName] = val;
|
|
884
|
+
setItems.push(`r.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
885
|
+
params[paramName] = coerceWriteValue(val, edgePropDef);
|
|
799
886
|
}
|
|
800
887
|
if (setItems.length > 0)
|
|
801
888
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -1108,9 +1195,10 @@ export class MutationCompiler {
|
|
|
1108
1195
|
if (val === undefined)
|
|
1109
1196
|
continue;
|
|
1110
1197
|
assertSafePropertyName(prop, 'edge property');
|
|
1198
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1111
1199
|
const paramName = `${itemPrefix}_conn${ci}_edge_${prop}`;
|
|
1112
|
-
setItems.push(`r_conn_${key}_${ci}.${escapeIdentifier(prop)} =
|
|
1113
|
-
params[paramName] = val;
|
|
1200
|
+
setItems.push(`r_conn_${key}_${ci}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
1201
|
+
params[paramName] = coerceWriteValue(val, edgePropDef);
|
|
1114
1202
|
}
|
|
1115
1203
|
if (setItems.length > 0)
|
|
1116
1204
|
lines.push(`SET ${setItems.join(', ')}`);
|
|
@@ -1146,9 +1234,10 @@ export class MutationCompiler {
|
|
|
1146
1234
|
if (val === undefined)
|
|
1147
1235
|
continue;
|
|
1148
1236
|
assertSafePropertyName(prop, 'nested update property');
|
|
1237
|
+
const nodePropDef = targetNodeDef.properties.get(prop);
|
|
1149
1238
|
const paramName = `${itemPrefix}_set_${prop}`;
|
|
1150
|
-
setClauses.push(`${updateVar}.${escapeIdentifier(prop)} =
|
|
1151
|
-
setParams[paramName] = val;
|
|
1239
|
+
setClauses.push(`${updateVar}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, nodePropDef)}`);
|
|
1240
|
+
setParams[paramName] = coerceWriteValue(val, nodePropDef);
|
|
1152
1241
|
}
|
|
1153
1242
|
// Pre-compute SET clauses for edge (relationship) properties
|
|
1154
1243
|
const edgeClauses = [];
|
|
@@ -1158,9 +1247,10 @@ export class MutationCompiler {
|
|
|
1158
1247
|
if (val === undefined)
|
|
1159
1248
|
continue;
|
|
1160
1249
|
assertSafePropertyName(prop, 'edge update property');
|
|
1250
|
+
const edgePropDef = this.getEdgePropDef(relDef, prop);
|
|
1161
1251
|
const paramName = `${itemPrefix}_edge_${prop}`;
|
|
1162
|
-
edgeClauses.push(`${relVar}.${escapeIdentifier(prop)} =
|
|
1163
|
-
edgeParams[paramName] = val;
|
|
1252
|
+
edgeClauses.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapTemporalWriteExpr(`$${paramName}`, val, edgePropDef)}`);
|
|
1253
|
+
edgeParams[paramName] = coerceWriteValue(val, edgePropDef);
|
|
1164
1254
|
}
|
|
1165
1255
|
// Pre-compute nested relationship operations
|
|
1166
1256
|
const nestedParams = {};
|
|
@@ -1246,15 +1336,16 @@ export class MutationCompiler {
|
|
|
1246
1336
|
assertSafePropertyName(baseProp, 'where property');
|
|
1247
1337
|
const paramName = `${prefix}_NOT_${notProp}`;
|
|
1248
1338
|
const escapedProp = escapeIdentifier(baseProp);
|
|
1339
|
+
const paramRef = wrapTemporalWhereParam(`$${paramName}`, notVal, targetNodeDef?.properties.get(baseProp), notProp.slice(baseProp.length));
|
|
1249
1340
|
let expr;
|
|
1250
1341
|
if (template === '%v = $%p')
|
|
1251
1342
|
// Simple equality → negate to <>
|
|
1252
|
-
expr = `${targetVar}.${escapedProp} <>
|
|
1343
|
+
expr = `${targetVar}.${escapedProp} <> ${paramRef}`;
|
|
1253
1344
|
else {
|
|
1254
1345
|
// Operator suffix — wrap with NOT if not already negated
|
|
1255
1346
|
const resolved = template
|
|
1256
1347
|
.replace('%v', `${targetVar}.${escapedProp}`)
|
|
1257
|
-
.replace('$%p',
|
|
1348
|
+
.replace('$%p', paramRef);
|
|
1258
1349
|
expr = resolved.startsWith('NOT ') ? resolved : `NOT ${resolved}`;
|
|
1259
1350
|
}
|
|
1260
1351
|
conditions.push(expr);
|
|
@@ -1271,9 +1362,10 @@ export class MutationCompiler {
|
|
|
1271
1362
|
const { baseProp, template } = this.parseOperatorSuffix(prop);
|
|
1272
1363
|
assertSafePropertyName(baseProp, 'where property');
|
|
1273
1364
|
const paramName = `${prefix}_${prop}`;
|
|
1365
|
+
const paramRef = wrapTemporalWhereParam(`$${paramName}`, val, targetNodeDef?.properties.get(baseProp), prop.slice(baseProp.length));
|
|
1274
1366
|
conditions.push(template
|
|
1275
1367
|
.replace('%v', `${targetVar}.${escapeIdentifier(baseProp)}`)
|
|
1276
|
-
.replace('$%p',
|
|
1368
|
+
.replace('$%p', paramRef));
|
|
1277
1369
|
params[paramName] = val;
|
|
1278
1370
|
}
|
|
1279
1371
|
return conditions;
|
|
@@ -1438,7 +1530,7 @@ export class MutationCompiler {
|
|
|
1438
1530
|
}
|
|
1439
1531
|
return false;
|
|
1440
1532
|
}
|
|
1441
|
-
extractConnectWhereConditions(item, targetVar, itemVar) {
|
|
1533
|
+
extractConnectWhereConditions(item, targetVar, itemVar, targetNodeDef) {
|
|
1442
1534
|
const conditions = [];
|
|
1443
1535
|
const whereSpec = item.where;
|
|
1444
1536
|
if (!whereSpec)
|
|
@@ -1449,7 +1541,9 @@ export class MutationCompiler {
|
|
|
1449
1541
|
assertSafePropertyName(baseProp, 'connect where property');
|
|
1450
1542
|
// Replace template placeholders with dynamic UNWIND references
|
|
1451
1543
|
// Note: itemVar map access is NOT escaped (parameter map keys, not Cypher identifiers)
|
|
1452
|
-
|
|
1544
|
+
// Temporal wrap keys off the FIRST item's value type — the UNWIND
|
|
1545
|
+
// fast path already requires shape-homogeneous items.
|
|
1546
|
+
const valueRef = wrapTemporalWhereParam(`${itemVar}.where.node.${key}`, nodeWhere[key], targetNodeDef?.properties.get(baseProp), key.slice(baseProp.length));
|
|
1453
1547
|
conditions.push(template
|
|
1454
1548
|
.replace('%v', `${targetVar}.${escapeIdentifier(baseProp)}`)
|
|
1455
1549
|
.replace('$%p', valueRef));
|