689 lines
31 KiB
JavaScript
689 lines
31 KiB
JavaScript
import { Sequelize, DataTypes } from 'sequelize';
|
|
import dotenv from 'dotenv';
|
|
|
|
dotenv.config();
|
|
|
|
// 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
|
|
},
|
|
});
|
|
|
|
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) {
|
|
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)) {
|
|
// 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}...`);
|
|
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(` ⚠️ 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) {
|
|
console.log(` 🗑️ Dropping constraint: ${fk.constraint_name}`);
|
|
await sequelize.query(`
|
|
ALTER TABLE "${schema}"."${tableName}"
|
|
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}" CASCADE
|
|
`);
|
|
}
|
|
console.log(` ✅ All foreign keys removed for ${model.name}`);
|
|
} else {
|
|
console.log(` ✅ No foreign keys found for ${model.name}`);
|
|
}
|
|
} catch (fkError) {
|
|
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message);
|
|
console.warn(` ⚠️ Error details:`, fkError);
|
|
}
|
|
|
|
console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
|
|
try {
|
|
// Bereinige doppelte pg_description Einträge vor dem Sync, um "mehr als eine Zeile" Fehler zu vermeiden
|
|
try {
|
|
const tableName = model.tableName;
|
|
const schema = model.options?.schema || 'public';
|
|
// Verwende direkte Parameter-Einsetzung, da DO $$ keine Parameterbindung unterstützt
|
|
// Die Parameter sind sicher, da sie von Sequelize-Modell-Eigenschaften kommen
|
|
await sequelize.query(`
|
|
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
|
|
)
|
|
`);
|
|
} catch (descError) {
|
|
console.warn(` ⚠️ Could not clean up duplicate pg_description entries for ${model.name}:`, descError.message);
|
|
}
|
|
|
|
await model.sync({ alter: true, force: false, constraints: false });
|
|
} catch (syncError) {
|
|
// Wenn Sequelize einen "mehr als eine Zeile" Fehler hat, bereinige pg_description und versuche erneut
|
|
if (syncError.message && (syncError.message.includes('mehr als eine Zeile') || syncError.message.includes('more than one row'))) {
|
|
console.log(` ⚠️ Sequelize encountered duplicate pg_description entries, cleaning up and retrying...`);
|
|
try {
|
|
const tableName = model.tableName;
|
|
const schema = model.options?.schema || 'public';
|
|
// Verwende direkte Parameter-Einsetzung, da DO $$ keine Parameterbindung unterstützt
|
|
// Die Parameter sind sicher, da sie von Sequelize-Modell-Eigenschaften kommen
|
|
await sequelize.query(`
|
|
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
|
|
)
|
|
`);
|
|
// Versuche Sync erneut nach Bereinigung
|
|
console.log(` 🔄 Retrying sync after cleaning duplicate pg_description entries...`);
|
|
await model.sync({ alter: true, force: false, constraints: false });
|
|
} catch (retryError) {
|
|
console.error(` ❌ Retry after pg_description cleanup failed:`, retryError.message);
|
|
// Kombiniere beide Fehler für besseres Debugging
|
|
const combinedError = new Error(`Sync failed: ${syncError.message}. Retry after pg_description cleanup also failed: ${retryError.message}`);
|
|
combinedError.originalError = syncError;
|
|
combinedError.retryError = retryError;
|
|
throw combinedError;
|
|
}
|
|
}
|
|
// 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 };
|