← back to Watches

utils/migrations.js

160 lines

/**
 * Database Migration System
 * Handles database schema changes and data migrations
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export class MigrationManager {
  constructor(dataPath = './data/watches.json') {
    this.dataPath = dataPath;
    this.migrationsDir = path.join(__dirname, '../migrations');
    this.migrations = [];

    if (!fs.existsSync(this.migrationsDir)) {
      fs.mkdirSync(this.migrationsDir, { recursive: true });
    }

    this.loadMigrations();
  }

  loadMigrations() {
    if (!fs.existsSync(this.migrationsDir)) {
      return;
    }

    const files = fs.readdirSync(this.migrationsDir)
      .filter(f => f.endsWith('.js'))
      .sort();

    this.migrations = files.map(file => ({
      name: file,
      path: path.join(this.migrationsDir, file)
    }));
  }

  async runMigrations() {
    console.log('Starting database migrations...\n');

    if (this.migrations.length === 0) {
      console.log('No migrations to run.');
      return;
    }

    const data = this.loadData();
    let migratedData = { ...data };

    for (const migration of this.migrations) {
      console.log(`Running migration: ${migration.name}`);

      try {
        const migrationModule = await import(migration.path);
        migratedData = migrationModule.up(migratedData);
        console.log(`✓ ${migration.name} completed`);
      } catch (error) {
        console.error(`✗ ${migration.name} failed:`, error.message);
        throw error;
      }
    }

    this.saveData(migratedData);
    console.log('\nAll migrations completed successfully!');
    return migratedData;
  }

  loadData() {
    return JSON.parse(fs.readFileSync(this.dataPath, 'utf8'));
  }

  saveData(data) {
    // Create backup before saving
    const backupDir = path.join(path.dirname(this.dataPath), 'backups');
    if (!fs.existsSync(backupDir)) {
      fs.mkdirSync(backupDir, { recursive: true });
    }

    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const backupPath = path.join(backupDir, `watches-backup-${timestamp}.json`);
    fs.writeFileSync(backupPath, JSON.stringify(this.loadData(), null, 2));
    console.log(`Backup created: ${backupPath}`);

    // Save migrated data
    fs.writeFileSync(this.dataPath, JSON.stringify(data, null, 2));
  }

  createMigration(name) {
    const timestamp = Date.now();
    const filename = `${timestamp}_${name}.js`;
    const filepath = path.join(this.migrationsDir, filename);

    const template = `/**
 * Migration: ${name}
 * Created: ${new Date().toISOString()}
 */

export function up(data) {
  console.log('  Applying migration: ${name}');

  // Add your migration logic here
  // Example:
  // data.watches.forEach(watch => {
  //   if (!watch.newField) {
  //     watch.newField = 'default value';
  //   }
  // });

  return data;
}

export function down(data) {
  console.log('  Reverting migration: ${name}');

  // Add your rollback logic here
  // Example:
  // data.watches.forEach(watch => {
  //   delete watch.newField;
  // });

  return data;
}
`;

    fs.writeFileSync(filepath, template);
    console.log(`Migration created: ${filename}`);
    return filepath;
  }
}

// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
  const command = process.argv[2];
  const manager = new MigrationManager();

  if (command === 'run') {
    manager.runMigrations().catch(error => {
      console.error('Migration failed:', error);
      process.exit(1);
    });
  } else if (command === 'create') {
    const name = process.argv[3] || 'unnamed_migration';
    manager.createMigration(name);
  } else {
    console.log(`
Database Migration Manager

Usage:
  node migrations.js run           - Run all pending migrations
  node migrations.js create <name> - Create a new migration file

Examples:
  node migrations.js run
  node migrations.js create add_rating_field
    `);
  }
}