fix: resolve PostgreSQL migration issues for development environment
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m29s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m0s
Version and Release / version-bump (push) Successful in 40s
Version and Release / trigger-drone (push) Successful in 3s

- 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 <noreply@anthropic.com>
This commit is contained in:
2025-07-25 14:30:52 +02:00
parent 02c407d431
commit ee855a3502
5 changed files with 189 additions and 67 deletions
+39
View File
@@ -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=
@@ -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: `<h2>Restore Operation Completed</h2>
subject: '✅ Restore Completed Successfully',
body_html: `<h2>Restore Operation Completed</h2>
<p>A restore operation has completed successfully.</p>
<h3>Details:</h3>
@@ -141,20 +159,7 @@ exports.up = async function(knex) {
</ul>
<p>Please verify that all systems are functioning correctly after the restore.</p>`,
body_html_de: `<h2>Wiederherstellungsvorgang abgeschlossen</h2>
<p>Ein Wiederherstellungsvorgang wurde erfolgreich abgeschlossen.</p>
<h3>Details:</h3>
<ul>
<li><strong>Wiederherstellungstyp:</strong> {{restore_type}}</li>
<li><strong>Dauer:</strong> {{duration}}</li>
<li><strong>Wiederhergestellte Dateien:</strong> {{files_restored}}</li>
<li><strong>Backup-ID:</strong> {{backup_id}}</li>
<li><strong>Zeitstempel:</strong> {{timestamp}}</li>
</ul>
<p>Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemäß funktionieren.</p>`,
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: `<h2>Restore Operation Failed</h2>
subject: '❌ Restore Operation Failed',
body_html: `<h2>Restore Operation Failed</h2>
<p>A restore operation has failed and requires attention.</p>
<h3>Details:</h3>
@@ -197,20 +189,7 @@ Bitte überprüfen Sie, ob alle Systeme nach der Wiederherstellung ordnungsgemä
<p>Please check the system logs for more details and take appropriate action.</p>
<p><strong>Important:</strong> If a pre-restore backup was created, it may be used for recovery.</p>`,
body_html_de: `<h2>Wiederherstellungsvorgang fehlgeschlagen</h2>
<p>Ein Wiederherstellungsvorgang ist fehlgeschlagen und erfordert Ihre Aufmerksamkeit.</p>
<h3>Details:</h3>
<ul>
<li><strong>Wiederherstellungstyp:</strong> {{restore_type}}</li>
<li><strong>Fehler:</strong> {{error_message}}</li>
<li><strong>Zeitstempel:</strong> {{timestamp}}</li>
</ul>
<p>Bitte überprüfen Sie die Systemprotokolle für weitere Details und ergreifen Sie entsprechende Maßnahmen.</p>
<p><strong>Wichtig:</strong> Falls ein Backup vor der Wiederherstellung erstellt wurde, kann es zur Wiederherstellung verwendet werden.</p>`,
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) {
+61
View File
@@ -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();
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env node
/**
* Mark a specific migration as applied without running it
* Usage: node scripts/mark-migration-applied.js <migration-filename>
*/
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 <migration-filename>');
console.error('Example: node scripts/mark-migration-applied.js 032_add_restore_runs_table.js');
process.exit(1);
}
markMigrationAsApplied(migrationFile);
+1
View File
@@ -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