de973f5613
Test and Lint / backend-test (push) Successful in 1m9s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m15s
Version and Release / version-bump (push) Successful in 36s
Version and Release / trigger-drone (push) Successful in 4s
Major fixes for production deployment issues: 1. Migration System: - Add safe migration runner that handles existing schema - Create migration helper functions for idempotent operations - Auto-detect existing tables and mark migrations as applied - Handle "relation already exists" errors gracefully 2. Production Initialization: - Create init-production.sh script for proper startup sequence - Fix directory creation and permissions - Add admin user creation from environment variables - Ensure proper service initialization order 3. Documentation: - Add comprehensive PRODUCTION_DEPLOYMENT_GUIDE.md - Add MIGRATION_ERROR_FIX.md for immediate issue resolution - Document all known production issues and solutions - Include backup/restore procedures 4. Safety Improvements: - Add migrate:safe npm script for production use - Update wait-for-db.sh to use safe migrations in production - Add proper error handling and logging This resolves the "relation already exists" error and prevents similar issues in future deployments. The safe migration system can handle both fresh installations and existing databases. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
170 lines
5.1 KiB
JavaScript
170 lines
5.1 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: 'init.js' },
|
|
{ table: 'photos', migration: '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' },
|
|
];
|
|
|
|
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(filename) {
|
|
try {
|
|
const migrationPath = path.join(__dirname, filename);
|
|
const migration = require(migrationPath);
|
|
|
|
if (migration.up) {
|
|
console.log(`Running migration: ${filename}`);
|
|
|
|
// 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 ${filename} 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 ${filename} - schema already exists, marking as applied`);
|
|
await markMigrationAsApplied(filename);
|
|
} 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();
|
|
|
|
// Detect and mark existing schema
|
|
await detectExistingSchema();
|
|
|
|
// Get all migration files
|
|
const files = await fs.readdir(__dirname);
|
|
const migrationFiles = files
|
|
.filter(f => f.match(/^\d{3}_.*\.js$/) || f === 'init.js')
|
|
.sort((a, b) => {
|
|
// Ensure init.js runs first
|
|
if (a === 'init.js') return -1;
|
|
if (b === 'init.js') return 1;
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
// Run pending migrations
|
|
let pendingCount = 0;
|
|
let skippedCount = 0;
|
|
|
|
for (const file of migrationFiles) {
|
|
const isApplied = await isMigrationApplied(file);
|
|
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 }; |