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.
@@ -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, wrapListItemExpr, wrapWhereParam, wrapWriteExpr, } 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
- setClauses.push(`n.${escapeIdentifier(key)} = $${paramName}`);
184
- params[paramName] = value;
185
+ const valueExpr = wrapWriteExpr(`$${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
- mergeProps.push(`${escapeIdentifier(key)}: $${paramName}`);
267
+ const valueExpr = wrapWriteExpr(`$${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
- createSets.push(`n.${escapeIdentifier(key)} = $${paramName}`);
303
- params[paramName] = value;
308
+ const valueExpr = wrapWriteExpr(`$${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 = wrapWriteExpr(`$${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
- updateSets.push(`n.${escapeIdentifier(key)} = $${paramName}`);
317
- params[paramName] = value;
342
+ const valueExpr = wrapWriteExpr(`$${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)}: ${wrapListItemExpr(`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)} = ${wrapListItemExpr(`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)} = ${wrapWriteExpr(`$${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)}: ${wrapListItemExpr(`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)}: ${wrapWriteExpr(`$${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
- parts.push(`${escapeIdentifier(key)}: $${paramName}`);
458
- propParams[paramName] = value;
522
+ const valueExpr = wrapWriteExpr(`$${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 = wrapWriteExpr(`$${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)} = $${paramName}`);
565
- params[paramName] = val;
648
+ setItems.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = $${paramName}`);
617
- params[paramName] = val;
701
+ setItems.push(`r_conn_${startCounter + counter - 1}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = $${paramName}`);
688
- params[paramName] = val;
773
+ setItems.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = ${wrapListItemExpr(`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)} = $${paramName}`);
798
- params[paramName] = val;
884
+ setItems.push(`r.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = $${paramName}`);
1113
- params[paramName] = val;
1200
+ setItems.push(`r_conn_${key}_${ci}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = $${paramName}`);
1151
- setParams[paramName] = val;
1239
+ setClauses.push(`${updateVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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)} = $${paramName}`);
1163
- edgeParams[paramName] = val;
1252
+ edgeClauses.push(`${relVar}.${escapeIdentifier(prop)} = ${wrapWriteExpr(`$${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 = wrapWhereParam(`$${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} <> $${paramName}`;
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', `$${paramName}`);
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 = wrapWhereParam(`$${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', `$${paramName}`));
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
- const valueRef = `${itemVar}.where.node.${key}`;
1544
+ // Temporal wrap keys off the FIRST item's value type — the UNWIND
1545
+ // fast path already requires shape-homogeneous items.
1546
+ const valueRef = wrapWhereParam(`${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));