outlet-orm 6.5.0 → 7.0.0
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/bin/init.js +122 -0
- package/bin/mcp.js +78 -0
- package/bin/migrate.js +25 -0
- package/docs/skills/outlet-orm/ADVANCED.md +575 -0
- package/docs/skills/outlet-orm/AI.md +220 -0
- package/docs/skills/outlet-orm/API.md +522 -0
- package/docs/skills/outlet-orm/BACKUP.md +150 -0
- package/docs/skills/outlet-orm/MIGRATIONS.md +605 -0
- package/docs/skills/outlet-orm/MODELS.md +427 -0
- package/docs/skills/outlet-orm/QUERIES.md +345 -0
- package/docs/skills/outlet-orm/RELATIONS.md +555 -0
- package/docs/skills/outlet-orm/SECURITY.md +386 -0
- package/docs/skills/outlet-orm/SEEDS.md +98 -0
- package/docs/skills/outlet-orm/SKILL.md +205 -0
- package/docs/skills/outlet-orm/TYPESCRIPT.md +480 -0
- package/package.json +7 -3
- package/src/AI/AISafetyGuardrails.js +146 -0
- package/src/AI/MCPServer.js +685 -0
- package/src/AI/PromptGenerator.js +318 -0
- package/src/index.js +11 -1
- package/types/index.d.ts +106 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
# Outlet ORM - Migrations & Schema Builder
|
|
2
|
+
|
|
3
|
+
[← Back to Index](SKILL.md) | [Previous: Relationships](RELATIONS.md) | [Next: Advanced →](ADVANCED.md)
|
|
4
|
+
|
|
5
|
+
> 📘 **TypeScript** : Use`MigrationInterface`,`SchemaBuilder`,`TableBuilder`,`ColumnBuilder`for type-safe migrations. See [TYPESCRIPT.md](TYPESCRIPT.md#migrations-typedes-v400)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Project Structure for Migrations (Layered Architecture)
|
|
10
|
+
|
|
11
|
+
> 🔐 **Security**: Database credentials should be in`.env`(never committed).
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
my-project/
|
|
15
|
+
├── .env # ⚠️ NEVER commit
|
|
16
|
+
├── src/
|
|
17
|
+
│ ├── controllers/ # 🎮 HTTP handling only
|
|
18
|
+
│ ├── services/ # ⚙️ Business logic
|
|
19
|
+
│ ├── repositories/ # 📦 Data access layer
|
|
20
|
+
│ ├── models/ # 📊 outlet-orm models
|
|
21
|
+
│ ├── middlewares/ # 🔒 Security
|
|
22
|
+
│ ├── config/
|
|
23
|
+
│ │ └── database.js # Reads from .env
|
|
24
|
+
│ └── utils/ # 🔒 Hash, tokens
|
|
25
|
+
├── database/
|
|
26
|
+
│ ├── config.js # Migration config
|
|
27
|
+
│ └── migrations/
|
|
28
|
+
├── public/ # ✅ Static files
|
|
29
|
+
├── logs/ # 📋 Not versioned
|
|
30
|
+
└── tests/
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## CLI Commands
|
|
36
|
+
|
|
37
|
+
### Initialise Project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
outlet-init
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Generates:
|
|
44
|
+
-`database/config.js`- Configuration
|
|
45
|
+
-`.env`- Environment variables
|
|
46
|
+
- Example model
|
|
47
|
+
- Usage file
|
|
48
|
+
|
|
49
|
+
### Create Migration
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
outlet-migrate make create_users_table
|
|
53
|
+
outlet-migrate make add_email_to_users_table
|
|
54
|
+
outlet-migrate make alter_posts_table
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Run Migrations
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Run all pending migrations
|
|
61
|
+
outlet-migrate migrate
|
|
62
|
+
|
|
63
|
+
# Interactive menu
|
|
64
|
+
outlet-migrate
|
|
65
|
+
# Then choose option 1
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Migration Status
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
outlet-migrate status
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Rollback
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# Rollback last batch
|
|
78
|
+
outlet-migrate rollback --steps 1
|
|
79
|
+
|
|
80
|
+
# Reset all
|
|
81
|
+
outlet-migrate reset --yes
|
|
82
|
+
|
|
83
|
+
# Refresh (reset + migrate)
|
|
84
|
+
outlet-migrate refresh --yes
|
|
85
|
+
|
|
86
|
+
# Fresh (drop all + migrate)
|
|
87
|
+
outlet-migrate fresh --yes
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Convert SQL to Models
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
outlet-convert
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Options:
|
|
97
|
+
1. From local SQL file
|
|
98
|
+
2. From connected database
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Creating a Migration
|
|
103
|
+
|
|
104
|
+
### Create Table Migration
|
|
105
|
+
|
|
106
|
+
```javascript
|
|
107
|
+
const Migration = require('../../lib/Migrations/Migration');
|
|
108
|
+
|
|
109
|
+
class CreateUsersTable extends Migration {
|
|
110
|
+
async up() {
|
|
111
|
+
const schema = this.getSchema();
|
|
112
|
+
|
|
113
|
+
await schema.create('users', (table) => {
|
|
114
|
+
table.id();
|
|
115
|
+
table.string('name', 100);
|
|
116
|
+
table.string('email').unique();
|
|
117
|
+
table.string('password');
|
|
118
|
+
table.boolean('is_active').default(true);
|
|
119
|
+
table.timestamps();
|
|
120
|
+
table.softDeletes();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async down() {
|
|
125
|
+
const schema = this.getSchema();
|
|
126
|
+
await schema.dropIfExists('users');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = CreateUsersTable;
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Alter Table Migration
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
class AddPhoneToUsersTable extends Migration {
|
|
137
|
+
async up() {
|
|
138
|
+
const schema = this.getSchema();
|
|
139
|
+
|
|
140
|
+
await schema.table('users', (table) => {
|
|
141
|
+
table.string('phone', 20).nullable().after('email');
|
|
142
|
+
table.index('phone');
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async down() {
|
|
147
|
+
const schema = this.getSchema();
|
|
148
|
+
|
|
149
|
+
await schema.table('users', (table) => {
|
|
150
|
+
table.dropColumn('phone');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = AddPhoneToUsersTable;
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Column Types
|
|
161
|
+
|
|
162
|
+
### Basic Types
|
|
163
|
+
|
|
164
|
+
| Method | SQL Type |
|
|
165
|
+
|--------|----------|
|
|
166
|
+
|`table.id()`| BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY |
|
|
167
|
+
|`table.string('col', 100)`| VARCHAR(100) |
|
|
168
|
+
|`table.text('col')`| TEXT |
|
|
169
|
+
|`table.integer('col')`| INT |
|
|
170
|
+
|`table.bigInteger('col')`| BIGINT |
|
|
171
|
+
|`table.boolean('col')`| TINYINT(1) |
|
|
172
|
+
|`table.date('col')`| DATE |
|
|
173
|
+
|`table.datetime('col')`| DATETIME |
|
|
174
|
+
|`table.timestamp('col')`| TIMESTAMP |
|
|
175
|
+
|`table.decimal('col', 8, 2)`| DECIMAL(8,2) |
|
|
176
|
+
|`table.float('col', 3, 1)`| FLOAT(3,1) |
|
|
177
|
+
|`table.json('col')`| JSON |
|
|
178
|
+
|`table.enum('col', ['a', 'b'])`| ENUM('a', 'b') |
|
|
179
|
+
|`table.uuid('col')`| CHAR(36) |
|
|
180
|
+
|
|
181
|
+
### Special Types
|
|
182
|
+
|
|
183
|
+
| Method | Description |
|
|
184
|
+
|--------|-------------|
|
|
185
|
+
|`table.foreignId('user_id')`| BIGINT UNSIGNED (for FKs) |
|
|
186
|
+
|`table.timestamps()`| created_at, updated_at |
|
|
187
|
+
|`table.softDeletes()`| deleted_at (TIMESTAMP NULL) |
|
|
188
|
+
|
|
189
|
+
### Usage Example
|
|
190
|
+
|
|
191
|
+
```javascript
|
|
192
|
+
await schema.create('posts', (table) => {
|
|
193
|
+
table.id(); // Primary key
|
|
194
|
+
table.string('title', 200); // VARCHAR(200)
|
|
195
|
+
table.text('content'); // TEXT
|
|
196
|
+
table.integer('views').default(0); // INT DEFAULT 0
|
|
197
|
+
table.decimal('price', 10, 2).nullable(); // DECIMAL(10,2) NULL
|
|
198
|
+
table.boolean('published').default(false); // TINYINT(1) DEFAULT 0
|
|
199
|
+
table.json('metadata'); // JSON
|
|
200
|
+
table.enum('status', ['draft', 'published', 'archived']).default('draft');
|
|
201
|
+
table.datetime('published_at').nullable(); // DATETIME NULL
|
|
202
|
+
table.timestamps(); // created_at, updated_at
|
|
203
|
+
table.softDeletes(); // deleted_at
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Column Modifiers
|
|
210
|
+
|
|
211
|
+
```javascript
|
|
212
|
+
// Nullable
|
|
213
|
+
table.string('bio').nullable();
|
|
214
|
+
|
|
215
|
+
// Default value
|
|
216
|
+
table.integer('count').default(0);
|
|
217
|
+
table.boolean('active').default(true);
|
|
218
|
+
table.string('role').default('user');
|
|
219
|
+
|
|
220
|
+
// Unique constraint
|
|
221
|
+
table.string('email').unique();
|
|
222
|
+
|
|
223
|
+
// Comment
|
|
224
|
+
table.string('name').comment('User full name');
|
|
225
|
+
|
|
226
|
+
// Unsigned
|
|
227
|
+
table.integer('age').unsigned();
|
|
228
|
+
|
|
229
|
+
// Position after column
|
|
230
|
+
table.string('middle_name').after('first_name');
|
|
231
|
+
|
|
232
|
+
// Position first
|
|
233
|
+
table.string('id').first();
|
|
234
|
+
|
|
235
|
+
// Auto timestamp
|
|
236
|
+
table.timestamp('created_at').useCurrent();
|
|
237
|
+
table.timestamp('updated_at').useCurrent().useCurrentOnUpdate();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Foreign Keys
|
|
243
|
+
|
|
244
|
+
### Explicit Syntax
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
await schema.create('posts', (table) => {
|
|
248
|
+
table.id();
|
|
249
|
+
table.foreignId('user_id');
|
|
250
|
+
table.string('title');
|
|
251
|
+
table.timestamps();
|
|
252
|
+
|
|
253
|
+
table.foreign('user_id')
|
|
254
|
+
.references('id')
|
|
255
|
+
.on('users')
|
|
256
|
+
.onDelete('CASCADE')
|
|
257
|
+
.onUpdate('CASCADE');
|
|
258
|
+
});
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Simplified Syntax
|
|
262
|
+
|
|
263
|
+
```javascript
|
|
264
|
+
// Infers table from column name (user_id → users)
|
|
265
|
+
table.foreignId('user_id').constrained();
|
|
266
|
+
|
|
267
|
+
// Explicit table
|
|
268
|
+
table.foreignId('author_id').constrained('users');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Cascade Options
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
// Cascade on delete and update
|
|
275
|
+
table.foreign('user_id')
|
|
276
|
+
.references('id')
|
|
277
|
+
.on('users')
|
|
278
|
+
.cascadeOnDelete()
|
|
279
|
+
.cascadeOnUpdate();
|
|
280
|
+
|
|
281
|
+
// Other options: CASCADE, SET NULL, NO ACTION, RESTRICT
|
|
282
|
+
table.foreign('category_id')
|
|
283
|
+
.references('id')
|
|
284
|
+
.on('categories')
|
|
285
|
+
.onDelete('SET NULL')
|
|
286
|
+
.onUpdate('CASCADE');
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Drop Foreign Key
|
|
290
|
+
|
|
291
|
+
```javascript
|
|
292
|
+
await schema.table('posts', (table) => {
|
|
293
|
+
table.dropForeign(['user_id']);
|
|
294
|
+
});
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Indexes
|
|
300
|
+
|
|
301
|
+
### Create Indexes
|
|
302
|
+
|
|
303
|
+
```javascript
|
|
304
|
+
await schema.create('users', (table) => {
|
|
305
|
+
table.id();
|
|
306
|
+
table.string('email');
|
|
307
|
+
table.string('first_name');
|
|
308
|
+
table.string('last_name');
|
|
309
|
+
|
|
310
|
+
// Simple index
|
|
311
|
+
table.index('email');
|
|
312
|
+
|
|
313
|
+
// Composite index
|
|
314
|
+
table.index(['first_name', 'last_name']);
|
|
315
|
+
|
|
316
|
+
// Unique index
|
|
317
|
+
table.unique('email');
|
|
318
|
+
|
|
319
|
+
// Full text index
|
|
320
|
+
table.fullText('bio');
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Drop Indexes
|
|
325
|
+
|
|
326
|
+
```javascript
|
|
327
|
+
await schema.table('users', (table) => {
|
|
328
|
+
table.dropIndex(['email']);
|
|
329
|
+
table.dropIndex(['first_name', 'last_name']);
|
|
330
|
+
});
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Table Operations
|
|
336
|
+
|
|
337
|
+
### Create Table
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
await schema.create('users', (table) => {
|
|
341
|
+
table.id();
|
|
342
|
+
table.string('name');
|
|
343
|
+
table.timestamps();
|
|
344
|
+
});
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Modify Existing Table
|
|
348
|
+
|
|
349
|
+
```javascript
|
|
350
|
+
await schema.table('users', (table) => {
|
|
351
|
+
table.string('bio').nullable();
|
|
352
|
+
table.index('email');
|
|
353
|
+
});
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
### Rename Table
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
await schema.rename('old_users', 'users');
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Drop Table
|
|
363
|
+
|
|
364
|
+
```javascript
|
|
365
|
+
await schema.drop('users');
|
|
366
|
+
await schema.dropIfExists('users'); // Safe drop
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Check Existence
|
|
370
|
+
|
|
371
|
+
```javascript
|
|
372
|
+
const exists = await schema.hasTable('users');
|
|
373
|
+
const hasColumn = await schema.hasColumn('users', 'email');
|
|
374
|
+
|
|
375
|
+
if (!exists) {
|
|
376
|
+
await schema.create('users', (table) => {
|
|
377
|
+
table.id();
|
|
378
|
+
table.string('name');
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Column Manipulation
|
|
386
|
+
|
|
387
|
+
### Rename Column
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
await schema.table('users', (table) => {
|
|
391
|
+
table.renameColumn('name', 'full_name');
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Drop Columns
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
await schema.table('users', (table) => {
|
|
399
|
+
// Single column
|
|
400
|
+
table.dropColumn('phone');
|
|
401
|
+
|
|
402
|
+
// Multiple columns
|
|
403
|
+
table.dropColumn(['bio', 'avatar']);
|
|
404
|
+
|
|
405
|
+
// Drop timestamps
|
|
406
|
+
table.dropTimestamps(); // Removes created_at and updated_at
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## Raw SQL in Migrations
|
|
413
|
+
|
|
414
|
+
```javascript
|
|
415
|
+
class AddFulltextSearch extends Migration {
|
|
416
|
+
async up() {
|
|
417
|
+
await this.execute(`
|
|
418
|
+
ALTER TABLE posts
|
|
419
|
+
ADD FULLTEXT INDEX posts_search_idx (title, content)
|
|
420
|
+
`);
|
|
421
|
+
|
|
422
|
+
await this.execute(`
|
|
423
|
+
CREATE VIEW active_posts AS
|
|
424
|
+
SELECT * FROM posts
|
|
425
|
+
WHERE status = 'published'
|
|
426
|
+
AND deleted_at IS NULL
|
|
427
|
+
`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async down() {
|
|
431
|
+
await this.execute('DROP VIEW IF EXISTS active_posts');
|
|
432
|
+
await this.execute('ALTER TABLE posts DROP INDEX posts_search_idx');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## Complete Migration Example
|
|
440
|
+
|
|
441
|
+
```javascript
|
|
442
|
+
const Migration = require('../../lib/Migrations/Migration');
|
|
443
|
+
|
|
444
|
+
class CreateBlogTables extends Migration {
|
|
445
|
+
async up() {
|
|
446
|
+
const schema = this.getSchema();
|
|
447
|
+
|
|
448
|
+
// Users table
|
|
449
|
+
await schema.create('users', (table) => {
|
|
450
|
+
table.id();
|
|
451
|
+
table.string('name', 100);
|
|
452
|
+
table.string('email').unique();
|
|
453
|
+
table.string('password');
|
|
454
|
+
table.boolean('is_admin').default(false);
|
|
455
|
+
table.timestamps();
|
|
456
|
+
table.softDeletes();
|
|
457
|
+
|
|
458
|
+
table.index('email');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Categories table (with self-reference)
|
|
462
|
+
await schema.create('categories', (table) => {
|
|
463
|
+
table.id();
|
|
464
|
+
table.string('name', 50).unique();
|
|
465
|
+
table.string('slug', 50).unique();
|
|
466
|
+
table.text('description').nullable();
|
|
467
|
+
table.foreignId('parent_id').nullable();
|
|
468
|
+
table.timestamps();
|
|
469
|
+
|
|
470
|
+
table.foreign('parent_id')
|
|
471
|
+
.references('id')
|
|
472
|
+
.on('categories')
|
|
473
|
+
.onDelete('CASCADE');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Posts table
|
|
477
|
+
await schema.create('posts', (table) => {
|
|
478
|
+
table.id();
|
|
479
|
+
table.foreignId('user_id').constrained().cascadeOnDelete();
|
|
480
|
+
table.foreignId('category_id').constrained().cascadeOnDelete();
|
|
481
|
+
table.string('title');
|
|
482
|
+
table.string('slug').unique();
|
|
483
|
+
table.text('excerpt').nullable();
|
|
484
|
+
table.text('content');
|
|
485
|
+
table.enum('status', ['draft', 'published', 'archived']).default('draft');
|
|
486
|
+
table.integer('views').default(0).unsigned();
|
|
487
|
+
table.timestamp('published_at').nullable();
|
|
488
|
+
table.timestamps();
|
|
489
|
+
table.softDeletes();
|
|
490
|
+
|
|
491
|
+
table.index(['user_id', 'status']);
|
|
492
|
+
table.index('published_at');
|
|
493
|
+
table.fullText('content');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Tags table
|
|
497
|
+
await schema.create('tags', (table) => {
|
|
498
|
+
table.id();
|
|
499
|
+
table.string('name', 50).unique();
|
|
500
|
+
table.string('slug', 50).unique();
|
|
501
|
+
table.timestamps();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Pivot table
|
|
505
|
+
await schema.create('post_tag', (table) => {
|
|
506
|
+
table.id();
|
|
507
|
+
table.foreignId('post_id').constrained().cascadeOnDelete();
|
|
508
|
+
table.foreignId('tag_id').constrained().cascadeOnDelete();
|
|
509
|
+
table.timestamps();
|
|
510
|
+
|
|
511
|
+
table.unique(['post_id', 'tag_id']);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async down() {
|
|
516
|
+
const schema = this.getSchema();
|
|
517
|
+
|
|
518
|
+
// Drop in reverse order (FK dependencies)
|
|
519
|
+
await schema.dropIfExists('post_tag');
|
|
520
|
+
await schema.dropIfExists('tags');
|
|
521
|
+
await schema.dropIfExists('posts');
|
|
522
|
+
await schema.dropIfExists('categories');
|
|
523
|
+
await schema.dropIfExists('users');
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
module.exports = CreateBlogTables;
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
---
|
|
531
|
+
|
|
532
|
+
## Migration Best Practices
|
|
533
|
+
|
|
534
|
+
### 1. Migration Naming
|
|
535
|
+
|
|
536
|
+
```
|
|
537
|
+
✅ Good:
|
|
538
|
+
20231011_120000_create_users_table.js
|
|
539
|
+
20231011_120100_add_email_to_users_table.js
|
|
540
|
+
|
|
541
|
+
❌ Bad:
|
|
542
|
+
migration1.js
|
|
543
|
+
users.js
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### 2. Always Implement down()
|
|
547
|
+
|
|
548
|
+
```javascript
|
|
549
|
+
// ✅ Good - Reversible
|
|
550
|
+
async down() {
|
|
551
|
+
await schema.dropIfExists('users');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// ❌ Bad - Not reversible
|
|
555
|
+
async down() {
|
|
556
|
+
// Empty
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### 3. Respect FK Order
|
|
561
|
+
|
|
562
|
+
```javascript
|
|
563
|
+
async down() {
|
|
564
|
+
// Drop child tables first
|
|
565
|
+
await schema.dropIfExists('posts'); // Has FK to users
|
|
566
|
+
await schema.dropIfExists('users'); // Parent table
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### 4. Keep Migrations Atomic
|
|
571
|
+
|
|
572
|
+
One migration = one task. Related tables can be together.
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
// ✅ Good - Related tables together
|
|
576
|
+
create_blog_tables.js // users, posts, comments
|
|
577
|
+
|
|
578
|
+
// ✅ Good - Independent feature
|
|
579
|
+
create_analytics_tables.js // analytics, events
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
---
|
|
583
|
+
|
|
584
|
+
## NPM Scripts Integration
|
|
585
|
+
|
|
586
|
+
```json
|
|
587
|
+
{
|
|
588
|
+
"scripts": {
|
|
589
|
+
"db:init": "outlet-init",
|
|
590
|
+
"db:migrate": "outlet-migrate migrate",
|
|
591
|
+
"db:migrate:make": "outlet-migrate make",
|
|
592
|
+
"db:migrate:status": "outlet-migrate status",
|
|
593
|
+
"db:rollback": "outlet-migrate rollback --steps 1",
|
|
594
|
+
"db:fresh": "outlet-migrate fresh --yes",
|
|
595
|
+
"db:convert": "outlet-convert"
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
---
|
|
601
|
+
|
|
602
|
+
## Next Steps
|
|
603
|
+
|
|
604
|
+
- [Advanced Features →](ADVANCED.md)
|
|
605
|
+
- [API Reference →](API.md)
|