Änderungen: - Integration des Taxi-Minispiels mit neuen Routen und Komponenten im Backend und Frontend. - Erstellung von Modellen und Datenbank-Schemas für das Taxi-Spiel, einschließlich TaxiGameState, TaxiLevelStats und TaxiMap. - Erweiterung der Navigationsstruktur und der Benutzeroberfläche, um das Taxi-Spiel und die zugehörigen Tools zu unterstützen. - Aktualisierung der Übersetzungen für das Taxi-Minispiel in Deutsch und Englisch. Diese Anpassungen erweitern die Funktionalität der Anwendung um ein neues Minispiel und verbessern die Benutzererfahrung durch neue Features und Inhalte.
418 lines
15 KiB
JavaScript
418 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');
|
|
await sequelize.query('CREATE SCHEMA IF NOT EXISTS taxi');
|
|
};
|
|
|
|
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 };
|