masterrecord 0.3.30 → 0.3.32
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/Entity/entityModelBuilder.js +44 -1
- package/Migrations/cli.js +17 -4
- package/Migrations/dependencyGraph.js +108 -0
- package/Migrations/migrationMySQLQuery.js +10 -0
- package/Migrations/migrationPostgresQuery.js +10 -0
- package/Migrations/migrationSQLiteQuery.js +10 -0
- package/Migrations/migrationTemplate.js +176 -0
- package/Migrations/migrations.js +121 -12
- package/Migrations/schema.js +63 -0
- package/context.js +288 -0
- package/package.json +1 -1
- package/readme.md +812 -37
- 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)
|
|
@@ -1900,6 +1903,797 @@ Migrations automatically include rollback logic. Running `masterrecord migrate d
|
|
|
1900
1903
|
|
|
1901
1904
|
---
|
|
1902
1905
|
|
|
1906
|
+
## Composite Indexes
|
|
1907
|
+
|
|
1908
|
+
Create multi-column indexes for queries that filter or sort on multiple columns together.
|
|
1909
|
+
|
|
1910
|
+
### API - Two Ways to Define
|
|
1911
|
+
|
|
1912
|
+
**Option A: Entity Class (Recommended for core indexes)**
|
|
1913
|
+
|
|
1914
|
+
```javascript
|
|
1915
|
+
class CreditLedger {
|
|
1916
|
+
id(db) {
|
|
1917
|
+
db.integer().primary().auto();
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
organization_id(db) {
|
|
1921
|
+
db.integer().notNullable();
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
created_at(db) {
|
|
1925
|
+
db.timestamp().default('CURRENT_TIMESTAMP');
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
resource_type(db) {
|
|
1929
|
+
db.string().notNullable();
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
resource_id(db) {
|
|
1933
|
+
db.integer().notNullable();
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// Define composite indexes in entity
|
|
1937
|
+
static compositeIndexes = [
|
|
1938
|
+
// Simple array - auto-generates name
|
|
1939
|
+
['organization_id', 'created_at'],
|
|
1940
|
+
['resource_type', 'resource_id'],
|
|
1941
|
+
|
|
1942
|
+
// With custom name
|
|
1943
|
+
{
|
|
1944
|
+
columns: ['status', 'created_at'],
|
|
1945
|
+
name: 'idx_status_timeline'
|
|
1946
|
+
},
|
|
1947
|
+
|
|
1948
|
+
// Unique composite index
|
|
1949
|
+
{
|
|
1950
|
+
columns: ['email', 'tenant_id'],
|
|
1951
|
+
unique: true
|
|
1952
|
+
}
|
|
1953
|
+
];
|
|
1954
|
+
}
|
|
1955
|
+
```
|
|
1956
|
+
|
|
1957
|
+
**Option C: Context-Level (For environment-specific or centralized schema)**
|
|
1958
|
+
|
|
1959
|
+
```javascript
|
|
1960
|
+
class AppContext extends context {
|
|
1961
|
+
onConfig() {
|
|
1962
|
+
this.dbset(CreditLedger);
|
|
1963
|
+
|
|
1964
|
+
// Define composite indexes in context
|
|
1965
|
+
this.compositeIndex(CreditLedger, ['organization_id', 'created_at']);
|
|
1966
|
+
this.compositeIndex(CreditLedger, ['resource_type', 'resource_id']);
|
|
1967
|
+
this.compositeIndex(CreditLedger, ['status', 'created_at'], {
|
|
1968
|
+
name: 'idx_status_timeline'
|
|
1969
|
+
});
|
|
1970
|
+
this.compositeIndex(CreditLedger, ['email', 'tenant_id'], {
|
|
1971
|
+
unique: true
|
|
1972
|
+
});
|
|
1973
|
+
|
|
1974
|
+
// Can also use table name as string
|
|
1975
|
+
this.compositeIndex('CreditLedger', ['user_id', 'created_at']);
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
```
|
|
1979
|
+
|
|
1980
|
+
**Combined Usage (Best of Both)**
|
|
1981
|
+
|
|
1982
|
+
```javascript
|
|
1983
|
+
class User {
|
|
1984
|
+
email(db) { db.string(); }
|
|
1985
|
+
tenant_id(db) { db.integer(); }
|
|
1986
|
+
last_name(db) { db.string(); }
|
|
1987
|
+
first_name(db) { db.string(); }
|
|
1988
|
+
|
|
1989
|
+
// Core indexes in entity
|
|
1990
|
+
static compositeIndexes = [
|
|
1991
|
+
['last_name', 'first_name']
|
|
1992
|
+
];
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
class AppContext extends context {
|
|
1996
|
+
onConfig() {
|
|
1997
|
+
this.dbset(User);
|
|
1998
|
+
|
|
1999
|
+
// Add tenant-specific index for multi-tenant deployments
|
|
2000
|
+
if (process.env.MULTI_TENANT === 'true') {
|
|
2001
|
+
this.compositeIndex(User, ['tenant_id', 'email'], { unique: true });
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// Add performance index for production
|
|
2005
|
+
if (process.env.NODE_ENV === 'production') {
|
|
2006
|
+
this.compositeIndex(User, ['tenant_id', 'last_name']);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
### When to Use Composite Indexes
|
|
2013
|
+
|
|
2014
|
+
Composite indexes are most effective for queries that:
|
|
2015
|
+
1. **Filter on multiple columns**: `WHERE org_id = ? AND status = ?`
|
|
2016
|
+
2. **Filter and sort**: `WHERE status = ? ORDER BY created_at`
|
|
2017
|
+
3. **Enforce uniqueness**: Unique constraint on multiple columns together
|
|
2018
|
+
|
|
2019
|
+
**Example queries that benefit:**
|
|
2020
|
+
|
|
2021
|
+
```javascript
|
|
2022
|
+
// Benefits from composite index (organization_id, created_at)
|
|
2023
|
+
const ledger = await db.CreditLedger
|
|
2024
|
+
.where(c => c.organization_id == $$, orgId)
|
|
2025
|
+
.orderBy(c => c.created_at)
|
|
2026
|
+
.toList();
|
|
2027
|
+
|
|
2028
|
+
// Benefits from composite index (resource_type, resource_id)
|
|
2029
|
+
const entry = await db.CreditLedger
|
|
2030
|
+
.where(c => c.resource_type == $$ && c.resource_id == $$, 'Order', 123)
|
|
2031
|
+
.single();
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
### Column Order Matters
|
|
2035
|
+
|
|
2036
|
+
The order of columns in a composite index affects query performance:
|
|
2037
|
+
|
|
2038
|
+
```javascript
|
|
2039
|
+
static compositeIndexes = [
|
|
2040
|
+
// Index: (status, created_at)
|
|
2041
|
+
['status', 'created_at']
|
|
2042
|
+
];
|
|
2043
|
+
|
|
2044
|
+
// ✅ FAST: Uses index efficiently
|
|
2045
|
+
// WHERE status = ? ORDER BY created_at
|
|
2046
|
+
await db.Orders
|
|
2047
|
+
.where(o => o.status == $$, 'pending')
|
|
2048
|
+
.orderBy(o => o.created_at)
|
|
2049
|
+
.toList();
|
|
2050
|
+
|
|
2051
|
+
// ⚠️ SLOWER: Can only use first column
|
|
2052
|
+
// WHERE created_at > ?
|
|
2053
|
+
await db.Orders
|
|
2054
|
+
.where(o => o.created_at > $$, yesterday)
|
|
2055
|
+
.toList();
|
|
2056
|
+
```
|
|
2057
|
+
|
|
2058
|
+
**Rule of thumb:** Put the most selective (filtered) columns first, then sort columns.
|
|
2059
|
+
|
|
2060
|
+
### Automatic Migration Generation
|
|
2061
|
+
|
|
2062
|
+
```javascript
|
|
2063
|
+
// Your entity definition triggers migration
|
|
2064
|
+
class CreditLedger {
|
|
2065
|
+
organization_id(db) { db.integer(); }
|
|
2066
|
+
created_at(db) { db.timestamp(); }
|
|
2067
|
+
|
|
2068
|
+
static compositeIndexes = [
|
|
2069
|
+
['organization_id', 'created_at']
|
|
2070
|
+
];
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Generated migration (automatic)
|
|
2074
|
+
class Migration_20250101 extends masterrecord.schema {
|
|
2075
|
+
async up(table) {
|
|
2076
|
+
this.init(table);
|
|
2077
|
+
this.createCompositeIndex({
|
|
2078
|
+
tableName: 'CreditLedger',
|
|
2079
|
+
columns: ['organization_id', 'created_at'],
|
|
2080
|
+
indexName: 'idx_creditleger_organization_id_created_at',
|
|
2081
|
+
unique: false
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
async down(table) {
|
|
2086
|
+
this.init(table);
|
|
2087
|
+
this.dropCompositeIndex({
|
|
2088
|
+
tableName: 'CreditLedger',
|
|
2089
|
+
columns: ['organization_id', 'created_at'],
|
|
2090
|
+
indexName: 'idx_creditleger_organization_id_created_at',
|
|
2091
|
+
unique: false
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
```
|
|
2096
|
+
|
|
2097
|
+
### Single vs Composite Indexes
|
|
2098
|
+
|
|
2099
|
+
```javascript
|
|
2100
|
+
class User {
|
|
2101
|
+
email(db) {
|
|
2102
|
+
db.string().index(); // Single-column index
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
first_name(db) {
|
|
2106
|
+
db.string(); // Part of composite below
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
last_name(db) {
|
|
2110
|
+
db.string(); // Part of composite below
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
static compositeIndexes = [
|
|
2114
|
+
// Composite index for name lookups
|
|
2115
|
+
['last_name', 'first_name']
|
|
2116
|
+
];
|
|
2117
|
+
}
|
|
2118
|
+
```
|
|
2119
|
+
|
|
2120
|
+
**When to use single vs composite:**
|
|
2121
|
+
- **Single index**: Column queried independently (`WHERE email = ?`)
|
|
2122
|
+
- **Composite index**: Columns queried together (`WHERE last_name = ? AND first_name = ?`)
|
|
2123
|
+
|
|
2124
|
+
---
|
|
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
|
+
|
|
1903
2697
|
## Business Logic Validation
|
|
1904
2698
|
|
|
1905
2699
|
Add validators to your entity definitions for automatic validation on property assignment.
|
|
@@ -2411,59 +3205,40 @@ await db.saveChanges(); // Batch insert
|
|
|
2411
3205
|
|
|
2412
3206
|
### 3. Use Indexes
|
|
2413
3207
|
|
|
2414
|
-
|
|
3208
|
+
**Single-column indexes:**
|
|
2415
3209
|
|
|
2416
3210
|
```javascript
|
|
2417
3211
|
class User {
|
|
2418
|
-
id(db) {
|
|
2419
|
-
db.integer().primary().auto(); // Primary keys are automatically indexed
|
|
2420
|
-
}
|
|
2421
|
-
|
|
2422
3212
|
email(db) {
|
|
2423
|
-
db.string()
|
|
2424
|
-
.notNullable()
|
|
2425
|
-
.unique()
|
|
2426
|
-
.index(); // Creates: idx_user_email
|
|
2427
|
-
}
|
|
2428
|
-
|
|
2429
|
-
last_name(db) {
|
|
2430
|
-
db.string().index(); // Creates: idx_user_last_name
|
|
2431
|
-
}
|
|
2432
|
-
|
|
2433
|
-
status(db) {
|
|
2434
|
-
db.string().index('idx_user_status'); // Custom index name
|
|
3213
|
+
db.string().index(); // Single column
|
|
2435
3214
|
}
|
|
2436
3215
|
}
|
|
2437
3216
|
```
|
|
2438
3217
|
|
|
2439
|
-
**
|
|
3218
|
+
**Composite indexes for multi-column queries:**
|
|
2440
3219
|
|
|
2441
3220
|
```javascript
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
indexName: 'idx_user_email'
|
|
2447
|
-
});
|
|
2448
|
-
```
|
|
3221
|
+
class Order {
|
|
3222
|
+
user_id(db) { db.integer(); }
|
|
3223
|
+
status(db) { db.string(); }
|
|
3224
|
+
created_at(db) { db.timestamp(); }
|
|
2449
3225
|
|
|
2450
|
-
|
|
3226
|
+
static compositeIndexes = [
|
|
3227
|
+
// For: WHERE user_id = ? AND status = ?
|
|
3228
|
+
['user_id', 'status'],
|
|
2451
3229
|
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
columnName: 'email',
|
|
2457
|
-
indexName: 'idx_user_email'
|
|
2458
|
-
});
|
|
3230
|
+
// For: WHERE status = ? ORDER BY created_at
|
|
3231
|
+
['status', 'created_at']
|
|
3232
|
+
];
|
|
3233
|
+
}
|
|
2459
3234
|
```
|
|
2460
3235
|
|
|
2461
3236
|
**Best practices:**
|
|
2462
|
-
- Index
|
|
2463
|
-
-
|
|
3237
|
+
- Index foreign keys for join performance
|
|
3238
|
+
- Use composite indexes for queries with multiple WHERE conditions
|
|
3239
|
+
- Column order matters: most selective (filtered) columns first
|
|
2464
3240
|
- Don't over-index - each index adds write overhead
|
|
2465
|
-
- Primary keys are automatically indexed
|
|
2466
|
-
- Use `.unique()` for data integrity, `.index()` for query performance
|
|
3241
|
+
- Primary keys are automatically indexed
|
|
2467
3242
|
|
|
2468
3243
|
### 4. Limit Result Sets
|
|
2469
3244
|
|