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