outlet-orm 2.5.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,326 @@
1
+ /**
2
+ * Migration Manager
3
+ * Handles running, rolling back, and managing migrations
4
+ */
5
+
6
+ const fs = require('fs').promises;
7
+ const path = require('path');
8
+
9
+ class MigrationManager {
10
+ constructor(connection, migrationsPath = './database/migrations') {
11
+ this.connection = connection;
12
+ this.migrationsPath = path.resolve(process.cwd(), migrationsPath);
13
+ this.migrationsTable = 'migrations';
14
+ }
15
+
16
+ /**
17
+ * Initialize the migrations table
18
+ */
19
+ async initialize() {
20
+ const { Schema } = require('../Schema/Schema');
21
+ const schema = new Schema(this.connection);
22
+
23
+ const tableExists = await schema.hasTable(this.migrationsTable);
24
+
25
+ if (!tableExists) {
26
+ await schema.create(this.migrationsTable, (table) => {
27
+ table.id();
28
+ table.string('migration');
29
+ table.integer('batch');
30
+ table.timestamp('created_at').useCurrent();
31
+ });
32
+ console.log('✓ Migrations table created');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Run all pending migrations
38
+ */
39
+ async run() {
40
+ await this.initialize();
41
+
42
+ const pending = await this.getPendingMigrations();
43
+
44
+ if (pending.length === 0) {
45
+ console.log('✓ No pending migrations');
46
+ return;
47
+ }
48
+
49
+ const batch = await this.getNextBatchNumber();
50
+
51
+ console.log(`Running ${pending.length} migration(s)...\n`);
52
+
53
+ for (const migration of pending) {
54
+ await this.runMigration(migration, batch);
55
+ }
56
+
57
+ console.log(`\n✓ All migrations completed successfully`);
58
+ }
59
+
60
+ /**
61
+ * Run a single migration
62
+ */
63
+ async runMigration(migrationFile, batch) {
64
+ const startTime = Date.now();
65
+ const migrationPath = path.join(this.migrationsPath, migrationFile);
66
+
67
+ try {
68
+ // Load the migration file
69
+ delete require.cache[require.resolve(migrationPath)];
70
+ const MigrationClass = require(migrationPath);
71
+ const migration = new MigrationClass(this.connection);
72
+
73
+ // Run the migration
74
+ await migration.up();
75
+
76
+ // Record in migrations table
77
+ await this.recordMigration(migrationFile, batch);
78
+
79
+ const duration = Date.now() - startTime;
80
+ console.log(`✓ ${migrationFile} (${duration}ms)`);
81
+ } catch (error) {
82
+ console.error(`✗ Failed to run migration: ${migrationFile}`);
83
+ console.error(error.message);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Rollback the last batch of migrations
90
+ */
91
+ async rollback(steps = 1) {
92
+ await this.initialize();
93
+
94
+ const migrations = await this.getLastBatchMigrations(steps);
95
+
96
+ if (migrations.length === 0) {
97
+ console.log('✓ No migrations to rollback');
98
+ return;
99
+ }
100
+
101
+ console.log(`Rolling back ${migrations.length} migration(s)...\n`);
102
+
103
+ // Rollback in reverse order
104
+ for (const migration of migrations.reverse()) {
105
+ await this.rollbackMigration(migration);
106
+ }
107
+
108
+ console.log(`\n✓ Rollback completed successfully`);
109
+ }
110
+
111
+ /**
112
+ * Rollback a single migration
113
+ */
114
+ async rollbackMigration(migrationRecord) {
115
+ const startTime = Date.now();
116
+ const migrationPath = path.join(this.migrationsPath, migrationRecord.migration);
117
+
118
+ try {
119
+ // Load the migration file
120
+ delete require.cache[require.resolve(migrationPath)];
121
+ const MigrationClass = require(migrationPath);
122
+ const migration = new MigrationClass(this.connection);
123
+
124
+ // Run the down method
125
+ await migration.down();
126
+
127
+ // Remove from migrations table
128
+ await this.removeMigrationRecord(migrationRecord.migration);
129
+
130
+ const duration = Date.now() - startTime;
131
+ console.log(`✓ ${migrationRecord.migration} (${duration}ms)`);
132
+ } catch (error) {
133
+ console.error(`✗ Failed to rollback migration: ${migrationRecord.migration}`);
134
+ console.error(error.message);
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Reset all migrations (rollback all)
141
+ */
142
+ async reset() {
143
+ await this.initialize();
144
+
145
+ const allMigrations = await this.getRanMigrations();
146
+
147
+ if (allMigrations.length === 0) {
148
+ console.log('✓ No migrations to reset');
149
+ return;
150
+ }
151
+
152
+ console.log(`Resetting ${allMigrations.length} migration(s)...\n`);
153
+
154
+ for (const migration of allMigrations.reverse()) {
155
+ await this.rollbackMigration(migration);
156
+ }
157
+
158
+ console.log(`\n✓ Reset completed successfully`);
159
+ }
160
+
161
+ /**
162
+ * Refresh migrations (reset + run)
163
+ */
164
+ async refresh() {
165
+ console.log('Refreshing migrations...\n');
166
+ await this.reset();
167
+ console.log('');
168
+ await this.run();
169
+ }
170
+
171
+ /**
172
+ * Fresh migrations (drop all tables + run)
173
+ */
174
+ async fresh() {
175
+ console.log('Fresh migration - dropping all tables...\n');
176
+
177
+ const { Schema } = require('../Schema/Schema');
178
+ const schema = new Schema(this.connection);
179
+
180
+ // Get all tables
181
+ const tables = await this.getAllTables();
182
+
183
+ // Drop all tables
184
+ for (const table of tables) {
185
+ await schema.dropIfExists(table);
186
+ }
187
+
188
+ console.log('');
189
+ await this.run();
190
+ }
191
+
192
+ /**
193
+ * Get migration status
194
+ */
195
+ async status() {
196
+ await this.initialize();
197
+
198
+ const allFiles = await this.getAllMigrationFiles();
199
+ const ranMigrations = await this.getRanMigrations();
200
+ const ranNames = new Set(ranMigrations.map(m => m.migration));
201
+
202
+ console.log('\n┌─────────────────────────────────────────────────────┬────────┐');
203
+ console.log('│ Migration │ Status │');
204
+ console.log('├─────────────────────────────────────────────────────┼────────┤');
205
+
206
+ for (const file of allFiles) {
207
+ const status = ranNames.has(file) ? ' Ran ' : 'Pending';
208
+ const paddedFile = file.padEnd(51);
209
+ console.log(`│ ${paddedFile} │ ${status} │`);
210
+ }
211
+
212
+ console.log('└─────────────────────────────────────────────────────┴────────┘\n');
213
+ }
214
+
215
+ /**
216
+ * Get all migration files
217
+ */
218
+ async getAllMigrationFiles() {
219
+ try {
220
+ const files = await fs.readdir(this.migrationsPath);
221
+ return files
222
+ .filter(f => f.endsWith('.js'))
223
+ .sort();
224
+ } catch (error) {
225
+ if (error.code === 'ENOENT') {
226
+ return [];
227
+ }
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Get pending migrations
234
+ */
235
+ async getPendingMigrations() {
236
+ const allFiles = await this.getAllMigrationFiles();
237
+ const ranMigrations = await this.getRanMigrations();
238
+ const ranNames = new Set(ranMigrations.map(m => m.migration));
239
+
240
+ return allFiles.filter(file => !ranNames.has(file));
241
+ }
242
+
243
+ /**
244
+ * Get all ran migrations
245
+ */
246
+ async getRanMigrations() {
247
+ try {
248
+ const sql = `SELECT * FROM ${this.migrationsTable} ORDER BY batch ASC, id ASC`;
249
+ return await this.connection.execute(sql);
250
+ } catch (error) {
251
+ // Table doesn't exist yet (first migration), return empty array
252
+ if (error.code === 'ER_NO_SUCH_TABLE' || error.message?.includes('no such table')) {
253
+ return [];
254
+ }
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get last batch migrations
261
+ */
262
+ async getLastBatchMigrations(steps = 1) {
263
+ const sql = `
264
+ SELECT * FROM ${this.migrationsTable}
265
+ WHERE batch >= (
266
+ SELECT MAX(batch) - ${steps - 1} FROM ${this.migrationsTable}
267
+ )
268
+ ORDER BY batch DESC, id DESC
269
+ `;
270
+ return await this.connection.execute(sql);
271
+ }
272
+
273
+ /**
274
+ * Get next batch number
275
+ */
276
+ async getNextBatchNumber() {
277
+ const sql = `SELECT MAX(batch) as max_batch FROM ${this.migrationsTable}`;
278
+ const result = await this.connection.execute(sql);
279
+ const maxBatch = result[0].max_batch || 0;
280
+ return maxBatch + 1;
281
+ }
282
+
283
+ /**
284
+ * Record a migration
285
+ */
286
+ async recordMigration(migration, batch) {
287
+ const sql = `INSERT INTO ${this.migrationsTable} (migration, batch) VALUES (?, ?)`;
288
+ await this.connection.execute(sql, [migration, batch]);
289
+ }
290
+
291
+ /**
292
+ * Remove a migration record
293
+ */
294
+ async removeMigrationRecord(migration) {
295
+ const sql = `DELETE FROM ${this.migrationsTable} WHERE migration = ?`;
296
+ await this.connection.execute(sql, [migration]);
297
+ }
298
+
299
+ /**
300
+ * Get all tables in the database
301
+ */
302
+ async getAllTables() {
303
+ const driver = this.connection.config.driver;
304
+ let sql;
305
+
306
+ switch (driver) {
307
+ case 'mysql':
308
+ sql = `SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()`;
309
+ break;
310
+ case 'postgres':
311
+ case 'postgresql':
312
+ sql = `SELECT tablename FROM pg_tables WHERE schemaname = 'public'`;
313
+ break;
314
+ case 'sqlite':
315
+ sql = `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
316
+ break;
317
+ default:
318
+ throw new Error(`Unsupported driver: ${driver}`);
319
+ }
320
+
321
+ const result = await this.connection.execute(sql);
322
+ return result.map(r => r.table_name || r.tablename || r.name);
323
+ }
324
+ }
325
+
326
+ module.exports = MigrationManager;