From ee855a3502ecd1a5556e378e9995de86e3548de1 Mon Sep 17 00:00:00 2001 From: paul Date: Fri, 25 Jul 2025 14:30:52 +0200 Subject: [PATCH] fix: resolve PostgreSQL migration issues for development environment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added DATABASE_CLIENT=pg to docker-compose.dev.yml for PostgreSQL connection - Fixed migration 032 to check if tables exist before creating - Removed language-specific email template columns (use standard columns) - Added conditional checks for app_settings and email_templates inserts - Created helper scripts for migration state management - Added .env.dev with PostgreSQL configuration for development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .env.dev | 39 +++++++ .../core/032_add_restore_runs_table.js | 109 +++++++----------- backend/scripts/fix-migration-state.js | 61 ++++++++++ backend/scripts/mark-migration-applied.js | 46 ++++++++ docker-compose.dev.yml | 1 + 5 files changed, 189 insertions(+), 67 deletions(-) create mode 100644 .env.dev create mode 100644 backend/scripts/fix-migration-state.js create mode 100644 backend/scripts/mark-migration-applied.js diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..04206fb --- /dev/null +++ b/.env.dev @@ -0,0 +1,39 @@ +# Development Environment with PostgreSQL +# Copy this to .env for PostgreSQL development with Docker Compose + +# JWT Secret (development only) +JWT_SECRET=dev-secret-key-do-not-use-in-production + +# Database Configuration (PostgreSQL) +DATABASE_CLIENT=pg +DB_USER=picpeak_dev +DB_PASSWORD=dev_password_123 +DB_NAME=picpeak_dev + +# Redis Configuration +REDIS_PASSWORD=dev_redis_pass + +# Admin Account (initial setup) +ADMIN_USERNAME=admin +ADMIN_EMAIL=admin@localhost + +# Email Configuration (Mailhog for development) +SMTP_HOST=mailhog +SMTP_PORT=1025 +SMTP_SECURE=false +SMTP_USER= +SMTP_PASS= +EMAIL_FROM=noreply@picpeak.local + +# Application URLs +FRONTEND_URL=http://localhost:3000 +ADMIN_URL=http://localhost:3001 +VITE_API_URL=http://localhost:3001/api + +# Timezone +TZ=UTC + +# Analytics (Optional - leave empty for development) +VITE_UMAMI_URL= +VITE_UMAMI_WEBSITE_ID= +VITE_UMAMI_SHARE_URL= \ No newline at end of file diff --git a/backend/migrations/core/032_add_restore_runs_table.js b/backend/migrations/core/032_add_restore_runs_table.js index 5154074..1bc06db 100644 --- a/backend/migrations/core/032_add_restore_runs_table.js +++ b/backend/migrations/core/032_add_restore_runs_table.js @@ -5,7 +5,9 @@ */ exports.up = async function(knex) { // Create restore_runs table - await knex.schema.createTable('restore_runs', table => { + const hasRestoreRunsTable = await knex.schema.hasTable('restore_runs'); + if (!hasRestoreRunsTable) { + await knex.schema.createTable('restore_runs', table => { table.increments('id').primary(); // Timing @@ -44,10 +46,13 @@ exports.up = async function(knex) { table.index(['status', 'started_at']); table.index(['restore_type', 'started_at']); - }); + }); + } // Create restore_file_operations table for tracking individual file operations - await knex.schema.createTable('restore_file_operations', table => { + const hasRestoreFileOperationsTable = await knex.schema.hasTable('restore_file_operations'); + if (!hasRestoreFileOperationsTable) { + await knex.schema.createTable('restore_file_operations', table => { table.increments('id').primary(); table.integer('restore_run_id').notNullable() @@ -67,10 +72,13 @@ exports.up = async function(knex) { table.index(['restore_run_id', 'status']); table.index(['file_path']); - }); + }); + } // Create restore_validation_results table - await knex.schema.createTable('restore_validation_results', table => { + const hasRestoreValidationResultsTable = await knex.schema.hasTable('restore_validation_results'); + if (!hasRestoreValidationResultsTable) { + await knex.schema.createTable('restore_validation_results', table => { table.increments('id').primary(); table.integer('restore_run_id').notNullable() @@ -86,10 +94,11 @@ exports.up = async function(knex) { table.timestamp('validated_at').notNullable().defaultTo(knex.fn.now()); table.index(['restore_run_id', 'validation_type']); - }); + }); + } // Add restore-related settings to app_settings - await knex('app_settings').insert([ + const restoreSettings = [ { setting_key: 'restore_allow_force', setting_value: JSON.stringify(false), @@ -120,15 +129,24 @@ exports.up = async function(knex) { setting_value: '30', setting_type: 'restore' } - ]); + ]; + + for (const setting of restoreSettings) { + const exists = await knex('app_settings') + .where('setting_key', setting.setting_key) + .first(); + + if (!exists) { + await knex('app_settings').insert(setting); + } + } // Add new email templates for restore notifications const emailTemplates = [ { template_key: 'restore_completed', - subject_en: '✅ Restore Completed Successfully', - subject_de: '✅ Wiederherstellung erfolgreich abgeschlossen', - body_html_en: `

Restore Operation Completed

