masterrecord 0.2.31 → 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.
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/components/models/app/models/**)",
5
+ "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/components/models/app/controllers/api/**)",
6
+ "Read(//Users/alexanderrich/Documents/development/bookbaghq/bookbag-ce/config/environments/**)"
7
+ ],
8
+ "deny": [],
9
+ "ask": []
10
+ }
11
+ }
package/Migrations/cli.js CHANGED
@@ -280,53 +280,173 @@ program.option('-V', 'output the version');
280
280
  contextFileName = contextFileName.toLowerCase();
281
281
  var migration = new Migration();
282
282
  try{
283
+ console.log(`\nšŸ” Searching for context snapshot '${contextFileName}_contextSnapShot.json'...`);
283
284
  // find context snapshot (cwd-based glob)
284
285
  var files = globSearch.sync(`**/*${contextFileName}_contextSnapShot.json`, { cwd: executedLocation, dot: true, windowsPathsNoEscape: true, nocase: true });
285
286
  var file = files && files[0] ? path.resolve(executedLocation, files[0]) : null;
286
- if(file){
287
- var contextSnapshot = require(file);
288
- const snapDir = path.dirname(file);
289
- const contextAbs = path.resolve(snapDir, contextSnapshot.contextLocation || '');
290
- const migBase = path.resolve(snapDir, contextSnapshot.migrationFolder || '.');
291
- var migrationFiles = globSearch.sync(`**/*_migration.js`, { cwd: migBase, dot: true, windowsPathsNoEscape: true });
292
- migrationFiles = (migrationFiles || []).map(f => path.resolve(migBase, f));
293
- if( migrationFiles && migrationFiles.length){
294
- // sort by timestamp prefix or file mtime as fallback
295
- var mFiles = migrationFiles.slice().sort(function(a, b){
296
- return __getMigrationTimestamp(a) - __getMigrationTimestamp(b);
297
- });
298
- var mFile = mFiles[mFiles.length -1];
299
-
300
- var migrationProjectFile = require(mFile);
301
- var ContextCtor = require(contextAbs);
302
- var contextInstance = new ContextCtor();
303
- var newMigrationProjectInstance = new migrationProjectFile(ContextCtor);
304
287
 
305
- var cleanEntities = migration.cleanEntities(contextInstance.__entities);
306
- var tableObj = migration.buildUpObject(contextSnapshot.schema, cleanEntities);
307
- newMigrationProjectInstance.up(tableObj);
308
-
309
- var snap = {
310
- file : contextAbs,
311
- executedLocation : executedLocation,
312
- context : contextInstance,
313
- contextEntities : cleanEntities,
314
- contextFileName: contextFileName
315
- }
316
-
317
- migration.createSnapShot(snap);
318
- console.log("āœ“ Database updated successfully");
319
- }
320
- else{
321
- console.log("Error - Cannot read or find migration file");
322
- }
288
+ if(!file){
289
+ console.error(`\nāŒ Error - Cannot find Context snapshot file`);
290
+ console.error(`\nSearched for: ${contextFileName}_contextSnapShot.json`);
291
+ console.error(`Searched in: ${executedLocation}`);
292
+ console.error(`\nšŸ’” Solution: Run 'masterrecord enable-migrations ${contextFileName}' first`);
293
+ return;
294
+ }
323
295
 
296
+ console.log(`āœ“ Found snapshot: ${file}`);
297
+
298
+ var contextSnapshot;
299
+ try{
300
+ contextSnapshot = require(file);
301
+ }catch(err){
302
+ console.error(`\nāŒ Error - Cannot load context snapshot`);
303
+ console.error(`\nFile: ${file}`);
304
+ console.error(`Details: ${err.message}`);
305
+ return;
324
306
  }
325
- else{
326
- console.log("Error - Cannot read or find Context file");
327
- }
307
+
308
+ const snapDir = path.dirname(file);
309
+ const contextAbs = path.resolve(snapDir, contextSnapshot.contextLocation || '');
310
+ const migBase = path.resolve(snapDir, contextSnapshot.migrationFolder || '.');
311
+
312
+ console.log(`\nšŸ” Searching for migration files in: ${migBase}`);
313
+ var migrationFiles = globSearch.sync(`**/*_migration.js`, { cwd: migBase, dot: true, windowsPathsNoEscape: true });
314
+ migrationFiles = (migrationFiles || []).map(f => path.resolve(migBase, f));
315
+
316
+ if(!(migrationFiles && migrationFiles.length)){
317
+ console.error(`\nāŒ Error - No migration files found`);
318
+ console.error(`\nSearched in: ${migBase}`);
319
+ console.error(`\nšŸ’” Solution: Run 'masterrecord add-migration Init ${contextFileName}' to create your first migration`);
320
+ return;
321
+ }
322
+
323
+ // sort by timestamp prefix or file mtime as fallback
324
+ var mFiles = migrationFiles.slice().sort(function(a, b){
325
+ return __getMigrationTimestamp(a) - __getMigrationTimestamp(b);
326
+ });
327
+ var mFile = mFiles[mFiles.length -1];
328
+ console.log(`āœ“ Found ${mFiles.length} migration file(s), using latest: ${path.basename(mFile)}`);
329
+
330
+ console.log(`\nšŸ” Loading Context file from: ${contextAbs}`);
331
+ var migrationProjectFile;
332
+ var ContextCtor;
333
+ try{
334
+ migrationProjectFile = require(mFile);
335
+ ContextCtor = require(contextAbs);
336
+ }catch(err){
337
+ console.error(`\nāŒ Error - Cannot load Context or migration file`);
338
+ console.error(`\nContext file: ${contextAbs}`);
339
+ console.error(`Migration file: ${mFile}`);
340
+ console.error(`\nDetails: ${err.message}`);
341
+ if(err.stack){
342
+ console.error(`\nStack trace:`);
343
+ console.error(err.stack);
344
+ }
345
+ return;
346
+ }
347
+
348
+ console.log(`āœ“ Context file loaded successfully`);
349
+ console.log(`\nšŸ” Instantiating Context (this will create the database if it doesn't exist)...`);
350
+
351
+ var contextInstance;
352
+ try{
353
+ contextInstance = new ContextCtor();
354
+ }catch(err){
355
+ console.error(`\nāŒ Error - Failed to instantiate Context`);
356
+ console.error(`\nContext file: ${contextAbs}`);
357
+ console.error(`\nThis usually happens when:`);
358
+ console.error(` • Environment configuration file is missing`);
359
+ console.error(` • Database connection settings are incorrect`);
360
+ console.error(` • The 'master' environment variable is not set`);
361
+ console.error(` • Required dependencies are not installed`);
362
+ console.error(`\nDetails: ${err.message}`);
363
+ if(err.stack){
364
+ console.error(`\nStack trace:`);
365
+ console.error(err.stack);
366
+ }
367
+ console.error(`\nšŸ’” Check your environment config file (e.g., config/environments/env.development.json)`);
368
+ console.error(`šŸ’” Make sure you're running: master=development masterrecord update-database ${contextFileName}`);
369
+ return;
370
+ }
371
+
372
+ console.log(`āœ“ Context instantiated successfully`);
373
+
374
+ // Log database connection details
375
+ if(contextInstance.isSQLite && contextInstance.db){
376
+ const dbPath = contextInstance.db.name || 'unknown';
377
+ console.log(`\nšŸ“Š Database Type: SQLite`);
378
+ console.log(`šŸ“ Database Path: ${dbPath}`);
379
+
380
+ // Check if the database file exists
381
+ if(fs.existsSync(dbPath)){
382
+ const stats = fs.statSync(dbPath);
383
+ console.log(`āœ“ Database file exists (${(stats.size / 1024).toFixed(2)} KB)`);
384
+ }else{
385
+ console.log(`āš ļø Database file does not exist yet (will be created during migration)`);
386
+ }
387
+ }else if(contextInstance.isMySQL){
388
+ console.log(`\nšŸ“Š Database Type: MySQL`);
389
+ }
390
+
391
+ console.log(`\nšŸ” Loading entities from context...`);
392
+ var cleanEntities = migration.cleanEntities(contextInstance.__entities);
393
+ console.log(`āœ“ Found ${cleanEntities.length} entity/entities`);
394
+
395
+ if(cleanEntities.length === 0){
396
+ console.error(`\nāš ļø Warning - No entities found in Context`);
397
+ console.error(`\nMake sure your Context file has dbset() calls to register entities`);
398
+ console.error(`Example:`);
399
+ console.error(` this.dbset(User, 'User');`);
400
+ console.error(` this.dbset(Post, 'Post');`);
401
+ }
402
+
403
+ console.log(`\nšŸš€ Running migration...`);
404
+ try{
405
+ var newMigrationProjectInstance = new migrationProjectFile(ContextCtor);
406
+ var tableObj = migration.buildUpObject(contextSnapshot.schema, cleanEntities);
407
+ newMigrationProjectInstance.up(tableObj);
408
+ }catch(err){
409
+ console.error(`\nāŒ Error - Migration failed during execution`);
410
+ console.error(`\nMigration file: ${mFile}`);
411
+ console.error(`\nDetails: ${err.message}`);
412
+ if(err.stack){
413
+ console.error(`\nStack trace:`);
414
+ console.error(err.stack);
415
+ }
416
+ return;
417
+ }
418
+
419
+ console.log(`\nšŸ’¾ Updating snapshot...`);
420
+ var snap = {
421
+ file : contextAbs,
422
+ executedLocation : executedLocation,
423
+ context : contextInstance,
424
+ contextEntities : cleanEntities,
425
+ contextFileName: contextFileName
426
+ }
427
+
428
+ migration.createSnapShot(snap);
429
+ console.log(`\nāœ… Database updated successfully!`);
430
+
431
+ // Final verification for SQLite
432
+ if(contextInstance.isSQLite && contextInstance.db){
433
+ const dbPath = contextInstance.db.name || 'unknown';
434
+ if(fs.existsSync(dbPath)){
435
+ const stats = fs.statSync(dbPath);
436
+ console.log(`\nšŸ“ Database file: ${dbPath}`);
437
+ console.log(`šŸ“Š Size: ${(stats.size / 1024).toFixed(2)} KB`);
438
+ }else{
439
+ console.error(`\nāš ļø Warning - Database file was not created at expected path: ${dbPath}`);
440
+ }
441
+ }
442
+
328
443
  }catch (e){
329
- console.log("Error - Cannot read or find file ", e);
444
+ console.error(`\nāŒ Unexpected error during update-database`);
445
+ console.error(`\nDetails: ${e.message}`);
446
+ if(e.stack){
447
+ console.error(`\nStack trace:`);
448
+ console.error(e.stack);
449
+ }
330
450
  }
