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.
- package/.claude/settings.local.json +2 -1
- package/SQLLiteEngine.js +30 -9
- package/context.js +44 -4
- package/mySQLEngine.js +24 -0
- package/package.json +1 -1
- package/postgresEngine.js +177 -3
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
47
|
-
|
|
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(
|
|
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
|
*/
|