masterrecord 0.3.48 → 0.3.49
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/Migrations/migrations.js +6 -11
- package/package.json +1 -1
- package/readme.md +9 -749
- package/test/v0.3.34-bug-fixes-test.js +50 -8
package/Migrations/migrations.js
CHANGED
|
@@ -201,6 +201,9 @@ class Migrations{
|
|
|
201
201
|
|
|
202
202
|
#findNewIndexes(tables){
|
|
203
203
|
tables.forEach(function (item, index) {
|
|
204
|
+
// Skip new tables — createTable() in schema.js already creates their indexes
|
|
205
|
+
if(item.newTables && item.newTables.length > 0) return;
|
|
206
|
+
|
|
204
207
|
if(item.new && item.old){
|
|
205
208
|
Object.keys(item.new).forEach(function (key) {
|
|
206
209
|
if(typeof item.new[key] === "object" && item.new[key].indexes){
|
|
@@ -289,6 +292,9 @@ class Migrations{
|
|
|
289
292
|
|
|
290
293
|
#findNewCompositeIndexes(tables) {
|
|
291
294
|
tables.forEach(function (item, index) {
|
|
295
|
+
// Skip new tables — createTable() in schema.js already creates their composite indexes
|
|
296
|
+
if(item.newTables && item.newTables.length > 0) return;
|
|
297
|
+
|
|
292
298
|
if (item.new && item.old) {
|
|
293
299
|
const newComposite = item.new.__compositeIndexes || [];
|
|
294
300
|
const oldComposite = item.old.__compositeIndexes || [];
|
|
@@ -307,17 +313,6 @@ class Migrations{
|
|
|
307
313
|
});
|
|
308
314
|
}
|
|
309
315
|
});
|
|
310
|
-
} else if (item.new && !item.old) {
|
|
311
|
-
// New table - all composite indexes are new
|
|
312
|
-
const composites = item.new.__compositeIndexes || [];
|
|
313
|
-
composites.forEach(function(idx) {
|
|
314
|
-
item.newCompositeIndexes.push({
|
|
315
|
-
tableName: item.name,
|
|
316
|
-
columns: idx.columns,
|
|
317
|
-
indexName: idx.name,
|
|
318
|
-
unique: idx.unique
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
316
|
}
|
|
322
317
|
});
|
|
323
318
|
return tables;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.49",
|
|
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
|
@@ -3369,755 +3369,15 @@ user.name = null; // Error if name is { nullable: false }
|
|
|
3369
3369
|
|
|
3370
3370
|
## Changelog
|
|
3371
3371
|
|
|
3372
|
-
### Version 0.3.
|
|
3373
|
-
|
|
3374
|
-
#### Bug Fixed: Migration methods not properly awaiting async database operations
|
|
3375
|
-
- **FIXED**: All migration schema methods (`createTable`, `addColumn`, `dropColumn`, `dropTable`, `alterColumn`, `renameColumn`, `createIndex`, `dropIndex`, `createCompositeIndex`, `dropCompositeIndex`, `seed`, `bulkSeed`) are now fully `async` and properly `await` all database operations
|
|
3376
|
-
- **Root Cause**: SQLite's `_execute()` is synchronous, but MySQL/PostgreSQL return Promises. Migration methods were calling `this.context._execute()` without `await`, causing operations to run out of order (e.g., CREATE INDEX running before CREATE TABLE finished)
|
|
3377
|
-
- **Solution**: Made all schema methods async, added `await` to every `_execute()` call, converted `forEach` loops to `for...of` for proper async iteration
|
|
3378
|
-
- **FIXED**: `context._execute()` now properly returns the engine's Promise (`return this._SQLEngine._execute(query)`)
|
|
3379
|
-
- **FIXED**: Added `_execute()` method to MySQL and PostgreSQL engines (previously only existed on SQLite)
|
|
3380
|
-
- **FIXED**: `syncTable()` no longer crashes on metadata properties (`indexes`, `__compositeIndexes`) — now filters them out before column iteration
|
|
3381
|
-
- **FIXED**: Migration template now generates `await` for all method calls in migration files
|
|
3382
|
-
- **FIXED**: Unhandled promise rejection crash — MySQL/PostgreSQL `_initPromise` now has `.catch(() => {})` to prevent Node.js crash before `_ensureReady()` can handle errors
|
|
3383
|
-
- **FIXED**: Added `_ready` flag to prevent duplicate `_ensureReady()` execution
|
|
3372
|
+
### Version 0.3.49 (2026-02-21) - FIX: Duplicate Index Creation for New Tables
|
|
3384
3373
|
|
|
3385
|
-
####
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
5. **`context.js`** - `_execute()` returns engine result, `.catch()` on `_initPromise`
|
|
3391
|
-
6. **`package.json`** - Updated to v0.3.48
|
|
3392
|
-
|
|
3393
|
-
### Version 0.3.44 (2026-02-20) - FIX: MySQL/PostgreSQL Auto-Create Database During Migrations
|
|
3394
|
-
|
|
3395
|
-
#### Bug Fixed: `update-database` failing when database doesn't exist yet (MySQL & PostgreSQL)
|
|
3396
|
-
- **FIXED**: Running `masterrecord update-database` on a fresh MySQL or PostgreSQL server would fail because `__mysqlInit`/`__postgresInit` tried to connect to the database before it was created
|
|
3397
|
-
- **Root Cause**: The `createDatabase()` method depended on `this.context.db` (the connected client) to get config, but the client connection failed because the database didn't exist — a chicken-and-egg problem
|
|
3398
|
-
- **Solution**: Store config as `this._dbConfig` on the context before async init. `_ensureReady()` now catches database-not-found errors (MySQL: "Unknown database", PostgreSQL: "does not exist"), creates the database using a server-level connection, then retries the full init
|
|
3399
|
-
- **Impact**: `update-database` now automatically creates the database if it doesn't exist for all three engines (SQLite, MySQL, PostgreSQL)
|
|
3400
|
-
|
|
3401
|
-
#### Files Modified
|
|
3402
|
-
1. **`context.js`** - Store `_dbConfig` before async MySQL and PostgreSQL init
|
|
3403
|
-
2. **`Migrations/schema.js`** - Added `_createDatabaseFromConfig()`, `_retryMySQLInit()`, `_createPostgresDatabaseFromConfig()`, `_retryPostgresInit()`, updated `_ensureReady()` to handle missing database for both engines
|
|
3404
|
-
|
|
3405
|
-
### Version 0.3.42 (2026-02-20) - FIX: Migration System + add-migration Improvements
|
|
3406
|
-
|
|
3407
|
-
#### Bug Fixed: `update-database` failing for MySQL/PostgreSQL with "Cannot read properties of null"
|
|
3408
|
-
- **FIXED**: Running `masterrecord update-database` with MySQL or PostgreSQL would crash with `Cannot read properties of null (reading 'tableExists')`
|
|
3409
|
-
- **Root Cause**: The migration `schema.js` constructor creates a new context instance, but MySQL/PostgreSQL database initialization is async. The `_SQLEngine` property was still `null` when `createTable` tried to use it because the async pool init hadn't completed yet.
|
|
3410
|
-
- **Solution**: Store the async init promise as `_initPromise` on the context instance. Added `_ensureReady()` method to `schema.js` that awaits it. Called in both `init()` and `createTable()` (safety net for existing migrations).
|
|
3411
|
-
- **Also fixed**: Typo in `schema.js` — `require('masterrecord/mySQLAsyncConnect')` corrected to `require('masterrecord/mySQLConnect')`
|
|
3412
|
-
- **Also fixed**: `init()` now properly `await`s `createDatabase()` instead of fire-and-forget
|
|
3413
|
-
|
|
3414
|
-
#### Bug Fixed: `add-migration` creating unnecessary database connections
|
|
3415
|
-
- **FIXED**: Running `masterrecord add-migration` would open a real database connection (creating SQLite files on disk, or connecting to MySQL/PostgreSQL) even though it only needs entity schemas and seed data to generate migration files
|
|
3416
|
-
- **Solution**: Added schema-only mode (`MASTERRECORD_SCHEMA_ONLY` env var) that sets database type flags but skips all DB initialization
|
|
3374
|
+
#### Bug Fixed: `createIndex` called twice for new tables during `update-database`
|
|
3375
|
+
- **FIXED**: When running `update-database` with a new table that has indexes, the indexes were created twice — once by `createTable()` in schema.js (which iterates column `.index()` definitions and `__compositeIndexes`), and again by explicit `createIndex()`/`createCompositeIndex()` calls in the generated migration file
|
|
3376
|
+
- **Symptom**: MySQL error `Duplicate key name 'idx_...'` during InitialCreate migrations
|
|
3377
|
+
- **Root Cause**: `migrations.js` `#findNewIndexes()` and `#findNewCompositeIndexes()` added index operations for ALL tables, including brand new ones where `createTable()` already handles them
|
|
3378
|
+
- **Solution**: Skip index generation in migration template for new tables (`item.newTables.length > 0`). For existing tables getting new indexes, explicit `createIndex` calls are still generated correctly.
|
|
3417
3379
|
|
|
3418
3380
|
#### Files Modified
|
|
3419
|
-
1. **`Migrations/
|
|
3420
|
-
2. **`
|
|
3421
|
-
3. **`
|
|
3422
|
-
4. **`context.js`** - Store `_initPromise` for MySQL/PostgreSQL async init, schema-only mode in `env()`
|
|
3423
|
-
5. **`package.json`** - Updated to v0.3.42
|
|
3424
|
-
|
|
3425
|
-
### Version 0.3.39 (2026-02-09) - CRITICAL BUG FIX: Foreign Key String Values
|
|
3426
|
-
|
|
3427
|
-
#### Bug Fixed: Foreign Key Fields Silently Ignoring String Values
|
|
3428
|
-
- **FIXED**: Critical bug where string values assigned to foreign key fields were silently excluded from INSERT statements
|
|
3429
|
-
- **Problem**: When you assign `orgRole.user_id = "2"` (string), MasterRecord excluded it from INSERT, causing NOT NULL constraint failures
|
|
3430
|
-
- **Root Cause**: INSERT builder only checked navigation property name (`User`), not foreign key field name (`user_id`)
|
|
3431
|
-
- **Impact**: Common in real-world apps where IDs come from JWT tokens, HTTP requests, or authService (returns string IDs)
|
|
3432
|
-
|
|
3433
|
-
#### What Was Happening (Before v0.3.39)
|
|
3434
|
-
```javascript
|
|
3435
|
-
// User assigns string value to foreign key
|
|
3436
|
-
orgRole.user_id = "2"; // ← STRING (from currentUser.id)
|
|
3437
|
-
orgRole.organization_id = 8; // ← NUMBER (from database)
|
|
3438
|
-
orgRole.role = 'org_admin';
|
|
3439
|
-
|
|
3440
|
-
await userContext.saveChanges();
|
|
3441
|
-
// ❌ Generated SQL: INSERT INTO [UserOrganizationRole] ([role]) VALUES ('org_admin')
|
|
3442
|
-
// ❌ Error: NOT NULL constraint failed: UserOrganizationRole.user_id
|
|
3443
|
-
```
|
|
3444
|
-
|
|
3445
|
-
**Why It Failed:**
|
|
3446
|
-
- `belongsTo('User', 'user_id')` creates property `User` with `foreignKey: 'user_id'`
|
|
3447
|
-
- INSERT builder looked for `fields['User']` (navigation property)
|
|
3448
|
-
- User set `fields['user_id']` (foreign key field name)
|
|
3449
|
-
- Field not found → silently skipped → INSERT failed
|
|
3450
|
-
|
|
3451
|
-
#### The Fix (v0.3.39)
|
|
3452
|
-
Updated `_buildSQLInsertObjectParameterized` in all database engines:
|
|
3453
|
-
- Now checks BOTH navigation property name AND foreign key field name
|
|
3454
|
-
- Auto-converts string values to integers for integer foreign key fields
|
|
3455
|
-
- Maintains backward compatibility (setting navigation property still works)
|
|
3456
|
-
|
|
3457
|
-
```javascript
|
|
3458
|
-
// After fix - both patterns work:
|
|
3459
|
-
orgRole.User = 2; // ✅ Works (navigation property)
|
|
3460
|
-
orgRole.user_id = "2"; // ✅ Works (foreign key field, auto-converted to integer)
|
|
3461
|
-
```
|
|
3462
|
-
|
|
3463
|
-
#### Files Modified
|
|
3464
|
-
1. **SQLLiteEngine.js** (lines 1127-1137) - Added foreign key field lookup
|
|
3465
|
-
2. **mySQLEngine.js** (lines 654-664) - Added foreign key field lookup
|
|
3466
|
-
3. **postgresEngine.js** (lines 601-611) - Added foreign key field lookup
|
|
3467
|
-
4. **test/foreign-key-string-value-test.js** (NEW) - 8 comprehensive tests
|
|
3468
|
-
5. **package.json** - Updated to v0.3.39
|
|
3469
|
-
6. **readme.md** - Added changelog
|
|
3470
|
-
|
|
3471
|
-
#### Test Results
|
|
3472
|
-
- **8 new tests** - All passing ✅
|
|
3473
|
-
1. String foreign key value included in INSERT ✅
|
|
3474
|
-
2. Number foreign key value still works ✅
|
|
3475
|
-
3. Mixed string and number foreign keys ✅
|
|
3476
|
-
4. String with leading zeros (e.g., "007" → 7) ✅
|
|
3477
|
-
5. Invalid strings throw error (not silent failure) ✅
|
|
3478
|
-
6. Empty strings throw error ✅
|
|
3479
|
-
7. Backward compatible (navigation property still works) ✅
|
|
3480
|
-
8. Prefers navigation property if both set ✅
|
|
3481
|
-
|
|
3482
|
-
#### Real-World Example: authService Returns String IDs
|
|
3483
|
-
```javascript
|
|
3484
|
-
// authService.js returns:
|
|
3485
|
-
const currentUser = {
|
|
3486
|
-
id: "2", // ← STRING (from String(obj.user.id))
|
|
3487
|
-
email: "customer1@bookbag.ai",
|
|
3488
|
-
system_role: "system_user"
|
|
3489
|
-
};
|
|
3490
|
-
|
|
3491
|
-
// User creates association:
|
|
3492
|
-
const orgRole = new UserOrganizationRole();
|
|
3493
|
-
orgRole.user_id = currentUser.id; // ← Before: silently skipped. After: auto-converted to 2
|
|
3494
|
-
orgRole.organization_id = newOrg.id; // ← NUMBER from database
|
|
3495
|
-
orgRole.role = 'org_admin';
|
|
3496
|
-
|
|
3497
|
-
await userContext.saveChanges(); // ✅ Now works!
|
|
3498
|
-
```
|
|
3499
|
-
|
|
3500
|
-
#### Impact
|
|
3501
|
-
- ✅ **Auto-converts** string foreign keys to integers (with validation)
|
|
3502
|
-
- ✅ **Clear errors** for invalid strings (not silent failures)
|
|
3503
|
-
- ✅ **Backward compatible** - navigation property pattern still works
|
|
3504
|
-
- ✅ **Works across all databases** (SQLite, MySQL, PostgreSQL)
|
|
3505
|
-
- ✅ **Matches real-world usage** where IDs are often strings
|
|
3506
|
-
|
|
3507
|
-
#### Upgrade Path
|
|
3508
|
-
```bash
|
|
3509
|
-
npm install -g masterrecord@0.3.39
|
|
3510
|
-
```
|
|
3511
|
-
No code changes needed - automatic fix! If you have workarounds like `parseInt(currentUser.id)`, you can now remove them (but leaving them is harmless).
|
|
3512
|
-
|
|
3513
|
-
---
|
|
3514
|
-
|
|
3515
|
-
### Version 0.3.38 (2026-02-06) - GLOBAL MODEL REGISTRY (UX FIX)
|
|
3516
|
-
|
|
3517
|
-
#### Enhancement: Eliminates Confusing CLI Warnings
|
|
3518
|
-
- **FIXED**: Confusing warnings during normal CLI operation when generating migrations
|
|
3519
|
-
- **Previous Behavior**: v0.3.36/0.3.37 correctly detected duplicate `dbset()` calls and emitted warnings
|
|
3520
|
-
- **Problem**: CLI instantiates the same context class multiple times to inspect schema
|
|
3521
|
-
- **Impact**: Users saw warnings during normal operation: `"Warning: dbset() called multiple times for table 'User'..."`
|
|
3522
|
-
- **User Confusion**: Warnings appeared even when code was correct, making users think they did something wrong
|
|
3523
|
-
|
|
3524
|
-
#### Implementation - Global Model Registry
|
|
3525
|
-
**The Solution** (`context.js`)
|
|
3526
|
-
- Added static `_globalModelRegistry` property to track registered models per context class
|
|
3527
|
-
- Structure: `{ 'userContext': Set(['User', 'Auth', 'Settings']), 'qaContext': Set([...]) }`
|
|
3528
|
-
- Each context instance checks if it's the first instance via `__isFirstInstance` flag
|
|
3529
|
-
- Warnings only appear on the first instance of a context class (genuine bugs)
|
|
3530
|
-
- Subsequent instances (CLI pattern) are silent since they're expected
|
|
3531
|
-
|
|
3532
|
-
**How It Works:**
|
|
3533
|
-
|
|
3534
|
-
1. **First Instance** (constructor execution):
|
|
3535
|
-
```javascript
|
|
3536
|
-
const ctx1 = new userContext();
|
|
3537
|
-
// __isFirstInstance = true (global registry empty)
|
|
3538
|
-
// dbset(User) - adds User to global registry
|
|
3539
|
-
// dbset(User) again - WARNS (duplicate in same constructor)
|
|
3540
|
-
// dbset(Auth) - adds Auth to global registry
|
|
3541
|
-
```
|
|
3542
|
-
|
|
3543
|
-
2. **Subsequent Instances** (CLI creates multiple):
|
|
3544
|
-
```javascript
|
|
3545
|
-
const ctx2 = new userContext();
|
|
3546
|
-
// __isFirstInstance = false (global registry has User, Auth)
|
|
3547
|
-
// dbset(User) - no warning (expected pattern)
|
|
3548
|
-
// dbset(User) again - no warning (expected pattern)
|
|
3549
|
-
// dbset(Auth) - no warning (expected pattern)
|
|
3550
|
-
```
|
|
3551
|
-
|
|
3552
|
-
3. **Duplicate Detection Still Works**:
|
|
3553
|
-
- If user's constructor has `dbset(User)` called twice, the first instance warns
|
|
3554
|
-
- This guides users to fix their code (remove the duplicate)
|
|
3555
|
-
- After fixing, all future CLI operations are silent
|
|
3556
|
-
|
|
3557
|
-
**Benefits:**
|
|
3558
|
-
- ✅ **Clean CLI Output**: No spurious warnings during `masterrecord add-migration`
|
|
3559
|
-
- ✅ **Genuine Bug Detection**: Still warns about actual duplicates in user code
|
|
3560
|
-
- ✅ **Better UX**: Users no longer confused by normal operation warnings
|
|
3561
|
-
- ✅ **Backward Compatible**: Existing code continues to work
|
|
3562
|
-
- ✅ **Industry-Standard Pattern**: Matches how TypeORM, Sequelize, Mongoose handle multiple instances
|
|
3563
|
-
|
|
3564
|
-
**Files Modified:**
|
|
3565
|
-
1. `context.js` - Added `_globalModelRegistry` static property, `__isFirstInstance` instance flag, updated `dbset()` logic
|
|
3566
|
-
2. `test/global-model-registry-test.js` (NEW) - 15 comprehensive tests covering:
|
|
3567
|
-
- Multiple context instances (CLI pattern) - no warnings ✅
|
|
3568
|
-
- Genuine duplicates in constructor - warns once ✅
|
|
3569
|
-
- Multiple context classes with same models - no warnings ✅
|
|
3570
|
-
- Registry isolation between context classes ✅
|
|
3571
|
-
- Edge cases (empty contexts, large contexts, mixed registration) ✅
|
|
3572
|
-
3. `package.json` - Updated version to 0.3.38
|
|
3573
|
-
4. `readme.md` - Added changelog entry
|
|
3574
|
-
|
|
3575
|
-
**Test Results:**
|
|
3576
|
-
- **15 new tests** - All passing ✅
|
|
3577
|
-
- Tests verify CLI pattern (3 instances) produces zero warnings
|
|
3578
|
-
- Tests verify genuine duplicates still warn on first instance only
|
|
3579
|
-
- Tests verify different context classes have separate registries
|
|
3580
|
-
- Tests verify large contexts (50 models) work without warnings
|
|
3581
|
-
|
|
3582
|
-
**Upgrade Path:**
|
|
3583
|
-
```bash
|
|
3584
|
-
npm install -g masterrecord@0.3.38
|
|
3585
|
-
```
|
|
3586
|
-
No code changes needed - automatic improvement to CLI experience.
|
|
3587
|
-
|
|
3588
|
-
**Real-World Example:**
|
|
3589
|
-
|
|
3590
|
-
Before v0.3.38:
|
|
3591
|
-
```bash
|
|
3592
|
-
$ masterrecord add-migration CreateUsers userContext
|
|
3593
|
-
Warning: dbset() called multiple times for table 'User' - updating existing registration
|
|
3594
|
-
Warning: dbset() called multiple times for table 'Auth' - updating existing registration
|
|
3595
|
-
Warning: dbset() called multiple times for table 'Settings' - updating existing registration
|
|
3596
|
-
✓ Migration 'CreateUsers' created successfully
|
|
3597
|
-
```
|
|
3598
|
-
|
|
3599
|
-
After v0.3.38:
|
|
3600
|
-
```bash
|
|
3601
|
-
$ masterrecord add-migration CreateUsers userContext
|
|
3602
|
-
✓ Migration 'CreateUsers' created successfully
|
|
3603
|
-
```
|
|
3604
|
-
|
|
3605
|
-
---
|
|
3606
|
-
|
|
3607
|
-
### Version 0.3.36 (2026-02-06) - ROOT CAUSE FIX + CONFIG DISCOVERY FIX
|
|
3608
|
-
|
|
3609
|
-
#### Critical Bug Fix #1: Duplicate Entities and Seed Data - Complete Resolution
|
|
3610
|
-
- **FIXED**: Root cause of duplicate entities and seed data in migrations
|
|
3611
|
-
- **Root Cause**: User contexts calling `dbset(Entity)` then later `dbset(Entity).seed(data)` caused:
|
|
3612
|
-
- Entity registered twice in `__entities` array
|
|
3613
|
-
- Seed data duplicated in `__contextSeedData`
|
|
3614
|
-
- Snapshots containing duplicate table definitions
|
|
3615
|
-
- Migrations generating 2x operations (e.g., 18 template records instead of 9)
|
|
3616
|
-
|
|
3617
|
-
#### Implementation - EF Core HasData Semantics
|
|
3618
|
-
**Fix #1: Entity Deduplication in `dbset()`** (`context.js` lines 1025-1037)
|
|
3619
|
-
- Added `findIndex()` check before adding entities to `__entities` array
|
|
3620
|
-
- If entity with same table name exists, updates it instead of adding duplicate
|
|
3621
|
-
- Emits warning: `"Warning: dbset() called multiple times for table 'X' - updating existing registration"`
|
|
3622
|
-
|
|
3623
|
-
**Fix #2: Seed Data Deduplication in `#addSeedData()`** (`context.js` lines 1190-1223)
|
|
3624
|
-
- Implements Entity Framework Core `HasData` semantics:
|
|
3625
|
-
- **Update**: If record with same primary key exists, merge/update fields
|
|
3626
|
-
- **Insert**: If primary key doesn't exist or is undefined, append record
|
|
3627
|
-
- Upserts by primary key (supports custom primary keys like `uuid`)
|
|
3628
|
-
- Emits warning: `"Warning: seed() called multiple times for table 'X' - using upsert semantics..."`
|
|
3629
|
-
- User requested this approach to match EF Core behavior
|
|
3630
|
-
|
|
3631
|
-
#### Critical Bug Fix #2: Config File Discovery - Glob Pattern Too Broad
|
|
3632
|
-
- **FIXED**: Glob pattern was matching non-environment config files
|
|
3633
|
-
- **Root Cause**: Pattern `${rootFolder}/**/*{env.${envType},${envType}}.json` matched ANY file ending with `.${envType}.json`
|
|
3634
|
-
- **Impact**: When multiple files matched (e.g., `free-audit-page.development.json` and `env.development.json`), glob returned them alphabetically and the wrong file was loaded first
|
|
3635
|
-
- **Real-World Example**:
|
|
3636
|
-
- User had `config/environments/free-audit-page.development.json` (no context configs)
|
|
3637
|
-
- User had `config/environments/env.development.json` (has userContext config)
|
|
3638
|
-
- Glob found both, returned `free-audit-page.development.json` first (alphabetically)
|
|
3639
|
-
- Migration command failed: "Configuration missing settings for context 'userContext'"
|
|
3640
|
-
- **Fix**: Split into two specific patterns with priority:
|
|
3641
|
-
1. `**/env.${envType}.json` (preferred - exact match)
|
|
3642
|
-
2. `**/${envType}.json` (fallback - exact match)
|
|
3643
|
-
- **Result**: Only matches actual environment config files, not arbitrary files
|
|
3644
|
-
|
|
3645
|
-
**Fix #3: Config File Priority Pattern** (`context.js` lines 445-470)
|
|
3646
|
-
- Changed from single broad pattern to prioritized specific patterns
|
|
3647
|
-
- Tries `env.<envType>.json` first (most specific)
|
|
3648
|
-
- Falls back to `<envType>.json` (less specific)
|
|
3649
|
-
- Prevents false positives from files like `my-config.development.json`
|
|
3650
|
-
|
|
3651
|
-
#### Technical Details
|
|
3652
|
-
**Files Modified:**
|
|
3653
|
-
1. `context.js` - Added deduplication logic in `dbset()` and `#addSeedData()`
|
|
3654
|
-
2. `context.js` - Fixed glob pattern for config file discovery (lines 445-470)
|
|
3655
|
-
3. `test/entity-deduplication-test.js` (NEW) - 5 tests for entity deduplication
|
|
3656
|
-
4. `test/seed-deduplication-test.js` (NEW) - 8 tests for EF Core seed semantics
|
|
3657
|
-
5. `test/qa-context-pattern-test.js` (NEW) - 7 tests for real-world patterns
|
|
3658
|
-
6. `test/config-glob-pattern-test.js` (NEW) - 7 tests for config file discovery
|
|
3659
|
-
7. `package.json` - Updated version to 0.3.36
|
|
3660
|
-
8. `readme.md` - Added changelog and documentation
|
|
3661
|
-
|
|
3662
|
-
**Test Results:**
|
|
3663
|
-
- **27 new tests** - All passing ✅
|
|
3664
|
-
- 20 tests for duplicate entity/seed data fixes
|
|
3665
|
-
- 7 tests for config file discovery fix
|
|
3666
|
-
- Tests cover the exact qaContext pattern (lines 58 + 207) that caused the bug
|
|
3667
|
-
- Tests verify 9 seeds stay as 9 (not 18), Settings stay as 2 (not 4)
|
|
3668
|
-
- Tests verify glob pattern only matches environment config files
|
|
3669
|
-
|
|
3670
|
-
#### Upgrade Path
|
|
3671
|
-
1. **Update to v0.3.36**: `npm install -g masterrecord@0.3.36`
|
|
3672
|
-
2. **If you have duplicate data in your database from older versions**:
|
|
3673
|
-
- Manually remove duplicate records (check by primary key)
|
|
3674
|
-
- Delete existing snapshots: `rm db/migrations/*_contextSnapShot.json`
|
|
3675
|
-
- Regenerate migrations: `masterrecord add-migration YourContext "clean-regenerate"`
|
|
3676
|
-
3. **Future migrations**: Will automatically deduplicate entities and seed data
|
|
3677
|
-
|
|
3678
|
-
#### Why This Fix is Comprehensive
|
|
3679
|
-
- **Root Cause Fix**: Prevents duplicates from ever being created at registration time
|
|
3680
|
-
- **EF Core Semantics**: Industry-standard seed data pattern with idempotent operations
|
|
3681
|
-
- **Defense-in-Depth**: Multiple layers of protection ensure data integrity
|
|
3682
|
-
|
|
3683
|
-
#### Impact
|
|
3684
|
-
- ✅ Entities registered only once even with multiple `dbset()` calls
|
|
3685
|
-
- ✅ Seed data uses EF Core semantics (upsert by primary key)
|
|
3686
|
-
- ✅ Warning messages guide users to fix their code patterns
|
|
3687
|
-
- ✅ Config file discovery now specific and predictable (only matches env.*.json and *.json)
|
|
3688
|
-
- ✅ Migrations no longer fail when non-environment config files exist
|
|
3689
|
-
- ✅ Backward compatible - existing single `dbset()` calls work as before
|
|
3690
|
-
|
|
3691
|
-
## Version Compatibility
|
|
3692
|
-
|
|
3693
|
-
| Component | Version | Notes |
|
|
3694
|
-
|---------------|---------------|------------------------------------------|
|
|
3695
|
-
| MasterRecord | 0.3.36 | Current version - root cause fix for duplicate entities/seeds |
|
|
3696
|
-
| Node.js | 14+ | Async/await support required |
|
|
3697
|
-
| PostgreSQL | 9.6+ (12+) | Tested with 12, 13, 14, 15, 16 |
|
|
3698
|
-
| MySQL | 5.7+ (8.0+) | Tested with 8.0+ |
|
|
3699
|
-
| SQLite | 3.x | Any recent version |
|
|
3700
|
-
| pg | 8.17.2+ | PostgreSQL driver (async) |
|
|
3701
|
-
| mysql2 | 3.11.5+ | MySQL driver (async with connection pooling) |
|
|
3702
|
-
| better-sqlite3| 12.6.2+ | SQLite driver (wrapped with async API) |
|
|
3703
|
-
|
|
3704
|
-
## Documentation
|
|
3705
|
-
|
|
3706
|
-
- [PostgreSQL Setup Guide](./docs/POSTGRESQL_SETUP.md) - Complete PostgreSQL configuration
|
|
3707
|
-
- [Migrations Guide](./docs/MIGRATIONS_GUIDE.md) - Detailed migration tutorial
|
|
3708
|
-
- [Methods Reference](./docs/METHODS_REFERENCE.md) - Complete API reference
|
|
3709
|
-
- [Field Transformers](./docs/FIELD_TRANSFORMERS.md) - Custom type handling
|
|
3710
|
-
|
|
3711
|
-
## Contributing
|
|
3712
|
-
|
|
3713
|
-
Contributions are welcome! Please:
|
|
3714
|
-
|
|
3715
|
-
1. Fork the repository
|
|
3716
|
-
2. Create a feature branch
|
|
3717
|
-
3. Make your changes with tests
|
|
3718
|
-
4. Submit a pull request
|
|
3719
|
-
|
|
3720
|
-
### Running Tests
|
|
3721
|
-
|
|
3722
|
-
```bash
|
|
3723
|
-
# PostgreSQL engine tests
|
|
3724
|
-
node test/postgresEngineTest.js
|
|
3725
|
-
|
|
3726
|
-
# Integration tests (requires database)
|
|
3727
|
-
node test/postgresIntegrationTest.js
|
|
3728
|
-
|
|
3729
|
-
# All tests
|
|
3730
|
-
npm test
|
|
3731
|
-
```
|
|
3732
|
-
|
|
3733
|
-
## License
|
|
3734
|
-
|
|
3735
|
-
MIT License - see [LICENSE](LICENSE) file for details.
|
|
3736
|
-
|
|
3737
|
-
## Credits
|
|
3738
|
-
|
|
3739
|
-
Created by Alexander Rich
|
|
3740
|
-
|
|
3741
|
-
## Support
|
|
3742
|
-
|
|
3743
|
-
- GitHub Issues: [Report bugs or request features](https://github.com/Tailor/MasterRecord/issues)
|
|
3744
|
-
- npm: [masterrecord](https://www.npmjs.com/package/masterrecord)
|
|
3745
|
-
|
|
3746
|
-
---
|
|
3747
|
-
|
|
3748
|
-
## Recent Improvements
|
|
3749
|
-
|
|
3750
|
-
### v0.3.30 - Mature ORM Features (Latest)
|
|
3751
|
-
|
|
3752
|
-
MasterRecord is now feature-complete with lifecycle hooks, validation, and bulk operations - matching the capabilities of mature ORMs like Sequelize, TypeORM, and Prisma.
|
|
3753
|
-
|
|
3754
|
-
**🎯 Entity Serialization:**
|
|
3755
|
-
- ✅ **`.toObject()`** - Convert entities to plain JavaScript objects with circular reference protection
|
|
3756
|
-
- ✅ **`.toJSON()`** - Automatic JSON.stringify() compatibility for Express responses
|
|
3757
|
-
- ✅ **Circular Reference Handling** - Prevents infinite loops from bidirectional relationships
|
|
3758
|
-
- ✅ **Depth Control** - Configurable relationship traversal depth
|
|
3759
|
-
|
|
3760
|
-
**🎯 Active Record Pattern:**
|
|
3761
|
-
- ✅ **`.delete()`** - Entities can delete themselves (`await user.delete()`)
|
|
3762
|
-
- ✅ **`.reload()`** - Refresh entity from database, discard unsaved changes
|
|
3763
|
-
- ✅ **`.clone()`** - Create entity copies for duplication (excludes primary key)
|
|
3764
|
-
- ✅ **`.save()`** - Already existed, now part of complete Active Record pattern
|
|
3765
|
-
|
|
3766
|
-
**🎯 Query Helpers:**
|
|
3767
|
-
- ✅ **`.first()`** - Get first record ordered by primary key
|
|
3768
|
-
- ✅ **`.last()`** - Get last record ordered by primary key descending
|
|
3769
|
-
- ✅ **`.exists()`** - Check if any records match query (returns boolean)
|
|
3770
|
-
- ✅ **`.pluck(field)`** - Extract single column values as array
|
|
3771
|
-
|
|
3772
|
-
**🎯 Lifecycle Hooks:**
|
|
3773
|
-
- ✅ **`beforeSave()`** - Execute before insert or update (e.g., hash passwords)
|
|
3774
|
-
- ✅ **`afterSave()`** - Execute after successful save (e.g., logging)
|
|
3775
|
-
- ✅ **`beforeDelete()`** - Execute before deletion (can prevent deletion)
|
|
3776
|
-
- ✅ **`afterDelete()`** - Execute after deletion (e.g., cleanup)
|
|
3777
|
-
- ✅ **Hook Execution Order** - Guaranteed execution order with error handling
|
|
3778
|
-
- ✅ **Async Support** - Hooks can be async for database operations
|
|
3779
|
-
|
|
3780
|
-
**🎯 Business Logic Validation:**
|
|
3781
|
-
- ✅ **`.required()`** - Field must have a value
|
|
3782
|
-
- ✅ **`.email()`** - Must be valid email format
|
|
3783
|
-
- ✅ **`.minLength()` / `.maxLength()`** - String length constraints
|
|
3784
|
-
- ✅ **`.min()` / `.max()`** - Numeric value constraints
|
|
3785
|
-
- ✅ **`.pattern()`** - Must match regex pattern
|
|
3786
|
-
- ✅ **`.custom()`** - Custom validation functions
|
|
3787
|
-
- ✅ **Chainable Validators** - Multiple validators per field
|
|
3788
|
-
- ✅ **Immediate Validation** - Errors thrown on property assignment
|
|
3789
|
-
|
|
3790
|
-
**🎯 Bulk Operations API:**
|
|
3791
|
-
- ✅ **`bulkCreate()`** - Create multiple entities efficiently in one transaction
|
|
3792
|
-
- ✅ **`bulkUpdate()`** - Update multiple entities by primary key
|
|
3793
|
-
- ✅ **`bulkDelete()`** - Delete multiple entities by primary key
|
|
3794
|
-
- ✅ **Lifecycle Hook Support** - Hooks execute for each entity in bulk operations
|
|
3795
|
-
- ✅ **Auto-Increment IDs** - IDs properly assigned after bulk inserts
|
|
3796
|
-
|
|
3797
|
-
**🎯 Critical Bug Fixes:**
|
|
3798
|
-
- ✅ **Auto-Increment ID Bug Fixed** - IDs now correctly set on entities after insert (SQLite, MySQL, PostgreSQL)
|
|
3799
|
-
- ✅ **Lifecycle Hook Isolation** - Hooks excluded from SQL queries and INSERT/UPDATE operations
|
|
3800
|
-
- ✅ **Circular Reference Prevention** - WeakSet-based tracking prevents infinite loops
|
|
3801
|
-
|
|
3802
|
-
**Example Usage:**
|
|
3803
|
-
|
|
3804
|
-
```javascript
|
|
3805
|
-
// Entity serialization
|
|
3806
|
-
const user = await db.User.findById(1);
|
|
3807
|
-
const plain = user.toObject({ includeRelationships: true, depth: 2 });
|
|
3808
|
-
res.json(user); // Works automatically with toJSON()
|
|
3809
|
-
|
|
3810
|
-
// Active Record pattern
|
|
3811
|
-
await user.delete(); // Entity deletes itself
|
|
3812
|
-
await user.reload(); // Discard changes
|
|
3813
|
-
const copy = user.clone(); // Duplicate entity
|
|
3814
|
-
|
|
3815
|
-
// Query helpers
|
|
3816
|
-
const first = await db.User.first();
|
|
3817
|
-
const exists = await db.User.where(u => u.email == $$, 'test@test.com').exists();
|
|
3818
|
-
const emails = await db.User.where(u => u.status == $$, 'active').pluck('email');
|
|
3819
|
-
|
|
3820
|
-
// Lifecycle hooks
|
|
3821
|
-
class User {
|
|
3822
|
-
beforeSave() {
|
|
3823
|
-
if (this.__dirtyFields.includes('password')) {
|
|
3824
|
-
this.password = bcrypt.hashSync(this.password, 10);
|
|
3825
|
-
}
|
|
3826
|
-
this.updated_at = new Date();
|
|
3827
|
-
}
|
|
3828
|
-
|
|
3829
|
-
beforeDelete() {
|
|
3830
|
-
if (this.role === 'admin') {
|
|
3831
|
-
throw new Error('Cannot delete admin user');
|
|
3832
|
-
}
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
|
|
3836
|
-
// Business validation
|
|
3837
|
-
class User {
|
|
3838
|
-
email(db) {
|
|
3839
|
-
db.string()
|
|
3840
|
-
.required('Email is required')
|
|
3841
|
-
.email('Must be a valid email address');
|
|
3842
|
-
}
|
|
3843
|
-
|
|
3844
|
-
age(db) {
|
|
3845
|
-
db.integer()
|
|
3846
|
-
.min(18, 'Must be at least 18 years old')
|
|
3847
|
-
.max(120);
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
|
-
// Bulk operations
|
|
3852
|
-
const users = await db.bulkCreate('User', [
|
|
3853
|
-
{ name: 'Alice', email: 'alice@example.com' },
|
|
3854
|
-
{ name: 'Bob', email: 'bob@example.com' },
|
|
3855
|
-
{ name: 'Charlie', email: 'charlie@example.com' }
|
|
3856
|
-
]);
|
|
3857
|
-
console.log(users.map(u => u.id)); // [1, 2, 3] - IDs assigned
|
|
3858
|
-
|
|
3859
|
-
await db.bulkUpdate('User', [
|
|
3860
|
-
{ id: 1, status: 'inactive' },
|
|
3861
|
-
{ id: 2, status: 'inactive' }
|
|
3862
|
-
]);
|
|
3863
|
-
|
|
3864
|
-
await db.bulkDelete('User', [3, 5, 7]);
|
|
3865
|
-
```
|
|
3866
|
-
|
|
3867
|
-
---
|
|
3868
|
-
|
|
3869
|
-
### v0.3.13 - FAANG Engineering Standards
|
|
3870
|
-
|
|
3871
|
-
MasterRecord has been upgraded to meet **FAANG engineering standards** (Google/Meta/Amazon) with critical bug fixes and performance improvements:
|
|
3872
|
-
|
|
3873
|
-
### Migration System Fixes (v0.3.13)
|
|
3874
|
-
|
|
3875
|
-
**Critical Path Bug Fixed:**
|
|
3876
|
-
- ✅ **Duplicate db/migrations Path Fixed** - Resolved bug where snapshot files were created with duplicate nested paths
|
|
3877
|
-
- **Before**: `/components/qa/app/models/db/migrations/db/migrations/qacontext_contextSnapShot.json` ❌
|
|
3878
|
-
- **After**: `/components/qa/app/models/db/migrations/qacontext_contextSnapShot.json` ✅
|
|
3879
|
-
- **Root Cause**: Incorrect glob API usage in `findContext` method (migrations.js:169-181)
|
|
3880
|
-
- **Fix**: Changed to use relative pattern + options object + `path.resolve()` for guaranteed absolute paths
|
|
3881
|
-
- ✅ **Smart Path Resolution** - Added `pathUtils.js` with intelligent path detection
|
|
3882
|
-
- ✅ **Prevents update-database-restart Failures** - Snapshot files now always created in the correct location
|
|
3883
|
-
- ✅ **Cross-Platform Support** - Works correctly on Windows and Unix-based systems
|
|
3884
|
-
|
|
3885
|
-
**Running Migrations - Important Notes:**
|
|
3886
|
-
- **Don't move migration files** - Leave them in their generated location (e.g., `/components/qa/db/migrations/`)
|
|
3887
|
-
- **Two ways to run migrations:**
|
|
3888
|
-
1. **From anywhere** - Run MasterRecord CLI from your project root, it will find migrations automatically:
|
|
3889
|
-
```bash
|
|
3890
|
-
npx masterrecord enable-migrations components/qa/app/models/qaContext
|
|
3891
|
-
npx masterrecord update-database components/qa/app/models/qaContext
|
|
3892
|
-
```
|
|
3893
|
-
2. **From migration directory** - cd into the specific migration area and run CLI there:
|
|
3894
|
-
```bash
|
|
3895
|
-
cd components/qa/db/migrations
|
|
3896
|
-
masterrecord enable-migrations qacontext
|
|
3897
|
-
```
|
|
3898
|
-
- MasterRecord uses intelligent path resolution to locate migrations regardless of where you run the command
|
|
3899
|
-
|
|
3900
|
-
### Core Improvements (context.js)
|
|
3901
|
-
|
|
3902
|
-
**Critical Fixes:**
|
|
3903
|
-
- ✅ **PostgreSQL Async Bug Fixed** - Resolved race condition where database returned before initialization completed
|
|
3904
|
-
- ✅ **Collision-Safe Entity Tracking** - Replaced random IDs with sequential IDs (zero collision risk)
|
|
3905
|
-
- ✅ **Input Validation** - Added validation to `dbset()` to prevent crashes and SQL injection
|
|
3906
|
-
- ✅ **Better Error Logging** - Configuration errors now logged with full context for debugging
|
|
3907
|
-
|
|
3908
|
-
**Code Quality:**
|
|
3909
|
-
- Modern JavaScript with `const`/`let` (no more `var`)
|
|
3910
|
-
- Comprehensive JSDoc documentation
|
|
3911
|
-
- Consistent code style following Google/Meta standards
|
|
3912
|
-
- Better error messages with actionable context
|
|
3913
|
-
|
|
3914
|
-
**Performance:**
|
|
3915
|
-
- Entity tracking: O(n) → O(1) lookups (100x faster)
|
|
3916
|
-
- Batch operations optimized for bulk inserts/updates/deletes
|
|
3917
|
-
|
|
3918
|
-
### Cascade Deletion Improvements (deleteManager.js)
|
|
3919
|
-
|
|
3920
|
-
**Critical Fixes:**
|
|
3921
|
-
- ✅ **Proper Error Handling** - Now throws Error objects (not strings) with full context
|
|
3922
|
-
- ✅ **Input Validation** - Validates entities before processing to prevent crashes
|
|
3923
|
-
- ✅ **Null Safety** - Handles null entities and arrays safely with clear error messages
|
|
3924
|
-
|
|
3925
|
-
**Code Quality:**
|
|
3926
|
-
- Refactored into smaller, focused methods (`_deleteSingleEntity`, `_deleteMultipleEntities`)
|
|
3927
|
-
- Constants for relationship types (no magic strings)
|
|
3928
|
-
- Comprehensive JSDoc documentation
|
|
3929
|
-
- Improved error messages that guide developers to solutions
|
|
3930
|
-
- Removed duplicate code between single/array handling
|
|
3931
|
-
|
|
3932
|
-
**Best Practices:**
|
|
3933
|
-
```javascript
|
|
3934
|
-
// Example: Cascade deletion with proper error handling
|
|
3935
|
-
const user = db.User.findById(123);
|
|
3936
|
-
db.User.remove(user);
|
|
3937
|
-
|
|
3938
|
-
try {
|
|
3939
|
-
db.saveChanges(); // Cascades to related entities
|
|
3940
|
-
} catch (error) {
|
|
3941
|
-
console.error('Deletion failed:', error.message);
|
|
3942
|
-
// Error: "Cannot delete User: required relationship 'Profile' is null.
|
|
3943
|
-
// Set nullable: true if this is intentional."
|
|
3944
|
-
}
|
|
3945
|
-
```
|
|
3946
|
-
|
|
3947
|
-
### Insert Manager Improvements (v0.3.13)
|
|
3948
|
-
|
|
3949
|
-
**Security Fixes:**
|
|
3950
|
-
- ✅ **SQL Injection Prevention** - Added identifier validation for dynamic query construction
|
|
3951
|
-
- Dynamic SQL identifiers are now validated with regex: `/^[a-zA-Z_][a-zA-Z0-9_]*$/`
|
|
3952
|
-
- Prevents malicious identifiers from breaking out of parameterized queries
|
|
3953
|
-
- Affects: hasOne relationship hydration (insertManager.js:181-186)
|
|
3954
|
-
- ✅ **Proper Error Objects** - All errors now throw Error instances with stack traces
|
|
3955
|
-
- Custom error classes: `InsertManagerError`, `RelationshipError`
|
|
3956
|
-
- Includes context for debugging (entity names, relationship info, available entities)
|
|
3957
|
-
- Before: `throw 'Relationship "..." could not be found'` (no stack trace)
|
|
3958
|
-
- After: `throw new RelationshipError(message, relationshipName, context)` (full stack)
|
|
3959
|
-
- ✅ **Error Logging** - Silent catch blocks now log warnings instead of suppressing errors
|
|
3960
|
-
- Hydration errors are logged but don't crash the insert operation
|
|
3961
|
-
- Console warnings include: property, error message, and child ID for debugging
|
|
3962
|
-
|
|
3963
|
-
**Performance:**
|
|
3964
|
-
- ✅ **50% Code Reduction** - Eliminated 50+ lines of duplicate code
|
|
3965
|
-
- hasMany and hasManyThrough shared nearly identical logic (89-110 vs 119-139)
|
|
3966
|
-
- Extracted to unified `_processArrayRelationship()` method
|
|
3967
|
-
- Reduces maintenance burden and bug surface area
|
|
3968
|
-
- ✅ **Entity Resolution Optimization** - Fallback entity resolution extracted and reusable
|
|
3969
|
-
- Triple fallback pattern (exact match → capitalized → property name) now in `_resolveEntityWithFallback()`
|
|
3970
|
-
- Can be cached or optimized in future without code duplication
|
|
3971
|
-
- ✅ **Loop Optimization** - Replaced for...in loops with for...of and Object.keys()
|
|
3972
|
-
- Prevents prototype chain pollution bugs
|
|
3973
|
-
- More predictable iteration behavior
|
|
3974
|
-
- Follows modern JavaScript best practices
|
|
3975
|
-
|
|
3976
|
-
**Code Quality:**
|
|
3977
|
-
- ✅ **Modern JavaScript** - All 24 `var` declarations replaced with `const`/`let`
|
|
3978
|
-
- Lines replaced: 3, 4, 20, 26, 30, 33, 34, 47, 48, 63, 64, 66, 149, 160, 161, 163, 164, 167, 168, 170, 184, 185, 200
|
|
3979
|
-
- Removed jQuery-style `$that` variable (lines 20, 160) by using arrow functions and `this`
|
|
3980
|
-
- Improved readability and follows ES6+ standards
|
|
3981
|
-
- ✅ **Comprehensive JSDoc** - Full documentation for all methods and class
|
|
3982
|
-
- Class-level documentation with usage examples
|
|
3983
|
-
- Method documentation with parameter types, return types, and @throws annotations
|
|
3984
|
-
- Private method markers (`@private`) to indicate internal APIs
|
|
3985
|
-
- ✅ **Constants Extraction** - Magic strings/numbers extracted to named constants
|
|
3986
|
-
- `TIMESTAMP_FIELDS.CREATED_AT` / `TIMESTAMP_FIELDS.UPDATED_AT` (instead of 'created_at', 'updated_at')
|
|
3987
|
-
- `RELATIONSHIP_TYPES.HAS_MANY`, `HAS_MANY_THROUGH`, `BELONGS_TO`, `HAS_ONE`
|
|
3988
|
-
- `MIN_OBJECT_KEYS = 0` for length comparisons
|
|
3989
|
-
- Easier to refactor and understand intent
|
|
3990
|
-
- ✅ **Strict Mode** - Added `'use strict';` at top of file
|
|
3991
|
-
- Catches common coding mistakes at runtime
|
|
3992
|
-
- Prevents accidental global variable creation
|
|
3993
|
-
- Better performance in modern JavaScript engines
|
|
3994
|
-
|
|
3995
|
-
**Before/After Example:**
|
|
3996
|
-
```javascript
|
|
3997
|
-
// BEFORE (v0.0.15) - vulnerable and duplicated:
|
|
3998
|
-
if(entityProperty.type === "hasMany"){
|
|
3999
|
-
if(tools.checkIfArrayLike(propertyModel)){
|
|
4000
|
-
const propertyKeys = Object.keys(propertyModel);
|
|
4001
|
-
for (const propertykey of propertyKeys) {
|
|
4002
|
-
let targetName = entityProperty.foreignTable || property;
|
|
4003
|
-
let resolved = tools.getEntity(targetName, $that._allEntities)
|
|
4004
|
-
|| tools.getEntity(tools.capitalize(targetName), $that._allEntities)
|
|
4005
|
-
|| tools.getEntity(property, $that._allEntities);
|
|
4006
|
-
if(!resolved){
|
|
4007
|
-
throw `Relationship entity for '${property}' could not be resolved`; // ❌ String throw
|
|
4008
|
-
}
|
|
4009
|
-
// ... 20 more lines
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
4012
|
-
}
|
|
4013
|
-
// ... 50 lines later, nearly identical code for hasManyThrough
|
|
4014
|
-
|
|
4015
|
-
// AFTER (v0.3.13) - secure and DRY:
|
|
4016
|
-
if (entityProperty.type === RELATIONSHIP_TYPES.HAS_MANY) {
|
|
4017
|
-
this._processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, RELATIONSHIP_TYPES.HAS_MANY);
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
if (entityProperty.type === RELATIONSHIP_TYPES.HAS_MANY_THROUGH) {
|
|
4021
|
-
this._processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, RELATIONSHIP_TYPES.HAS_MANY_THROUGH);
|
|
4022
|
-
}
|
|
4023
|
-
|
|
4024
|
-
// Unified method with proper error handling:
|
|
4025
|
-
_processArrayRelationship(propertyModel, entityProperty, property, currentModel, SQL, relationshipType) {
|
|
4026
|
-
const resolved = this._resolveEntityWithFallback(property, targetName);
|
|
4027
|
-
if (!resolved) {
|
|
4028
|
-
throw new RelationshipError(
|
|
4029
|
-
`Relationship entity for '${property}' could not be resolved`,
|
|
4030
|
-
property,
|
|
4031
|
-
{ targetName, relationshipType, availableEntities: this._allEntities.map(e => e.__name) }
|
|
4032
|
-
); // ✅ Proper Error object with context
|
|
4033
|
-
}
|
|
4034
|
-
// ... unified logic
|
|
4035
|
-
}
|
|
4036
|
-
```
|
|
4037
|
-
|
|
4038
|
-
**Verification Results:**
|
|
4039
|
-
```bash
|
|
4040
|
-
$ grep -n "^\s*var " insertManager.js
|
|
4041
|
-
# ✅ No results - all var declarations eliminated
|
|
4042
|
-
|
|
4043
|
-
$ grep -n "throw '" insertManager.js
|
|
4044
|
-
# ✅ No results - all string throws replaced with Error objects
|
|
4045
|
-
|
|
4046
|
-
$ grep -A1 "catch.*{$" insertManager.js | grep "^\s*}$"
|
|
4047
|
-
# ✅ No empty catch blocks - all log errors appropriately
|
|
4048
|
-
```
|
|
4049
|
-
|
|
4050
|
-
### Breaking Changes (v0.3.17+)
|
|
4051
|
-
|
|
4052
|
-
**🔴 CRITICAL: All databases now require async/await for consistency**
|
|
4053
|
-
|
|
4054
|
-
MasterRecord now provides a **unified async API** across all database engines (SQLite, MySQL, PostgreSQL). This follows industry best practices from Sequelize, TypeORM, and Prisma.
|
|
4055
|
-
|
|
4056
|
-
**1. Database Operations (All Engines)**
|
|
4057
|
-
```javascript
|
|
4058
|
-
// ✅ NEW (v0.3.17+): All databases use async/await
|
|
4059
|
-
const db = new AppContext();
|
|
4060
|
-
await db.saveChanges(); // Required for SQLite, MySQL, PostgreSQL
|
|
4061
|
-
|
|
4062
|
-
// ❌ OLD (v0.3.16 and earlier): Mixed sync/async
|
|
4063
|
-
db.saveChanges(); // SQLite/MySQL were sync (no longer works)
|
|
4064
|
-
await db.saveChanges(); // Only PostgreSQL was async
|
|
4065
|
-
```
|
|
4066
|
-
|
|
4067
|
-
**2. Migration Files (Critical)**
|
|
4068
|
-
```javascript
|
|
4069
|
-
// ✅ NEW (v0.3.17+): Migrations must be async
|
|
4070
|
-
class CreateUser extends masterrecord.schema {
|
|
4071
|
-
async up(table) { // Must be async
|
|
4072
|
-
this.init(table);
|
|
4073
|
-
await this.createTable(table.User); // Must await
|
|
4074
|
-
}
|
|
4075
|
-
|
|
4076
|
-
async down(table) { // Must be async
|
|
4077
|
-
this.init(table);
|
|
4078
|
-
this.dropTable(table.User);
|
|
4079
|
-
}
|
|
4080
|
-
}
|
|
4081
|
-
|
|
4082
|
-
// ❌ OLD (v0.3.16 and earlier): Migrations were sync
|
|
4083
|
-
up(table) {
|
|
4084
|
-
this.createTable(table.User); // No await (no longer works)
|
|
4085
|
-
}
|
|
4086
|
-
```
|
|
4087
|
-
|
|
4088
|
-
**3. MySQL Connection**
|
|
4089
|
-
```javascript
|
|
4090
|
-
// ✅ NEW (v0.3.17+): MySQL uses mysql2/promise with async connection pooling
|
|
4091
|
-
this.env({
|
|
4092
|
-
type: 'mysql',
|
|
4093
|
-
host: 'localhost',
|
|
4094
|
-
port: 3306,
|
|
4095
|
-
database: 'myapp',
|
|
4096
|
-
user: 'root',
|
|
4097
|
-
password: 'password',
|
|
4098
|
-
connectionLimit: 10 // Connection pool size
|
|
4099
|
-
});
|
|
4100
|
-
|
|
4101
|
-
// ❌ OLD (v0.3.16 and earlier): MySQL used sync-mysql2 (synchronous driver)
|
|
4102
|
-
```
|
|
4103
|
-
|
|
4104
|
-
**Why This Change?**
|
|
4105
|
-
- ✅ **Consistent API**: Same code works for SQLite, MySQL, and PostgreSQL
|
|
4106
|
-
- ✅ **Industry Standard**: Matches Sequelize, TypeORM, Prisma patterns
|
|
4107
|
-
- ✅ **Better Performance**: MySQL now uses connection pooling
|
|
4108
|
-
- ✅ **Real MySQL**: No longer using SQLite disguised as MySQL
|
|
4109
|
-
- ✅ **Portable Code**: Switch databases without code changes
|
|
4110
|
-
|
|
4111
|
-
**Migration Path:**
|
|
4112
|
-
1. Update all `db.saveChanges()` calls to use `await`
|
|
4113
|
-
2. Make all migration `up()` and `down()` methods async
|
|
4114
|
-
3. Add `await` before `createTable()` calls in migrations
|
|
4115
|
-
4. Update `package.json`: Remove `sync-mysql2`, ensure `mysql2@^3.11.5`
|
|
4116
|
-
|
|
4117
|
-
**For more details, see:** `CHANGES.md`
|
|
4118
|
-
|
|
4119
|
-
**For more details, see:** `CHANGES.md`
|
|
4120
|
-
|
|
4121
|
-
---
|
|
4122
|
-
|
|
4123
|
-
**MasterRecord** - Code-first ORM for Node.js with multi-database support
|
|
3381
|
+
1. **`Migrations/migrations.js`** - Skip `newIndexes` and `newCompositeIndexes` for new tables in `#findNewIndexes()` and `#findNewCompositeIndexes()`
|
|
3382
|
+
2. **`test/v0.3.34-bug-fixes-test.js`** - Updated tests to verify correct behavior + added tests for adding indexes to existing tables
|
|
3383
|
+
3. **`package.json`** - Updated to v0.3.49
|
|
@@ -116,7 +116,7 @@ test('Query builders correctly skip indexes property', () => {
|
|
|
116
116
|
// =============================================================================
|
|
117
117
|
console.log("\n📋 Test Suite 2: Index Creation with Await Keywords\n");
|
|
118
118
|
|
|
119
|
-
test('createIndex
|
|
119
|
+
test('New tables do NOT generate separate createIndex calls (createTable handles them)', () => {
|
|
120
120
|
const migrations = new Migrations();
|
|
121
121
|
const oldSchema = [];
|
|
122
122
|
const newSchema = [{
|
|
@@ -127,12 +127,12 @@ test('createIndex generates code with await keyword', () => {
|
|
|
127
127
|
|
|
128
128
|
const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
|
|
129
129
|
|
|
130
|
+
// For new tables, createTable() in schema.js handles indexes - no separate calls needed
|
|
130
131
|
const createIndexMatches = migrationCode.match(/await this\.createIndex/g);
|
|
131
|
-
assert(createIndexMatches
|
|
132
|
-
assert(!migrationCode.match(/\n\s+this\.createIndex\(/), 'Should not have createIndex without await');
|
|
132
|
+
assert(!createIndexMatches, 'New tables should NOT have separate createIndex calls');
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
test('createCompositeIndex
|
|
135
|
+
test('New tables do NOT generate separate createCompositeIndex calls (createTable handles them)', () => {
|
|
136
136
|
const migrations = new Migrations();
|
|
137
137
|
const oldSchema = [];
|
|
138
138
|
const newSchema = [{
|
|
@@ -147,8 +147,50 @@ test('createCompositeIndex generates code with await keyword', () => {
|
|
|
147
147
|
|
|
148
148
|
const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
|
|
149
149
|
|
|
150
|
+
// For new tables, createTable() in schema.js handles composite indexes
|
|
150
151
|
const compositeMatches = migrationCode.match(/await this\.createCompositeIndex/g);
|
|
151
|
-
assert(compositeMatches
|
|
152
|
+
assert(!compositeMatches, 'New tables should NOT have separate createCompositeIndex calls');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('Adding index to EXISTING table generates createIndex with await', () => {
|
|
156
|
+
const migrations = new Migrations();
|
|
157
|
+
const oldSchema = [{
|
|
158
|
+
__name: 'User',
|
|
159
|
+
id: { name: 'id', type: 'integer', primaryKey: true },
|
|
160
|
+
email: { name: 'email', type: 'text' }
|
|
161
|
+
}];
|
|
162
|
+
const newSchema = [{
|
|
163
|
+
__name: 'User',
|
|
164
|
+
id: { name: 'id', type: 'integer', primaryKey: true },
|
|
165
|
+
email: { name: 'email', type: 'text', indexes: ['idx_user_email'] }
|
|
166
|
+
}];
|
|
167
|
+
|
|
168
|
+
const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
|
|
169
|
+
|
|
170
|
+
const createIndexMatches = migrationCode.match(/await this\.createIndex/g);
|
|
171
|
+
assert(createIndexMatches && createIndexMatches.length > 0, 'Existing table with new index should have createIndex with await');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Adding composite index to EXISTING table generates createCompositeIndex with await', () => {
|
|
175
|
+
const migrations = new Migrations();
|
|
176
|
+
const oldSchema = [{
|
|
177
|
+
__name: 'User',
|
|
178
|
+
id: { name: 'id', type: 'integer', primaryKey: true }
|
|
179
|
+
}];
|
|
180
|
+
const newSchema = [{
|
|
181
|
+
__name: 'User',
|
|
182
|
+
id: { name: 'id', type: 'integer', primaryKey: true },
|
|
183
|
+
__compositeIndexes: [{
|
|
184
|
+
name: 'idx_composite',
|
|
185
|
+
columns: ['col1', 'col2'],
|
|
186
|
+
unique: false
|
|
187
|
+
}]
|
|
188
|
+
}];
|
|
189
|
+
|
|
190
|
+
const migrationCode = migrations.template('TestMigration', oldSchema, newSchema);
|
|
191
|
+
|
|
192
|
+
const compositeMatches = migrationCode.match(/await this\.createCompositeIndex/g);
|
|
193
|
+
assert(compositeMatches && compositeMatches.length > 0, 'Existing table with new composite index should have createCompositeIndex with await');
|
|
152
194
|
});
|
|
153
195
|
|
|
154
196
|
// =============================================================================
|
|
@@ -286,9 +328,9 @@ test('Complex scenario: Multiple tables with seed data, indexes, and composite i
|
|
|
286
328
|
assert(migrationCode.includes("this.seed('Settings'"), 'Should use this.seed() for Settings');
|
|
287
329
|
assert(!migrationCode.includes('table.Settings.create'), 'Should NOT use table.Settings.create');
|
|
288
330
|
|
|
289
|
-
//
|
|
290
|
-
assert(migrationCode.includes('await this.createIndex'), '
|
|
291
|
-
assert(migrationCode.includes('await this.createCompositeIndex'), '
|
|
331
|
+
// For new tables, createTable() handles indexes — no separate calls in template
|
|
332
|
+
assert(!migrationCode.includes('await this.createIndex'), 'New tables should not have separate createIndex calls');
|
|
333
|
+
assert(!migrationCode.includes('await this.createCompositeIndex'), 'New tables should not have separate createCompositeIndex calls');
|
|
292
334
|
});
|
|
293
335
|
|
|
294
336
|
// =============================================================================
|