masterrecord 0.3.0 → 0.3.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.
@@ -27,7 +27,8 @@
27
27
  "Bash(git commit:*)",
28
28
  "Bash(git push)",
29
29
  "Bash(find:*)",
30
- "Bash(npm install)"
30
+ "Bash(npm install)",
31
+ "Bash(ls:*)"
31
32
  ],
32
33
  "deny": [],
33
34
  "ask": []
package/SQLLiteEngine.js CHANGED
@@ -810,15 +810,12 @@ class SQLLiteEngine {
810
810
  } catch(typeError) {
811
811
  throw new Error(`UPDATE failed: ${typeError.message}`);
812
812
  }
813
- var bool;
814
- if(model.__entity[dirtyFields[column]].valueConversion){
815
- bool = tools.convertBooleanToNumber(boolValue);
816
- }
817
- else{
818
- bool = boolValue;
819
- }
813
+
814
+ // Convert to database-specific format (e.g., boolean → 1/0 for SQLite)
815
+ boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
816
+
820
817
  sqlParts.push(`[${dirtyFields[column]}] = ?`);
821
- params.push(bool);
818
+ params.push(boolValue);
822
819
  break;
823
820
  case "time":
824
821
  var timeValue = model[dirtyFields[column]];
@@ -921,7 +918,7 @@ class SQLLiteEngine {
921
918
  throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
922
919
 
923
920
  case "boolean":
924
- // Coerce to boolean
921
+ // Coerce to boolean (then convert for database)
925
922
  if(actualType === 'boolean'){
926
923
  return value;
927
924
  }
@@ -959,6 +956,27 @@ class SQLLiteEngine {
959
956
  }
960
957
  }
961
958
 
959
+ /**
960
+ * Convert validated value to database-specific format
961
+ * Modern ORM pattern: transparent database-specific conversions
962
+ *
963
+ * @param {*} value - Already validated value
964
+ * @param {string} fieldType - Field type from entity definition
965
+ * @returns {*} Database-ready value
966
+ */
967
+ _convertValueForDatabase(value, fieldType){
968
+ if(value === undefined || value === null){
969
+ return value;
970
+ }
971
+
972
+ // SQLite boolean conversion: JavaScript boolean → INTEGER (1/0)
973
+ if(fieldType === 'boolean' && typeof value === 'boolean'){
974
+ return value ? 1 : 0;
975
+ }
976
+
977
+ return value;
978
+ }
979
+
962
980
 
963
981
  // return columns and value strings
