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.
@@ -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)