import { Sequelize, DataTypes } from 'sequelize'; import dotenv from 'dotenv'; dotenv.config(); const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, process.env.DB_PASS, { host: process.env.DB_HOST, dialect: 'postgres', define: { timestamps: false, underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format }, }); const createSchemas = async () => { await sequelize.query('CREATE SCHEMA IF NOT EXISTS community'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS logs'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS type'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS service'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS forum'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_data'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_type'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_predefine'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS falukant_log'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS chat'); await sequelize.query('CREATE SCHEMA IF NOT EXISTS match3'); }; const initializeDatabase = async () => { await createSchemas(); const { default: models } = await import('../models/index.js'); await syncModels(models); }; const syncModels = async (models) => { for (const model of Object.values(models)) { // Verwende force: false und alter: false, um Constraints nicht neu zu erstellen // Nur beim ersten Mal oder bei expliziten Schema-Änderungen sollte alter: true verwendet werden await model.sync({ alter: false, force: false }); } }; // Intelligente Schema-Synchronisation - prüft ob Updates nötig sind const syncModelsWithUpdates = async (models) => { // Prüfe ob wir im Entwicklungsmodus sind if (process.env.STAGE !== 'dev') { console.log('🚫 Nicht im Entwicklungsmodus - überspringe Schema-Updates'); console.log('📊 Aktueller Stage:', process.env.STAGE || 'nicht gesetzt'); console.log('💡 Setze STAGE=dev in der .env Datei für automatische Schema-Updates'); console.log('🔒 Sicherheitsmodus: Schema-Updates sind deaktiviert'); // Normale Synchronisation ohne Updates for (const model of Object.values(models)) { await model.sync({ alter: false, force: false }); } return; } // Zusätzliche Sicherheitsabfrage für Produktionsumgebungen if (process.env.NODE_ENV === 'production' && process.env.STAGE !== 'dev') { console.log('🚨 PRODUKTIONSWARNUNG: Schema-Updates sind in Produktionsumgebungen deaktiviert!'); console.log('🔒 Verwende nur normale Synchronisation ohne Schema-Änderungen'); for (const model of Object.values(models)) { await model.sync({ alter: false, force: false }); } return; } console.log('🔍 Entwicklungsmodus aktiv - prüfe ob Schema-Updates nötig sind...'); try { // Prüfe ob neue Felder existieren müssen const needsUpdate = await checkSchemaUpdates(models); if (needsUpdate) { console.log('🔄 Schema-Updates nötig - verwende alter: true'); for (const model of Object.values(models)) { await model.sync({ alter: true, force: false }); } console.log('✅ Schema-Updates abgeschlossen'); } else { console.log('✅ Keine Schema-Updates nötig - verwende alter: false'); for (const model of Object.values(models)) { await model.sync({ alter: false, force: false }); } } } catch (error) { console.error('❌ Fehler bei Schema-Synchronisation:', error); // Fallback: Normale Synchronisation ohne Updates console.log('🔄 Fallback: Normale Synchronisation ohne Updates'); for (const model of Object.values(models)) { await model.sync({ alter: false, force: false }); } } }; // Prüft ob Schema-Updates nötig sind const checkSchemaUpdates = async (models) => { try { console.log('🔍 Prüfe alle Schemas auf Updates...'); // Alle verfügbaren Schemas const schemas = [ 'community', 'logs', 'type', 'service', 'forum', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'match3' ]; let needsUpdate = false; // Prüfe jedes Schema for (const schema of schemas) { const schemaNeedsUpdate = await checkSchemaForUpdates(schema, models); if (schemaNeedsUpdate) { needsUpdate = true; } } return needsUpdate; } catch (error) { console.error('❌ Fehler bei Schema-Prüfung:', error); return false; // Im Zweifelsfall: Keine Updates } }; // Prüft ein spezifisches Schema auf Updates const checkSchemaForUpdates = async (schemaName, models) => { try { console.log(`🔍 Prüfe Schema: ${schemaName}`); // Hole alle Tabellen in diesem Schema const tables = await sequelize.query(` SELECT table_name FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name `, { bind: [schemaName], type: sequelize.QueryTypes.SELECT }); if (tables.length === 0) { console.log(` 📊 Schema ${schemaName}: Keine Tabellen gefunden`); return false; } console.log(` 📊 Schema ${schemaName}: ${tables.length} Tabellen gefunden`); // Prüfe jede Tabelle auf Updates for (const table of tables) { const tableName = table.table_name; const tableNeedsUpdate = await checkTableForUpdates(schemaName, tableName, models); if (tableNeedsUpdate) { console.log(` 🔄 Tabelle ${tableName} braucht Updates`); return true; } } // Prüfe auf fehlende Tabellen (neue Models) const missingTables = await checkForMissingTables(schemaName, models); if (missingTables.length > 0) { console.log(` 🔄 Neue Tabellen gefunden: ${missingTables.join(', ')}`); return true; } console.log(` ✅ Schema ${schemaName}: Keine Updates nötig`); return false; } catch (error) { console.error(`❌ Fehler beim Prüfen von Schema ${schemaName}:`, error); return false; } }; // Prüft auf fehlende Tabellen (neue Models) const checkForMissingTables = async (schemaName, models) => { try { const missingTables = []; // Hole alle erwarteten Tabellen aus den Models for (const [modelName, model] of Object.entries(models)) { if (model._schema === schemaName) { const tableExists = await sequelize.query(` SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2 ); `, { bind: [schemaName, model.tableName], type: sequelize.QueryTypes.SELECT }); if (!tableExists[0]?.exists) { missingTables.push(model.tableName); } } } return missingTables; } catch (error) { console.error(`❌ Fehler beim Prüfen fehlender Tabellen:`, error); return []; } }; // Prüft eine spezifische Tabelle auf Updates const checkTableForUpdates = async (schemaName, tableName, models) => { try { // Finde das entsprechende Model const model = findModelForTable(schemaName, tableName, models); if (!model) { console.log(` ⚠️ Kein Model für Tabelle ${schemaName}.${tableName} gefunden`); return false; } // Hole aktuelle Spalten der Tabelle const currentColumns = await sequelize.query(` SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position `, { bind: [schemaName, tableName], type: sequelize.QueryTypes.SELECT }); // Hole erwartete Spalten aus dem Model const expectedColumns = Object.keys(model.rawAttributes); // Vergleiche aktuelle und erwartete Spalten const missingColumns = expectedColumns.filter(expectedCol => { return !currentColumns.some(currentCol => currentCol.column_name === expectedCol ); }); if (missingColumns.length > 0) { console.log(` 📊 Fehlende Spalten in ${tableName}: ${missingColumns.join(', ')}`); return true; } // Prüfe auf geänderte Spalten-Typen oder Constraints for (const expectedCol of expectedColumns) { const currentCol = currentColumns.find(col => col.column_name === expectedCol); if (currentCol) { const needsUpdate = await checkColumnForUpdates( schemaName, tableName, expectedCol, currentCol, model.rawAttributes[expectedCol] ); if (needsUpdate) { return true; } } } return false; } catch (error) { console.error(`❌ Fehler beim Prüfen von Tabelle ${schemaName}.${tableName}:`, error); return false; } }; // Prüft eine spezifische Spalte auf Updates const checkColumnForUpdates = async (schemaName, tableName, columnName, currentColumn, expectedAttribute) => { try { // Prüfe Datentyp-Änderungen if (currentColumn.data_type !== getExpectedDataType(expectedAttribute)) { console.log(` 🔄 Spalte ${columnName}: Datentyp geändert (${currentColumn.data_type} → ${getExpectedDataType(expectedAttribute)})`); return true; } // Prüfe NULL/NOT NULL Änderungen const currentNullable = currentColumn.is_nullable === 'YES'; const expectedNullable = expectedAttribute.allowNull !== false; if (currentNullable !== expectedNullable) { console.log(` 🔄 Spalte ${columnName}: NULL-Constraint geändert (${currentNullable} → ${expectedNullable})`); return true; } // Prüfe Standardwert-Änderungen if (expectedAttribute.defaultValue !== undefined && currentColumn.column_default !== getExpectedDefaultValue(expectedAttribute.defaultValue)) { console.log(` 🔄 Spalte ${columnName}: Standardwert geändert`); return true; } return false; } catch (error) { console.error(`❌ Fehler beim Prüfen von Spalte ${columnName}:`, error); return false; } }; // Hilfsfunktion: Findet Model für eine Tabelle const findModelForTable = (schemaName, tableName, models) => { // Suche nach dem Model basierend auf Schema und Tabellenname for (const [modelName, model] of Object.entries(models)) { if (model.tableName === tableName && model._schema === schemaName) { return model; } } return null; }; // Hilfsfunktion: Konvertiert Sequelize-Datentyp zu PostgreSQL-Datentyp const getExpectedDataType = (attribute) => { if (!attribute || !attribute.type) return 'text'; const type = attribute.type; // Direktklassentypen in Sequelize sind Funktionen/Klassen; "instanceof" funktioniert bei Wrappern wie DataTypes.INTEGER() nicht immer. // Deshalb vergleichen wir über den .key wenn verfügbar. const key = type.key || type.constructor?.key; switch (key) { case 'INTEGER': return 'integer'; case 'STRING': return 'character varying'; case 'TEXT': return 'text'; case 'BOOLEAN': return 'boolean'; case 'DATE': return 'timestamp without time zone'; case 'JSON': return 'json'; case 'DECIMAL': return 'numeric'; default: return 'text'; } }; // Hilfsfunktion: Konvertiert Sequelize-Default zu PostgreSQL-Default const getExpectedDefaultValue = (defaultValue) => { if (defaultValue === null) return null; if (typeof defaultValue === 'string') return `'${defaultValue}'`; if (typeof defaultValue === 'number') return defaultValue.toString(); if (typeof defaultValue === 'boolean') return defaultValue.toString(); if (defaultValue === sequelize.literal('CURRENT_TIMESTAMP')) return 'CURRENT_TIMESTAMP'; // Fallback return defaultValue?.toString() || null; }; // Separate Funktion für Schema-Updates (nur bei Bedarf aufrufen) const updateSchema = async (models) => { console.log('🔄 Aktualisiere Datenbankschema...'); for (const model of Object.values(models)) { await model.sync({ alter: true, force: false }); } console.log('✅ Datenbankschema aktualisiert'); }; async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null) { try { const result = await sequelize.query( `SELECT falukant_data.update_money( :falukantUserId, :moneyChange, :activity, :changedBy )`, { replacements: { falukantUserId, moneyChange, activity, changedBy, }, type: sequelize.QueryTypes.SELECT, } ); return { success: true, message: 'Money updated successfully', result }; } catch (error) { console.error('Error updating money:', error); return { success: false, message: error.message }; } } // Immer Schema-Updates (für Deployment) const syncModelsAlways = async (models) => { console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...'); try { for (const model of Object.values(models)) { await model.sync({ alter: true, force: false }); } console.log('✅ Schema-Updates für alle Models abgeschlossen'); } catch (error) { console.error('❌ Fehler bei Schema-Updates:', error); throw error; } }; export { sequelize, initializeDatabase, syncModels, syncModelsWithUpdates, syncModelsAlways, updateSchema, updateFalukantUserMoney };