964
982
  _buildSQLInsertObject(fields, modelEntity){
@@ -1055,6 +1073,9 @@ class SQLLiteEngine {
1055
1073
  throw new Error(`INSERT failed: ${typeError.message}`);
1056
1074
  }
1057
1075
 
1076
+ // Convert to database-specific format (e.g., boolean → 1/0 for SQLite)
1077
+ fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
1078
+
1058
1079
  var relationship = modelEntity[column].relationshipType;
1059
1080
  var actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
1060
1081
 
package/context.js CHANGED
@@ -456,10 +456,10 @@ class context {
456
456
  for (var model in tracked) {
457
457
  var currentModel = tracked[model];
458
458
  switch(currentModel.__state) {
459
- case "insert":
459
+ case "insert":
460
460
  var insert = new insertManager(this._SQLEngine, this._isModelValid, this.__entities);
461
461
  insert.init(currentModel);
462
-
462
+
463
463
  break;
464
464
  case "modified":
465
465
  if(currentModel.__dirtyFields.length > 0){
@@ -485,13 +485,53 @@ class context {
485
485
  case "delete":
486
486
  var deleteObject = new deleteManager(this._SQLEngine, this.__entities);
487
487
  deleteObject.init(currentModel);
488
-
488
+
489
489
  break;
490
- }
490
+ }
491
491
  }
492
492
  this.__clearErrorHandler();
493
493
  //this._SQLEngine.endTransaction();
494
494
  }
495
+ if(this.isPostgres){
496
+ // PostgreSQL async operations (no transaction control here)
497
+ for (var model in tracked) {
498
+ var currentModel = tracked[model];
499
+ switch(currentModel.__state) {
500
+ case "insert":
501
+ var insert = new insertManager(this._SQLEngine, this._isModelValid, this.__entities);
502
+ insert.init(currentModel);
503
+
504
+ break;
505
+ case "modified":
506
+ if(currentModel.__dirtyFields.length > 0){
507
+ var cleanCurrentModel = tools.removePrimarykeyandVirtual(currentModel, currentModel._entity);
508
+ // Use NEW SECURE parameterized version
509
+ var argu = this._SQLEngine._buildSQLEqualToParameterized(cleanCurrentModel);
510
+ if(argu !== -1 ){
511
+ var primaryKey = tools.getPrimaryKeyObject(cleanCurrentModel.__entity);
512
+ var sqlUpdate = {tableName: cleanCurrentModel.__entity.__name, arg: argu, primaryKey : primaryKey, primaryKeyValue : cleanCurrentModel[primaryKey] };
513
+ this._SQLEngine.update(sqlUpdate);
514
+ }
515
+ else{
516
+ console.log("Nothing has been tracked, modified, created or added");
517
+ }
518
+
519
+ }
520
+ else{
521
+ console.log("Tracked entity modified with no values being changed");
522
+ }
523
+
524
+ // code block
525
+ break;
526
+ case "delete":
527
+ var deleteObject = new deleteManager(this._SQLEngine, this.__entities);
528
+ deleteObject.init(currentModel);
529
+
530
+ break;
531
+ }
532
+ }
533
+ this.__clearErrorHandler();
534
+ }
495
535
  }
496
536
  else{
497
537
  console.log("save changes has no tracked entities");
package/mySQLEngine.js CHANGED
@@ -629,6 +629,7 @@ class SQLLiteEngine {
629
629
  catch(transformError) { throw new Error(`UPDATE failed: ${transformError.message}`); }
630
630
  try { boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]); }
631
631
  catch(typeError) { throw new Error(`UPDATE failed: ${typeError.message}`); }
632
+ boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
632
633
  var bool = model.__entity[dirtyFields[column]].valueConversion ? tools.convertBooleanToNumber(boolValue) : boolValue;
633
634
  sqlParts.push(`${dirtyFields[column]} = ?`);
634
635
  params.push(bool);
@@ -677,6 +678,8 @@ class SQLLiteEngine {
677
678
  try { fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column); }
678
679
  catch(typeError) { throw new Error(`INSERT failed: ${typeError.message}`); }
679
680
 
681
+ fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
682
+
680
683
  var relationship = modelEntity[column].relationshipType;
681
684
  var actualColumn = relationship === "belongsTo" ? modelEntity[column].foreignKey : column;
682
685
  columnNames.push(actualColumn);
@@ -707,6 +710,27 @@ class SQLLiteEngine {
707
710
  }
708
711
  }
709
712
 
713
+ /**
714
+ * Convert validated value to database-specific format
715
+ * Modern ORM pattern: transparent database-specific conversions
716
+ *
717
+ * @param {*} value - Already validated value
718
+ * @param {string} fieldType - Field type from entity definition
719
+ * @returns {*} Database-ready value
720
+ */
721
+ _convertValueForDatabase(value, fieldType){
722
+ if(value === undefined || value === null){
723
+ return value;
724
+ }
725
+
726
+ // MySQL boolean conversion: JavaScript boolean → TINYINT (1/0)
727
+ if(fieldType === 'boolean' && typeof value === 'boolean'){
728
+ return value ? 1 : 0;
729
+ }
730
+
731
+ return value;
732
+ }
733
+
710
734
  /**
711
735
  * Validate and coerce field value to match entity type definition
712
736
  * Throws detailed error if type cannot be coerced
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "masterrecord",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
5
5
  "main": "MasterRecord.js",
6
6
  "bin": {
package/postgresEngine.js CHANGED
@@ -43,10 +43,13 @@ class postgresEngine {
43
43
  * UPDATE with parameterized query
44
44
  */
