Files
picpeak/backend/migrations/run-migrations-safe.js
T
paul ba2c021c45
Mirror to GitHub / mirror (push) Successful in 23s
Test and Lint / backend-test (push) Successful in 1m26s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m10s
Version and Release / version-bump (push) Successful in 37s
Version and Release / trigger-drone (push) Has been skipped
Fix new deployment detection in migration runner
- Check for essential tables (events, photos, admin_users, activity_logs)
  to determine if it's truly a new deployment
- Only run detectExistingSchema() for actual existing deployments
- Remove obsolete init.js references (now 001_init.js)
- Fix migration filters to handle renamed init file

The issue was that detectExistingSchema() was marking migrations as
applied from previous failed runs, causing the system to incorrectly
treat new deployments as existing ones and run legacy migrations that
expect tables to already exist.
2025-07-25 12:45:25 +02:00

220 lines
7.4 KiB
JavaScript

const fs = require('fs').promises;
const path = require('path');
const { db } = require('../src/database/db');
/**
* Production-safe migration runner that handles existing schema
*/
// Create or verify migrations tracking table
async function ensureMigrationsTable() {
const tableExists = await db.schema.hasTable('migrations');
if (!tableExists) {
await db.schema.createTable('migrations', (table) => {
table.increments('id').primary();
table.string('filename').unique().notNullable();
table.timestamp('applied_at').defaultTo(db.fn.now());
});
console.log('Created migrations tracking table');
}
}
// Check if a migration has been applied
async function isMigrationApplied(filename) {
const result = await db('migrations').where('filename', filename).first();
return !!result;
}
// Mark migration as applied without running it (for existing schema)
async function markMigrationAsApplied(filename) {
await db('migrations').insert({ filename });
console.log(`Marked migration ${filename} as applied`);
}
// Detect existing schema and mark migrations as applied
async function detectExistingSchema() {
console.log('Detecting existing schema...');
const tableChecks = [
{ table: 'events', migration: '001_init.js' },
{ table: 'photos', migration: '001_init.js' },
{ table: 'photo_categories', migration: '004_add_categories_and_cms.js' },
{ table: 'cms_pages', migration: '004_add_categories_and_cms.js' },
{ table: 'login_attempts', migration: '015_add_login_attempts_table.js' },
{ table: 'token_blacklist', migration: '017_add_token_revocation_tables.js' },
{ table: 'backup_runs', migration: '029_add_backup_service_tables.js' },
{ table: 'gallery_feedback', migration: '033_add_gallery_feedback.js' },
];
for (const check of tableChecks) {
const exists = await db.schema.hasTable(check.table);
if (exists) {
const isApplied = await isMigrationApplied(check.migration);
if (!isApplied) {
await markMigrationAsApplied(check.migration);
}
}
}
}
// Run a single migration safely
async function runMigrationSafely(filepath) {
try {
const migrationPath = path.join(__dirname, filepath);
const migration = require(migrationPath);
const filename = path.basename(filepath);
if (migration.up) {
console.log(`Running migration: ${filepath}`);
// Run migration in a transaction if possible
if (db.client.config.client === 'pg') {
await db.transaction(async (trx) => {
await migration.up(trx);
});
} else {
await migration.up(db);
}
await db('migrations').insert({ filename });
console.log(`Migration ${filepath} completed successfully`);
}
} catch (error) {
// Check if error is because schema already exists
if (error.code === '42P07' || // PostgreSQL: relation already exists
error.code === 'SQLITE_ERROR' && error.message.includes('already exists')) {
console.log(`Migration ${filepath} - schema already exists, marking as applied`);
await markMigrationAsApplied(path.basename(filepath));
} else {
throw error;
}
}
}
// Main migration runner
async function runMigrations() {
let connection;
try {
console.log('Starting production-safe database migrations...');
// Ensure database connection is ready
await db.raw('SELECT 1');
console.log('Database connection verified');
// Create migrations tracking table
await ensureMigrationsTable();
// Check if essential tables exist to determine if this is truly a new deployment
const hasEventsTable = await db.schema.hasTable('events');
const hasPhotosTable = await db.schema.hasTable('photos');
const hasAdminTable = await db.schema.hasTable('admin_users');
const hasActivityLogsTable = await db.schema.hasTable('activity_logs');
// Get applied migrations
const appliedMigrations = await db('migrations').select('filename');
const appliedFilenames = appliedMigrations.map(m => m.filename);
// Check if this is a new deployment
// It's new if no essential tables exist OR no migrations have been applied
const isNewDeployment = (!hasEventsTable || !hasPhotosTable || !hasAdminTable || !hasActivityLogsTable) || appliedFilenames.length === 0;
// Only detect existing schema for truly existing deployments
if (!isNewDeployment) {
await detectExistingSchema();
}
// Get migration files from appropriate directories
let migrationFiles = [];
if (isNewDeployment) {
// For new deployments, only run core migrations
console.log('New deployment detected - running core migrations only');
const coreDir = path.join(__dirname, 'core');
const coreFiles = await fs.readdir(coreDir);
migrationFiles = coreFiles
.filter(f => f.match(/^\d{3}_.*\.js$/))
.map(f => path.join('core', f))
.sort();
} else {
// For existing deployments, run all migrations (legacy + core)
console.log('Existing deployment detected - checking all migrations');
// Get legacy migrations
const legacyDir = path.join(__dirname, 'legacy');
const legacyFiles = await fs.readdir(legacyDir);
const legacyMigrations = legacyFiles
.filter(f => f.match(/^\d{3}_.*\.js$/))
.map(f => path.join('legacy', f));
// Get core migrations
const coreDir = path.join(__dirname, 'core');
const coreFiles = await fs.readdir(coreDir);
const coreMigrations = coreFiles
.filter(f => f.match(/^\d{3}_.*\.js$/))
.map(f => path.join('core', f));
// Combine and sort by number
migrationFiles = [...legacyMigrations, ...coreMigrations]
.sort((a, b) => {
const baseA = path.basename(a);
const baseB = path.basename(b);
const numA = parseInt(baseA.split('_')[0]);
const numB = parseInt(baseB.split('_')[0]);
return numA - numB;
});
}
// Run pending migrations
let pendingCount = 0;
let skippedCount = 0;
for (const file of migrationFiles) {
const filename = path.basename(file);
const isApplied = appliedFilenames.includes(filename);
if (!isApplied) {
await runMigrationSafely(file);
pendingCount++;
} else {
skippedCount++;
}
}
console.log(`\nMigration Summary:`);
console.log(`- Applied: ${pendingCount} migration(s)`);
console.log(`- Skipped: ${skippedCount} migration(s) (already applied)`);
console.log(`- Total: ${migrationFiles.length} migration(s)`);
console.log('\nAll migrations completed successfully');
// Close database connection
await db.destroy();
process.exit(0);
} catch (error) {
console.error('\n❌ Migration failed:', error.message);
console.error('Error details:', error);
// Close database connection on error
try {
await db.destroy();
} catch (e) {
// Ignore
}
process.exit(1);
}
}
// Add delay for database readiness in production
async function waitAndRun() {
if (process.env.NODE_ENV === 'production') {
console.log('Waiting 2 seconds for database readiness...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
await runMigrations();
}
// Only run if called directly
if (require.main === module) {
waitAndRun();
}
module.exports = { runMigrations };