@aetherframework/database 1.0.9 → 1.1.1
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/package.json +1 -2
- package/src/DatabaseManager.js +0 -565
- package/src/core/ConnectionManager.js +0 -351
- package/src/core/DatabaseFactory.js +0 -188
- package/src/core/MongoQueryBuilder.js +0 -576
- package/src/core/PluginManager.js +0 -968
- package/src/core/QueryBuilder.js +0 -4398
- package/src/core/TransactionManager.js +0 -40
- package/src/drivers/clickhouse-driver.js +0 -272
- package/src/drivers/index.js +0 -273
- package/src/drivers/mongodb-driver.js +0 -87
- package/src/drivers/mssql-driver.js +0 -117
- package/src/drivers/mysql-driver.js +0 -169
- package/src/drivers/oracle-driver.js +0 -101
- package/src/drivers/postgres-driver.js +0 -234
- package/src/drivers/redis-driver.js +0 -52
- package/src/drivers/sqlite-driver.js +0 -67
- package/src/middleware/connection-pool.js +0 -455
- package/src/middleware/performance-monitor.js +0 -652
- package/src/middleware/query-cache.js +0 -500
- package/src/middleware/query-logger.js +0 -262
- package/src/plugins/AuditPlugin.js +0 -447
- package/src/plugins/BasePlugin.js +0 -418
- package/src/plugins/BatchOperationPlugin.js +0 -165
- package/src/plugins/CachePlugin.js +0 -407
- package/src/plugins/CtePlugin.js +0 -523
- package/src/plugins/DistributedPlugin.js +0 -543
- package/src/plugins/EncryptionPlugin.js +0 -211
- package/src/plugins/FullTextSearchPlugin.js +0 -164
- package/src/plugins/GeospatialPlugin.js +0 -219
- package/src/plugins/GraphQLPlugin.js +0 -162
- package/src/plugins/HookPlugin.js +0 -211
- package/src/plugins/JsonPlugin.js +0 -366
- package/src/plugins/OptimisticLockPlugin.js +0 -374
- package/src/plugins/PerformancePlugin.js +0 -175
- package/src/plugins/ResiliencePlugin.js +0 -114
- package/src/plugins/ShardingPlugin.js +0 -227
- package/src/plugins/SoftDeletePlugin.js +0 -258
- package/src/plugins/SyncPlugin.js +0 -373
- package/src/plugins/VersioningPlugin.js +0 -314
- package/src/plugins/WindowFunctionPlugin.js +0 -343
- package/src/utils/config-loader.js +0 -632
- package/src/utils/error-handler.js +0 -724
- package/src/utils/migration-runner.js +0 -1066
|
@@ -1,1066 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/utils/migration-runner
|
|
6
|
-
*/
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import path from 'path';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Migration Runner - Handles database migrations
|
|
15
|
-
*/
|
|
16
|
-
class MigrationRunner {
|
|
17
|
-
constructor(database, options = {}) {
|
|
18
|
-
this.database = database;
|
|
19
|
-
this.options = {
|
|
20
|
-
migrationsTable: options.migrationsTable || 'migrations',
|
|
21
|
-
migrationsPath: options.migrationsPath || './migrations',
|
|
22
|
-
useTransactions: options.useTransactions !== false,
|
|
23
|
-
...options
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
this.migrations = [];
|
|
27
|
-
this.appliedMigrations = [];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Initialize migrations table
|
|
32
|
-
* @returns {Promise<void>}
|
|
33
|
-
*/
|
|
34
|
-
async init() {
|
|
35
|
-
try {
|
|
36
|
-
// Check if migrations table exists
|
|
37
|
-
const tableExists = await this.checkMigrationsTable();
|
|
38
|
-
|
|
39
|
-
if (!tableExists) {
|
|
40
|
-
await this.createMigrationsTable();
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Load applied migrations
|
|
45
|
-
await this.loadAppliedMigrations();
|
|
46
|
-
|
|
47
|
-
// Discover migration files
|
|
48
|
-
await this.discoverMigrations();
|
|
49
|
-
|
|
50
|
-
} catch (error) {
|
|
51
|
-
console.error('❌ Failed to initialize migration runner:', error.message);
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Check if migrations table exists
|
|
58
|
-
* @returns {Promise<boolean>} True if table exists
|
|
59
|
-
*/
|
|
60
|
-
async checkMigrationsTable() {
|
|
61
|
-
try {
|
|
62
|
-
const sql = `
|
|
63
|
-
SELECT EXISTS (
|
|
64
|
-
SELECT FROM information_schema.tables
|
|
65
|
-
WHERE table_name = '${this.options.migrationsTable}'
|
|
66
|
-
) as exists;
|
|
67
|
-
`;
|
|
68
|
-
|
|
69
|
-
const result = await this.database.query(sql);
|
|
70
|
-
return result.rows && result.rows.length > 0 && result.rows[0].exists;
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// Table doesn't exist or query failed
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Create migrations table
|
|
79
|
-
* @returns {Promise<void>}
|
|
80
|
-
*/
|
|
81
|
-
async createMigrationsTable() {
|
|
82
|
-
const sql = `
|
|
83
|
-
CREATE TABLE ${this.options.migrationsTable} (
|
|
84
|
-
id SERIAL PRIMARY KEY,
|
|
85
|
-
name VARCHAR(255) NOT NULL UNIQUE,
|
|
86
|
-
batch INTEGER NOT NULL,
|
|
87
|
-
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
88
|
-
execution_time INTEGER,
|
|
89
|
-
status VARCHAR(50) DEFAULT 'completed',
|
|
90
|
-
error_message TEXT
|
|
91
|
-
);
|
|
92
|
-
`;
|
|
93
|
-
|
|
94
|
-
await this.database.query(sql);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Load applied migrations from database
|
|
99
|
-
* @returns {Promise<void>}
|
|
100
|
-
*/
|
|
101
|
-
async loadAppliedMigrations() {
|
|
102
|
-
try {
|
|
103
|
-
const sql = `SELECT * FROM ${this.options.migrationsTable} ORDER BY id ASC`;
|
|
104
|
-
const result = await this.database.query(sql);
|
|
105
|
-
|
|
106
|
-
if (result.rows) {
|
|
107
|
-
this.appliedMigrations = result.rows;
|
|
108
|
-
}
|
|
109
|
-
} catch (error) {
|
|
110
|
-
console.warn('⚠️ Could not load applied migrations:', error.message);
|
|
111
|
-
this.appliedMigrations = [];
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Discover migration files
|
|
117
|
-
* @returns {Promise<void>}
|
|
118
|
-
*/
|
|
119
|
-
async discoverMigrations() {
|
|
120
|
-
try {
|
|
121
|
-
const migrationsPath = path.isAbsolute(this.options.migrationsPath)
|
|
122
|
-
? this.options.migrationsPath
|
|
123
|
-
: path.join(process.cwd(), this.options.migrationsPath);
|
|
124
|
-
|
|
125
|
-
if (!fs.existsSync(migrationsPath)) {
|
|
126
|
-
console.warn(`⚠️ Migrations directory not found: ${migrationsPath}`);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const files = fs.readdirSync(migrationsPath)
|
|
131
|
-
.filter(file => file.endsWith('.js') || file.endsWith('.sql'))
|
|
132
|
-
.sort();
|
|
133
|
-
|
|
134
|
-
for (const file of files) {
|
|
135
|
-
const migration = await this.parseMigrationFile(file, migrationsPath);
|
|
136
|
-
if (migration) {
|
|
137
|
-
this.migrations.push(migration);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
} catch (error) {
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Parse migration file
|
|
147
|
-
* @param {string} filename - Migration filename
|
|
148
|
-
* @param {string} migrationsPath - Migrations directory path
|
|
149
|
-
* @returns {Promise<Object|null>} Migration object or null
|
|
150
|
-
*/
|
|
151
|
-
async parseMigrationFile(filename, migrationsPath) {
|
|
152
|
-
const filePath = path.join(migrationsPath, filename);
|
|
153
|
-
const match = filename.match(/^(\d+)_(.+)\.(js|sql)$/);
|
|
154
|
-
|
|
155
|
-
if (!match) {
|
|
156
|
-
console.warn(`⚠️ Skipping invalid migration file: ${filename}`);
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const [, timestamp, name, extension] = match;
|
|
161
|
-
|
|
162
|
-
// Check if migration is already applied
|
|
163
|
-
const isApplied = this.appliedMigrations.some(m => m.name === filename);
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
filename,
|
|
167
|
-
name,
|
|
168
|
-
timestamp: parseInt(timestamp),
|
|
169
|
-
path: filePath,
|
|
170
|
-
extension,
|
|
171
|
-
isApplied,
|
|
172
|
-
appliedAt: isApplied
|
|
173
|
-
? this.appliedMigrations.find(m => m.name === filename)?.applied_at
|
|
174
|
-
: null,
|
|
175
|
-
status: isApplied
|
|
176
|
-
? this.appliedMigrations.find(m => m.name === filename)?.status || 'completed'
|
|
177
|
-
: 'pending'
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Get pending migrations
|
|
183
|
-
* @returns {Array} Pending migrations
|
|
184
|
-
*/
|
|
185
|
-
getPendingMigrations() {
|
|
186
|
-
return this.migrations.filter(m => !m.isApplied);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Get applied migrations
|
|
191
|
-
* @returns {Array} Applied migrations
|
|
192
|
-
*/
|
|
193
|
-
getAppliedMigrations() {
|
|
194
|
-
return this.migrations.filter(m => m.isApplied);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Get migration status
|
|
199
|
-
* @returns {Object} Migration status
|
|
200
|
-
*/
|
|
201
|
-
getStatus() {
|
|
202
|
-
const pending = this.getPendingMigrations();
|
|
203
|
-
const applied = this.getAppliedMigrations();
|
|
204
|
-
|
|
205
|
-
return {
|
|
206
|
-
total: this.migrations.length,
|
|
207
|
-
pending: pending.length,
|
|
208
|
-
applied: applied.length,
|
|
209
|
-
pendingMigrations: pending.map(m => ({
|
|
210
|
-
name: m.name,
|
|
211
|
-
filename: m.filename,
|
|
212
|
-
timestamp: m.timestamp
|
|
213
|
-
})),
|
|
214
|
-
appliedMigrations: applied.map(m => ({
|
|
215
|
-
name: m.name,
|
|
216
|
-
filename: m.filename,
|
|
217
|
-
timestamp: m.timestamp,
|
|
218
|
-
appliedAt: m.appliedAt,
|
|
219
|
-
status: m.status
|
|
220
|
-
})),
|
|
221
|
-
lastApplied: applied.length > 0 ? applied[applied.length - 1] : null,
|
|
222
|
-
nextPending: pending.length > 0 ? pending[0] : null
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Run migrations
|
|
228
|
-
* @param {number} limit - Maximum number of migrations to run
|
|
229
|
-
* @returns {Promise<Object>} Migration results
|
|
230
|
-
*/
|
|
231
|
-
async runMigrations(limit = null) {
|
|
232
|
-
const pendingMigrations = this.getPendingMigrations();
|
|
233
|
-
const migrationsToRun = limit
|
|
234
|
-
? pendingMigrations.slice(0, limit)
|
|
235
|
-
: pendingMigrations;
|
|
236
|
-
|
|
237
|
-
if (migrationsToRun.length === 0) {
|
|
238
|
-
return {
|
|
239
|
-
success: true,
|
|
240
|
-
message: 'No pending migrations to run',
|
|
241
|
-
migrationsRun: 0,
|
|
242
|
-
details: []
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const results = [];
|
|
247
|
-
let batch = 1;
|
|
248
|
-
|
|
249
|
-
// Get current batch number
|
|
250
|
-
if (this.appliedMigrations.length > 0) {
|
|
251
|
-
const lastBatch = Math.max(...this.appliedMigrations.map(m => m.batch));
|
|
252
|
-
batch = lastBatch + 1;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
for (const migration of migrationsToRun) {
|
|
256
|
-
const startTime = Date.now();
|
|
257
|
-
|
|
258
|
-
try {
|
|
259
|
-
|
|
260
|
-
if (this.options.useTransactions) {
|
|
261
|
-
await this.database.transaction(async (trx) => {
|
|
262
|
-
await this.executeMigration(migration, trx);
|
|
263
|
-
});
|
|
264
|
-
} else {
|
|
265
|
-
await this.executeMigration(migration, this.database);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const executionTime = Date.now() - startTime;
|
|
269
|
-
|
|
270
|
-
// Record migration as applied
|
|
271
|
-
await this.recordMigration(migration, batch, executionTime, 'completed');
|
|
272
|
-
|
|
273
|
-
migration.isApplied = true;
|
|
274
|
-
migration.appliedAt = new Date().toISOString();
|
|
275
|
-
migration.status = 'completed';
|
|
276
|
-
|
|
277
|
-
results.push({
|
|
278
|
-
migration: migration.filename,
|
|
279
|
-
status: 'completed',
|
|
280
|
-
executionTime,
|
|
281
|
-
message: 'Migration completed successfully'
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
} catch (error) {
|
|
285
|
-
const executionTime = Date.now() - startTime;
|
|
286
|
-
|
|
287
|
-
// Record migration as failed
|
|
288
|
-
await this.recordMigration(migration, batch, executionTime, 'failed', error.message);
|
|
289
|
-
|
|
290
|
-
results.push({
|
|
291
|
-
migration: migration.filename,
|
|
292
|
-
status: 'failed',
|
|
293
|
-
executionTime,
|
|
294
|
-
error: error.message,
|
|
295
|
-
message: 'Migration failed'
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// If using transactions, stop on first failure
|
|
299
|
-
if (this.options.useTransactions) {
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// Reload applied migrations
|
|
306
|
-
await this.loadAppliedMigrations();
|
|
307
|
-
|
|
308
|
-
const success = results.every(r => r.status === 'completed');
|
|
309
|
-
|
|
310
|
-
return {
|
|
311
|
-
success,
|
|
312
|
-
message: success
|
|
313
|
-
? `Successfully ran ${results.length} migration(s)`
|
|
314
|
-
: 'Some migrations failed',
|
|
315
|
-
migrationsRun: results.length,
|
|
316
|
-
successful: results.filter(r => r.status === 'completed').length,
|
|
317
|
-
failed: results.filter(r => r.status === 'failed').length,
|
|
318
|
-
details: results
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Execute migration
|
|
324
|
-
* @param {Object} migration - Migration object
|
|
325
|
-
* @param {Object} connection - Database connection
|
|
326
|
-
* @returns {Promise<void>}
|
|
327
|
-
*/
|
|
328
|
-
async executeMigration(migration, connection) {
|
|
329
|
-
if (migration.extension === 'sql') {
|
|
330
|
-
// SQL file migration
|
|
331
|
-
const sql = fs.readFileSync(migration.path, 'utf8');
|
|
332
|
-
await connection.query(sql);
|
|
333
|
-
} else if (migration.extension === 'js') {
|
|
334
|
-
// JavaScript migration
|
|
335
|
-
const migrationModule = await import(`file://${migration.path}`);
|
|
336
|
-
|
|
337
|
-
if (typeof migrationModule.up === 'function') {
|
|
338
|
-
await migrationModule.up(connection);
|
|
339
|
-
} else {
|
|
340
|
-
throw new Error(`Migration ${migration.filename} does not export an 'up' function`);
|
|
341
|
-
}
|
|
342
|
-
} else {
|
|
343
|
-
throw new Error(`Unsupported migration type: ${migration.extension}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Record migration in database
|
|
349
|
-
* @param {Object} migration - Migration object
|
|
350
|
-
* @param {number} batch - Batch number
|
|
351
|
-
* @param {number} executionTime - Execution time in ms
|
|
352
|
-
* @param {string} status - Migration status
|
|
353
|
-
* @param {string} errorMessage - Error message (if failed)
|
|
354
|
-
* @returns {Promise<void>}
|
|
355
|
-
*/
|
|
356
|
-
async recordMigration(migration, batch, executionTime, status = 'completed', errorMessage = null) {
|
|
357
|
-
const sql = `
|
|
358
|
-
INSERT INTO ${this.options.migrationsTable}
|
|
359
|
-
(name, batch, execution_time, status, error_message)
|
|
360
|
-
VALUES ($1, $2, $3, $4, $5)
|
|
361
|
-
`;
|
|
362
|
-
|
|
363
|
-
await this.database.query(sql, [
|
|
364
|
-
migration.filename,
|
|
365
|
-
batch,
|
|
366
|
-
executionTime,
|
|
367
|
-
status,
|
|
368
|
-
errorMessage
|
|
369
|
-
]);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Rollback migrations
|
|
374
|
-
* @param {number} steps - Number of migrations to rollback
|
|
375
|
-
* @returns {Promise<Object>} Rollback results
|
|
376
|
-
*/
|
|
377
|
-
async rollbackMigrations(steps = 1) {
|
|
378
|
-
const appliedMigrations = this.getAppliedMigrations();
|
|
379
|
-
const migrationsToRollback = appliedMigrations.slice(-steps).reverse();
|
|
380
|
-
|
|
381
|
-
if (migrationsToRollback.length === 0) {
|
|
382
|
-
return {
|
|
383
|
-
success: true,
|
|
384
|
-
message: 'No migrations to rollback',
|
|
385
|
-
migrationsRolledBack: 0,
|
|
386
|
-
details: []
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const results = [];
|
|
391
|
-
|
|
392
|
-
for (const migration of migrationsToRollback) {
|
|
393
|
-
const startTime = Date.now();
|
|
394
|
-
|
|
395
|
-
try {
|
|
396
|
-
|
|
397
|
-
if (this.options.useTransactions) {
|
|
398
|
-
await this.database.transaction(async (trx) => {
|
|
399
|
-
await this.executeRollback(migration, trx);
|
|
400
|
-
});
|
|
401
|
-
} else {
|
|
402
|
-
await this.executeRollback(migration, this.database);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const executionTime = Date.now() - startTime;
|
|
406
|
-
|
|
407
|
-
// Remove migration record
|
|
408
|
-
await this.removeMigrationRecord(migration);
|
|
409
|
-
|
|
410
|
-
migration.isApplied = false;
|
|
411
|
-
migration.appliedAt = null;
|
|
412
|
-
migration.status = 'rolled back';
|
|
413
|
-
|
|
414
|
-
results.push({
|
|
415
|
-
migration: migration.filename,
|
|
416
|
-
status: 'rolled back',
|
|
417
|
-
executionTime,
|
|
418
|
-
message: 'Migration rolled back successfully'
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
} catch (error) {
|
|
422
|
-
const executionTime = Date.now() - startTime;
|
|
423
|
-
|
|
424
|
-
results.push({
|
|
425
|
-
migration: migration.filename,
|
|
426
|
-
status: 'failed',
|
|
427
|
-
executionTime,
|
|
428
|
-
error: error.message,
|
|
429
|
-
message: 'Rollback failed'
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
console.error(`❌ Rollback failed: ${migration.filename}`, error.message);
|
|
433
|
-
|
|
434
|
-
// If using transactions, stop on first failure
|
|
435
|
-
if (this.options.useTransactions) {
|
|
436
|
-
break;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Reload applied migrations
|
|
442
|
-
await this.loadAppliedMigrations();
|
|
443
|
-
|
|
444
|
-
const success = results.every(r => r.status === 'rolled back');
|
|
445
|
-
|
|
446
|
-
return {
|
|
447
|
-
success,
|
|
448
|
-
message: success
|
|
449
|
-
? `Successfully rolled back ${results.length} migration(s)`
|
|
450
|
-
: 'Some rollbacks failed',
|
|
451
|
-
migrationsRolledBack: results.filter(r => r.status === 'rolled back').length,
|
|
452
|
-
failed: results.filter(r => r.status === 'failed').length,
|
|
453
|
-
details: results
|
|
454
|
-
};
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Execute rollback
|
|
458
|
-
* @param {Object} migration - Migration object
|
|
459
|
-
* @param {Object} connection - Database connection
|
|
460
|
-
* @returns {Promise<void>}
|
|
461
|
-
*/
|
|
462
|
-
async executeRollback(migration, connection) {
|
|
463
|
-
if (migration.extension === 'sql') {
|
|
464
|
-
// For SQL files, we need to parse and execute down statements
|
|
465
|
-
// This requires migration files to have -- DOWN comment section
|
|
466
|
-
const sql = fs.readFileSync(migration.path, 'utf8');
|
|
467
|
-
const downSql = this.extractDownSQL(sql);
|
|
468
|
-
|
|
469
|
-
if (!downSql) {
|
|
470
|
-
throw new Error(`Migration ${migration.filename} does not contain DOWN section`);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
await connection.query(downSql);
|
|
474
|
-
} else if (migration.extension === 'js') {
|
|
475
|
-
// JavaScript migration
|
|
476
|
-
const migrationModule = await import(`file://${migration.path}`);
|
|
477
|
-
|
|
478
|
-
if (typeof migrationModule.down === 'function') {
|
|
479
|
-
await migrationModule.down(connection);
|
|
480
|
-
} else {
|
|
481
|
-
throw new Error(`Migration ${migration.filename} does not export a 'down' function`);
|
|
482
|
-
}
|
|
483
|
-
} else {
|
|
484
|
-
throw new Error(`Unsupported migration type: ${migration.extension}`);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* Extract DOWN SQL from migration file
|
|
490
|
-
* @param {string} sql - Full SQL content
|
|
491
|
-
* @returns {string|null} DOWN SQL or null
|
|
492
|
-
*/
|
|
493
|
-
extractDownSQL(sql) {
|
|
494
|
-
const lines = sql.split('\n');
|
|
495
|
-
let inDownSection = false;
|
|
496
|
-
let downSql = [];
|
|
497
|
-
|
|
498
|
-
for (const line of lines) {
|
|
499
|
-
if (line.trim().toUpperCase().startsWith('-- DOWN')) {
|
|
500
|
-
inDownSection = true;
|
|
501
|
-
continue;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
if (line.trim().toUpperCase().startsWith('-- UP') && inDownSection) {
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (inDownSection && !line.trim().startsWith('--')) {
|
|
509
|
-
downSql.push(line);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
return downSql.length > 0 ? downSql.join('\n').trim() : null;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Remove migration record from database
|
|
518
|
-
* @param {Object} migration - Migration object
|
|
519
|
-
* @returns {Promise<void>}
|
|
520
|
-
*/
|
|
521
|
-
async removeMigrationRecord(migration) {
|
|
522
|
-
const sql = `DELETE FROM ${this.options.migrationsTable} WHERE name = $1`;
|
|
523
|
-
await this.database.query(sql, [migration.filename]);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Create migration file
|
|
528
|
-
* @param {string} name - Migration name
|
|
529
|
-
* @param {string} type - Migration type (sql or js)
|
|
530
|
-
* @returns {Promise<string>} Created migration file path
|
|
531
|
-
*/
|
|
532
|
-
async createMigration(name, type = 'js') {
|
|
533
|
-
const timestamp = Date.now();
|
|
534
|
-
const filename = `${timestamp}_${name}.${type}`;
|
|
535
|
-
const migrationsPath = path.isAbsolute(this.options.migrationsPath)
|
|
536
|
-
? this.options.migrationsPath
|
|
537
|
-
: path.join(process.cwd(), this.options.migrationsPath);
|
|
538
|
-
|
|
539
|
-
// Create migrations directory if it doesn't exist
|
|
540
|
-
if (!fs.existsSync(migrationsPath)) {
|
|
541
|
-
fs.mkdirSync(migrationsPath, { recursive: true });
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const filePath = path.join(migrationsPath, filename);
|
|
545
|
-
|
|
546
|
-
if (type === 'sql') {
|
|
547
|
-
await this.createSQLMigration(filePath, name);
|
|
548
|
-
} else if (type === 'js') {
|
|
549
|
-
await this.createJSMigration(filePath, name);
|
|
550
|
-
} else {
|
|
551
|
-
throw new Error(`Unsupported migration type: ${type}`);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return filePath;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Create SQL migration file
|
|
559
|
-
* @param {string} filePath - File path
|
|
560
|
-
* @param {string} name - Migration name
|
|
561
|
-
* @returns {Promise<void>}
|
|
562
|
-
*/
|
|
563
|
-
async createSQLMigration(filePath, name) {
|
|
564
|
-
const template = `-- Migration: ${name}
|
|
565
|
-
-- Created at: ${new Date().toISOString()}
|
|
566
|
-
|
|
567
|
-
-- UP: Apply migration
|
|
568
|
-
-- Add your SQL statements here
|
|
569
|
-
-- Example:
|
|
570
|
-
-- CREATE TABLE users (
|
|
571
|
-
-- id SERIAL PRIMARY KEY,
|
|
572
|
-
-- name VARCHAR(100) NOT NULL,
|
|
573
|
-
-- email VARCHAR(255) UNIQUE NOT NULL,
|
|
574
|
-
-- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
575
|
-
-- );
|
|
576
|
-
|
|
577
|
-
-- DOWN: Rollback migration
|
|
578
|
-
-- Add your rollback SQL statements here
|
|
579
|
-
-- Example:
|
|
580
|
-
-- DROP TABLE IF EXISTS users;
|
|
581
|
-
`;
|
|
582
|
-
|
|
583
|
-
fs.writeFileSync(filePath, template, 'utf8');
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Create JavaScript migration file
|
|
588
|
-
* @param {string} filePath - File path
|
|
589
|
-
* @param {string} name - Migration name
|
|
590
|
-
* @returns {Promise<void>}
|
|
591
|
-
*/
|
|
592
|
-
async createJSMigration(filePath, name) {
|
|
593
|
-
const template = `/**
|
|
594
|
-
* Migration: ${name}
|
|
595
|
-
* Created at: ${new Date().toISOString()}
|
|
596
|
-
*/
|
|
597
|
-
|
|
598
|
-
/**
|
|
599
|
-
* Apply migration
|
|
600
|
-
* @param {Object} db - Database connection
|
|
601
|
-
* @returns {Promise<void>}
|
|
602
|
-
*/
|
|
603
|
-
export async function up(db) {
|
|
604
|
-
// Add your migration logic here
|
|
605
|
-
// Example:
|
|
606
|
-
// await db.query(\`
|
|
607
|
-
// CREATE TABLE users (
|
|
608
|
-
// id SERIAL PRIMARY KEY,
|
|
609
|
-
// name VARCHAR(100) NOT NULL,
|
|
610
|
-
// email VARCHAR(255) UNIQUE NOT NULL,
|
|
611
|
-
// created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
612
|
-
// )
|
|
613
|
-
// \`);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
/**
|
|
617
|
-
* Rollback migration
|
|
618
|
-
* @param {Object} db - Database connection
|
|
619
|
-
* @returns {Promise<void>}
|
|
620
|
-
*/
|
|
621
|
-
export async function down(db) {
|
|
622
|
-
// Add your rollback logic here
|
|
623
|
-
// Example:
|
|
624
|
-
// await db.query('DROP TABLE IF EXISTS users');
|
|
625
|
-
}
|
|
626
|
-
`;
|
|
627
|
-
|
|
628
|
-
fs.writeFileSync(filePath, template, 'utf8');
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/**
|
|
632
|
-
* Reset all migrations (development only)
|
|
633
|
-
* @returns {Promise<Object>} Reset results
|
|
634
|
-
*/
|
|
635
|
-
async resetMigrations() {
|
|
636
|
-
console.warn('⚠️ WARNING: This will remove all migration records from the database!');
|
|
637
|
-
console.warn('⚠️ This operation is irreversible and should only be used in development.');
|
|
638
|
-
|
|
639
|
-
try {
|
|
640
|
-
const sql = `DROP TABLE IF EXISTS ${this.options.migrationsTable}`;
|
|
641
|
-
await this.database.query(sql);
|
|
642
|
-
|
|
643
|
-
// Recreate migrations table
|
|
644
|
-
await this.createMigrationsTable();
|
|
645
|
-
|
|
646
|
-
// Reload migrations
|
|
647
|
-
await this.discoverMigrations();
|
|
648
|
-
this.appliedMigrations = [];
|
|
649
|
-
|
|
650
|
-
return {
|
|
651
|
-
success: true,
|
|
652
|
-
message: 'Migrations reset successfully',
|
|
653
|
-
migrationsTable: this.options.migrationsTable,
|
|
654
|
-
resetAt: new Date().toISOString()
|
|
655
|
-
};
|
|
656
|
-
} catch (error) {
|
|
657
|
-
console.error('❌ Failed to reset migrations:', error.message);
|
|
658
|
-
throw error;
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
/**
|
|
663
|
-
* Get migration history
|
|
664
|
-
* @param {Object} options - Query options
|
|
665
|
-
* @returns {Promise<Array>} Migration history
|
|
666
|
-
*/
|
|
667
|
-
async getMigrationHistory(options = {}) {
|
|
668
|
-
const { limit = 50, offset = 0, status = null, batch = null } = options;
|
|
669
|
-
|
|
670
|
-
let sql = `SELECT * FROM ${this.options.migrationsTable}`;
|
|
671
|
-
const params = [];
|
|
672
|
-
const conditions = [];
|
|
673
|
-
|
|
674
|
-
if (status) {
|
|
675
|
-
conditions.push('status = $' + (params.length + 1));
|
|
676
|
-
params.push(status);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
if (batch) {
|
|
680
|
-
conditions.push('batch = $' + (params.length + 1));
|
|
681
|
-
params.push(batch);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
if (conditions.length > 0) {
|
|
685
|
-
sql += ' WHERE ' + conditions.join(' AND ');
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
sql += ' ORDER BY id DESC LIMIT $' + (params.length + 1) + ' OFFSET $' + (params.length + 2);
|
|
689
|
-
params.push(limit, offset);
|
|
690
|
-
|
|
691
|
-
const result = await this.database.query(sql, params);
|
|
692
|
-
return result.rows || [];
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
/**
|
|
696
|
-
* Get migration batches
|
|
697
|
-
* @returns {Promise<Array>} Migration batches
|
|
698
|
-
*/
|
|
699
|
-
async getMigrationBatches() {
|
|
700
|
-
const sql = `
|
|
701
|
-
SELECT
|
|
702
|
-
batch,
|
|
703
|
-
COUNT(*) as migration_count,
|
|
704
|
-
MIN(applied_at) as started_at,
|
|
705
|
-
MAX(applied_at) as completed_at,
|
|
706
|
-
SUM(execution_time) as total_execution_time,
|
|
707
|
-
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_count
|
|
708
|
-
FROM ${this.options.migrationsTable}
|
|
709
|
-
GROUP BY batch
|
|
710
|
-
ORDER BY batch DESC
|
|
711
|
-
`;
|
|
712
|
-
|
|
713
|
-
const result = await this.database.query(sql);
|
|
714
|
-
return result.rows || [];
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
/**
|
|
718
|
-
* Get migration statistics
|
|
719
|
-
* @returns {Promise<Object>} Migration statistics
|
|
720
|
-
*/
|
|
721
|
-
async getMigrationStats() {
|
|
722
|
-
const sql = `
|
|
723
|
-
SELECT
|
|
724
|
-
COUNT(*) as total_migrations,
|
|
725
|
-
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed_migrations,
|
|
726
|
-
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_migrations,
|
|
727
|
-
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending_migrations,
|
|
728
|
-
COUNT(DISTINCT batch) as total_batches,
|
|
729
|
-
AVG(execution_time) as avg_execution_time,
|
|
730
|
-
MAX(execution_time) as max_execution_time,
|
|
731
|
-
MIN(execution_time) as min_execution_time,
|
|
732
|
-
SUM(execution_time) as total_execution_time
|
|
733
|
-
FROM ${this.options.migrationsTable}
|
|
734
|
-
`;
|
|
735
|
-
|
|
736
|
-
const result = await this.database.query(sql);
|
|
737
|
-
const stats = result.rows?.[0] || {};
|
|
738
|
-
|
|
739
|
-
// Add pending migrations from discovered files
|
|
740
|
-
const pendingMigrations = this.getPendingMigrations();
|
|
741
|
-
stats.pending_migrations = pendingMigrations.length;
|
|
742
|
-
stats.total_discovered = this.migrations.length;
|
|
743
|
-
|
|
744
|
-
return stats;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
/**
|
|
748
|
-
* Validate migration files
|
|
749
|
-
* @returns {Promise<Object>} Validation results
|
|
750
|
-
*/
|
|
751
|
-
async validateMigrations() {
|
|
752
|
-
const issues = [];
|
|
753
|
-
const warnings = [];
|
|
754
|
-
|
|
755
|
-
for (const migration of this.migrations) {
|
|
756
|
-
// Check file exists
|
|
757
|
-
if (!fs.existsSync(migration.path)) {
|
|
758
|
-
issues.push({
|
|
759
|
-
migration: migration.filename,
|
|
760
|
-
type: 'file_not_found',
|
|
761
|
-
message: 'Migration file not found',
|
|
762
|
-
severity: 'error'
|
|
763
|
-
});
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
// Check file format
|
|
768
|
-
if (migration.extension === 'sql') {
|
|
769
|
-
const content = fs.readFileSync(migration.path, 'utf8');
|
|
770
|
-
|
|
771
|
-
// Check for UP section
|
|
772
|
-
if (!content.includes('-- UP')) {
|
|
773
|
-
warnings.push({
|
|
774
|
-
migration: migration.filename,
|
|
775
|
-
type: 'missing_up_section',
|
|
776
|
-
message: 'SQL migration missing -- UP section',
|
|
777
|
-
severity: 'warning'
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Check for DOWN section
|
|
782
|
-
if (!content.includes('-- DOWN')) {
|
|
783
|
-
warnings.push({
|
|
784
|
-
migration: migration.filename,
|
|
785
|
-
type: 'missing_down_section',
|
|
786
|
-
message: 'SQL migration missing -- DOWN section',
|
|
787
|
-
severity: 'warning'
|
|
788
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
// Check for valid SQL syntax (basic check)
|
|
792
|
-
const sqlLines = content.split('\n').filter(line =>
|
|
793
|
-
!line.trim().startsWith('--') && line.trim().length > 0
|
|
794
|
-
);
|
|
795
|
-
|
|
796
|
-
if (sqlLines.length === 0) {
|
|
797
|
-
warnings.push({
|
|
798
|
-
migration: migration.filename,
|
|
799
|
-
type: 'empty_migration',
|
|
800
|
-
message: 'SQL migration appears to be empty',
|
|
801
|
-
severity: 'warning'
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
} else if (migration.extension === 'js') {
|
|
805
|
-
try {
|
|
806
|
-
const migrationModule = await import(`file://${migration.path}`);
|
|
807
|
-
|
|
808
|
-
// Check for up function
|
|
809
|
-
if (typeof migrationModule.up !== 'function') {
|
|
810
|
-
issues.push({
|
|
811
|
-
migration: migration.filename,
|
|
812
|
-
type: 'missing_up_function',
|
|
813
|
-
message: 'JavaScript migration missing export.up function',
|
|
814
|
-
severity: 'error'
|
|
815
|
-
});
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
// Check for down function
|
|
819
|
-
if (typeof migrationModule.down !== 'function') {
|
|
820
|
-
warnings.push({
|
|
821
|
-
migration: migration.filename,
|
|
822
|
-
type: 'missing_down_function',
|
|
823
|
-
message: 'JavaScript migration missing export.down function',
|
|
824
|
-
severity: 'warning'
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
} catch (error) {
|
|
828
|
-
issues.push({
|
|
829
|
-
migration: migration.filename,
|
|
830
|
-
type: 'module_error',
|
|
831
|
-
message: `Failed to load migration module: ${error.message}`,
|
|
832
|
-
severity: 'error'
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
// Check for duplicate timestamps
|
|
839
|
-
const timestamps = this.migrations.map(m => m.timestamp);
|
|
840
|
-
const duplicateTimestamps = timestamps.filter((t, i) => timestamps.indexOf(t) !== i);
|
|
841
|
-
|
|
842
|
-
if (duplicateTimestamps.length > 0) {
|
|
843
|
-
issues.push({
|
|
844
|
-
type: 'duplicate_timestamps',
|
|
845
|
-
message: `Duplicate migration timestamps found: ${duplicateTimestamps.join(', ')}`,
|
|
846
|
-
severity: 'error'
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
// Check for gaps in applied migrations
|
|
851
|
-
const appliedTimestamps = this.appliedMigrations
|
|
852
|
-
.map(m => parseInt(m.name.split('_')[0]))
|
|
853
|
-
.sort((a, b) => a - b);
|
|
854
|
-
|
|
855
|
-
if (appliedTimestamps.length > 1) {
|
|
856
|
-
for (let i = 1; i < appliedTimestamps.length; i++) {
|
|
857
|
-
if (appliedTimestamps[i] - appliedTimestamps[i-1] > 1) {
|
|
858
|
-
warnings.push({
|
|
859
|
-
type: 'timestamp_gap',
|
|
860
|
-
message: `Gap in applied migration timestamps: ${appliedTimestamps[i-1]} -> ${appliedTimestamps[i]}`,
|
|
861
|
-
severity: 'warning'
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return {
|
|
868
|
-
valid: issues.length === 0,
|
|
869
|
-
issues,
|
|
870
|
-
warnings,
|
|
871
|
-
migrationCount: this.migrations.length,
|
|
872
|
-
appliedCount: this.appliedMigrations.length,
|
|
873
|
-
pendingCount: this.getPendingMigrations().length
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
/**
|
|
878
|
-
* Generate migration report
|
|
879
|
-
* @returns {Promise<Object>} Migration report
|
|
880
|
-
*/
|
|
881
|
-
async generateReport() {
|
|
882
|
-
const status = this.getStatus();
|
|
883
|
-
const stats = await this.getMigrationStats();
|
|
884
|
-
const batches = await this.getMigrationBatches();
|
|
885
|
-
const validation = await this.validateMigrations();
|
|
886
|
-
|
|
887
|
-
return {
|
|
888
|
-
timestamp: new Date().toISOString(),
|
|
889
|
-
status,
|
|
890
|
-
stats,
|
|
891
|
-
batches,
|
|
892
|
-
validation,
|
|
893
|
-
recommendations: this.generateRecommendations(status, stats, validation)
|
|
894
|
-
};
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
/**
|
|
898
|
-
* Generate recommendations based on migration status
|
|
899
|
-
* @param {Object} status - Migration status
|
|
900
|
-
* @param {Object} stats - Migration statistics
|
|
901
|
-
* @param {Object} validation - Validation results
|
|
902
|
-
* @returns {Array} Recommendations
|
|
903
|
-
*/
|
|
904
|
-
generateRecommendations(status, stats, validation) {
|
|
905
|
-
const recommendations = [];
|
|
906
|
-
|
|
907
|
-
// Check for pending migrations
|
|
908
|
-
if (status.pending > 0) {
|
|
909
|
-
recommendations.push({
|
|
910
|
-
type: 'pending_migrations',
|
|
911
|
-
message: `There are ${status.pending} pending migrations. Run them with runMigrations().`,
|
|
912
|
-
priority: 'high'
|
|
913
|
-
});
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
// Check for failed migrations
|
|
917
|
-
if (stats.failed_migrations > 0) {
|
|
918
|
-
recommendations.push({
|
|
919
|
-
type: 'failed_migrations',
|
|
920
|
-
message: `There are ${stats.failed_migrations} failed migrations. Review and fix them.`,
|
|
921
|
-
priority: 'high'
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
// Check for validation issues
|
|
926
|
-
if (!validation.valid) {
|
|
927
|
-
recommendations.push({
|
|
928
|
-
type: 'validation_issues',
|
|
929
|
-
message: `There are ${validation.issues.length} validation issues that need to be fixed.`,
|
|
930
|
-
priority: 'high'
|
|
931
|
-
});
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
// Check for validation warnings
|
|
935
|
-
if (validation.warnings.length > 0) {
|
|
936
|
-
recommendations.push({
|
|
937
|
-
type: 'validation_warnings',
|
|
938
|
-
message: `There are ${validation.warnings.length} validation warnings to review.`,
|
|
939
|
-
priority: 'medium'
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// Check for old migrations
|
|
944
|
-
if (status.applied > 0 && status.applied > 10) {
|
|
945
|
-
recommendations.push({
|
|
946
|
-
type: 'many_migrations',
|
|
947
|
-
message: `There are ${status.applied} applied migrations. Consider consolidating old migrations.`,
|
|
948
|
-
priority: 'low'
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
// Check for migration performance
|
|
953
|
-
if (stats.avg_execution_time > 1000) {
|
|
954
|
-
recommendations.push({
|
|
955
|
-
type: 'slow_migrations',
|
|
956
|
-
message: `Average migration execution time is ${stats.avg_execution_time.toFixed(2)}ms. Consider optimizing slow migrations.`,
|
|
957
|
-
priority: 'medium'
|
|
958
|
-
});
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
return recommendations;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
/**
|
|
965
|
-
* Export migration data
|
|
966
|
-
* @param {string} format - Export format (json, csv)
|
|
967
|
-
* @returns {Promise<string>} Exported data
|
|
968
|
-
*/
|
|
969
|
-
async export(format = 'json') {
|
|
970
|
-
const report = await this.generateReport();
|
|
971
|
-
|
|
972
|
-
switch (format.toLowerCase()) {
|
|
973
|
-
case 'csv':
|
|
974
|
-
return this.exportToCSV(report);
|
|
975
|
-
case 'json':
|
|
976
|
-
default:
|
|
977
|
-
return JSON.stringify(report, null, 2);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
/**
|
|
982
|
-
* Export to CSV
|
|
983
|
-
* @param {Object} report - Migration report
|
|
984
|
-
* @returns {string} CSV data
|
|
985
|
-
*/
|
|
986
|
-
exportToCSV(report) {
|
|
987
|
-
const csvLines = [];
|
|
988
|
-
|
|
989
|
-
// Export migration status
|
|
990
|
-
csvLines.push('=== MIGRATION STATUS ===');
|
|
991
|
-
csvLines.push('Metric,Value');
|
|
992
|
-
csvLines.push(`Total Migrations,${report.status.total}`);
|
|
993
|
-
csvLines.push(`Applied Migrations,${report.status.applied}`);
|
|
994
|
-
csvLines.push(`Pending Migrations,${report.status.pending}`);
|
|
995
|
-
|
|
996
|
-
// Export migration statistics
|
|
997
|
-
csvLines.push('\n=== MIGRATION STATISTICS ===');
|
|
998
|
-
csvLines.push('Metric,Value');
|
|
999
|
-
csvLines.push(`Total Migrations,${report.stats.total_migrations || 0}`);
|
|
1000
|
-
csvLines.push(`Completed Migrations,${report.stats.completed_migrations || 0}`);
|
|
1001
|
-
csvLines.push(`Failed Migrations,${report.stats.failed_migrations || 0}`);
|
|
1002
|
-
csvLines.push(`Pending Migrations,${report.stats.pending_migrations || 0}`);
|
|
1003
|
-
csvLines.push(`Total Batches,${report.stats.total_batches || 0}`);
|
|
1004
|
-
csvLines.push(`Average Execution Time,${report.stats.avg_execution_time || 0}`);
|
|
1005
|
-
csvLines.push(`Max Execution Time,${report.stats.max_execution_time || 0}`);
|
|
1006
|
-
csvLines.push(`Min Execution Time,${report.stats.min_execution_time || 0}`);
|
|
1007
|
-
csvLines.push(`Total Execution Time,${report.stats.total_execution_time || 0}`);
|
|
1008
|
-
|
|
1009
|
-
// Export applied migrations
|
|
1010
|
-
if (report.status.appliedMigrations.length > 0) {
|
|
1011
|
-
csvLines.push('\n=== APPLIED MIGRATIONS ===');
|
|
1012
|
-
const headers = Object.keys(report.status.appliedMigrations[0]).join(',');
|
|
1013
|
-
csvLines.push(headers);
|
|
1014
|
-
report.status.appliedMigrations.forEach(migration => {
|
|
1015
|
-
const values = Object.values(migration).map(v =>
|
|
1016
|
-
typeof v === 'string' ? `"${v.replace(/"/g, '""')}"` : v
|
|
1017
|
-
).join(',');
|
|
1018
|
-
csvLines.push(values);
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Export pending migrations
|
|
1023
|
-
if (report.status.pendingMigrations.length > 0) {
|
|
1024
|
-
csvLines.push('\n=== PENDING MIGRATIONS ===');
|
|
1025
|
-
const headers = Object.keys(report.status.pendingMigrations[0]).join(',');
|
|
1026
|
-
csvLines.push(headers);
|
|
1027
|
-
report.status.pendingMigrations.forEach(migration => {
|
|
1028
|
-
const values = Object.values(migration).map(v =>
|
|
1029
|
-
typeof v === 'string' ? `"${v.replace(/"/g, '""')}"` : v
|
|
1030
|
-
).join(',');
|
|
1031
|
-
csvLines.push(values);
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Export validation issues
|
|
1036
|
-
if (report.validation.issues.length > 0) {
|
|
1037
|
-
csvLines.push('\n=== VALIDATION ISSUES ===');
|
|
1038
|
-
csvLines.push('Migration,Type,Message,Severity');
|
|
1039
|
-
report.validation.issues.forEach(issue => {
|
|
1040
|
-
csvLines.push(`${issue.migration || 'N/A'},${issue.type},${issue.message},${issue.severity}`);
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Export validation warnings
|
|
1045
|
-
if (report.validation.warnings.length > 0) {
|
|
1046
|
-
csvLines.push('\n=== VALIDATION WARNINGS ===');
|
|
1047
|
-
csvLines.push('Migration,Type,Message,Severity');
|
|
1048
|
-
report.validation.warnings.forEach(warning => {
|
|
1049
|
-
csvLines.push(`${warning.migration || 'N/A'},${warning.type},${warning.message},${warning.severity}`);
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
// Export recommendations
|
|
1054
|
-
if (report.recommendations.length > 0) {
|
|
1055
|
-
csvLines.push('\n=== RECOMMENDATIONS ===');
|
|
1056
|
-
csvLines.push('Type,Message,Priority');
|
|
1057
|
-
report.recommendations.forEach(rec => {
|
|
1058
|
-
csvLines.push(`${rec.type},${rec.message},${rec.priority}`);
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
return csvLines.join('\n');
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
export default MigrationRunner;
|