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.
- package/LICENSE +21 -0
- package/README.md +705 -0
- package/bin/convert.js +679 -0
- package/bin/init.js +190 -0
- package/bin/migrate.js +442 -0
- package/lib/Database/DatabaseConnection.js +4 -0
- package/lib/Migrations/Migration.js +48 -0
- package/lib/Migrations/MigrationManager.js +326 -0
- package/lib/Schema/Schema.js +790 -0
- package/package.json +75 -0
- package/src/DatabaseConnection.js +697 -0
- package/src/Model.js +659 -0
- package/src/QueryBuilder.js +710 -0
- package/src/Relations/BelongsToManyRelation.js +466 -0
- package/src/Relations/BelongsToRelation.js +127 -0
- package/src/Relations/HasManyRelation.js +125 -0
- package/src/Relations/HasManyThroughRelation.js +112 -0
- package/src/Relations/HasOneRelation.js +114 -0
- package/src/Relations/HasOneThroughRelation.js +105 -0
- package/src/Relations/MorphManyRelation.js +69 -0
- package/src/Relations/MorphOneRelation.js +68 -0
- package/src/Relations/MorphToRelation.js +110 -0
- package/src/Relations/Relation.js +31 -0
- package/src/index.js +23 -0
- package/types/index.d.ts +272 -0
|
@@ -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;
|