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.
@@ -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.48",
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.48 (2026-02-20) - FIX: Complete Async Migration Pipeline for MySQL/PostgreSQL
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
- #### Files Modified
3386
- 1. **`Migrations/schema.js`** - All methods async, all `_execute` calls awaited, forEach→for...of, syncTable metadata filtering, `_ready` flag
3387
- 2. **`Migrations/migrationTemplate.js`** - All generated method calls now include `await`
3388
- 3. **`mySQLEngine.js`** - Added `_execute()` method
3389
- 4. **`postgresEngine.js`** - Added `_execute()` method
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/schema.js`** - Added `_ensureReady()`, made `init()` async with proper awaits, fixed `mySQLConnect` require
3420
- 2. **`Migrations/migrationTemplate.js`** - New migrations now `await this.init(table)`
3421
- 3. **`Migrations/cli.js`** - Schema-only mode for `add-migration`
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 generates code with await keyword', () => {
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 && createIndexMatches.length > 0, 'createIndex should have await keyword');
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 generates code with await keyword', () => {
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 && compositeMatches.length > 0, 'createCompositeIndex should have await keyword');
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
- // Check indexes have await
290
- assert(migrationCode.includes('await this.createIndex'), 'Indexes should have await');
291
- assert(migrationCode.includes('await this.createCompositeIndex'), 'Composite indexes should have await');
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
  // =============================================================================