← 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
`);
}
}