masterrecord 0.3.37 → 0.3.39
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/FOREIGN_KEY_STRING_FIX.md +288 -0
- package/GLOBAL_REGISTRY_VERIFICATION.md +375 -0
- package/SQLLiteEngine.js +8 -0
- package/context.js +29 -4
- package/mySQLEngine.js +8 -0
- package/package.json +1 -1
- package/postgresEngine.js +8 -0
- package/readme.md +182 -0
- package/test/foreign-key-string-value-test.js +406 -0
- package/test/global-model-registry-test.js +538 -0
package/context.js
CHANGED
|
@@ -177,6 +177,11 @@ class context {
|
|
|
177
177
|
// Sequential ID counter for collision-safe entity tracking
|
|
178
178
|
static _nextEntityId = 1;
|
|
179
179
|
|
|
180
|
+
// Global model registry - tracks registered models per context class
|
|
181
|
+
// Structure: { 'userContext': Set(['User', 'Auth', 'Settings']), 'qaContext': Set([...]) }
|
|
182
|
+
// Purpose: Prevents duplicate warnings when CLI instantiates same context multiple times
|
|
183
|
+
static _globalModelRegistry = {};
|
|
184
|
+
|
|
180
185
|
/**
|
|
181
186
|
* Creates a new database context instance
|
|
182
187
|
*
|
|
@@ -189,6 +194,17 @@ class context {
|
|
|
189
194
|
this._SQLEngine = null; // Will be set during database initialization
|
|
190
195
|
this.__trackedEntitiesMap = new Map(); // Initialize Map for O(1) lookups
|
|
191
196
|
|
|
197
|
+
// Track if this is the first instance of this context class
|
|
198
|
+
// Used to determine if duplicate warnings should be shown
|
|
199
|
+
const globalRegistry = context._globalModelRegistry[this.__name];
|
|
200
|
+
this.__isFirstInstance = !globalRegistry || globalRegistry.size === 0;
|
|
201
|
+
|
|
202
|
+
// Initialize global model registry for this context class if not exists
|
|
203
|
+
// This prevents duplicate warnings when CLI instantiates the same context multiple times
|
|
204
|
+
if (!context._globalModelRegistry[this.__name]) {
|
|
205
|
+
context._globalModelRegistry[this.__name] = new Set();
|
|
206
|
+
}
|
|
207
|
+
|
|
192
208
|
// Initialize shared query cache (only once across all instances)
|
|
193
209
|
if (!context._sharedQueryCache) {
|
|
194
210
|
const cacheConfig = {
|
|
@@ -1033,20 +1049,29 @@ class context {
|
|
|
1033
1049
|
// Merge context-level composite indexes with entity-defined indexes
|
|
1034
1050
|
this.#mergeCompositeIndexes(validModel, tableName);
|
|
1035
1051
|
|
|
1036
|
-
// Check if
|
|
1052
|
+
// Check if model is registered in this specific instance
|
|
1037
1053
|
const existingIndex = this.__entities.findIndex(e => e.__name === tableName);
|
|
1054
|
+
|
|
1038
1055
|
if (existingIndex !== -1) {
|
|
1039
|
-
//
|
|
1040
|
-
|
|
1056
|
+
// Model already registered in THIS instance - this is a duplicate within same constructor
|
|
1057
|
+
// Only warn on the first instance of this context class (subsequent instances expected to have same pattern)
|
|
1058
|
+
if (this.__isFirstInstance) {
|
|
1059
|
+
console.warn(`Warning: dbset() called multiple times for table '${tableName}' in constructor - updating existing registration`);
|
|
1060
|
+
}
|
|
1061
|
+
// Update existing registration
|
|
1041
1062
|
this.__entities[existingIndex] = validModel;
|
|
1042
1063
|
this.__builderEntities[existingIndex] = tools.createNewInstance(validModel, query, this);
|
|
1043
1064
|
} else {
|
|
1044
|
-
//
|
|
1065
|
+
// Model not registered in this instance - add it
|
|
1045
1066
|
this.__entities.push(validModel); // Store model object
|
|
1046
1067
|
const buildMod = tools.createNewInstance(validModel, query, this);
|
|
1047
1068
|
this.__builderEntities.push(buildMod); // Store query builder entity
|
|
1048
1069
|
}
|
|
1049
1070
|
|
|
1071
|
+
// Always mark model as globally seen (after handling instance registration)
|
|
1072
|
+
const globalRegistry = context._globalModelRegistry[this.__name];
|
|
1073
|
+
globalRegistry.add(tableName);
|
|
1074
|
+
|
|
1050
1075
|
// Use getter to return fresh query instance each time (prevents parameter accumulation)
|
|
1051
1076
|
Object.defineProperty(this, validModel.__name, {
|
|
1052
1077
|
get: function() {
|
package/mySQLEngine.js
CHANGED
|
@@ -653,6 +653,14 @@ class MySQLEngine {
|
|
|
653
653
|
if (column.indexOf("__") === -1) {
|
|
654
654
|
let fieldColumn = fields[column];
|
|
655
655
|
|
|
656
|
+
// 🔥 FIX: For belongsTo relationships, also check the foreignKey field name
|
|
657
|
+
// Users can set either orgRole.User = obj OR orgRole.user_id = 2
|
|
658
|
+
if ((fieldColumn === undefined || fieldColumn === null) &&
|
|
659
|
+
modelEntity[column].relationshipType === "belongsTo" &&
|
|
660
|
+
modelEntity[column].foreignKey) {
|
|
661
|
+
fieldColumn = fields[modelEntity[column].foreignKey];
|
|
662
|
+
}
|
|
663
|
+
|
|
656
664
|
if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
|
|
657
665
|
try {
|
|
658
666
|
fieldColumn = FieldTransformer.toDatabase(fieldColumn, modelEntity[column], modelEntity.__name, column);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.39",
|
|
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
|
@@ -600,6 +600,14 @@ class postgresEngine {
|
|
|
600
600
|
if (column.indexOf("__") === -1) {
|
|
601
601
|
let fieldColumn = fields[column];
|
|
602
602
|
|
|
603
|
+
// 🔥 FIX: For belongsTo relationships, also check the foreignKey field name
|
|
604
|
+
// Users can set either orgRole.User = obj OR orgRole.user_id = 2
|
|
605
|
+
if ((fieldColumn === undefined || fieldColumn === null) &&
|
|
606
|
+
modelEntity[column].relationshipType === "belongsTo" &&
|
|
607
|
+
modelEntity[column].foreignKey) {
|
|
608
|
+
fieldColumn = fields[modelEntity[column].foreignKey];
|
|
609
|
+
}
|
|
610
|
+
|
|
603
611
|
if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
|
|
604
612
|
// Apply toDatabase transformer
|
|
605
613
|
try {
|
package/readme.md
CHANGED
|
@@ -3369,6 +3369,188 @@ user.name = null; // Error if name is { nullable: false }
|
|
|
3369
3369
|
|
|
3370
3370
|
## Changelog
|
|
3371
3371
|
|
|
3372
|
+
### Version 0.3.39 (2026-02-09) - CRITICAL BUG FIX: Foreign Key String Values
|
|
3373
|
+
|
|
3374
|
+
#### Bug Fixed: Foreign Key Fields Silently Ignoring String Values
|
|
3375
|
+
- **FIXED**: Critical bug where string values assigned to foreign key fields were silently excluded from INSERT statements
|
|
3376
|
+
- **Problem**: When you assign `orgRole.user_id = "2"` (string), MasterRecord excluded it from INSERT, causing NOT NULL constraint failures
|
|
3377
|
+
- **Root Cause**: INSERT builder only checked navigation property name (`User`), not foreign key field name (`user_id`)
|
|
3378
|
+
- **Impact**: Common in real-world apps where IDs come from JWT tokens, HTTP requests, or authService (returns string IDs)
|
|
3379
|
+
|
|
3380
|
+
#### What Was Happening (Before v0.3.39)
|
|
3381
|
+
```javascript
|
|
3382
|
+
// User assigns string value to foreign key
|
|
3383
|
+
orgRole.user_id = "2"; // ← STRING (from currentUser.id)
|
|
3384
|
+
orgRole.organization_id = 8; // ← NUMBER (from database)
|
|
3385
|
+
orgRole.role = 'org_admin';
|
|
3386
|
+
|
|
3387
|
+
await userContext.saveChanges();
|
|
3388
|
+
// ❌ Generated SQL: INSERT INTO [UserOrganizationRole] ([role]) VALUES ('org_admin')
|
|
3389
|
+
// ❌ Error: NOT NULL constraint failed: UserOrganizationRole.user_id
|
|
3390
|
+
```
|
|
3391
|
+
|
|
3392
|
+
**Why It Failed:**
|
|
3393
|
+
- `belongsTo('User', 'user_id')` creates property `User` with `foreignKey: 'user_id'`
|
|
3394
|
+
- INSERT builder looked for `fields['User']` (navigation property)
|
|
3395
|
+
- User set `fields['user_id']` (foreign key field name)
|
|
3396
|
+
- Field not found → silently skipped → INSERT failed
|
|
3397
|
+
|
|
3398
|
+
#### The Fix (v0.3.39)
|
|
3399
|
+
Updated `_buildSQLInsertObjectParameterized` in all database engines:
|
|
3400
|
+
- Now checks BOTH navigation property name AND foreign key field name
|
|
3401
|
+
- Auto-converts string values to integers for integer foreign key fields
|
|
3402
|
+
- Maintains backward compatibility (setting navigation property still works)
|
|
3403
|
+
|
|
3404
|
+
```javascript
|
|
3405
|
+
// After fix - both patterns work:
|
|
3406
|
+
orgRole.User = 2; // ✅ Works (navigation property)
|
|
3407
|
+
orgRole.user_id = "2"; // ✅ Works (foreign key field, auto-converted to integer)
|
|
3408
|
+
```
|
|
3409
|
+
|
|
3410
|
+
#### Files Modified
|
|
3411
|
+
1. **SQLLiteEngine.js** (lines 1127-1137) - Added foreign key field lookup
|
|
3412
|
+
2. **mySQLEngine.js** (lines 654-664) - Added foreign key field lookup
|
|
3413
|
+
3. **postgresEngine.js** (lines 601-611) - Added foreign key field lookup
|
|
3414
|
+
4. **test/foreign-key-string-value-test.js** (NEW) - 8 comprehensive tests
|
|
3415
|
+
5. **package.json** - Updated to v0.3.39
|
|
3416
|
+
6. **readme.md** - Added changelog
|
|
3417
|
+
|
|
3418
|
+
#### Test Results
|
|
3419
|
+
- **8 new tests** - All passing ✅
|
|
3420
|
+
1. String foreign key value included in INSERT ✅
|
|
3421
|
+
2. Number foreign key value still works ✅
|
|
3422
|
+
3. Mixed string and number foreign keys ✅
|
|
3423
|
+
4. String with leading zeros (e.g., "007" → 7) ✅
|
|
3424
|
+
5. Invalid strings throw error (not silent failure) ✅
|
|
3425
|
+
6. Empty strings throw error ✅
|
|
3426
|
+
7. Backward compatible (navigation property still works) ✅
|
|
3427
|
+
8. Prefers navigation property if both set ✅
|
|
3428
|
+
|
|
3429
|
+
#### Real-World Example: authService Returns String IDs
|
|
3430
|
+
```javascript
|
|
3431
|
+
// authService.js returns:
|
|
3432
|
+
const currentUser = {
|
|
3433
|
+
id: "2", // ← STRING (from String(obj.user.id))
|
|
3434
|
+
email: "customer1@bookbag.ai",
|
|
3435
|
+
system_role: "system_user"
|
|
3436
|
+
};
|
|
3437
|
+
|
|
3438
|
+
// User creates association:
|
|
3439
|
+
const orgRole = new UserOrganizationRole();
|
|
3440
|
+
orgRole.user_id = currentUser.id; // ← Before: silently skipped. After: auto-converted to 2
|
|
3441
|
+
orgRole.organization_id = newOrg.id; // ← NUMBER from database
|
|
3442
|
+
orgRole.role = 'org_admin';
|
|
3443
|
+
|
|
3444
|
+
await userContext.saveChanges(); // ✅ Now works!
|
|
3445
|
+
```
|
|
3446
|
+
|
|
3447
|
+
#### Impact
|
|
3448
|
+
- ✅ **Auto-converts** string foreign keys to integers (with validation)
|
|
3449
|
+
- ✅ **Clear errors** for invalid strings (not silent failures)
|
|
3450
|
+
- ✅ **Backward compatible** - navigation property pattern still works
|
|
3451
|
+
- ✅ **Works across all databases** (SQLite, MySQL, PostgreSQL)
|
|
3452
|
+
- ✅ **Matches real-world usage** where IDs are often strings
|
|
3453
|
+
|
|
3454
|
+
#### Upgrade Path
|
|
3455
|
+
```bash
|
|
3456
|
+
npm install -g masterrecord@0.3.39
|
|
3457
|
+
```
|
|
3458
|
+
No code changes needed - automatic fix! If you have workarounds like `parseInt(currentUser.id)`, you can now remove them (but leaving them is harmless).
|
|
3459
|
+
|
|
3460
|
+
---
|
|
3461
|
+
|
|
3462
|
+
### Version 0.3.38 (2026-02-06) - GLOBAL MODEL REGISTRY (UX FIX)
|
|
3463
|
+
|
|
3464
|
+
#### Enhancement: Eliminates Confusing CLI Warnings
|
|
3465
|
+
- **FIXED**: Confusing warnings during normal CLI operation when generating migrations
|
|
3466
|
+
- **Previous Behavior**: v0.3.36/0.3.37 correctly detected duplicate `dbset()` calls and emitted warnings
|
|
3467
|
+
- **Problem**: CLI instantiates the same context class multiple times to inspect schema
|
|
3468
|
+
- **Impact**: Users saw warnings during normal operation: `"Warning: dbset() called multiple times for table 'User'..."`
|
|
3469
|
+
- **User Confusion**: Warnings appeared even when code was correct, making users think they did something wrong
|
|
3470
|
+
|
|
3471
|
+
#### Implementation - Global Model Registry
|
|
3472
|
+
**The Solution** (`context.js`)
|
|
3473
|
+
- Added static `_globalModelRegistry` property to track registered models per context class
|
|
3474
|
+
- Structure: `{ 'userContext': Set(['User', 'Auth', 'Settings']), 'qaContext': Set([...]) }`
|
|
3475
|
+
- Each context instance checks if it's the first instance via `__isFirstInstance` flag
|
|
3476
|
+
- Warnings only appear on the first instance of a context class (genuine bugs)
|
|
3477
|
+
- Subsequent instances (CLI pattern) are silent since they're expected
|
|
3478
|
+
|
|
3479
|
+
**How It Works:**
|
|
3480
|
+
|
|
3481
|
+
1. **First Instance** (constructor execution):
|
|
3482
|
+
```javascript
|
|
3483
|
+
const ctx1 = new userContext();
|
|
3484
|
+
// __isFirstInstance = true (global registry empty)
|
|
3485
|
+
// dbset(User) - adds User to global registry
|
|
3486
|
+
// dbset(User) again - WARNS (duplicate in same constructor)
|
|
3487
|
+
// dbset(Auth) - adds Auth to global registry
|
|
3488
|
+
```
|
|
3489
|
+
|
|
3490
|
+
2. **Subsequent Instances** (CLI creates multiple):
|
|
3491
|
+
```javascript
|
|
3492
|
+
const ctx2 = new userContext();
|
|
3493
|
+
// __isFirstInstance = false (global registry has User, Auth)
|
|
3494
|
+
// dbset(User) - no warning (expected pattern)
|
|
3495
|
+
// dbset(User) again - no warning (expected pattern)
|
|
3496
|
+
// dbset(Auth) - no warning (expected pattern)
|
|
3497
|
+
```
|
|
3498
|
+
|
|
3499
|
+
3. **Duplicate Detection Still Works**:
|
|
3500
|
+
- If user's constructor has `dbset(User)` called twice, the first instance warns
|
|
3501
|
+
- This guides users to fix their code (remove the duplicate)
|
|
3502
|
+
- After fixing, all future CLI operations are silent
|
|
3503
|
+
|
|
3504
|
+
**Benefits:**
|
|
3505
|
+
- ✅ **Clean CLI Output**: No spurious warnings during `masterrecord add-migration`
|
|
3506
|
+
- ✅ **Genuine Bug Detection**: Still warns about actual duplicates in user code
|
|
3507
|
+
- ✅ **Better UX**: Users no longer confused by normal operation warnings
|
|
3508
|
+
- ✅ **Backward Compatible**: Existing code continues to work
|
|
3509
|
+
- ✅ **Industry-Standard Pattern**: Matches how TypeORM, Sequelize, Mongoose handle multiple instances
|
|
3510
|
+
|
|
3511
|
+
**Files Modified:**
|
|
3512
|
+
1. `context.js` - Added `_globalModelRegistry` static property, `__isFirstInstance` instance flag, updated `dbset()` logic
|
|
3513
|
+
2. `test/global-model-registry-test.js` (NEW) - 15 comprehensive tests covering:
|
|
3514
|
+
- Multiple context instances (CLI pattern) - no warnings ✅
|
|
3515
|
+
- Genuine duplicates in constructor - warns once ✅
|
|
3516
|
+
- Multiple context classes with same models - no warnings ✅
|
|
3517
|
+
- Registry isolation between context classes ✅
|
|
3518
|
+
- Edge cases (empty contexts, large contexts, mixed registration) ✅
|
|
3519
|
+
3. `package.json` - Updated version to 0.3.38
|
|
3520
|
+
4. `readme.md` - Added changelog entry
|
|
3521
|
+
|
|
3522
|
+
**Test Results:**
|
|
3523
|
+
- **15 new tests** - All passing ✅
|
|
3524
|
+
- Tests verify CLI pattern (3 instances) produces zero warnings
|
|
3525
|
+
- Tests verify genuine duplicates still warn on first instance only
|
|
3526
|
+
- Tests verify different context classes have separate registries
|
|
3527
|
+
- Tests verify large contexts (50 models) work without warnings
|
|
3528
|
+
|
|
3529
|
+
**Upgrade Path:**
|
|
3530
|
+
```bash
|
|
3531
|
+
npm install -g masterrecord@0.3.38
|
|
3532
|
+
```
|
|
3533
|
+
No code changes needed - automatic improvement to CLI experience.
|
|
3534
|
+
|
|
3535
|
+
**Real-World Example:**
|
|
3536
|
+
|
|
3537
|
+
Before v0.3.38:
|
|
3538
|
+
```bash
|
|
3539
|
+
$ masterrecord add-migration CreateUsers userContext
|
|
3540
|
+
Warning: dbset() called multiple times for table 'User' - updating existing registration
|
|
3541
|
+
Warning: dbset() called multiple times for table 'Auth' - updating existing registration
|
|
3542
|
+
Warning: dbset() called multiple times for table 'Settings' - updating existing registration
|
|
3543
|
+
✓ Migration 'CreateUsers' created successfully
|
|
3544
|
+
```
|
|
3545
|
+
|
|
3546
|
+
After v0.3.38:
|
|
3547
|
+
```bash
|
|
3548
|
+
$ masterrecord add-migration CreateUsers userContext
|
|
3549
|
+
✓ Migration 'CreateUsers' created successfully
|
|
3550
|
+
```
|
|
3551
|
+
|
|
3552
|
+
---
|
|
3553
|
+
|
|
3372
3554
|
### Version 0.3.36 (2026-02-06) - ROOT CAUSE FIX + CONFIG DISCOVERY FIX
|
|
3373
3555
|
|
|
3374
3556
|
#### Critical Bug Fix #1: Duplicate Entities and Seed Data - Complete Resolution
|