519518ed6c
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m23s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m5s
Version and Release / version-bump (push) Failing after 39s
Version and Release / trigger-drone (push) Has been skipped
- Renamed core/init.js to core/001_init.js to ensure it runs first - Updated detectExistingSchema() to reference 001_init.js - This fixes the issue where backup migrations tried to access app_settings table before it was created - Migrations now run in correct order: init first, then numbered The error occurred because alphabetical sorting put 029 before init, causing migrations to fail on new deployments.
214 lines
7.0 KiB
JavaScript
214 lines
7.0 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();
|
|
|
|
// Detect and mark existing schema
|
|
await detectExistingSchema();
|
|
|
|
// Get applied migrations
|
|
const appliedMigrations = await db('migrations').select('filename');
|
|
const appliedFilenames = appliedMigrations.map(m => m.filename);
|
|
|
|
// Check if this is a new deployment (no migrations have been applied)
|
|
const isNewDeployment = appliedFilenames.length === 0;
|
|
|
|
// 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$/) || f === 'init.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$/) || f === 'init.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);
|
|
// Ensure init.js runs first
|
|
if (baseA === 'init.js') return -1;
|
|
if (baseB === 'init.js') return 1;
|
|
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 }; |