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