Files
yourpart3/backend/utils/sequelize.js

544 lines
22 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`);
}
}
try {
// 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);
// Entferne temporär alle Associations, damit Sequelize keine Foreign Keys erstellt
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;
const schema = model.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(` ⚠️ Removing ${foreignKeys.length} existing foreign keys from ${model.name} before sync`);
for (const fk of foreignKeys) {
await sequelize.query(`
ALTER TABLE "${schema}"."${tableName}"
DROP CONSTRAINT IF EXISTS "${fk.constraint_name}"
`);
}
}
} catch (fkError) {
console.warn(` ⚠️ Could not remove foreign keys for ${model.name}:`, fkError.message);
}
console.log(` 🔄 Syncing model ${model.name} with constraints: false`);
await model.sync({ alter: true, force: false, constraints: false });
// Stelle die Associations wieder her
if (associationKeys.length > 0) {
console.log(` ✅ Restoring ${associationKeys.length} associations for ${model.name}`);
model.associations = originalAssociations;
}
} finally {
// 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 };