|
|
@@ -0,0 +1,116 @@
|
|
|
+import Database from 'better-sqlite3';
|
|
|
+import fs from 'fs';
|
|
|
+import path from 'path';
|
|
|
+
|
|
|
+export class MigrationRunner {
|
|
|
+ private db: Database.Database;
|
|
|
+ private migrationsDir: string;
|
|
|
+
|
|
|
+ constructor(db: Database.Database, migrationsDir: string) {
|
|
|
+ this.db = db;
|
|
|
+ this.migrationsDir = migrationsDir;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Initialize the migrations table if it doesn't exist
|
|
|
+ */
|
|
|
+ init() {
|
|
|
+ this.db.exec(`
|
|
|
+ CREATE TABLE IF NOT EXISTS migrations (
|
|
|
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
|
+ name TEXT NOT NULL UNIQUE,
|
|
|
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
|
+ );
|
|
|
+ `);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get list of applied migrations
|
|
|
+ */
|
|
|
+ getAppliedMigrations(): string[] {
|
|
|
+ const rows = this.db.prepare('SELECT name FROM migrations ORDER BY id').all() as { name: string }[];
|
|
|
+ return rows.map(row => row.name);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Get list of available migration files
|
|
|
+ */
|
|
|
+ getAvailableMigrations(): string[] {
|
|
|
+ if (!fs.existsSync(this.migrationsDir)) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ return fs.readdirSync(this.migrationsDir)
|
|
|
+ .filter(file => file.endsWith('.sql'))
|
|
|
+ .sort();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Apply a single migration
|
|
|
+ */
|
|
|
+ applyMigration(migrationName: string): void {
|
|
|
+ const migrationPath = path.join(this.migrationsDir, migrationName);
|
|
|
+
|
|
|
+ if (!fs.existsSync(migrationPath)) {
|
|
|
+ throw new Error(`Migration file not found: ${migrationPath}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const sql = fs.readFileSync(migrationPath, 'utf8');
|
|
|
+
|
|
|
+ console.log(`Applying migration: ${migrationName}`);
|
|
|
+
|
|
|
+ // Execute the migration in a transaction
|
|
|
+ const transaction = this.db.transaction(() => {
|
|
|
+ this.db.exec(sql);
|
|
|
+ this.db.prepare('INSERT INTO migrations (name) VALUES (?)').run(migrationName);
|
|
|
+ });
|
|
|
+
|
|
|
+ transaction();
|
|
|
+ console.log(`Migration applied: ${migrationName}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Apply all pending migrations
|
|
|
+ */
|
|
|
+ applyPendingMigrations(): void {
|
|
|
+ this.init();
|
|
|
+
|
|
|
+ const applied = this.getAppliedMigrations();
|
|
|
+ const available = this.getAvailableMigrations();
|
|
|
+
|
|
|
+ const pending = available.filter(migration => !applied.includes(migration));
|
|
|
+
|
|
|
+ if (pending.length === 0) {
|
|
|
+ console.log('No pending migrations');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(`Applying ${pending.length} pending migrations...`);
|
|
|
+
|
|
|
+ for (const migration of pending) {
|
|
|
+ this.applyMigration(migration);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('All migrations applied');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Create a new migration file
|
|
|
+ */
|
|
|
+ createMigration(name: string): string {
|
|
|
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
|
+ const filename = `${timestamp}_${name}.sql`;
|
|
|
+ const filepath = path.join(this.migrationsDir, filename);
|
|
|
+
|
|
|
+ const template = `-- Migration: ${name}
|
|
|
+-- Created at: ${new Date().toISOString()}
|
|
|
+
|
|
|
+-- Add your SQL migration here
|
|
|
+
|
|
|
+`;
|
|
|
+
|
|
|
+ fs.writeFileSync(filepath, template);
|
|
|
+ console.log(`Created migration: ${filepath}`);
|
|
|
+ return filepath;
|
|
|
+ }
|
|
|
+}
|