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/.claude/settings.local.json +2 -1
- package/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +5 -0
- package/Migrations/migrationPostgresQuery.js +5 -0
- package/Migrations/migrationSQLiteQuery.js +6 -1
- package/Migrations/migrationTemplate.js +144 -0
- package/Migrations/migrations.js +42 -10
- package/context.js +205 -0
- package/package.json +1 -1
- package/readme.md +574 -0
- package/test/index-bug-fix-test.js +188 -0
- package/test/seed-data-test.js +212 -0
- package/test/seed-features-integration-test.js +418 -0
- package/test/seed-migration-template-test.js +220 -0
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.
|