masterrecord 0.3.31 → 0.3.33

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
@@ -51,11 +51,14 @@
51
51
  - [.exists()](#exists)
52
52
  - [.pluck()](#pluckfieldname)
53
53
  - [Lifecycle Hooks](#lifecycle-hooks)
54
+ - [Field Constraints & Indexes](#field-constraints--indexes)
54
55
  - [Business Logic Validation](#business-logic-validation)
55
56
  - [Bulk Operations API](#bulk-operations-api)
56
57
  - [bulkCreate()](#bulkcreateentityname-data)
57
58
  - [bulkUpdate()](#bulkupdateentityname-updates)
58
59
  - [bulkDelete()](#bulkdeleteentityname-ids)
60
+ - [Composite Indexes](#composite-indexes)
61
+ - [Seed Data](#seed-data)
59
62
  - [Migrations](#migrations)
60
63
  - [Advanced Features](#advanced-features)
61
64
  - [Query Result Caching](#query-result-caching)
@@ -2120,6 +2123,577 @@ class User {
2120
2123
 
2121
2124
  ---
2122
2125
 
2126
+ ## Seed Data
2127
+
2128
+ Define seed data in your context file that automatically generates migration code using the ORM.
2129
+
2130
+ ### Context-Level Seed API (Recommended)
2131
+
2132
+ ```javascript
2133
+ class AppContext extends context {
2134
+ onConfig() {
2135
+ // Single seed record
2136
+ this.dbset(User).seed({
2137
+ user_name: 'admin',
2138
+ first_name: 'System',
2139
+ last_name: 'Administrator',
2140
+ email: 'admin@bookbag.ai',
2141
+ system_role: 'system_admin',
2142
+ admin_type: 'engineering',
2143
+ onboarding_completed: 1,
2144
+ availability_status: 'online'
2145
+ });
2146
+
2147
+ // Chain multiple records
2148
+ this.dbset(Post)
2149
+ .seed({ title: 'Welcome', content: 'Hello world', author_id: 1 })
2150
+ .seed({ title: 'Getting Started', content: 'Tutorial', author_id: 1 });
2151
+
2152
+ // Bulk seed with array
2153
+ this.dbset(Category).seed([
2154
+ { name: 'Technology', slug: 'tech' },
2155
+ { name: 'Business', slug: 'biz' },
2156
+ { name: 'Science', slug: 'science' }
2157
+ ]);
2158
+ }
2159
+ }
2160
+ ```
2161
+
2162
+ ### Automatic Migration Generation
2163
+
2164
+ When you define seed data in the context, MasterRecord generates migration code using the ORM:
2165
+
2166
+ ```javascript
2167
+ // Your context definition triggers this migration
2168
+ class Migration_20250205_123456 extends masterrecord.schema {
2169
+ async up(table) {
2170
+ this.init(table);
2171
+
2172
+ // Generated ORM create calls
2173
+ await table.User.create({
2174
+ user_name: 'admin',
2175
+ first_name: 'System',
2176
+ last_name: 'Administrator',
2177
+ email: 'admin@bookbag.ai',
2178
+ system_role: 'system_admin',
2179
+ admin_type: 'engineering',
2180
+ onboarding_completed: 1,
2181
+ availability_status: 'online'
2182
+ });
2183
+
2184
+ await table.Post.create({
2185
+ title: 'Welcome',
2186
+ content: 'Hello world',
2187
+ author_id: 1
2188
+ });
2189
+
2190
+ await table.Post.create({
2191
+ title: 'Getting Started',
2192
+ content: 'Tutorial',
2193
+ author_id: 1
2194
+ });
2195
+ }
2196
+
2197
+ async down(table) {
2198
+ this.init(table);
2199
+ // Seed data typically not removed in down migrations
2200
+ }
2201
+ }
2202
+ ```
2203
+
2204
+ ### Benefits of ORM-Based Seeding
2205
+
2206
+ 1. **Lifecycle Hooks**: Triggers `beforeSave` and `afterSave` hooks
2207
+ 2. **Validation**: Uses entity field definitions and validators
2208
+ 3. **Type Safety**: Ensures fields match entity schema
2209
+ 4. **Maintainable**: Changes to entity structure reflected automatically
2210
+
2211
+ ### Manual Seed Methods (Advanced)
2212
+
2213
+ For more control, use raw SQL seed methods directly in migrations:
2214
+
2215
+ ```javascript
2216
+ class Migration_20250205_123456 extends masterrecord.schema {
2217
+ async up(table) {
2218
+ this.init(table);
2219
+
2220
+ // Single record with raw SQL
2221
+ this.seed('User', {
2222
+ user_name: 'admin',
2223
+ email: 'admin@bookbag.ai'
2224
+ });
2225
+
2226
+ // Bulk insert with raw SQL (more performant for large datasets)
2227
+ this.bulkSeed('Category', [
2228
+ { name: 'Technology', slug: 'tech' },
2229
+ { name: 'Business', slug: 'biz' },
2230
+ { name: 'Science', slug: 'science' }
2231
+ ]);
2232
+ }
2233
+ }
2234
+ ```
2235
+
2236
+ **When to use manual seed methods:**
2237
+ - Large datasets (1000+ records) - `bulkSeed()` is more performant
2238
+ - Need raw SQL control
2239
+ - Don't need lifecycle hooks or validation
2240
+
2241
+ ### Idempotency
2242
+
2243
+ **ORM approach** (context-level seed):
2244
+ - Generates plain `create()` calls
2245
+ - Fails if primary key exists (user must remove seed data after first migration)
2246
+ - Best for one-time initial setup data
2247
+
2248
+ **Manual approach** (idempotent):
2249
+ - Uses database-specific INSERT OR IGNORE syntax
2250
+ - SQLite: `INSERT OR IGNORE INTO`
2251
+ - MySQL: `INSERT IGNORE INTO`
2252
+ - PostgreSQL: `INSERT ... ON CONFLICT DO NOTHING`
2253
+ - Best for repeatable migrations and re-seeding
2254
+
2255
+ Example:
2256
+ ```javascript
2257
+ // Context-level (runs once)
2258
+ this.dbset(User).seed({ id: 1, name: 'admin' });
2259
+ // After first migration, remove or comment out seed data
2260
+
2261
+ // Manual (repeatable)
2262
+ class Migration_xyz extends masterrecord.schema {
2263
+ async up(table) {
2264
+ this.init(table);
2265
+ // Can run multiple times without error
2266
+ this.seed('User', { id: 1, name: 'admin' });
2267
+ }
2268
+ }
2269
+ ```
2270
+
2271
+ ### Best Practices
2272
+
2273
+ 1. **Use context-level seed** for one-time initial setup (admin users, default categories)
2274
+ - Remove seed data from context after first successful migration
2275
+ - Or comment out after initial setup
2276
+ 2. **Use manual seed methods** for repeatable/idempotent seeding
2277
+ 3. **Use manual bulkSeed** for large datasets (1000+ records) - more performant
2278
+ 4. **Keep seed data minimal** - only essential bootstrap data
2279
+ 5. **Use fixtures/factories** for test data, not seed methods
2280
+ 6. **Don't delete seed data** in down migrations (can cause referential integrity issues)
2281
+
2282
+ ### Example: Multi-Tenant Seed Data
2283
+
2284
+ ```javascript
2285
+ class AppContext extends context {
2286
+ onConfig() {
2287
+ this.dbset(User);
2288
+ this.dbset(Tenant);
2289
+ this.dbset(Permission);
2290
+
2291
+ // Seed default tenant
2292
+ this.dbset(Tenant).seed({
2293
+ name: 'Default Organization',
2294
+ slug: 'default',
2295
+ is_active: 1
2296
+ });
2297
+
2298
+ // Seed system admin
2299
+ this.dbset(User).seed({
2300
+ email: 'admin@system.com',
2301
+ tenant_id: 1,
2302
+ role: 'system_admin'
2303
+ });
2304
+
2305
+ // Seed default permissions
2306
+ this.dbset(Permission).seed([
2307
+ { name: 'users.read', description: 'Read users' },
2308
+ { name: 'users.write', description: 'Create/update users' },
2309
+ { name: 'users.delete', description: 'Delete users' }
2310
+ ]);
2311
+ }
2312
+ }
2313
+ ```
2314
+
2315
+ ---
2316
+
2317
+ ## Advanced Seed Data Features
2318
+
2319
+ MasterRecord provides 5 enterprise-grade seed data enhancements for production-ready data management:
2320
+
2321
+ ### 1. Down Migrations - Automatic Rollback
2322
+
2323
+ Enable automatic cleanup of seed data in down migrations:
2324
+
2325
+ ```javascript
2326
+ class AppContext extends context {
2327
+ onConfig() {
2328
+ // Enable down migration generation
2329
+ this.seedConfig({
2330
+ generateDownMigrations: true, // Default: false
2331
+ downStrategy: 'delete', // 'delete' | 'skip'
2332
+ onRollbackError: 'warn' // 'warn' | 'throw' | 'ignore'
2333
+ });
2334
+
2335
+ this.dbset(User).seed({ id: 1, name: 'admin', email: 'admin@example.com' });
2336
+ }
2337
+ }
2338
+ ```
2339
+
2340
+ **Generated Migration:**
2341
+ ```javascript
2342
+ async up(table) {
2343
+ this.init(table);
2344
+ await table.User.create({ id: 1, name: 'admin', email: 'admin@example.com' });
2345
+ }
2346
+
2347
+ async down(table) {
2348
+ this.init(table);
2349
+ // Auto-generated rollback (reverse order for FK safety)
2350
+ try {
2351
+ const record = await table.User.findById(1);
2352
+ if (record) await record.delete();
2353
+ } catch (e) {
2354
+ console.warn('Seed rollback: User id=1 not found');
2355
+ }
2356
+ }
2357
+ ```
2358
+
2359
+ **Use Cases:**
2360
+ - Development environments where you frequently rollback migrations
2361
+ - Testing scenarios requiring clean database state
2362
+ - Staged deployments where rollback may be necessary
2363
+
2364
+ **Note:** Production environments typically don't rollback seed data due to referential integrity concerns.
2365
+
2366
+ ---
2367
+
2368
+ ### 2. Conditional Seeding - Environment-Based Data
2369
+
2370
+ Seed different data based on environment:
2371
+
2372
+ ```javascript
2373
+ class AppContext extends context {
2374
+ onConfig() {
2375
+ // Development/test only seed data
2376
+ this.dbset(User)
2377
+ .seed({ name: 'Test User', email: 'test@example.com' })
2378
+ .when('development', 'test');
2379
+
2380
+ // Production-only seed data
2381
+ this.dbset(Config)
2382
+ .seed({ key: 'api_endpoint', value: 'https://api.production.com' })
2383
+ .when('production');
2384
+
2385
+ // Multiple environments
2386
+ this.dbset(Feature)
2387
+ .seed({ name: 'beta_feature', enabled: true })
2388
+ .when('staging', 'production');
2389
+ }
2390
+ }
2391
+ ```
2392
+
2393
+ **How It Works:**
2394
+ - Migration code is filtered at **generation time** (not runtime)
2395
+ - Only seed data matching current environment is included in migration
2396
+ - Cleaner migrations, no runtime overhead
2397
+
2398
+ **Environment Detection:**
2399
+ - Uses `process.env.NODE_ENV` or `process.env.master`
2400
+ - Defaults to 'development' if not set
2401
+ - Supports multiple environments per seed
2402
+
2403
+ ---
2404
+
2405
+ ### 3. Automatic Dependency Ordering
2406
+
2407
+ Seeds are automatically ordered based on foreign key relationships:
2408
+
2409
+ ```javascript
2410
+ class AppContext extends context {
2411
+ onConfig() {
2412
+ // Order doesn't matter - automatically sorted!
2413
+ this.dbset(Post).seed({
2414
+ title: 'Welcome',
2415
+ user_id: 1 // Foreign key to User
2416
+ });
2417
+
2418
+ this.dbset(User).seed({
2419
+ id: 1,
2420
+ name: 'admin'
2421
+ });
2422
+
2423
+ // Generated migration will seed User BEFORE Post
2424
+ }
2425
+ }
2426
+ ```
2427
+
2428
+ **How It Works:**
2429
+ - Analyzes `belongsTo` relationships in entity definitions
2430
+ - Builds dependency graph using topological sort (Kahn's algorithm)
2431
+ - Parents are always seeded before children
2432
+ - Detects circular dependencies and warns
2433
+
2434
+ **Circular Dependency Handling:**
2435
+ ```javascript
2436
+ this.seedConfig({
2437
+ detectCircularDependencies: true,
2438
+ circularStrategy: 'warn' // 'warn' | 'throw' | 'ignore'
2439
+ });
2440
+ ```
2441
+
2442
+ **Benefits:**
2443
+ - Prevents foreign key constraint violations
2444
+ - No manual ordering required
2445
+ - Works with complex multi-level dependencies
2446
+ - Junction tables (many-to-many) handled automatically
2447
+
2448
+ ---
2449
+
2450
+ ### 4. Seed Factories - Parameterized Data Generation
2451
+
2452
+ Generate multiple seed records with variations:
2453
+
2454
+ ```javascript
2455
+ class AppContext extends context {
2456
+ onConfig() {
2457
+ // Inline factory with generator function
2458
+ this.dbset(User).seedFactory(10, i => ({
2459
+ name: `User ${i}`,
2460
+ email: `user${i}@example.com`,
2461
+ role: 'member',
2462
+ created_at: Date.now()
2463
+ }));
2464
+
2465
+ // External factory class
2466
+ this.dbset(User).seed(UserFactory.admin({
2467
+ email: 'custom@example.com'
2468
+ }));
2469
+ }
2470
+ }
2471
+
2472
+ // External factory pattern
2473
+ class UserFactory {
2474
+ static admin(overrides = {}) {
2475
+ return {
2476
+ name: 'Admin User',
2477
+ email: 'admin@example.com',
2478
+ role: 'admin',
2479
+ is_active: true,
2480
+ ...overrides
2481
+ };
2482
+ }
2483
+
2484
+ static members(count) {
2485
+ return Array.from({ length: count }, (_, i) => ({
2486
+ name: `Member ${i}`,
2487
+ email: `member${i}@example.com`,
2488
+ role: 'member'
2489
+ }));
2490
+ }
2491
+ }
2492
+ ```
2493
+
2494
+ **Generated Migration (Optimized):**
2495
+ ```javascript
2496
+ // Bulk insert with loop (10+ records)
2497
+ const factoryRecords = [
2498
+ {"name":"User 0","email":"user0@example.com","role":"member"},
2499
+ {"name":"User 1","email":"user1@example.com","role":"member"},
2500
+ // ... 8 more
2501
+ ];
2502
+ for (const record of factoryRecords) {
2503
+ await table.User.create(record);
2504
+ }
2505
+ ```
2506
+
2507
+ **Use Cases:**
2508
+ - Generate test users for development
2509
+ - Create sample data for demos
2510
+ - Populate lookup tables with variations
2511
+ - Bulk seed with consistent patterns
2512
+
2513
+ **Faker Integration (Optional):**
2514
+ ```javascript
2515
+ const { faker } = require('@faker-js/faker');
2516
+
2517
+ this.dbset(User).seedFactory(100, i => ({
2518
+ name: faker.person.fullName(),
2519
+ email: faker.internet.email(),
2520
+ bio: faker.lorem.paragraph()
2521
+ }));
2522
+ ```
2523
+
2524
+ ---
2525
+
2526
+ ### 5. Upsert - Update if Exists, Insert if Not
2527
+
2528
+ Create idempotent seed data that can run multiple times:
2529
+
2530
+ ```javascript
2531
+ class AppContext extends context {
2532
+ onConfig() {
2533
+ // Upsert by primary key
2534
+ this.dbset(User)
2535
+ .seed({ id: 1, name: 'admin', email: 'admin@example.com' })
2536
+ .upsert();
2537
+
2538
+ // Upsert by custom field (business key)
2539
+ this.dbset(User)
2540
+ .seed({ email: 'admin@example.com', name: 'Administrator' })
2541
+ .upsert({ conflictKey: 'email' });
2542
+
2543
+ // Partial update (only update specific fields)
2544
+ this.dbset(Config)
2545
+ .seed({ key: 'api_url', value: 'https://new-api.com', updated_at: Date.now() })
2546
+ .upsert({
2547
+ conflictKey: 'key',
2548
+ updateFields: ['value', 'updated_at'] // Don't update other fields
2549
+ });
2550
+
2551
+ // Context-level default (all seeds become upserts)
2552
+ this.seedConfig({
2553
+ defaultStrategy: 'upsert'
2554
+ });
2555
+ }
2556
+ }
2557
+ ```
2558
+
2559
+ **Generated Migration:**
2560
+ ```javascript
2561
+ async up(table) {
2562
+ this.init(table);
2563
+
2564
+ // Check-then-update pattern (database-agnostic)
2565
+ {
2566
+ const existing = await table.User.where(r => r.email == 'admin@example.com').single();
2567
+ if (existing) {
2568
+ existing.name = 'Administrator';
2569
+ await existing.save();
2570
+ } else {
2571
+ await table.User.create({ email: 'admin@example.com', name: 'Administrator' });
2572
+ }
2573
+ }
2574
+ }
2575
+ ```
2576
+
2577
+ **Use Cases:**
2578
+ - Configuration tables that need updates
2579
+ - Master data that changes over time
2580
+ - Idempotent migrations (can run multiple times safely)
2581
+ - CI/CD pipelines where migrations may re-run
2582
+
2583
+ **Benefits:**
2584
+ - Database-agnostic (works on SQLite, MySQL, PostgreSQL)
2585
+ - Triggers ORM lifecycle hooks (`beforeSave`, `afterSave`)
2586
+ - Type-safe and validated
2587
+ - Prevents duplicate key errors
2588
+
2589
+ ---
2590
+
2591
+ ### Advanced Example - All Features Together
2592
+
2593
+ ```javascript
2594
+ class AppContext extends context {
2595
+ constructor() {
2596
+ super();
2597
+ this.env('./config');
2598
+
2599
+ // Global seed configuration
2600
+ this.seedConfig({
2601
+ generateDownMigrations: true, // Enable rollback
2602
+ defaultStrategy: 'upsert', // Idempotent by default
2603
+ detectCircularDependencies: true, // Warn on cycles
2604
+ circularStrategy: 'warn'
2605
+ });
2606
+
2607
+ // Define entities
2608
+ this.dbset(User);
2609
+ this.dbset(Organization);
2610
+ this.dbset(Post);
2611
+ this.dbset(Category);
2612
+
2613
+ // Seed with all features combined
2614
+ this.dbset(Organization)
2615
+ .seed([
2616
+ { id: 1, name: 'Default Org', slug: 'default' },
2617
+ { id: 2, name: 'Partner Org', slug: 'partner' }
2618
+ ])
2619
+ .upsert({ conflictKey: 'slug' });
2620
+
2621
+ this.dbset(User)
2622
+ .seedFactory(5, i => ({
2623
+ id: i + 1,
2624
+ name: `Admin ${i}`,
2625
+ email: `admin${i}@example.com`,
2626
+ org_id: 1, // Foreign key (dependency)
2627
+ role: 'admin'
2628
+ }))
2629
+ .when('development', 'test') // Only in dev/test
2630
+ .upsert({ conflictKey: 'email' });
2631
+
2632
+ this.dbset(Category)
2633
+ .seed([
2634
+ { name: 'Technology' },
2635
+ { name: 'Business' },
2636
+ { name: 'Science' }
2637
+ ])
2638
+ .upsert();
2639
+
2640
+ this.dbset(Post)
2641
+ .seedFactory(10, i => ({
2642
+ title: `Sample Post ${i}`,
2643
+ content: 'Lorem ipsum...',
2644
+ user_id: 1, // Depends on User
2645
+ category_id: 1 // Depends on Category
2646
+ }))
2647
+ .when('development');
2648
+ }
2649
+ }
2650
+ ```
2651
+
2652
+ **What Happens:**
2653
+ 1. **Dependency ordering**: Organization → User → Category → Post (automatic)
2654
+ 2. **Conditional filtering**: User and Post seeds only in dev/test
2655
+ 3. **Upsert safety**: Won't fail on duplicate keys
2656
+ 4. **Factory generation**: 5 users and 10 posts created with variations
2657
+ 5. **Rollback support**: Down migration deletes in reverse order
2658
+
2659
+ ---
2660
+
2661
+ ### Seed Configuration API
2662
+
2663
+ ```javascript
2664
+ // In context constructor
2665
+ this.seedConfig({
2666
+ generateDownMigrations: false, // Enable/disable rollback generation
2667
+ downStrategy: 'delete', // 'delete' | 'skip'
2668
+ defaultStrategy: 'insert', // 'insert' | 'upsert'
2669
+ detectCircularDependencies: true, // Detect circular FK references
2670
+ circularStrategy: 'warn', // 'warn' | 'throw' | 'ignore'
2671
+ deleteByPrimaryKey: true, // Use PK for down migrations
2672
+ onRollbackError: 'warn' // 'warn' | 'throw' | 'ignore'
2673
+ });
2674
+ ```
2675
+
2676
+ ### Enhanced Seed Methods
2677
+
2678
+ ```javascript
2679
+ // Context-level seed API (extended)
2680
+ this.dbset(EntityName).seed(data) // Basic seed
2681
+ .seed(moreData) // Chainable
2682
+ .seedFactory(count, generatorFn) // Factory pattern
2683
+ .when(...environments) // Conditional
2684
+ .upsert({ conflictKey, updateFields }) // Upsert mode
2685
+
2686
+ // Examples
2687
+ this.dbset(User)
2688
+ .seed({ name: 'admin' }) // Single record
2689
+ .seed([{ name: 'user1' }, { name: 'user2' }]) // Array
2690
+ .seedFactory(10, i => ({ name: `User ${i}` })) // Factory
2691
+ .when('development', 'test') // Conditional
2692
+ .upsert({ conflictKey: 'email' }); // Upsert
2693
+ ```
2694
+
2695
+ ---
2696
+
2123
2697
  ## Business Logic Validation
2124
2698
 
2125
2699
  Add validators to your entity definitions for automatic validation on property assignment.