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 CHANGED
@@ -1,5 +1,82 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.0 (2026-08-13) โ€” ๐Ÿงญ Writes now store what the schema declares
4
+
5
+ > **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.
6
+ >
7
+ > **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.
8
+
9
+ ### ๐Ÿ› `Int` / `BigInt` writes are bound as Neo4j Integer ([#5](https://github.com/neomodular/grafeo-ogm/issues/5))
10
+
11
+ 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:
12
+
13
+ ```
14
+ Failed to invoke function `apoc.coll.sortMulti`:
15
+ java.lang.ClassCastException: class java.lang.Double cannot be cast to class java.lang.Long
16
+ ```
17
+
18
+ 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.
19
+
20
+ **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.
21
+
22
+ ### ๐Ÿ› Temporal writes are stored as native temporal types
23
+
24
+ `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**.
25
+
26
+ 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:
27
+
28
+ ```cypher
29
+ CREATE (n:`Event` { `startsAt`: datetime($create0_startsAt) })
30
+ SET n.`day` = date($update_day)
31
+ // list fields wrap element-wise:
32
+ `slots`: [wc_t IN $create0_slots | datetime(wc_t)]
33
+ ```
34
+
35
+ 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.
36
+
37
+ ### ๐Ÿ› `BigInt` honours its generated contract in both directions
38
+
39
+ 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:
40
+
41
+ ```typescript
42
+ BigInt: { input: string | number | bigint; output: number | bigint };
43
+ ```
44
+
45
+ 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.
46
+
47
+ ### ๐Ÿ› `@default` is now applied on create
48
+
49
+ `@default` was parsed into schema metadata and consumed by **nothing** โ€” a documented silent no-op. It now applies wherever a node is created:
50
+
51
+ - `create` (nested relationship creates included)
52
+ - `upsert` โ€” `ON CREATE SET` branch only, never `ON MATCH`, and never for MERGE-key fields
53
+ - `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)
54
+
55
+ 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.
56
+
57
+ ### โš ๏ธ Migrating data written before 2.1.0
58
+
59
+ 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:
60
+
61
+ ```cypher
62
+ // String-stored temporals โ†’ native (repeat per temporal field/label)
63
+ MATCH (n:Event) WHERE valueType(n.startsAt) STARTS WITH 'STRING'
64
+ SET n.startsAt = datetime(n.startsAt);
65
+
66
+ // Double-stored Ints โ†’ Long (only needed for Long-sensitive consumers,
67
+ // e.g. apoc.coll.sortMulti โ€” Cypher numeric comparisons already match)
68
+ MATCH (n:ShelfRow) WHERE valueType(n.position) = 'FLOAT'
69
+ SET n.position = toInteger(n.position);
70
+ ```
71
+
72
+ Until the temporal migration runs, WHERE filters on temporal fields will not match old String-stored rows (they compare as cross-type โ†’ `NULL`).
73
+
74
+ ### Tests
75
+
76
+ - `tests/int-write-coercion.spec.ts` โ€” 14 tests pinning Integer coercion across every mutation path.
77
+ - `tests/type-faithful-writes.spec.ts` โ€” 24 tests pinning temporal wrapping (writes + WHERE), integer-string coercion, and `@default` semantics.
78
+ - 12 existing assertions that pinned the old behavior (raw-number edge params, unwrapped temporal comparisons, `DateTime` strings passed verbatim) updated to the corrected contract.
79
+
3
80
  ## 2.0.0 (2026-08-04) โ€” ๐Ÿ’ฅ Inputs that were silently truncated are now rejected
4
81
 
5
82
  > **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":"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"}
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
- setClauses.push(`n.${(0, validation_1.escapeIdentifier)(key)} = $${paramName}`);
187
- params[paramName] = value;
188
+ const valueExpr = (0, write_coercion_1.wrapTemporalWriteExpr)(`$${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
- mergeProps.push(`${(0, validation_1.escapeIdentifier)(key)}: $${paramName}`);
270
+ const valueExpr = (0, write_coercion_1.wrapTemporalWriteExpr)(`$${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
- createSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = $${paramName}`);
306
- params[paramName] = value;
311
+ const valueExpr = (0, write_coercion_1.wrapTemporalWriteExpr)(`$${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.wrapTemporalWriteExpr)(`$${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
- updateSets.push(`n.${(0, validation_1.escapeIdentifier)(key)} = $${paramName}`);
320
- params[paramName] = value;
345
+ const valueExpr = (0, write_coercion_1.wrapTemporalWriteExpr)(`$${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.wrapTemporalListItemExpr)(`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.wrapTemporalListItemExpr)(`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.wrapTemporalWriteExpr)(`$${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.wrapTemporalListItemExpr)(`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.wrapTemporalWriteExpr)(`$${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
- parts.push(`${(0, validation_1.escapeIdentifier)(key)}: $${paramName}`);
461
- propParams[paramName] = value;
525
+ const valueExpr = (0, write_coercion_1.wrapTemporalWriteExpr)(`$${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.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
568
- params[paramName] = val;
651
+ setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
620
- params[paramName] = val;
704
+ setItems.push(`r_conn_${startCounter + counter - 1}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
691
- params[paramName] = val;
776
+ setItems.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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.wrapTemporalListItemExpr)(`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)} = $${paramName}`);
801
- params[paramName] = val;
887
+ setItems.push(`r.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
1116
- params[paramName] = val;
1203
+ setItems.push(`r_conn_${key}_${ci}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
1154
- setParams[paramName] = val;
1242
+ setClauses.push(`${updateVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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)} = $${paramName}`);
1166
- edgeParams[paramName] = val;
1255
+ edgeClauses.push(`${relVar}.${(0, validation_1.escapeIdentifier)(prop)} = ${(0, write_coercion_1.wrapTemporalWriteExpr)(`$${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.wrapTemporalWhereParam)(`$${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} <> $${paramName}`;
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', `$${paramName}`);
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.wrapTemporalWhereParam)(`$${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', `$${paramName}`));
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
- const valueRef = `${itemVar}.where.node.${key}`;
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.wrapTemporalWhereParam)(`${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));