+ subject: '✅ Restore Completed Successfully', + body_html: `

Restore Operation Completed

A restore operation has completed successfully.

Details:

@@ -141,20 +159,7 @@ exports.up = async function(knex) {

Please verify that all systems are functioning correctly after the restore.

`, - body_html_de: `

Wiederherstellungsvorgang abgeschlossen

-

Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.

- -

Details:

- - -

Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.

`, - body_text_en: `Restore Operation Completed + body_text: `Restore Operation Completed A restore operation has completed successfully. @@ -166,25 +171,12 @@ Details: - Timestamp: {{timestamp}} Please verify that all systems are functioning correctly after the restore.`, - body_text_de: `Wiederherstellungsvorgang abgeschlossen - -Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen. - -Details: -- Wiederherstellungstyp: {{restore_type}} -- Dauer: {{duration}} -- Wiederhergestellte Dateien: {{files_restored}} -- Backup-ID: {{backup_id}} -- Zeitstempel: {{timestamp}} - -Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.`, variables: JSON.stringify(['restore_type', 'duration', 'files_restored', 'backup_id', 'timestamp']) }, { template_key: 'restore_failed', - subject_en: '❌ Restore Operation Failed', - subject_de: '❌ Wiederherstellungsvorgang fehlgeschlagen', - body_html_en: `

Restore Operation Failed

+ subject: '❌ Restore Operation Failed', + body_html: `

Restore Operation Failed

A restore operation has failed and requires attention.

Details:

@@ -197,20 +189,7 @@ Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemä

Please check the system logs for more details and take appropriate action.

Important: If a pre-restore backup was created, it may be used for recovery.

`, - body_html_de: `

Wiederherstellungsvorgang fehlgeschlagen

-

Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.

- -

Details:

- - -

Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.

- -

Wichtig: Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.

`, - body_text_en: `Restore Operation Failed + body_text: `Restore Operation Failed A restore operation has failed and requires attention. @@ -222,23 +201,19 @@ Details: Please check the system logs for more details and take appropriate action. Important: If a pre-restore backup was created, it may be used for recovery.`, - body_text_de: `Wiederherstellungsvorgang fehlgeschlagen - -Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit. - -Details: -- Wiederherstellungstyp: {{restore_type}} -- Fehler: {{error_message}} -- Zeitstempel: {{timestamp}} - -Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen. - -Wichtig: Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.`, variables: JSON.stringify(['restore_type', 'error_message', 'timestamp']) } ]; - await knex('email_templates').insert(emailTemplates); + for (const template of emailTemplates) { + const exists = await knex('email_templates') + .where('template_key', template.template_key) + .first(); + + if (!exists) { + await knex('email_templates').insert(template); + } + } }; exports.down = async function(knex) { diff --git a/backend/scripts/fix-migration-state.js b/backend/scripts/fix-migration-state.js new file mode 100644 index 0000000..88cf79f --- /dev/null +++ b/backend/scripts/fix-migration-state.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Fix migration state by marking migrations as applied if their tables already exist + */ + +const { db } = require('../src/database/db'); + +async function fixMigrationState() { + try { + console.log('Checking migration state...'); + + // Ensure migrations table exists + const hasMigrationsTable = await db.schema.hasTable('migrations'); + if (!hasMigrationsTable) { + 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 for specific tables and mark their migrations as applied + const tableChecks = [ + { table: 'restore_runs', migration: '032_add_restore_runs_table.js' }, + { table: 'restore_file_operations', migration: '032_add_restore_runs_table.js' }, + { table: 'restore_validation_results', migration: '032_add_restore_runs_table.js' }, + { table: 'gallery_feedback', migration: '033_add_gallery_feedback.js' }, + { table: 'feedback_photos', migration: '033_add_gallery_feedback.js' }, + ]; + + for (const check of tableChecks) { + const tableExists = await db.schema.hasTable(check.table); + if (tableExists) { + const migrationApplied = await db('migrations') + .where('filename', check.migration) + .first(); + + if (!migrationApplied) { + await db('migrations').insert({ + filename: check.migration, + applied_at: new Date() + }); + console.log(`✅ Marked ${check.migration} as applied (table ${check.table} exists)`); + } else { + console.log(`ℹ️ ${check.migration} already marked as applied`); + } + } + } + + console.log('\nMigration state fixed successfully!'); + } catch (error) { + console.error('Error fixing migration state:', error.message); + process.exit(1); + } finally { + await db.destroy(); + } +} + +fixMigrationState(); \ No newline at end of file diff --git a/backend/scripts/mark-migration-applied.js b/backend/scripts/mark-migration-applied.js new file mode 100644 index 0000000..0e93b19 --- /dev/null +++ b/backend/scripts/mark-migration-applied.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/** + * Mark a specific migration as applied without running it + * Usage: node scripts/mark-migration-applied.js + */ + +const { db } = require('../src/database/db'); + +async function markMigrationAsApplied(filename) { + try { + // Check if migration is already marked + const existing = await db('migrations') + .where('filename', filename) + .first(); + + if (existing) { + console.log(`Migration ${filename} is already marked as applied`); + return; + } + + // Mark as applied + await db('migrations').insert({ + filename, + applied_at: new Date() + }); + + console.log(`✅ Migration ${filename} marked as applied`); + } catch (error) { + console.error('Error marking migration:', error.message); + process.exit(1); + } finally { + await db.destroy(); + } +} + +// Get migration filename from command line +const migrationFile = process.argv[2]; + +if (!migrationFile) { + console.error('Usage: node scripts/mark-migration-applied.js '); + console.error('Example: node scripts/mark-migration-applied.js 032_add_restore_runs_table.js'); + process.exit(1); +} + +markMigrationAsApplied(migrationFile); \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ecd6db3..8732b90 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,6 +13,7 @@ services: - JWT_SECRET=${JWT_SECRET} - ADMIN_USERNAME=${ADMIN_USERNAME:-admin} - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.com} + - DATABASE_CLIENT=pg - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME} - DB_TYPE=postgresql - DB_HOST=postgres