45
45
  async update(query) {
46
- if (query.arg && query.arg.query && query.arg.params) {
47
- // Parameterized UPDATE
46
+ // Use parameterized query for security
47
+ // query.arg now contains {query, params} from _buildSQLEqualToParameterized
48
+ if (query.arg && typeof query.arg === 'object' && query.arg.query && query.arg.params) {
49
+ const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg.query} WHERE ${query.tableName}.${query.primaryKey} = $${query.arg.params.length + 1}`;
50
+ // Add primaryKeyValue to params array
48
51
  const params = [...query.arg.params, query.primaryKeyValue];
49
- return await this._runWithParams(query.arg.query, params);
52
+ return await this._runWithParams(sqlQuery, params);
50
53
  } else {
51
54
  // Fallback for legacy support
52
55
  const sqlQuery = `UPDATE ${query.tableName} SET ${query.arg} WHERE ${query.tableName}.${query.primaryKey} = $1`;
@@ -381,6 +384,152 @@ class postgresEngine {
381
384
  return name;
382
385
  }
383
386
 
387
+ /**
388
+ * Build SQL SET clause with parameterized queries for UPDATE (PostgreSQL)
389
+ * Returns {query: "column1 = $1, column2 = $2", params: [value1, value2]}
390
+ */
391
+ _buildSQLEqualToParameterized(model) {
392
+ const $that = this;
393
+ const sqlParts = [];
394
+ const params = [];
395
+ const dirtyFields = model.__dirtyFields;
396
+ let paramIndex = 1;
397
+
398
+ for (let column in dirtyFields) {
399
+ const fieldName = dirtyFields[column];
400
+ const entityDef = model.__entity[fieldName];
401
+
402
+ // Check for required fields
403
+ if (entityDef && entityDef.nullable === false && entityDef.primary !== true) {
404
+ let persistedValue;
405
+ switch (entityDef.type) {
406
+ case "integer":
407
+ persistedValue = model["_" + fieldName];
408
+ break;
409
+ case "belongsTo":
410
+ persistedValue = model["_" + fieldName] !== undefined ? model["_" + fieldName] : model[fieldName];
411
+ break;
412
+ default:
413
+ persistedValue = model[fieldName];
414
+ }
415
+ const isEmptyString = (typeof persistedValue === 'string') && (persistedValue.trim() === '');
416
+ if (persistedValue === undefined || persistedValue === null || isEmptyString) {
417
+ throw new Error(`Entity ${model.__entity.__name} column ${fieldName} is a required Field`);
418
+ }
419
+ }
420
+
421
+ let type = model.__entity[dirtyFields[column]].type;
422
+ if (model.__entity[dirtyFields[column]].relationshipType === "belongsTo") {
423
+ type = "belongsTo";
424
+ }
425
+
426
+ switch (type) {
427
+ case "belongsTo":
428
+ const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
429
+ let fkValue = model[dirtyFields[column]];
430
+ // Apply toDatabase transformer
431
+ try {
432
+ fkValue = FieldTransformer.toDatabase(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
433
+ } catch (transformError) {
434
+ throw new Error(`UPDATE failed: ${transformError.message}`);
435
+ }
436
+ try {
437
+ fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
438
+ } catch (typeError) {
439
+ throw new Error(`UPDATE failed: ${typeError.message}`);
440
+ }
441
+ fkValue = $that._convertValueForDatabase(fkValue, model.__entity[dirtyFields[column]].type);
442
+ const fore = `_${dirtyFields[column]}`;
443
+ sqlParts.push(`${foreignKey} = $${paramIndex++}`);
444
+ params.push(model[fore]);
445
+ break;
446
+
447
+ case "integer":
448
+ let intValue = model["_" + dirtyFields[column]];
449
+ // Apply toDatabase transformer
450
+ try {
451
+ intValue = FieldTransformer.toDatabase(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
452
+ } catch (transformError) {
453
+ throw new Error(`UPDATE failed: ${transformError.message}`);
454
+ }
455
+ try {
456
+ intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
457
+ } catch (typeError) {
458
+ throw new Error(`UPDATE failed: ${typeError.message}`);
459
+ }
460
+ intValue = $that._convertValueForDatabase(intValue, model.__entity[dirtyFields[column]].type);
461
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
462
+ params.push(intValue);
463
+ break;
464
+
465
+ case "string":
466
+ let strValue = model[dirtyFields[column]];
467
+ // Apply toDatabase transformer
468
+ try {
469
+ strValue = FieldTransformer.toDatabase(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
470
+ } catch (transformError) {
471
+ throw new Error(`UPDATE failed: ${transformError.message}`);
472
+ }
473
+ try {
474
+ strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
475
+ } catch (typeError) {
476
+ throw new Error(`UPDATE failed: ${typeError.message}`);
477
+ }
478
+ strValue = $that._convertValueForDatabase(strValue, model.__entity[dirtyFields[column]].type);
479
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
480
+ params.push(strValue);
481
+ break;
482
+
483
+ case "boolean":
484
+ let boolValue = model[dirtyFields[column]];
485
+ // Apply toDatabase transformer
486
+ try {
487
+ boolValue = FieldTransformer.toDatabase(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
488
+ } catch (transformError) {
489
+ throw new Error(`UPDATE failed: ${transformError.message}`);
490
+ }
491
+ try {
492
+ boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
493
+ } catch (typeError) {
494
+ throw new Error(`UPDATE failed: ${typeError.message}`);
495
+ }
496
+ boolValue = $that._convertValueForDatabase(boolValue, model.__entity[dirtyFields[column]].type);
497
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
498
+ params.push(boolValue);
499
+ break;
500
+
501
+ case "time":
502
+ let timeValue = model[dirtyFields[column]];
503
+ // Apply toDatabase transformer
504
+ try {
505
+ timeValue = FieldTransformer.toDatabase(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
506
+ } catch (transformError) {
507
+ throw new Error(`UPDATE failed: ${transformError.message}`);
508
+ }
509
+ try {
510
+ timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
511
+ } catch (typeError) {
512
+ throw new Error(`UPDATE failed: ${typeError.message}`);
513
+ }
514
+ timeValue = $that._convertValueForDatabase(timeValue, model.__entity[dirtyFields[column]].type);
515
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
516
+ params.push(timeValue);
517
+ break;
518
+
519
+ case "hasMany":
520
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
521
+ params.push(model[dirtyFields[column]]);
522
+ break;
523
+
524
+ default:
525
+ sqlParts.push(`${dirtyFields[column]} = $${paramIndex++}`);
526
+ params.push(model[dirtyFields[column]]);
527
+ }
528
+ }
529
+
530
+ return sqlParts.length > 0 ? { query: sqlParts.join(', '), params: params } : -1;
531
+ }
532
+
384
533
  /**
385
534
  * Build parameterized INSERT object for PostgreSQL
386
535
  * Uses $1, $2, $3... instead of ?
@@ -410,6 +559,9 @@ class postgresEngine {
410
559
  throw new Error(`INSERT failed: ${typeError.message}`);
411
560
  }
412
561
 
562
+ // Convert to database-specific format
563
+ fieldColumn = $that._convertValueForDatabase(fieldColumn, modelEntity[column].type);
564
+
413
565
  // Skip auto-increment primary keys
414
566
  if (modelEntity[column].auto !== true) {
415
567
  columnNames.push(column);
@@ -506,6 +658,28 @@ class postgresEngine {
506
658
  }
507
659
  }
508
660
 
661
+ /**
662
+ * Convert validated value to database-specific format
663
+ * Modern ORM pattern: transparent database-specific conversions
664
+ *
665
+ * @param {*} value - Already validated value
666
+ * @param {string} fieldType - Field type from entity definition
667
+ * @returns {*} Database-ready value
668
+ */
669
+ _convertValueForDatabase(value, fieldType){
670
+ if(value === undefined || value === null){
671
+ return value;
672
+ }
673
+
674
+ // PostgreSQL accepts native booleans, but we convert to 1/0 for consistency
675
+ // The pg driver will convert to PostgreSQL TRUE/FALSE
676
+ if(fieldType === 'boolean' && typeof value === 'boolean'){
677
+ return value ? 1 : 0;
678
+ }
679
+
680
+ return value;
681
+ }
682
+
509
683
  /**
510
684
  * Execute parameterized query with pg library
511
685
  */