- Added connection pool settings to optimize database connection management. - Introduced a queryWithTimeout helper function to handle long-running queries, improving error handling and preventing indefinite hangs. - Updated syncModelsAlways function to utilize queryWithTimeout for foreign key checks and cleanup operations, enhancing robustness and logging for better visibility during synchronization.
839 lines
40 KiB
JavaScript
839 lines
40 KiB
JavaScript
import { Sequelize, DataTypes } from 'sequelize';
|
||
import dotenv from 'dotenv';
|
||
|
||
dotenv.config();
|
||
|
||
// Optionales Performance-Logging (aktivierbar per ENV)
|
||
// - SQL_BENCHMARK=1: Sequelize liefert Query-Timing (ms) an logger
|
||
// - SQL_SLOW_MS=200: Logge nur Queries ab dieser Dauer (wenn SQL_LOG_ALL nicht gesetzt)
|
||
// - SQL_LOG_ALL=1: Logge alle Queries (auch ohne benchmark)
|
||
const SQL_BENCHMARK = process.env.SQL_BENCHMARK === '1';
|
||
const SQL_LOG_ALL = process.env.SQL_LOG_ALL === '1';
|
||
const SQL_SLOW_MS = Number.parseInt(process.env.SQL_SLOW_MS || '200', 10);
|
||
const sqlLogger = (sql, timing) => {
|
||
// Sequelize ruft logging(sql) oder logging(sql, timing) abhängig von benchmark auf.
|
||
if (!SQL_BENCHMARK) {
|
||
if (SQL_LOG_ALL) console.log(sql);
|
||
return;
|
||
}
|
||
const ms = typeof timing === 'number' ? timing : 0;
|
||
if (SQL_LOG_ALL || ms >= SQL_SLOW_MS) {
|
||
console.log(`🛢️ SQL ${ms}ms: ${sql}`);
|
||
}
|
||
};
|
||
|
||
// Validiere Umgebungsvariablen
|
||
const dbName = process.env.DB_NAME;
|
||
const dbUser = process.env.DB_USER;
|
||
const dbPass = process.env.DB_PASS || ''; // Fallback auf leeren String
|
||
const dbHost = process.env.DB_HOST;
|
||
|
||
console.log('Database configuration:');
|
||
console.log('DB_NAME:', dbName);
|
||
console.log('DB_USER:', dbUser);
|
||
console.log('DB_PASS:', dbPass ? '[SET]' : '[NOT SET]');
|
||
console.log('DB_HOST:', dbHost);
|
||
|
||
if (!dbName || !dbUser || !dbHost) {
|
||
throw new Error('Missing required database environment variables: DB_NAME, DB_USER, or DB_HOST');
|
||
}
|
||
|
||
const sequelize = new Sequelize(dbName, dbUser, dbPass, {
|
||
host: dbHost,
|
||
dialect: 'postgres',
|
||
define: {
|
||
timestamps: false,
|
||
underscored: true // WICHTIG: Alle Datenbankfelder im snake_case Format
|
||
},
|
||
benchmark: SQL_BENCHMARK,
|
||
logging: sqlLogger,
|
||
pool: {
|
||
max: 20, // Maximale Anzahl von Verbindungen im Pool
|
||
min: 5, // Minimale Anzahl von Verbindungen im Pool
|
||
acquire: 60000, // Maximale Zeit (ms) zum Erwerb einer Verbindung (60 Sekunden)
|
||
idle: 10000, // Maximale Zeit (ms), die eine Verbindung idle sein kann, bevor sie entfernt wird
|
||
evict: 1000 // Intervall (ms) zum Prüfen auf idle Verbindungen
|
||
},
|
||
dialectOptions: {
|
||
connectTimeout: 60000 // Timeout für Verbindungsaufbau (60 Sekunden)
|
||
}
|
||
});
|
||
|
||
// Helper: Query mit Timeout (muss nach sequelize Initialisierung definiert werden)
|
||
const queryWithTimeout = async (query, timeoutMs = 10000, description = 'Query') => {
|
||
const timeoutPromise = new Promise((_, reject) => {
|
||
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
|
||
});
|
||
|
||
try {
|
||
const result = await Promise.race([
|
||
sequelize.query(query),
|
||
timeoutPromise
|
||
]);
|
||
return result;
|
||
} catch (error) {
|
||
if (error.message.includes('Timeout')) {
|
||
throw error; // Re-throw für bessere Fehlerbehandlung
|
||
}
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
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');
|
||
await sequelize.query('CREATE SCHEMA IF NOT EXISTS taxi');
|
||
};
|
||
|
||
const initializeDatabase = async () => {
|
||
await createSchemas();
|
||
|
||
// Aktiviere die pgcrypto Erweiterung für die digest() Funktion
|
||
try {
|
||
await sequelize.query('CREATE EXTENSION IF NOT EXISTS pgcrypto;');
|
||
console.log('✅ pgcrypto Erweiterung aktiviert');
|
||
} catch (error) {
|
||
console.warn('⚠️ Konnte pgcrypto Erweiterung nicht aktivieren:', error.message);
|
||
// Fortfahren, da die Erweiterung möglicherweise bereits aktiviert ist
|
||
}
|
||
|
||
// Modelle nur laden, aber an dieser Stelle NICHT syncen.
|
||
// Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral
|
||
// über syncModelsWithUpdates()/syncModelsAlways gesteuert.
|
||
await import('../models/index.js');
|
||
};
|
||
|
||
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)) {
|
||
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
|
||
await model.sync({ alter: true, force: false, constraints: 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', 'taxi'
|
||
];
|
||
|
||
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)) {
|
||
// constraints: false verhindert, dass Sequelize Foreign Keys automatisch erstellt
|
||
await model.sync({ alter: true, force: false, constraints: false });
|
||
}
|
||
console.log('✅ Datenbankschema aktualisiert');
|
||
};
|
||
|
||
async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, changedBy = null, transaction = 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,
|
||
transaction: transaction || undefined,
|
||
}
|
||
);
|
||
return {
|
||
success: true,
|
||
message: 'Money updated successfully',
|
||
result
|
||
};
|
||
} catch (error) {
|
||
console.error('Error updating money:', error);
|
||
return {
|
||
success: false,
|
||
message: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
// Helper: Sync mit Timeout
|
||
const syncModelWithTimeout = async (model, timeoutMs = 60000) => {
|
||
const timeoutPromise = new Promise((_, reject) => {
|
||
setTimeout(() => reject(new Error(`Model sync timeout after ${timeoutMs}ms`)), timeoutMs);
|
||
});
|
||
|
||
try {
|
||
await Promise.race([
|
||
model.sync({ alter: true, force: false, constraints: false }),
|
||
timeoutPromise
|
||
]);
|
||
} catch (error) {
|
||
if (error.message.includes('timeout')) {
|
||
console.warn(` ⚠️ ${model.name} sync timeout nach ${timeoutMs}ms - überspringe...`);
|
||
return false;
|
||
}
|
||
throw error;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// Immer Schema-Updates (für Deployment)
|
||
const syncModelsAlways = async (models) => {
|
||
console.log('🔍 Deployment-Modus: Führe immer Schema-Updates durch...');
|
||
|
||
const modelArray = Object.values(models);
|
||
const totalModels = modelArray.length;
|
||
let currentModel = 0;
|
||
|
||
try {
|
||
for (const model of modelArray) {
|
||
currentModel++;
|
||
console.log(` 🔄 Syncing model ${model.name} (${currentModel}/${totalModels})...`);
|
||
// Temporarily remove VIRTUAL fields before sync to prevent sync errors
|
||
const originalAttributes = model.rawAttributes;
|
||
const virtualFields = {};
|
||
|
||
// Find and temporarily remove VIRTUAL fields
|
||
// Check multiple ways to identify VIRTUAL fields
|
||
for (const [key, attr] of Object.entries(originalAttributes)) {
|
||
// Check if it's a VIRTUAL field by checking the type
|
||
let isVirtual = false;
|
||
|
||
if (attr.type) {
|
||
// Method 1: Check if type key is VIRTUAL (most reliable)
|
||
if (attr.type.key === 'VIRTUAL') {
|
||
isVirtual = true;
|
||
}
|
||
// Method 2: Direct comparison with DataTypes.VIRTUAL
|
||
else if (attr.type === DataTypes.VIRTUAL) {
|
||
isVirtual = true;
|
||
}
|
||
// Method 3: Check toString representation
|
||
else if (typeof attr.type.toString === 'function') {
|
||
const typeStr = attr.type.toString();
|
||
if (typeStr === 'VIRTUAL' || typeStr.includes('VIRTUAL')) {
|
||
isVirtual = true;
|
||
}
|
||
}
|
||
// Method 4: Check constructor name
|
||
else if (attr.type.constructor && attr.type.constructor.name === 'VIRTUAL') {
|
||
isVirtual = true;
|
||
}
|
||
}
|
||
|
||
// Also check if field has a getter but no setter and no field mapping (common pattern for VIRTUAL fields)
|
||
// But only if it doesn't have a 'field' property, which means it's not mapped to a database column
|
||
if (!isVirtual && attr.get && !attr.set && !attr.field) {
|
||
// This might be a VIRTUAL field, but be careful not to remove real fields
|
||
// Only remove if we're certain it's VIRTUAL
|
||
}
|
||
|
||
if (isVirtual) {
|
||
virtualFields[key] = attr;
|
||
delete model.rawAttributes[key];
|
||
console.log(` ⚠️ Temporarily removed VIRTUAL field: ${key} from model ${model.name}`);
|
||
}
|
||
}
|
||
|
||
// Special handling for Notification model: ensure characterName VIRTUAL field is removed
|
||
// This is a workaround for Sequelize bug where it confuses characterName (VIRTUAL) with character_name (STRING)
|
||
if (model.name === 'Notification' && model.rawAttributes.characterName) {
|
||
if (!virtualFields.characterName) {
|
||
virtualFields.characterName = model.rawAttributes.characterName;
|
||
delete model.rawAttributes.characterName;
|
||
console.log(` ⚠️ Explicitly removed VIRTUAL field: characterName from Notification model`);
|
||
}
|
||
}
|
||
|
||
// constraints: false wird von Sequelize ignoriert wenn Associations vorhanden sind
|
||
// Wir müssen die Associations temporär entfernen, um Foreign Keys zu verhindern
|
||
const originalAssociations = model.associations ? { ...model.associations } : {};
|
||
const associationKeys = Object.keys(originalAssociations);
|
||
|
||
try {
|
||
// Entferne temporär alle Associations, damit Sequelize keine Foreign Keys erstellt
|
||
// Dies muss innerhalb des try Blocks sein, damit die Wiederherstellung im finally Block garantiert ist
|
||
if (associationKeys.length > 0) {
|
||
console.log(` ⚠️ Temporarily removing ${associationKeys.length} associations from ${model.name} to prevent FK creation`);
|
||
// Lösche alle Associations temporär
|
||
for (const key of associationKeys) {
|
||
delete model.associations[key];
|
||
}
|
||
}
|
||
|
||
// Entferne bestehende Foreign Keys vor dem Sync, damit Sequelize sie nicht aktualisiert
|
||
try {
|
||
const tableName = model.tableName;
|
||
// Schema kann eine Funktion sein, daher prüfen wir model.options.schema direkt
|
||
const schema = model.options?.schema || 'public';
|
||
|
||
console.log(` 🔍 Checking for foreign keys in ${schema}.${tableName}...`);
|
||
// Verwende queryWithTimeout für Foreign Key Queries
|
||
const foreignKeys = await queryWithTimeout(`
|
||
SELECT tc.constraint_name
|
||
FROM information_schema.table_constraints AS tc
|
||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||
AND tc.table_name = '${tableName.replace(/'/g, "''")}'
|
||
AND tc.table_schema = '${schema.replace(/'/g, "''")}'
|
||
`, 10000, `FK check for ${model.name}`);
|
||
|
||
if (foreignKeys && foreignKeys.length > 0) {
|
||
console.log(` ⚠️ Found ${foreignKeys.length} existing foreign keys:`, foreignKeys.map(fk => fk.constraint_name).join(', '));
|
||
console.log(` ⚠️ Removing ${foreignKeys.length} existing foreign keys from ${model.name} (schema: ${schema}) before sync`);
|
||
for (const fk of foreignKeys) {
|
||
try {
|
||
console.log(` 🗑️ Dropping constraint: ${fk.constraint_name}`);
|
||
await queryWithTimeout(`
|
||
ALTER TABLE "${schema}"."${tableName}"
|
||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||
`, 10000, `Drop FK ${fk.constraint_name}`);
|
||
} catch (dropError) {
|
||
if (dropError.message.includes('Timeout')) {
|
||
console.warn(` ⚠️ Timeout beim Entfernen von ${fk.constraint_name} - überspringe...`);
|
||
} else {
|
||
console.warn(` ⚠️ Konnte ${fk.constraint_name} nicht entfernen:`, dropError.message?.substring(0, 100));
|
||
}
|
||
}
|
||
}
|
||
console.log(` ✅ Foreign key removal completed for ${model.name}`);
|
||
} else {
|
||
console.log(` ✅ No foreign keys found for ${model.name}`);
|
||
}
|
||
} catch (fkError) {
|
||
// Ignoriere Timeout-Fehler - nicht kritisch
|
||
if (fkError.message && fkError.message.includes('Timeout')) {
|
||
console.warn(` ⚠️ Timeout beim Prüfen der Foreign Keys für ${model.name} - überspringe...`);
|
||
} else {
|
||
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message?.substring(0, 100));
|
||
}
|
||
}
|
||
|
||
console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
|
||
try {
|
||
// Versuche doppelte pg_description Einträge vor dem Sync zu bereinigen
|
||
// Hinweis: Benötigt Superuser-Rechte oder spezielle Berechtigungen
|
||
// Überspringe diese Query, da sie oft fehlschlägt und Verbindungen blockiert
|
||
// Die doppelten pg_description Einträge sind nicht kritisch für die Funktionalität
|
||
try {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
// Verwende queryWithTimeout mit kurzem Timeout, da diese Query oft hängt
|
||
await queryWithTimeout(`
|
||
DELETE FROM pg_catalog.pg_description d1
|
||
WHERE d1.objoid IN (
|
||
SELECT c.oid
|
||
FROM pg_catalog.pg_class c
|
||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||
WHERE c.relname = '${tableName.replace(/'/g, "''")}'
|
||
AND n.nspname = '${schema.replace(/'/g, "''")}'
|
||
)
|
||
AND EXISTS (
|
||
SELECT 1
|
||
FROM pg_catalog.pg_description d2
|
||
WHERE d2.objoid = d1.objoid
|
||
AND d2.objsubid = d1.objsubid
|
||
AND d2.ctid < d1.ctid
|
||
)
|
||
`, 5000, `pg_description cleanup for ${model.name}`);
|
||
} catch (descError) {
|
||
// Ignoriere alle Fehler - diese Query ist optional und blockiert oft
|
||
if (descError.message && (descError.message.includes('Berechtigung') || descError.message.includes('timeout') || descError.message.includes('Timeout'))) {
|
||
// Stille Warnung - nicht kritisch
|
||
} else {
|
||
// Nur bei unerwarteten Fehlern warnen
|
||
console.warn(` ⚠️ Could not clean up duplicate pg_description entries for ${model.name}:`, descError.message?.substring(0, 100));
|
||
}
|
||
}
|
||
|
||
// Verwende syncModelWithTimeout für große Tabellen
|
||
const syncSuccess = await syncModelWithTimeout(model, 60000);
|
||
if (!syncSuccess) {
|
||
console.warn(` ⚠️ ${model.name} wurde übersprungen aufgrund von Timeout`);
|
||
// Restore associations before continuing
|
||
if (associationKeys.length > 0) {
|
||
model.associations = originalAssociations;
|
||
}
|
||
// Restore virtual fields
|
||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||
model.rawAttributes[key] = attr;
|
||
}
|
||
continue;
|
||
}
|
||
} catch (syncError) {
|
||
// Wenn Sequelize einen "mehr als eine Zeile" Fehler hat, überspringe das Model
|
||
// Dies kann durch doppelte pg_description Einträge oder mehrere Tabellen mit demselben Namen verursacht werden
|
||
if (syncError.message && (syncError.message.includes('mehr als eine Zeile') || syncError.message.includes('more than one row'))) {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
console.error(` ❌ Cannot sync ${model.name} (${schema}.${tableName}) due to Sequelize describeTable error`);
|
||
console.error(` ❌ This is likely caused by multiple tables with the same name in different schemas`);
|
||
console.error(` ❌ or duplicate pg_description entries (requires superuser to fix)`);
|
||
console.error(` ⚠️ Skipping sync for ${model.name} - Schema is likely already correct`);
|
||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||
continue;
|
||
}
|
||
// Wenn eine referenzierte Tabelle noch nicht existiert, erstelle die Tabelle ohne Foreign Key
|
||
else if (syncError.message && (syncError.message.includes('existiert nicht') || syncError.message.includes('does not exist') || syncError.message.includes('Relation'))) {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
console.warn(` ⚠️ Cannot create ${model.name} (${schema}.${tableName}) with Foreign Key - referenced table does not exist yet`);
|
||
console.warn(` ⚠️ Attempting to create table without Foreign Key constraint...`);
|
||
|
||
try {
|
||
// Prüfe, ob die Tabelle bereits existiert
|
||
const [tableExists] = await sequelize.query(`
|
||
SELECT EXISTS (
|
||
SELECT 1 FROM information_schema.tables
|
||
WHERE table_schema = :schema
|
||
AND table_name = :tableName
|
||
) as exists
|
||
`, {
|
||
replacements: { schema, tableName },
|
||
type: sequelize.QueryTypes.SELECT
|
||
});
|
||
|
||
if (tableExists && tableExists.exists) {
|
||
console.log(` ℹ️ Table ${schema}.${tableName} already exists, skipping creation`);
|
||
continue;
|
||
}
|
||
|
||
// Erstelle die Tabelle manuell ohne Foreign Key
|
||
// Verwende queryInterface.createTable mit den Attributen, aber ohne Foreign Keys
|
||
const queryInterface = sequelize.getQueryInterface();
|
||
const attributes = {};
|
||
|
||
// Kopiere alle Attribute aus dem Model, aber entferne references
|
||
for (const [key, attr] of Object.entries(model.rawAttributes)) {
|
||
attributes[key] = { ...attr };
|
||
// Entferne references, damit kein Foreign Key erstellt wird
|
||
if (attributes[key].references) {
|
||
delete attributes[key].references;
|
||
}
|
||
}
|
||
|
||
// Erstelle die Tabelle mit queryInterface.createTable ohne Foreign Keys
|
||
await queryInterface.createTable(tableName, attributes, {
|
||
schema,
|
||
// Stelle sicher, dass keine Foreign Keys erstellt werden
|
||
charset: model.options?.charset,
|
||
collate: model.options?.collate
|
||
});
|
||
console.log(` ✅ Table ${schema}.${tableName} created successfully without Foreign Key`);
|
||
} catch (createError) {
|
||
console.error(` ❌ Failed to create table ${schema}.${tableName} without Foreign Key:`, createError.message);
|
||
console.error(` ⚠️ Skipping ${model.name} - will retry after dependencies are created`);
|
||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||
continue;
|
||
}
|
||
}
|
||
// Wenn Sequelize einen Foreign Key Constraint Fehler hat, entferne verwaiste Einträge oder überspringe das Model
|
||
else if (syncError.name === 'SequelizeForeignKeyConstraintError' || (syncError.message && (syncError.message.includes('FOREIGN KEY') || syncError.message.includes('Fremdschlüssel')))) {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
console.error(` ❌ Cannot sync ${model.name} (${schema}.${tableName}) due to Foreign Key Constraint Error`);
|
||
console.error(` ❌ Detail: ${syncError.parent?.detail || syncError.message}`);
|
||
console.error(` ⚠️ This usually means there are orphaned records. Cleanup should have removed them.`);
|
||
console.error(` ⚠️ Skipping sync for ${model.name} - please check and fix orphaned records manually`);
|
||
// Überspringe dieses Model und fahre mit dem nächsten fort
|
||
continue;
|
||
}
|
||
// Wenn Sequelize versucht, Foreign Keys zu erstellen, entferne sie nach dem Fehler
|
||
else if (syncError.message && syncError.message.includes('REFERENCES')) {
|
||
console.log(` ⚠️ Sequelize tried to create FK despite constraints: false, removing any created FKs...`);
|
||
try {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
const foreignKeys = await sequelize.query(`
|
||
SELECT tc.constraint_name
|
||
FROM information_schema.table_constraints AS tc
|
||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||
AND tc.table_name = :tableName
|
||
AND tc.table_schema = :schema
|
||
`, {
|
||
replacements: { tableName, schema },
|
||
type: sequelize.QueryTypes.SELECT
|
||
});
|
||
|
||
if (foreignKeys && foreignKeys.length > 0) {
|
||
for (const fk of foreignKeys) {
|
||
await sequelize.query(`
|
||
ALTER TABLE "${schema}"."${tableName}"
|
||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||
`);
|
||
}
|
||
}
|
||
// Versuche Sync erneut ohne Foreign Keys
|
||
console.log(` 🔄 Retrying sync without foreign keys...`);
|
||
await model.sync({ alter: true, force: false, constraints: false });
|
||
} catch (retryError) {
|
||
console.error(` ❌ Retry failed:`, retryError.message);
|
||
console.error(` ❌ Original sync error:`, syncError.message);
|
||
// Kombiniere beide Fehler für besseres Debugging
|
||
const combinedError = new Error(`Sync failed: ${syncError.message}. Retry also failed: ${retryError.message}`);
|
||
combinedError.originalError = syncError;
|
||
combinedError.retryError = retryError;
|
||
throw combinedError;
|
||
}
|
||
} else {
|
||
throw syncError;
|
||
}
|
||
}
|
||
|
||
// Entferne alle Foreign Keys, die Sequelize möglicherweise trotzdem erstellt hat
|
||
try {
|
||
const tableName = model.tableName;
|
||
const schema = model.options?.schema || 'public';
|
||
const foreignKeys = await sequelize.query(`
|
||
SELECT tc.constraint_name
|
||
FROM information_schema.table_constraints AS tc
|
||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||
AND tc.table_name = :tableName
|
||
AND tc.table_schema = :schema
|
||
`, {
|
||
replacements: { tableName, schema },
|
||
type: sequelize.QueryTypes.SELECT
|
||
});
|
||
|
||
if (foreignKeys && foreignKeys.length > 0) {
|
||
console.log(` ⚠️ Sequelize created ${foreignKeys.length} foreign keys despite constraints: false, removing them...`);
|
||
for (const fk of foreignKeys) {
|
||
await sequelize.query(`
|
||
ALTER TABLE "${schema}"."${tableName}"
|
||
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
||
`);
|
||
}
|
||
}
|
||
} catch (fkError) {
|
||
console.warn(` ⚠️ Could not check/remove foreign keys after sync:`, fkError.message);
|
||
}
|
||
} finally {
|
||
// Stelle die Associations wieder her (IMMER, auch bei Fehlern)
|
||
if (associationKeys.length > 0) {
|
||
console.log(` ✅ Restoring ${associationKeys.length} associations for ${model.name}`);
|
||
model.associations = originalAssociations;
|
||
}
|
||
|
||
// Restore VIRTUAL fields after sync
|
||
for (const [key, attr] of Object.entries(virtualFields)) {
|
||
model.rawAttributes[key] = attr;
|
||
}
|
||
}
|
||
}
|
||
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 };
|