masterrecord 0.2.32 → 0.2.33
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/SQLLiteEngine.js +144 -10
- package/mySQLEngine.js +145 -11
- package/package.json +1 -1
- package/readme.md +92 -2
package/SQLLiteEngine.js
CHANGED
|
@@ -571,29 +571,64 @@ class SQLLiteEngine {
|
|
|
571
571
|
switch(type){
|
|
572
572
|
case "belongsTo" :
|
|
573
573
|
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
574
|
-
|
|
574
|
+
let fkValue = model[dirtyFields[column]];
|
|
575
|
+
// 🔥 NEW: Validate foreign key type
|
|
576
|
+
try {
|
|
577
|
+
fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
578
|
+
} catch(typeError) {
|
|
579
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
580
|
+
}
|
|
581
|
+
argument = `${foreignKey} = ${fkValue},`;
|
|
575
582
|
break;
|
|
576
583
|
case "integer" :
|
|
577
584
|
//model.__entity[dirtyFields[column]].skipGetFunction = true;
|
|
578
585
|
var columneValue = model[`_${dirtyFields[column]}`];
|
|
579
|
-
|
|
586
|
+
var intValue = columneValue !== undefined ? columneValue : model[dirtyFields[column]];
|
|
587
|
+
// 🔥 NEW: Validate integer type
|
|
588
|
+
try {
|
|
589
|
+
intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
590
|
+
} catch(typeError) {
|
|
591
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
592
|
+
}
|
|
593
|
+
argument = argument === null ? `[${dirtyFields[column]}] = ${intValue},` : `${argument} [${dirtyFields[column]}] = ${intValue},`;
|
|
580
594
|
//model.__entity[dirtyFields[column]].skipGetFunction = false;
|
|
581
595
|
break;
|
|
582
596
|
case "string" :
|
|
583
|
-
|
|
597
|
+
var strValue = model[dirtyFields[column]];
|
|
598
|
+
// 🔥 NEW: Validate string type
|
|
599
|
+
try {
|
|
600
|
+
strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
601
|
+
} catch(typeError) {
|
|
602
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
603
|
+
}
|
|
604
|
+
argument = argument === null ? `[${dirtyFields[column]}] = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} [${dirtyFields[column]}] = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
|
|
584
605
|
break;
|
|
585
606
|
case "boolean" :
|
|
586
607
|
var bool = "";
|
|
608
|
+
var boolValue = model[dirtyFields[column]];
|
|
609
|
+
// 🔥 NEW: Validate boolean type
|
|
610
|
+
try {
|
|
611
|
+
boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
612
|
+
} catch(typeError) {
|
|
613
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
614
|
+
}
|
|
587
615
|
if(model.__entity[dirtyFields[column]].valueConversion){
|
|
588
|
-
bool = tools.convertBooleanToNumber(
|
|
616
|
+
bool = tools.convertBooleanToNumber(boolValue);
|
|
589
617
|
}
|
|
590
618
|
else{
|
|
591
|
-
bool =
|
|
619
|
+
bool = boolValue;
|
|
592
620
|
}
|
|
593
621
|
argument = argument === null ? `[${dirtyFields[column]}] = '${bool}',` : `${argument} [${dirtyFields[column]}] = '${bool}',`;
|
|
594
622
|
break;
|
|
595
623
|
case "time" :
|
|
596
|
-
|
|
624
|
+
var timeValue = model[dirtyFields[column]];
|
|
625
|
+
// 🔥 NEW: Validate time type
|
|
626
|
+
try {
|
|
627
|
+
timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
628
|
+
} catch(typeError) {
|
|
629
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
630
|
+
}
|
|
631
|
+
argument = argument === null ? `[${dirtyFields[column]}] = '${timeValue}',` : `${argument} [${dirtyFields[column]}] = '${timeValue}',`;
|
|
597
632
|
break;
|
|
598
633
|
case "belongsTo" :
|
|
599
634
|
var fore = `_${dirtyFields[column]}`;
|
|
@@ -624,6 +659,98 @@ class SQLLiteEngine {
|
|
|
624
659
|
return {tableName: tableName, primaryKey : primaryKey, value : value};
|
|
625
660
|
}
|
|
626
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Validate and coerce field value to match entity type definition
|
|
664
|
+
* Throws detailed error if type cannot be coerced
|
|
665
|
+
* @param {*} value - The field value to validate
|
|
666
|
+
* @param {object} entityDef - The entity definition for this field
|
|
667
|
+
* @param {string} entityName - Name of the entity (for error messages)
|
|
668
|
+
* @param {string} fieldName - Name of the field (for error messages)
|
|
669
|
+
* @returns {*} - The validated/coerced value
|
|
670
|
+
*/
|
|
671
|
+
_validateAndCoerceFieldType(value, entityDef, entityName, fieldName){
|
|
672
|
+
if(value === undefined || value === null){
|
|
673
|
+
return value; // Let nullable validation handle this
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const expectedType = entityDef.type;
|
|
677
|
+
const actualType = typeof value;
|
|
678
|
+
|
|
679
|
+
switch(expectedType){
|
|
680
|
+
case "integer":
|
|
681
|
+
// Coerce to integer if possible
|
|
682
|
+
if(actualType === 'number'){
|
|
683
|
+
if(!Number.isInteger(value)){
|
|
684
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Expected integer but got float ${value}, rounding to ${Math.round(value)}`);
|
|
685
|
+
return Math.round(value);
|
|
686
|
+
}
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
if(actualType === 'string'){
|
|
690
|
+
const parsed = parseInt(value, 10);
|
|
691
|
+
if(isNaN(parsed)){
|
|
692
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got string "${value}" which cannot be converted to a number`);
|
|
693
|
+
}
|
|
694
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to integer ${parsed}`);
|
|
695
|
+
return parsed;
|
|
696
|
+
}
|
|
697
|
+
if(actualType === 'boolean'){
|
|
698
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting boolean ${value} to integer ${value ? 1 : 0}`);
|
|
699
|
+
return value ? 1 : 0;
|
|
700
|
+
}
|
|
701
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
702
|
+
|
|
703
|
+
case "string":
|
|
704
|
+
// Coerce to string
|
|
705
|
+
if(actualType === 'string'){
|
|
706
|
+
return value;
|
|
707
|
+
}
|
|
708
|
+
// Allow auto-conversion from primitives
|
|
709
|
+
if(['number', 'boolean'].includes(actualType)){
|
|
710
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting ${actualType} ${value} to string "${String(value)}"`);
|
|
711
|
+
return String(value);
|
|
712
|
+
}
|
|
713
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
714
|
+
|
|
715
|
+
case "boolean":
|
|
716
|
+
// Coerce to boolean
|
|
717
|
+
if(actualType === 'boolean'){
|
|
718
|
+
return value;
|
|
719
|
+
}
|
|
720
|
+
if(actualType === 'number'){
|
|
721
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting number ${value} to boolean ${value !== 0}`);
|
|
722
|
+
return value !== 0;
|
|
723
|
+
}
|
|
724
|
+
if(actualType === 'string'){
|
|
725
|
+
const lower = value.toLowerCase().trim();
|
|
726
|
+
if(['true', '1', 'yes'].includes(lower)){
|
|
727
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean true`);
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
if(['false', '0', 'no', ''].includes(lower)){
|
|
731
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean false`);
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got string "${value}" which cannot be converted`);
|
|
735
|
+
}
|
|
736
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
737
|
+
|
|
738
|
+
case "time":
|
|
739
|
+
// Time fields should be strings or timestamps
|
|
740
|
+
if(actualType === 'string' || actualType === 'number'){
|
|
741
|
+
return value;
|
|
742
|
+
}
|
|
743
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected time (string/number), got ${actualType} with value ${JSON.stringify(value)}`);
|
|
744
|
+
|
|
745
|
+
default:
|
|
746
|
+
// For unknown types, allow the value through but warn
|
|
747
|
+
if(actualType === 'object'){
|
|
748
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Setting object value for type "${expectedType}". This may cause issues.`);
|
|
749
|
+
}
|
|
750
|
+
return value;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
627
754
|
|
|
628
755
|
// return columns and value strings
|
|
629
756
|
_buildSQLInsertObject(fields, modelEntity){
|
|
@@ -639,12 +766,19 @@ class SQLLiteEngine {
|
|
|
639
766
|
fieldColumn = fields[column];
|
|
640
767
|
|
|
641
768
|
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
769
|
+
// 🔥 NEW: Validate and coerce field type before processing
|
|
770
|
+
try {
|
|
771
|
+
fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
772
|
+
} catch(typeError) {
|
|
773
|
+
throw new Error(`INSERT failed: ${typeError.message}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
642
776
|
switch(modelEntity[column].type){
|
|
643
|
-
case "string" :
|
|
644
|
-
fieldColumn = `'${$that._santizeSingleQuotes(
|
|
777
|
+
case "string" :
|
|
778
|
+
fieldColumn = `'${$that._santizeSingleQuotes(fieldColumn, { entityName: modelEntity.__name, fieldName: column })}'`;
|
|
645
779
|
break;
|
|
646
|
-
case "time" :
|
|
647
|
-
fieldColumn =
|
|
780
|
+
case "time" :
|
|
781
|
+
fieldColumn = fieldColumn;
|
|
648
782
|
break;
|
|
649
783
|
}
|
|
650
784
|
|
package/mySQLEngine.js
CHANGED
|
@@ -443,21 +443,56 @@ class SQLLiteEngine {
|
|
|
443
443
|
switch(type){
|
|
444
444
|
case "belongsTo" :
|
|
445
445
|
const foreignKey = model.__entity[dirtyFields[column]].foreignKey;
|
|
446
|
-
|
|
446
|
+
let fkValue = model[dirtyFields[column]];
|
|
447
|
+
// 🔥 NEW: Validate foreign key type
|
|
448
|
+
try {
|
|
449
|
+
fkValue = $that._validateAndCoerceFieldType(fkValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
450
|
+
} catch(typeError) {
|
|
451
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
452
|
+
}
|
|
453
|
+
argument = `${foreignKey} = ${fkValue},`;
|
|
447
454
|
break;
|
|
448
455
|
case "integer" :
|
|
449
456
|
const columneValue = model[`_${dirtyFields[column]}`];
|
|
450
|
-
|
|
457
|
+
var intValue = columneValue !== undefined ? columneValue : model[dirtyFields[column]];
|
|
458
|
+
// 🔥 NEW: Validate integer type
|
|
459
|
+
try {
|
|
460
|
+
intValue = $that._validateAndCoerceFieldType(intValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
461
|
+
} catch(typeError) {
|
|
462
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
463
|
+
}
|
|
464
|
+
argument = argument === null ? `[${dirtyFields[column]}] = ${intValue},` : `${argument} [${dirtyFields[column]}] = ${intValue},`;
|
|
451
465
|
break;
|
|
452
466
|
case "string" :
|
|
453
|
-
|
|
467
|
+
var strValue = model[dirtyFields[column]];
|
|
468
|
+
// 🔥 NEW: Validate string type
|
|
469
|
+
try {
|
|
470
|
+
strValue = $that._validateAndCoerceFieldType(strValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
471
|
+
} catch(typeError) {
|
|
472
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
473
|
+
}
|
|
474
|
+
argument = argument === null ? `${dirtyFields[column]} = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} ${dirtyFields[column]} = '${$that._santizeSingleQuotes(strValue, { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
|
|
454
475
|
break;
|
|
455
476
|
case "time" :
|
|
456
477
|
// Always quote time values so empty strings remain valid ('')
|
|
457
|
-
|
|
478
|
+
var timeValue = model[dirtyFields[column]];
|
|
479
|
+
// 🔥 NEW: Validate time type
|
|
480
|
+
try {
|
|
481
|
+
timeValue = $that._validateAndCoerceFieldType(timeValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
482
|
+
} catch(typeError) {
|
|
483
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
484
|
+
}
|
|
485
|
+
argument = argument === null ? `${dirtyFields[column]} = '${timeValue}',` : `${argument} ${dirtyFields[column]} = '${timeValue}',`;
|
|
458
486
|
break;
|
|
459
487
|
case "boolean" :
|
|
460
|
-
|
|
488
|
+
var boolValue = model[dirtyFields[column]];
|
|
489
|
+
// 🔥 NEW: Validate boolean type
|
|
490
|
+
try {
|
|
491
|
+
boolValue = $that._validateAndCoerceFieldType(boolValue, model.__entity[dirtyFields[column]], model.__entity.__name, dirtyFields[column]);
|
|
492
|
+
} catch(typeError) {
|
|
493
|
+
throw new Error(`UPDATE failed: ${typeError.message}`);
|
|
494
|
+
}
|
|
495
|
+
argument = argument === null ? `${dirtyFields[column]} = '${this.boolType(boolValue)}',` : `${argument} ${dirtyFields[column]} = '${this.boolType(boolValue)}',`;
|
|
461
496
|
break;
|
|
462
497
|
default:
|
|
463
498
|
argument = argument === null ? `${dirtyFields[column]} = '${model[dirtyFields[column]]}',` : `${argument} ${dirtyFields[column]} = '${model[dirtyFields[column]]}',`;
|
|
@@ -493,6 +528,98 @@ class SQLLiteEngine {
|
|
|
493
528
|
return {tableName: tableName, primaryKey : primaryKey, value : value};
|
|
494
529
|
}
|
|
495
530
|
|
|
531
|
+
/**
|
|
532
|
+
* Validate and coerce field value to match entity type definition
|
|
533
|
+
* Throws detailed error if type cannot be coerced
|
|
534
|
+
* @param {*} value - The field value to validate
|
|
535
|
+
* @param {object} entityDef - The entity definition for this field
|
|
536
|
+
* @param {string} entityName - Name of the entity (for error messages)
|
|
537
|
+
* @param {string} fieldName - Name of the field (for error messages)
|
|
538
|
+
* @returns {*} - The validated/coerced value
|
|
539
|
+
*/
|
|
540
|
+
_validateAndCoerceFieldType(value, entityDef, entityName, fieldName){
|
|
541
|
+
if(value === undefined || value === null){
|
|
542
|
+
return value; // Let nullable validation handle this
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const expectedType = entityDef.type;
|
|
546
|
+
const actualType = typeof value;
|
|
547
|
+
|
|
548
|
+
switch(expectedType){
|
|
549
|
+
case "integer":
|
|
550
|
+
// Coerce to integer if possible
|
|
551
|
+
if(actualType === 'number'){
|
|
552
|
+
if(!Number.isInteger(value)){
|
|
553
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Expected integer but got float ${value}, rounding to ${Math.round(value)}`);
|
|
554
|
+
return Math.round(value);
|
|
555
|
+
}
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
if(actualType === 'string'){
|
|
559
|
+
const parsed = parseInt(value, 10);
|
|
560
|
+
if(isNaN(parsed)){
|
|
561
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got string "${value}" which cannot be converted to a number`);
|
|
562
|
+
}
|
|
563
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to integer ${parsed}`);
|
|
564
|
+
return parsed;
|
|
565
|
+
}
|
|
566
|
+
if(actualType === 'boolean'){
|
|
567
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting boolean ${value} to integer ${value ? 1 : 0}`);
|
|
568
|
+
return value ? 1 : 0;
|
|
569
|
+
}
|
|
570
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
571
|
+
|
|
572
|
+
case "string":
|
|
573
|
+
// Coerce to string
|
|
574
|
+
if(actualType === 'string'){
|
|
575
|
+
return value;
|
|
576
|
+
}
|
|
577
|
+
// Allow auto-conversion from primitives
|
|
578
|
+
if(['number', 'boolean'].includes(actualType)){
|
|
579
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting ${actualType} ${value} to string "${String(value)}"`);
|
|
580
|
+
return String(value);
|
|
581
|
+
}
|
|
582
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected string, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
583
|
+
|
|
584
|
+
case "boolean":
|
|
585
|
+
// Coerce to boolean
|
|
586
|
+
if(actualType === 'boolean'){
|
|
587
|
+
return value;
|
|
588
|
+
}
|
|
589
|
+
if(actualType === 'number'){
|
|
590
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting number ${value} to boolean ${value !== 0}`);
|
|
591
|
+
return value !== 0;
|
|
592
|
+
}
|
|
593
|
+
if(actualType === 'string'){
|
|
594
|
+
const lower = value.toLowerCase().trim();
|
|
595
|
+
if(['true', '1', 'yes'].includes(lower)){
|
|
596
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean true`);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
if(['false', '0', 'no', ''].includes(lower)){
|
|
600
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to boolean false`);
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got string "${value}" which cannot be converted`);
|
|
604
|
+
}
|
|
605
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected boolean, got ${actualType} with value ${JSON.stringify(value)}`);
|
|
606
|
+
|
|
607
|
+
case "time":
|
|
608
|
+
// Time fields should be strings or timestamps
|
|
609
|
+
if(actualType === 'string' || actualType === 'number'){
|
|
610
|
+
return value;
|
|
611
|
+
}
|
|
612
|
+
throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected time (string/number), got ${actualType} with value ${JSON.stringify(value)}`);
|
|
613
|
+
|
|
614
|
+
default:
|
|
615
|
+
// For unknown types, allow the value through but warn
|
|
616
|
+
if(actualType === 'object'){
|
|
617
|
+
console.warn(`⚠️ Field ${entityName}.${fieldName}: Setting object value for type "${expectedType}". This may cause issues.`);
|
|
618
|
+
}
|
|
619
|
+
return value;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
496
623
|
|
|
497
624
|
// return columns and value strings
|
|
498
625
|
_buildSQLInsertObject(fields, modelEntity){
|
|
@@ -506,18 +633,25 @@ class SQLLiteEngine {
|
|
|
506
633
|
var fieldColumn = "";
|
|
507
634
|
// check if get function is avaliable if so use that
|
|
508
635
|
fieldColumn = fields[column];
|
|
509
|
-
|
|
636
|
+
|
|
510
637
|
if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
|
|
638
|
+
// 🔥 NEW: Validate and coerce field type before processing
|
|
639
|
+
try {
|
|
640
|
+
fieldColumn = $that._validateAndCoerceFieldType(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
|
641
|
+
} catch(typeError) {
|
|
642
|
+
throw new Error(`INSERT failed: ${typeError.message}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
511
645
|
switch(modelEntity[column].type){
|
|
512
|
-
case "string" :
|
|
513
|
-
fieldColumn = `'${$that._santizeSingleQuotes(
|
|
646
|
+
case "string" :
|
|
647
|
+
fieldColumn = `'${$that._santizeSingleQuotes(fieldColumn, { entityName: modelEntity.__name, fieldName: column })}'`;
|
|
514
648
|
break;
|
|
515
|
-
case "time" :
|
|
649
|
+
case "time" :
|
|
516
650
|
// Quote time values to prevent blank values from producing malformed SQL
|
|
517
|
-
fieldColumn = `'${
|
|
651
|
+
fieldColumn = `'${fieldColumn}'`;
|
|
518
652
|
break;
|
|
519
653
|
}
|
|
520
|
-
|
|
654
|
+
|
|
521
655
|
var relationship = modelEntity[column].relationshipType
|
|
522
656
|
if(relationship === "belongsTo"){
|
|
523
657
|
column = modelEntity[column].foreignKey
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.33",
|
|
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/readme.md
CHANGED
|
@@ -96,8 +96,14 @@ Backward-compatible rollout tip:
|
|
|
96
96
|
- Cannot connect to DB: confirm `master=<env>` is set and `env.<env>.json` exists with correct credentials and paths.
|
|
97
97
|
- MySQL type mismatches: the migration engine maps MasterRecord types to SQL types; verify your entity field `type` values are correct.
|
|
98
98
|
|
|
99
|
-
### Recent improvements (2025-
|
|
100
|
-
|
|
99
|
+
### Recent improvements (2025-10)
|
|
100
|
+
|
|
101
|
+
- **Type validation and coercion (Entity Framework-style)**:
|
|
102
|
+
- INSERT and UPDATE operations now validate field types against entity definitions.
|
|
103
|
+
- Auto-converts compatible types with warnings (e.g., string "4" → integer 4).
|
|
104
|
+
- Throws clear errors for incompatible types with detailed context.
|
|
105
|
+
- Prevents silent failures where fields were skipped due to type mismatches.
|
|
106
|
+
- See [Type Validation](#type-validation) section below for details.
|
|
101
107
|
- Query language and SQL engines:
|
|
102
108
|
- Correct parsing of multi-char operators (>=, <=, ===, !==) and spaced logical operators.
|
|
103
109
|
- Support for grouped OR conditions rendered as parenthesized OR in WHERE across SQLite/MySQL.
|
|
@@ -179,6 +185,90 @@ Notes:
|
|
|
179
185
|
- For large SQLite tables, a rebuild copies data; consider maintenance windows.
|
|
180
186
|
- Use `master=development masterrecord get-migrations AppContext` to inspect migration order.
|
|
181
187
|
|
|
188
|
+
## Type Validation
|
|
189
|
+
|
|
190
|
+
MasterRecord now validates and coerces field types during INSERT and UPDATE operations, similar to Entity Framework. This prevents silent failures where fields were skipped due to type mismatches.
|
|
191
|
+
|
|
192
|
+
### How it works
|
|
193
|
+
|
|
194
|
+
When you assign a value to an entity field, MasterRecord:
|
|
195
|
+
1. **Validates** the value against the field's type definition
|
|
196
|
+
2. **Auto-converts** compatible types with console warnings
|
|
197
|
+
3. **Throws clear errors** for incompatible types
|
|
198
|
+
|
|
199
|
+
### Type conversion rules
|
|
200
|
+
|
|
201
|
+
#### Integer fields (`db.integer()`)
|
|
202
|
+
- ✅ **Accepts**: integer numbers
|
|
203
|
+
- ⚠️ **Auto-converts with warning**:
|
|
204
|
+
- Float → integer (rounds: `3.7` → `4`)
|
|
205
|
+
- Valid string → integer (`"42"` → `42`)
|
|
206
|
+
- Boolean → integer (`true` → `1`, `false` → `0`)
|
|
207
|
+
- ❌ **Throws error**: invalid strings (`"abc"`)
|
|
208
|
+
|
|
209
|
+
#### String fields (`db.string()`)
|
|
210
|
+
- ✅ **Accepts**: strings
|
|
211
|
+
- ⚠️ **Auto-converts with warning**:
|
|
212
|
+
- Number → string (`42` → `"42"`)
|
|
213
|
+
- Boolean → string (`true` → `"true"`)
|
|
214
|
+
- ❌ **Throws error**: objects, arrays
|
|
215
|
+
|
|
216
|
+
#### Boolean fields (`db.boolean()`)
|
|
217
|
+
- ✅ **Accepts**: booleans
|
|
218
|
+
- ⚠️ **Auto-converts with warning**:
|
|
219
|
+
- Number → boolean (`0` → `false`, others → `true`)
|
|
220
|
+
- String → boolean (`"true"/"1"/"yes"` → `true`, `"false"/"0"/"no"/""`→ `false`)
|
|
221
|
+
- ❌ **Throws error**: invalid strings, objects
|
|
222
|
+
|
|
223
|
+
#### Time fields (`db.time()`, timestamps)
|
|
224
|
+
- ✅ **Accepts**: strings or numbers
|
|
225
|
+
- ❌ **Throws error**: objects, booleans
|
|
226
|
+
|
|
227
|
+
### Example warnings and errors
|
|
228
|
+
|
|
229
|
+
**Auto-conversion warning (non-breaking):**
|
|
230
|
+
```javascript
|
|
231
|
+
const chunk = new DocumentChunk();
|
|
232
|
+
chunk.document_id = "4"; // string assigned to integer field
|
|
233
|
+
context.DocumentChunk.add(chunk);
|
|
234
|
+
context.saveChanges();
|
|
235
|
+
```
|
|
236
|
+
Console output:
|
|
237
|
+
```
|
|
238
|
+
⚠️ Field DocumentChunk.document_id: Auto-converting string "4" to integer 4
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Type mismatch error (breaks execution):**
|
|
242
|
+
```javascript
|
|
243
|
+
const chunk = new DocumentChunk();
|
|
244
|
+
chunk.document_id = "invalid"; // non-numeric string
|
|
245
|
+
context.DocumentChunk.add(chunk);
|
|
246
|
+
context.saveChanges(); // throws error
|
|
247
|
+
```
|
|
248
|
+
Error thrown:
|
|
249
|
+
```
|
|
250
|
+
INSERT failed: Type mismatch for DocumentChunk.document_id: Expected integer, got string "invalid" which cannot be converted to a number
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Migration from older versions
|
|
254
|
+
|
|
255
|
+
If your code relies on implicit type coercion that was previously silent:
|
|
256
|
+
- **No breaking changes**: Compatible types are still auto-converted
|
|
257
|
+
- **New warnings**: You'll see console warnings for auto-conversions
|
|
258
|
+
- **New errors**: Incompatible types that were silently skipped now throw errors
|
|
259
|
+
|
|
260
|
+
**Recommendation**: Review warnings and fix type mismatches in your code for cleaner, more predictable behavior.
|
|
261
|
+
|
|
262
|
+
### Benefits
|
|
263
|
+
|
|
264
|
+
1. **No more silent field skipping**: Previously, if you assigned a string to an integer field, the ORM would silently skip it in the INSERT/UPDATE statement. Now you get immediate feedback.
|
|
265
|
+
|
|
266
|
+
2. **Clear error messages**: Errors include entity name, field name, expected type, actual type, and the problematic value.
|
|
267
|
+
|
|
268
|
+
3. **Predictable behavior**: Auto-conversions match common patterns (e.g., database IDs returned as strings from some drivers are converted to integers).
|
|
269
|
+
|
|
270
|
+
4. **Better debugging**: Type issues are caught at save time, not when you query the data later.
|
|
271
|
+
|
|
182
272
|
## Multi-context (multi-database) projects
|
|
183
273
|
|
|
184
274
|
When your project defines multiple Context files (e.g., `userContext.js`, `modelContext.js`, `mailContext.js`, `chatContext.js`) across different packages or feature directories, MasterRecord can auto-detect and operate on all of them.
|