@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.
Files changed (44) hide show
  1. package/package.json +1 -2
  2. package/src/DatabaseManager.js +0 -565
  3. package/src/core/ConnectionManager.js +0 -351
  4. package/src/core/DatabaseFactory.js +0 -188
  5. package/src/core/MongoQueryBuilder.js +0 -576
  6. package/src/core/PluginManager.js +0 -968
  7. package/src/core/QueryBuilder.js +0 -4398
  8. package/src/core/TransactionManager.js +0 -40
  9. package/src/drivers/clickhouse-driver.js +0 -272
  10. package/src/drivers/index.js +0 -273
  11. package/src/drivers/mongodb-driver.js +0 -87
  12. package/src/drivers/mssql-driver.js +0 -117
  13. package/src/drivers/mysql-driver.js +0 -169
  14. package/src/drivers/oracle-driver.js +0 -101
  15. package/src/drivers/postgres-driver.js +0 -234
  16. package/src/drivers/redis-driver.js +0 -52
  17. package/src/drivers/sqlite-driver.js +0 -67
  18. package/src/middleware/connection-pool.js +0 -455
  19. package/src/middleware/performance-monitor.js +0 -652
  20. package/src/middleware/query-cache.js +0 -500
  21. package/src/middleware/query-logger.js +0 -262
  22. package/src/plugins/AuditPlugin.js +0 -447
  23. package/src/plugins/BasePlugin.js +0 -418
  24. package/src/plugins/BatchOperationPlugin.js +0 -165
  25. package/src/plugins/CachePlugin.js +0 -407
  26. package/src/plugins/CtePlugin.js +0 -523
  27. package/src/plugins/DistributedPlugin.js +0 -543
  28. package/src/plugins/EncryptionPlugin.js +0 -211
  29. package/src/plugins/FullTextSearchPlugin.js +0 -164
  30. package/src/plugins/GeospatialPlugin.js +0 -219
  31. package/src/plugins/GraphQLPlugin.js +0 -162
  32. package/src/plugins/HookPlugin.js +0 -211
  33. package/src/plugins/JsonPlugin.js +0 -366
  34. package/src/plugins/OptimisticLockPlugin.js +0 -374
  35. package/src/plugins/PerformancePlugin.js +0 -175
  36. package/src/plugins/ResiliencePlugin.js +0 -114
  37. package/src/plugins/ShardingPlugin.js +0 -227
  38. package/src/plugins/SoftDeletePlugin.js +0 -258
  39. package/src/plugins/SyncPlugin.js +0 -373
  40. package/src/plugins/VersioningPlugin.js +0 -314
  41. package/src/plugins/WindowFunctionPlugin.js +0 -343
  42. package/src/utils/config-loader.js +0 -632
  43. package/src/utils/error-handler.js +0 -724
  44. 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;