Files
yourpart3/backend/utils/sequelize.js

670 lines
30 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 {
// Versuche doppelte pg_description Einträge vor dem Sync zu bereinigen
// Hinweis: Benötigt Superuser-Rechte oder spezielle Berechtigungen
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) {
// Ignoriere Berechtigungsfehler - das ist normal, wenn der Benutzer keine Superuser-Rechte hat
if (descError.message && descError.message.includes('Berechtigung')) {
console.log(` Cannot clean up duplicate pg_description entries (requires superuser privileges): ${model.name}`);
} else {
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, ü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 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 };