331
451
  });
332
452
 
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
- argument = `${foreignKey} = ${model[dirtyFields[column]]},`;
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
- argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${columneValue},`;
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
- argument = argument === null ? `[${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} [${dirtyFields[column]}] = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
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(model[dirtyFields[column]]);
616
+ bool = tools.convertBooleanToNumber(boolValue);
589
617
  }
590
618
  else{
591
- bool = model[dirtyFields[column]];
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
- argument = argument === null ? `[${dirtyFields[column]}] = '${model[dirtyFields[column]]}',` : `${argument} [${dirtyFields[column]}] = '${model[dirtyFields[column]]}',`;
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(fields[column], { entityName: modelEntity.__name, fieldName: column })}'`;
777
+ case "string" :
778
+ fieldColumn = `'${$that._santizeSingleQuotes(fieldColumn, { entityName: modelEntity.__name, fieldName: column })}'`;
645
779
  break;
646
- case "time" :
647
- fieldColumn = fields[column];
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
- argument = `${foreignKey} = ${model[dirtyFields[column]]},`;
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
- argument = argument === null ? `[${dirtyFields[column]}] = ${model[dirtyFields[column]]},` : `${argument} [${dirtyFields[column]}] = ${columneValue},`;
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
- argument = argument === null ? `${dirtyFields[column]} = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',` : `${argument} ${dirtyFields[column]} = '${$that._santizeSingleQuotes(model[dirtyFields[column]], { entityName: model.__entity.__name, fieldName: dirtyFields[column] })}',`;
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
- argument = argument === null ? `${dirtyFields[column]} = '${model[dirtyFields[column]]}',` : `${argument} ${dirtyFields[column]} = '${model[dirtyFields[column]]}',`;
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
- argument = argument === null ? `${dirtyFields[column]} = '${this.boolType(model[dirtyFields[column]])}',` : `${argument} ${dirtyFields[column]} = '${this.boolType(model[dirtyFields[column]])}',`;
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(fields[column], { entityName: modelEntity.__name, fieldName: column })}'`;
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 = `'${fields[column]}'`;
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.31",
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-09)
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.