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.
@@ -62,7 +62,8 @@
62
62
  "Bash(masterrecord update-database:*)",
63
63
  "Bash(npx mocha:*)",
64
64
  "Bash(masterrecord add-migration:*)",
65
- "Bash(masterrecord:*)"
65
+ "Bash(masterrecord:*)",
66
+ "Bash(git -C /Users/alexanderrich/Documents/development/bookbaghq/bookbag-training log --oneline --all -- *MASTERRECORD_ISSUE*)"
66
67
  ],
67
68
  "deny": [],
68
69
  "ask": []
@@ -0,0 +1,288 @@
1
+ # Foreign Key String Value Bug - FIXED in v0.3.39
2
+
3
+ ## Problem Summary
4
+
5
+ When assigning string values to foreign key fields defined via `belongsTo()`, MasterRecord silently excluded them from INSERT statements, causing NOT NULL constraint failures.
6
+
7
+ ## Example of the Bug
8
+
9
+ ```javascript
10
+ // Entity with belongsTo relationship
11
+ class UserOrganizationRole extends EntityModel {
12
+ User(db) {
13
+ db.belongsTo('User', 'user_id'); // Creates foreignKey 'user_id'
14
+ }
15
+
16
+ Organization(db) {
17
+ db.belongsTo('Organization', 'organization_id');
18
+ }
19
+ }
20
+
21
+ // User code
22
+ const orgRole = new UserOrganizationRole();
23
+ orgRole.user_id = currentUser.id; // currentUser.id is STRING "2"
24
+ orgRole.organization_id = newOrg.id; // newOrg.id is NUMBER 8
25
+ orgRole.role = 'org_admin';
26
+
27
+ await userContext.saveChanges();
28
+ // ❌ BEFORE: SqliteError: NOT NULL constraint failed: UserOrganizationRole.user_id
29
+ // ✅ AFTER: Works perfectly!
30
+ ```
31
+
32
+ ## Root Cause
33
+
34
+ 1. `belongsTo('User', 'user_id')` creates an entity property named `User` with metadata `foreignKey: 'user_id'`
35
+ 2. The INSERT builder looked for `fields['User']` (navigation property name)
36
+ 3. But users set `fields['user_id']` (foreign key field name)
37
+ 4. Field not found → silently skipped → INSERT statement missing the field
38
+
39
+ **Generated SQL (BEFORE):**
40
+ ```sql
41
+ INSERT INTO [UserOrganizationRole] ([organization_id], [role])
42
+ VALUES (8, 'org_admin')
43
+ -- ❌ user_id is missing!
44
+ ```
45
+
46
+ **Generated SQL (AFTER):**
47
+ ```sql
48
+ INSERT INTO [UserOrganizationRole] ([user_id], [organization_id], [role])
49
+ VALUES (2, 8, 'org_admin')
50
+ -- ✅ user_id is included and auto-converted from "2" to 2
51
+ ```
52
+
53
+ ## The Fix
54
+
55
+ Modified `_buildSQLInsertObjectParameterized()` in all three database engines to check BOTH the navigation property name AND the foreign key field name:
56
+
57
+ ```javascript
58
+ // SQLLiteEngine.js, mySQLEngine.js, postgresEngine.js
59
+ for (const column in modelEntity) {
60
+ if (column.indexOf("__") === -1) {
61
+ let fieldColumn = fields[column]; // Check navigation property (e.g., 'User')
62
+
63
+ // 🔥 NEW: Also check the foreignKey field name (e.g., 'user_id')
64
+ if ((fieldColumn === undefined || fieldColumn === null) &&
65
+ modelEntity[column].relationshipType === "belongsTo" &&
66
+ modelEntity[column].foreignKey) {
67
+ fieldColumn = fields[modelEntity[column].foreignKey];
68
+ }
69
+
70
+ if ((fieldColumn !== undefined && fieldColumn !== null) && typeof(fieldColumn) !== "object") {
71
+ // Existing validation and type coercion logic...
72
+ // This already handles string-to-integer conversion!
73
+ }
74
+ }
75
+ }
76
+ ```
77
+
78
+ ## Auto-Conversion
79
+
80
+ The existing `_validateAndCoerceFieldType()` method already handled string-to-integer conversion:
81
+
82
+ ```javascript
83
+ case "integer":
84
+ if (actualType === 'string') {
85
+ const parsed = parseInt(value, 10);
86
+ if (isNaN(parsed)) {
87
+ throw new Error(`Type mismatch for ${entityName}.${fieldName}: Expected integer, got string "${value}" which cannot be converted`);
88
+ }
89
+ console.warn(`⚠️ Field ${entityName}.${fieldName}: Auto-converting string "${value}" to integer ${parsed}`);
90
+ return parsed;
91
+ }
92
+ ```
93
+
94
+ So once the field is found, the conversion happens automatically!
95
+
96
+ ## Why String IDs Happen in Real Apps
97
+
98
+ ### 1. authService Returns String IDs
99
+ ```javascript
100
+ // authService.js line 167:
101
+ res.id = String(obj.user.id); // Explicit string conversion
102
+
103
+ // Returns:
104
+ {
105
+ id: "2", // ← STRING
106
+ email: "customer1@bookbag.ai",
107
+ system_role: "system_user"
108
+ }
109
+ ```
110
+
111
+ ### 2. HTTP Requests (JSON)
112
+ ```javascript
113
+ // Client sends:
114
+ { "user_id": "2", "organization_id": "8" }
115
+
116
+ // Express parses as strings
117
+ req.body.user_id // "2" (string)
118
+ ```
119
+
120
+ ### 3. JWT Tokens
121
+ ```javascript
122
+ const token = jwt.decode(req.headers.authorization);
123
+ token.userId // "2" (string from JWT claim)
124
+ ```
125
+
126
+ ### 4. Database Returns Numbers
127
+ ```javascript
128
+ const newOrg = new Organization();
129
+ // ... set properties ...
130
+ await organizationContext.saveChanges();
131
+
132
+ newOrg.id // 8 (NUMBER from auto-increment)
133
+ ```
134
+
135
+ **Result:** Mixed types are common in real-world apps!
136
+
137
+ ## Both Patterns Now Work
138
+
139
+ ### Pattern 1: Navigation Property (OLD, still works)
140
+ ```javascript
141
+ const user = await ctx.User.where(u => u.id == 2).first();
142
+ orgRole.User = user; // Set navigation property
143
+ ```
144
+
145
+ ### Pattern 2: Foreign Key Field (NEW, now works!)
146
+ ```javascript
147
+ orgRole.user_id = currentUser.id; // "2" (string)
148
+ // Auto-converted to 2 (integer) ✅
149
+ ```
150
+
151
+ ### Pattern 3: Direct Integer (always worked)
152
+ ```javascript
153
+ orgRole.user_id = 2; // Already a number
154
+ ```
155
+
156
+ ## Test Coverage
157
+
158
+ **8 comprehensive tests** verify:
159
+ 1. ✅ String foreign key value included in INSERT
160
+ 2. ✅ Number foreign key value still works
161
+ 3. ✅ Mixed string and number foreign keys
162
+ 4. ✅ String with leading zeros ("007" → 7)
163
+ 5. ✅ Invalid strings throw clear error
164
+ 6. ✅ Empty strings throw clear error
165
+ 7. ✅ Backward compatible (navigation property)
166
+ 8. ✅ Prefers navigation property if both set
167
+
168
+ Run tests:
169
+ ```bash
170
+ node test/foreign-key-string-value-test.js
171
+ ```
172
+
173
+ ## Affected Database Engines
174
+
175
+ All three engines fixed:
176
+ - ✅ SQLiteEngine (`SQLLiteEngine.js`)
177
+ - ✅ MySQLEngine (`mySQLEngine.js`)
178
+ - ✅ PostgresEngine (`postgresEngine.js`)
179
+
180
+ ## Upgrade Path
181
+
182
+ ```bash
183
+ npm install -g masterrecord@0.3.39
184
+ ```
185
+
186
+ **No code changes needed!** The fix is automatic.
187
+
188
+ ### If You Have Workarounds
189
+
190
+ If you added `parseInt()` workarounds like this:
191
+ ```javascript
192
+ orgRole.user_id = parseInt(currentUser.id, 10);
193
+ ```
194
+
195
+ You can now remove them (but leaving them is harmless):
196
+ ```javascript
197
+ orgRole.user_id = currentUser.id; // Works directly now!
198
+ ```
199
+
200
+ ## Error Messages (Before vs After)
201
+
202
+ ### Before v0.3.39
203
+ ```
204
+ SqliteError: NOT NULL constraint failed: UserOrganizationRole.user_id
205
+ ```
206
+ **Confusing!** The field WAS set, but silently skipped.
207
+
208
+ ### After v0.3.39
209
+ If you pass an invalid string:
210
+ ```
211
+ INSERT failed: Type mismatch for UserOrganizationRole.User: Expected integer, got string "invalid" which cannot be converted to a number
212
+ ```
213
+ **Clear!** Tells you exactly what's wrong.
214
+
215
+ ## Impact
216
+
217
+ - ✅ **Auto-converts** string foreign keys to integers
218
+ - ✅ **Clear errors** for invalid values (not silent failures)
219
+ - ✅ **Backward compatible** - all existing code works
220
+ - ✅ **Matches real-world usage** where IDs are often strings
221
+ - ✅ **Works across all databases** (SQLite, MySQL, PostgreSQL)
222
+
223
+ ## Files Changed
224
+
225
+ 1. **SQLLiteEngine.js** - Lines 1127-1137
226
+ 2. **mySQLEngine.js** - Lines 654-664
227
+ 3. **postgresEngine.js** - Lines 601-611
228
+ 4. **test/foreign-key-string-value-test.js** (NEW)
229
+ 5. **package.json** - Version 0.3.39
230
+ 6. **readme.md** - Changelog entry
231
+ 7. **MEMORY.md** - Implementation notes
232
+
233
+ ## Related Issues
234
+
235
+ This bug was reported by Bookbag.ai Engineering Team and affects any application that:
236
+ - Uses JWT tokens for authentication (IDs as strings)
237
+ - Receives data from HTTP requests (JSON has string values)
238
+ - Mixes data from different sources (auth service + database)
239
+ - Uses authService pattern (converts IDs to strings for consistency)
240
+
241
+ ## Verification
242
+
243
+ To verify the fix works with your code:
244
+
245
+ ```javascript
246
+ const UserOrganizationRole = require('./models/userOrganizationRole');
247
+ const userContext = require('./models/userContext');
248
+
249
+ async function testFix() {
250
+ const ctx = new userContext();
251
+
252
+ const orgRole = new UserOrganizationRole();
253
+ orgRole.user_id = "2"; // ← STRING VALUE
254
+ orgRole.organization_id = 1;
255
+ orgRole.role = "org_admin";
256
+ orgRole.created_at = Date.now().toString();
257
+ orgRole.updated_at = Date.now().toString();
258
+
259
+ ctx.UserOrganizationRole.add(orgRole);
260
+
261
+ try {
262
+ await ctx.saveChanges();
263
+ console.log('✅ SUCCESS! String foreign key value was auto-converted.');
264
+ console.log('Inserted record:', orgRole);
265
+ } catch (err) {
266
+ console.error('❌ FAILED:', err.message);
267
+ }
268
+ }
269
+
270
+ testFix();
271
+ ```
272
+
273
+ Expected output:
274
+ ```
275
+ ⚠️ Field UserOrganizationRole.User: Auto-converting string "2" to integer 2
276
+ ✅ SUCCESS! String foreign key value was auto-converted.
277
+ Inserted record: { id: 1, user_id: 2, organization_id: 1, role: 'org_admin', ... }
278
+ ```
279
+
280
+ ## Conclusion
281
+
282
+ The bug is **completely fixed** in v0.3.39. String values assigned to foreign key fields are now:
283
+ 1. **Detected** (checked in both navigation property and foreign key field name)
284
+ 2. **Validated** (throws error for invalid strings)
285
+ 3. **Converted** (auto-converts to integer)
286
+ 4. **Included** in INSERT statements (no more silent failures)
287
+
288
+ Upgrade to v0.3.39 and enjoy hassle-free foreign key handling! 🎉
@@ -0,0 +1,375 @@
1
+ # Global Model Registry - Verification Document
2
+
3
+ ## Issue Fixed
4
+ MasterRecord v0.3.38 eliminates confusing warnings during CLI operations when the same context class is instantiated multiple times.
5
+
6
+ ## The Problem (v0.3.36/v0.3.37)
7
+ When users ran `masterrecord add-migration`, they saw warnings:
8
+ ```
9
+ Warning: dbset() called multiple times for table 'User' - updating existing registration
10
+ Warning: dbset() called multiple times for table 'Auth' - updating existing registration
11
+ ...
12
+ ```
13
+
14
+ These warnings appeared during **normal operation** because:
15
+ 1. The CLI creates 2-3 instances of the context class to inspect schema
16
+ 2. Each instance runs the constructor
17
+ 3. Each constructor calls `dbset()` for each entity
18
+ 4. The duplicate detection (added in v0.3.36) triggered warnings
19
+
20
+ ## The Solution (v0.3.38)
21
+ Added a global model registry that tracks which models have been registered per context class:
22
+ - First instance of a context class: Warns about genuine duplicates in constructor
23
+ - Subsequent instances: Silent (expected CLI behavior)
24
+
25
+ ## Technical Implementation
26
+
27
+ ### 1. Static Global Registry
28
+ ```javascript
29
+ // context.js - Line 180
30
+ static _globalModelRegistry = {};
31
+ // Structure: { 'userContext': Set(['User', 'Auth', 'Settings']), 'qaContext': Set([...]) }
32
+ ```
33
+
34
+ ### 2. Instance-Level First Instance Flag
35
+ ```javascript
36
+ // context.js - Constructor (Line 192)
37
+ const globalRegistry = context._globalModelRegistry[this.__name];
38
+ this.__isFirstInstance = !globalRegistry || globalRegistry.size === 0;
39
+ ```
40
+
41
+ ### 3. Conditional Warning in dbset()
42
+ ```javascript
43
+ // context.js - dbset() method (Line 1050)
44
+ if (existingIndex !== -1) {
45
+ // Duplicate detected in THIS instance
46
+ if (this.__isFirstInstance) {
47
+ // Only warn on first instance
48
+ console.warn(`Warning: dbset() called multiple times for table '${tableName}'...`);
49
+ }
50
+ // Update registration
51
+ this.__entities[existingIndex] = validModel;
52
+ } else {
53
+ // New entity - add to arrays
54
+ this.__entities.push(validModel);
55
+ }
56
+
57
+ // Always mark as globally seen
58
+ const globalRegistry = context._globalModelRegistry[this.__name];
59
+ globalRegistry.add(tableName);
60
+ ```
61
+
62
+ ## Verification Tests
63
+
64
+ ### Test 1: Multiple Context Instances (CLI Pattern)
65
+ ```javascript
66
+ class userContext extends context {
67
+ constructor() {
68
+ super();
69
+ this.dbset(User);
70
+ this.dbset(Auth);
71
+ this.dbset(Settings);
72
+ }
73
+ }
74
+
75
+ // CLI behavior simulation
76
+ const ctx1 = new userContext(); // First instance
77
+ const ctx2 = new userContext(); // Second instance
78
+ const ctx3 = new userContext(); // Third instance
79
+
80
+ // RESULT: Zero warnings (all instances work correctly)
81
+ ```
82
+
83
+ ### Test 2: Genuine Duplicate in Constructor
84
+ ```javascript
85
+ class buggyContext extends context {
86
+ constructor() {
87
+ super();
88
+ this.dbset(User); // Line 5
89
+ this.dbset(User); // Line 6 - DUPLICATE!
90
+ }
91
+ }
92
+
93
+ const ctx1 = new buggyContext(); // First instance - WARNS
94
+ const ctx2 = new buggyContext(); // Second instance - Silent
95
+
96
+ // RESULT:
97
+ // - First instance: Warns about duplicate (helps user fix their code)
98
+ // - Subsequent instances: Silent (user already warned)
99
+ ```
100
+
101
+ ### Test 3: Different Context Classes
102
+ ```javascript
103
+ class userContext extends context {
104
+ constructor() {
105
+ super();
106
+ this.dbset(User);
107
+ }
108
+ }
109
+
110
+ class adminContext extends context {
111
+ constructor() {
112
+ super();
113
+ this.dbset(User); // Same model, different context
114
+ }
115
+ }
116
+
117
+ const userCtx = new userContext();
118
+ const adminCtx = new adminContext();
119
+
120
+ // RESULT: Zero warnings (different context classes have separate registries)
121
+ ```
122
+
123
+ ## Real-World Scenario: User's qaContext
124
+
125
+ ### User's Code Pattern (BEFORE)
126
+ ```javascript
127
+ class qaContext extends context {
128
+ constructor() {
129
+ super();
130
+ // Line 58
131
+ this.dbset(TaxonomyTemplate);
132
+
133
+ // ... 150 lines of other code ...
134
+
135
+ // Line 207 - Common pattern: seed data
136
+ this.dbset(TaxonomyTemplate).seed(templates);
137
+ }
138
+ }
139
+ ```
140
+
141
+ ### What Happened Before v0.3.38
142
+ ```bash
143
+ $ masterrecord add-migration InitialCreate qaContext
144
+ Warning: dbset() called multiple times for table 'TaxonomyTemplate' - updating existing registration
145
+ Warning: dbset() called multiple times for table 'TaxonomyTemplate' - updating existing registration
146
+ Warning: dbset() called multiple times for table 'TaxonomyTemplate' - updating existing registration
147
+ ✓ Migration 'InitialCreate' created successfully
148
+ ```
149
+ - User confused: "Did I do something wrong?"
150
+ - In reality: CLI just instantiated context 3 times (normal behavior)
151
+
152
+ ### What Happens With v0.3.38
153
+ ```bash
154
+ $ masterrecord add-migration InitialCreate qaContext
155
+ Warning: dbset() called multiple times for table 'TaxonomyTemplate' in constructor - updating existing registration
156
+ ✓ Migration 'InitialCreate' created successfully
157
+ ```
158
+ - **One warning** (first instance) - Alerts user to duplicate `dbset()` in their code
159
+ - User can fix: Remove line 58 or line 207 (depends on pattern)
160
+ - After fix: Zero warnings on all future CLI operations
161
+
162
+ ### User's Fixed Code
163
+ ```javascript
164
+ class qaContext extends context {
165
+ constructor() {
166
+ super();
167
+ // Only call dbset() once, with seed data attached
168
+ this.dbset(TaxonomyTemplate).seed(templates);
169
+ }
170
+ }
171
+ ```
172
+
173
+ ```bash
174
+ $ masterrecord add-migration InitialCreate qaContext
175
+ ✓ Migration 'InitialCreate' created successfully
176
+ ```
177
+ ✅ Clean output, no warnings!
178
+
179
+ ## Test Results Summary
180
+
181
+ ### Global Model Registry Tests (test/global-model-registry-test.js)
182
+ - **15 tests** - All passing ✅
183
+ 1. Multiple instances should not warn (CLI pattern)
184
+ 2. Models should be added to global registry on first instance
185
+ 3. Global registry should not have duplicates after multiple instances
186
+ 4. Genuine duplicate in constructor should warn
187
+ 5. Duplicate should warn only on first instance
188
+ 6. Entity should be registered once despite duplicate in constructor
189
+ 7. Same model in different context classes should not warn
190
+ 8. Different context classes should have separate registries
191
+ 9. Multiple instances of different contexts should not warn
192
+ 10. qaContext pattern (dbset then dbset.seed) should warn about duplicate
193
+ 11. Mixed registration should warn only about duplicates
194
+ 12. Empty context should not warn
195
+ 13. Large context with 50 models should not warn on multiple instances
196
+ 14. Registry should not pollute other context classes
197
+ 15. Many context classes should work independently
198
+
199
+ ### Integration with Existing Tests
200
+ - **Entity Deduplication Tests** (test/entity-deduplication-test.js): 5/5 passing ✅
201
+ - **Seed Deduplication Tests** (test/seed-deduplication-test.js): 8/8 passing ✅
202
+ - **qaContext Pattern Tests** (test/qa-context-pattern-test.js): 7/7 passing ✅
203
+
204
+ ## Edge Cases Handled
205
+
206
+ ### 1. Empty Context
207
+ ```javascript
208
+ class emptyContext extends context {
209
+ constructor() {
210
+ super();
211
+ // No entities
212
+ }
213
+ }
214
+
215
+ const ctx1 = new emptyContext();
216
+ const ctx2 = new emptyContext();
217
+ // RESULT: Zero warnings, registry exists but empty
218
+ ```
219
+
220
+ ### 2. Large Context (50+ Models)
221
+ ```javascript
222
+ class largeContext extends context {
223
+ constructor() {
224
+ super();
225
+ for (let i = 0; i < 50; i++) {
226
+ this.dbset(models[i]);
227
+ }
228
+ }
229
+ }
230
+
231
+ const ctx1 = new largeContext();
232
+ const ctx2 = new largeContext();
233
+ // RESULT: Zero warnings, all 50 models registered correctly
234
+ ```
235
+
236
+ ### 3. Mixed Registration
237
+ ```javascript
238
+ class mixedContext extends context {
239
+ constructor() {
240
+ super();
241
+ this.dbset(User); // New
242
+ this.dbset(Auth); // New
243
+ this.dbset(User); // Duplicate
244
+ this.dbset(Settings); // New
245
+ this.dbset(Auth); // Duplicate
246
+ }
247
+ }
248
+
249
+ const ctx = new mixedContext();
250
+ // RESULT: 2 warnings (User and Auth duplicates)
251
+ // ctx.__entities.length === 3 (User, Auth, Settings)
252
+ ```
253
+
254
+ ## Memory Considerations
255
+
256
+ ### Registry Size
257
+ - **Per context class**: One Set object
258
+ - **Per model**: One string (table name)
259
+ - **Typical application**: 3-10 context classes, 5-20 models each
260
+ - **Memory footprint**: ~1-5 KB total (negligible)
261
+
262
+ ### Lifetime
263
+ - Registry persists for application lifetime (intentional caching)
264
+ - Not a memory leak - limited by number of context classes (fixed at compile time)
265
+ - Cleared only on process restart or explicit call to `context.clearModelRegistry()` (if needed for testing)
266
+
267
+ ## Backward Compatibility
268
+
269
+ ### Existing Code Works Unchanged
270
+ ```javascript
271
+ // v0.3.36 code
272
+ class userContext extends context {
273
+ constructor() {
274
+ super();
275
+ this.dbset(User);
276
+ this.dbset(Auth);
277
+ }
278
+ }
279
+
280
+ // Still works in v0.3.38, no code changes needed
281
+ ```
282
+
283
+ ### Warning Messages Still Appear for Genuine Bugs
284
+ ```javascript
285
+ // This still warns (on first instance)
286
+ class buggyContext extends context {
287
+ constructor() {
288
+ super();
289
+ this.dbset(User);
290
+ this.dbset(User); // Still warns: "Warning: dbset() called multiple times..."
291
+ }
292
+ }
293
+ ```
294
+
295
+ ## Industry Standard Comparison
296
+
297
+ ### TypeORM Pattern
298
+ ```typescript
299
+ @Entity()
300
+ class User { ... }
301
+
302
+ // Multiple data sources use same entity definition
303
+ const ds1 = new DataSource({ entities: [User] });
304
+ const ds2 = new DataSource({ entities: [User] });
305
+ // No warnings, no re-registration
306
+ ```
307
+
308
+ ### Sequelize Pattern
309
+ ```javascript
310
+ const UserModel = (sequelize) => sequelize.define('User', { ... });
311
+
312
+ const db1 = new Sequelize();
313
+ const User1 = UserModel(db1);
314
+
315
+ const db2 = new Sequelize();
316
+ const User2 = UserModel(db2);
317
+ // No warnings, each connection gets its own model instance
318
+ ```
319
+
320
+ ### Mongoose Pattern
321
+ ```javascript
322
+ const User = mongoose.model('User', userSchema);
323
+
324
+ // Subsequent calls return cached model (no warning)
325
+ const User2 = mongoose.model('User');
326
+ ```
327
+
328
+ **MasterRecord v0.3.38 Now Matches This Pattern:**
329
+ - First instance registers models, adds to global registry
330
+ - Subsequent instances expected, no warnings
331
+ - Genuine duplicates within same constructor still warn
332
+
333
+ ## Upgrade Path
334
+
335
+ ### For MasterRecord Users
336
+
337
+ 1. **Update to v0.3.38:**
338
+ ```bash
339
+ npm install -g masterrecord@0.3.38
340
+ ```
341
+
342
+ 2. **No code changes needed** - CLI warnings automatically cleaned up
343
+
344
+ 3. **If you see warnings** (on first instance only):
345
+ - Check your context constructor for duplicate `dbset()` calls
346
+ - Common pattern: `dbset(Entity)` + later `dbset(Entity).seed(data)`
347
+ - Fix: Remove one of the `dbset()` calls
348
+
349
+ 4. **After fixing duplicates:**
350
+ ```bash
351
+ masterrecord add-migration YourMigration yourContext
352
+ ```
353
+ Should see clean output with zero warnings.
354
+
355
+ ## Success Criteria Checklist
356
+
357
+ ✅ CLI commands produce clean output (no spurious warnings)
358
+ ✅ Multiple context instances work without warnings
359
+ ✅ Genuine duplicates in constructor still emit warnings (first instance only)
360
+ ✅ Different context classes maintain separate registries
361
+ ✅ All existing tests still pass (entity deduplication, seed deduplication, etc.)
362
+ ✅ 15 new tests pass (global registry functionality)
363
+ ✅ Memory usage remains acceptable (<5 KB overhead)
364
+ ✅ Backward compatible - existing user code continues to work
365
+ ✅ Matches industry-standard ORM patterns (TypeORM, Sequelize, Mongoose)
366
+
367
+ ## Conclusion
368
+
369
+ MasterRecord v0.3.38 successfully eliminates confusing CLI warnings while preserving the ability to detect genuine bugs in user code. The global model registry provides a clean, intuitive developer experience that matches industry-standard ORM patterns.
370
+
371
+ **User Impact:**
372
+ - ✅ Clean CLI output during migration generation
373
+ - ✅ Clear guidance when actual bugs exist (warns once on first instance)
374
+ - ✅ No code changes required (automatic improvement)
375
+ - ✅ Better developer experience overall
package/SQLLiteEngine.js CHANGED
@@ -1126,6 +1126,14 @@ class SQLLiteEngine {
1126
1126
  if(column.indexOf("__") === -1 ){
1127
1127
  var fieldColumn = fields[column];
1128
1128
 
1129
+ // 🔥 FIX: For belongsTo relationships, also check the foreignKey field name
1130
+ // Users can set either orgRole.User = obj OR orgRole.user_id = 2
1131
+ if((fieldColumn === undefined || fieldColumn === null) &&
1132
+ modelEntity[column].relationshipType === "belongsTo" &&
1133
+ modelEntity[column].foreignKey) {
1134
+ fieldColumn = fields[modelEntity[column].foreignKey];
1135
+ }
1136
+
1129
1137
  if((fieldColumn !== undefined && fieldColumn !== null ) && typeof(fieldColumn) !== "object"){
1130
1138
  // 🔥 Apply toDatabase transformer before validation
1131
1139
  try {