// syncDatabase.js import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js'; import initializeTypes from './initializeTypes.js'; import initializeSettings from './initializeSettings.js'; import initializeUserRights from './initializeUserRights.js'; import initializeImageTypes from './initializeImageTypes.js'; import initializeFalukant from './initializeFalukant.js'; import setupAssociations from '../models/associations.js'; import models from '../models/index.js'; import { createTriggers } from '../models/trigger.js'; import initializeForum from './initializeForum.js'; import initializeChat from './initializeChat.js'; import initializeMatch3Data from './initializeMatch3.js'; import updateExistingMatch3Levels from './updateExistingMatch3Levels.js'; import initializeTaxi from './initializeTaxi.js'; // Normale Synchronisation (nur bei STAGE=dev Schema-Updates) const syncDatabase = async () => { try { // Zeige den aktuellen Stage an const currentStage = process.env.STAGE || 'nicht gesetzt'; console.log(`🚀 Starte Datenbank-Synchronisation (Stage: ${currentStage})`); if (currentStage !== 'dev') { console.log('⚠️ WARNUNG: Automatische Schema-Updates sind deaktiviert'); console.log('💡 Setze STAGE=dev in der .env Datei für automatische Schema-Updates'); console.log('🔒 Produktionsmodus: Nur normale Synchronisation ohne Schema-Änderungen'); } else { console.log('✅ Entwicklungsmodus aktiv - Schema-Updates sind aktiviert'); } console.log("Initializing database schemas..."); await initializeDatabase(); // Vokabeltrainer: Tabellen sicherstellen (auch ohne manuell ausgeführte Migrations) // Hintergrund: In Produktion sind Schema-Updates deaktiviert, und Migrations werden nicht automatisch ausgeführt. // Damit API/Menu nicht mit "relation does not exist" (42P01) scheitert, legen wir die Tabellen idempotent an. console.log("Ensuring Vocab-Trainer tables exist..."); try { await sequelize.query(` CREATE TABLE IF NOT EXISTS community.vocab_language ( id SERIAL PRIMARY KEY, owner_user_id INTEGER NOT NULL, name TEXT NOT NULL, share_code TEXT NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_language_owner_fk FOREIGN KEY (owner_user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_language_share_code_uniq UNIQUE (share_code) ); CREATE TABLE IF NOT EXISTS community.vocab_language_subscription ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, language_id INTEGER NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_language_subscription_user_fk FOREIGN KEY (user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_language_subscription_language_fk FOREIGN KEY (language_id) REFERENCES community.vocab_language(id) ON DELETE CASCADE, CONSTRAINT vocab_language_subscription_uniq UNIQUE (user_id, language_id) ); CREATE INDEX IF NOT EXISTS vocab_language_owner_idx ON community.vocab_language(owner_user_id); CREATE INDEX IF NOT EXISTS vocab_language_subscription_user_idx ON community.vocab_language_subscription(user_id); CREATE INDEX IF NOT EXISTS vocab_language_subscription_language_idx ON community.vocab_language_subscription(language_id); CREATE TABLE IF NOT EXISTS community.vocab_chapter ( id SERIAL PRIMARY KEY, language_id INTEGER NOT NULL, title TEXT NOT NULL, created_by_user_id INTEGER NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_chapter_language_fk FOREIGN KEY (language_id) REFERENCES community.vocab_language(id) ON DELETE CASCADE, CONSTRAINT vocab_chapter_creator_fk FOREIGN KEY (created_by_user_id) REFERENCES community."user"(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS vocab_chapter_language_idx ON community.vocab_chapter(language_id); CREATE TABLE IF NOT EXISTS community.vocab_lexeme ( id SERIAL PRIMARY KEY, language_id INTEGER NOT NULL, text TEXT NOT NULL, normalized TEXT NOT NULL, created_by_user_id INTEGER NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_lexeme_language_fk FOREIGN KEY (language_id) REFERENCES community.vocab_language(id) ON DELETE CASCADE, CONSTRAINT vocab_lexeme_creator_fk FOREIGN KEY (created_by_user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_lexeme_unique_per_language UNIQUE (language_id, normalized) ); CREATE INDEX IF NOT EXISTS vocab_lexeme_language_idx ON community.vocab_lexeme(language_id); CREATE TABLE IF NOT EXISTS community.vocab_chapter_lexeme ( id SERIAL PRIMARY KEY, chapter_id INTEGER NOT NULL, learning_lexeme_id INTEGER NOT NULL, reference_lexeme_id INTEGER NOT NULL, created_by_user_id INTEGER NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_chlex_chapter_fk FOREIGN KEY (chapter_id) REFERENCES community.vocab_chapter(id) ON DELETE CASCADE, CONSTRAINT vocab_chlex_learning_fk FOREIGN KEY (learning_lexeme_id) REFERENCES community.vocab_lexeme(id) ON DELETE CASCADE, CONSTRAINT vocab_chlex_reference_fk FOREIGN KEY (reference_lexeme_id) REFERENCES community.vocab_lexeme(id) ON DELETE CASCADE, CONSTRAINT vocab_chlex_creator_fk FOREIGN KEY (created_by_user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_chlex_unique UNIQUE (chapter_id, learning_lexeme_id, reference_lexeme_id) ); CREATE INDEX IF NOT EXISTS vocab_chlex_chapter_idx ON community.vocab_chapter_lexeme(chapter_id); CREATE INDEX IF NOT EXISTS vocab_chlex_learning_idx ON community.vocab_chapter_lexeme(learning_lexeme_id); CREATE INDEX IF NOT EXISTS vocab_chlex_reference_idx ON community.vocab_chapter_lexeme(reference_lexeme_id); // Kurs-Tabellen CREATE TABLE IF NOT EXISTS community.vocab_course ( id SERIAL PRIMARY KEY, owner_user_id INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, language_id INTEGER NOT NULL, native_language_id INTEGER, difficulty_level INTEGER DEFAULT 1, is_public BOOLEAN DEFAULT false, share_code TEXT, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_course_owner_fk FOREIGN KEY (owner_user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_course_language_fk FOREIGN KEY (language_id) REFERENCES community.vocab_language(id) ON DELETE CASCADE, CONSTRAINT vocab_course_native_language_fk FOREIGN KEY (native_language_id) REFERENCES community.vocab_language(id) ON DELETE SET NULL, CONSTRAINT vocab_course_share_code_uniq UNIQUE (share_code) ); CREATE TABLE IF NOT EXISTS community.vocab_course_lesson ( id SERIAL PRIMARY KEY, course_id INTEGER NOT NULL, chapter_id INTEGER, lesson_number INTEGER NOT NULL, title TEXT NOT NULL, description TEXT, week_number INTEGER, day_number INTEGER, lesson_type TEXT DEFAULT 'vocab', audio_url TEXT, cultural_notes TEXT, target_minutes INTEGER, target_score_percent INTEGER DEFAULT 80, requires_review BOOLEAN DEFAULT false, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_course_lesson_course_fk FOREIGN KEY (course_id) REFERENCES community.vocab_course(id) ON DELETE CASCADE, CONSTRAINT vocab_course_lesson_chapter_fk FOREIGN KEY (chapter_id) REFERENCES community.vocab_chapter(id) ON DELETE CASCADE, CONSTRAINT vocab_course_lesson_unique UNIQUE (course_id, lesson_number) ); CREATE TABLE IF NOT EXISTS community.vocab_course_enrollment ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, course_id INTEGER NOT NULL, enrolled_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_course_enrollment_user_fk FOREIGN KEY (user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_course_enrollment_course_fk FOREIGN KEY (course_id) REFERENCES community.vocab_course(id) ON DELETE CASCADE, CONSTRAINT vocab_course_enrollment_unique UNIQUE (user_id, course_id) ); CREATE TABLE IF NOT EXISTS community.vocab_course_progress ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, course_id INTEGER NOT NULL, lesson_id INTEGER NOT NULL, completed BOOLEAN DEFAULT false, score INTEGER DEFAULT 0, last_accessed_at TIMESTAMP WITHOUT TIME ZONE, completed_at TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT vocab_course_progress_user_fk FOREIGN KEY (user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_course_progress_course_fk FOREIGN KEY (course_id) REFERENCES community.vocab_course(id) ON DELETE CASCADE, CONSTRAINT vocab_course_progress_lesson_fk FOREIGN KEY (lesson_id) REFERENCES community.vocab_course_lesson(id) ON DELETE CASCADE, CONSTRAINT vocab_course_progress_unique UNIQUE (user_id, lesson_id) ); CREATE INDEX IF NOT EXISTS vocab_course_owner_idx ON community.vocab_course(owner_user_id); CREATE INDEX IF NOT EXISTS vocab_course_language_idx ON community.vocab_course(language_id); CREATE INDEX IF NOT EXISTS vocab_course_native_language_idx ON community.vocab_course(native_language_id); CREATE INDEX IF NOT EXISTS vocab_course_public_idx ON community.vocab_course(is_public); CREATE INDEX IF NOT EXISTS vocab_course_lesson_course_idx ON community.vocab_course_lesson(course_id); CREATE INDEX IF NOT EXISTS vocab_course_lesson_chapter_idx ON community.vocab_course_lesson(chapter_id); CREATE INDEX IF NOT EXISTS vocab_course_lesson_week_idx ON community.vocab_course_lesson(course_id, week_number); CREATE INDEX IF NOT EXISTS vocab_course_lesson_type_idx ON community.vocab_course_lesson(lesson_type); CREATE INDEX IF NOT EXISTS vocab_course_enrollment_user_idx ON community.vocab_course_enrollment(user_id); CREATE INDEX IF NOT EXISTS vocab_course_enrollment_course_idx ON community.vocab_course_enrollment(course_id); CREATE INDEX IF NOT EXISTS vocab_course_progress_user_idx ON community.vocab_course_progress(user_id); CREATE INDEX IF NOT EXISTS vocab_course_progress_course_idx ON community.vocab_course_progress(course_id); CREATE INDEX IF NOT EXISTS vocab_course_progress_lesson_idx ON community.vocab_course_progress(lesson_id); // Grammatik-Übungstypen CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_type ( id SERIAL PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW() ); // Grammatik-Übungen CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise ( id SERIAL PRIMARY KEY, lesson_id INTEGER NOT NULL, exercise_type_id INTEGER NOT NULL, exercise_number INTEGER NOT NULL, title TEXT NOT NULL, instruction TEXT, question_data JSONB NOT NULL, answer_data JSONB NOT NULL, explanation TEXT, created_by_user_id INTEGER NOT NULL, created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(), CONSTRAINT vocab_grammar_exercise_lesson_fk FOREIGN KEY (lesson_id) REFERENCES community.vocab_course_lesson(id) ON DELETE CASCADE, CONSTRAINT vocab_grammar_exercise_type_fk FOREIGN KEY (exercise_type_id) REFERENCES community.vocab_grammar_exercise_type(id) ON DELETE CASCADE, CONSTRAINT vocab_grammar_exercise_creator_fk FOREIGN KEY (created_by_user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_grammar_exercise_unique UNIQUE (lesson_id, exercise_number) ); // Fortschritt für Grammatik-Übungen CREATE TABLE IF NOT EXISTS community.vocab_grammar_exercise_progress ( id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL, exercise_id INTEGER NOT NULL, attempts INTEGER DEFAULT 0, correct_attempts INTEGER DEFAULT 0, last_attempt_at TIMESTAMP WITHOUT TIME ZONE, completed BOOLEAN DEFAULT false, completed_at TIMESTAMP WITHOUT TIME ZONE, CONSTRAINT vocab_grammar_exercise_progress_user_fk FOREIGN KEY (user_id) REFERENCES community."user"(id) ON DELETE CASCADE, CONSTRAINT vocab_grammar_exercise_progress_exercise_fk FOREIGN KEY (exercise_id) REFERENCES community.vocab_grammar_exercise(id) ON DELETE CASCADE, CONSTRAINT vocab_grammar_exercise_progress_unique UNIQUE (user_id, exercise_id) ); CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_lesson_idx ON community.vocab_grammar_exercise(lesson_id); CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_type_idx ON community.vocab_grammar_exercise(exercise_type_id); CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_user_idx ON community.vocab_grammar_exercise_progress(user_id); CREATE INDEX IF NOT EXISTS vocab_grammar_exercise_progress_exercise_idx ON community.vocab_grammar_exercise_progress(exercise_id); -- Standard-Übungstypen einfügen INSERT INTO community.vocab_grammar_exercise_type (name, description) VALUES ('gap_fill', 'Lückentext-Übung'), ('multiple_choice', 'Multiple-Choice-Fragen'), ('sentence_building', 'Satzbau-Übung'), ('transformation', 'Satzumformung'), ('conjugation', 'Konjugations-Übung'), ('declension', 'Deklinations-Übung') ON CONFLICT (name) DO NOTHING; `); console.log("✅ Vocab-Trainer Tabellen sind vorhanden."); console.log("✅ Vocab-Course Tabellen sind vorhanden."); console.log("✅ Vocab-Grammar-Exercise Tabellen sind vorhanden."); } catch (e) { console.warn('⚠️ Konnte Vocab-Trainer Tabellen nicht sicherstellen:', e?.message || e); } // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt console.log("Pre-ensure Taxi columns (traffic_light) ..."); try { await sequelize.query(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile' AND column_name = 'traffic_light' ) THEN ALTER TABLE taxi.taxi_map_tile ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false; END IF; END $$; `); console.log("✅ traffic_light-Spalte ist vorhanden"); } catch (e) { console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); } // Cleanup: Entferne verwaiste Einträge vor Schema-Updates (nur wenn Schema-Updates aktiviert) if (currentStage === 'dev') { console.log("Cleaning up orphaned entries..."); try { // Cleanup user_param_visibility const result1 = await sequelize.query(` DELETE FROM community.user_param_visibility WHERE param_id NOT IN ( SELECT id FROM community.user_param ); `); const deletedCount1 = result1[1] || 0; if (deletedCount1 > 0) { console.log(`✅ ${deletedCount1} verwaiste user_param_visibility Einträge entfernt`); } // Cleanup stock mit ungültigen branch_id (0 oder nicht existierend) const result2 = await sequelize.query(` DELETE FROM falukant_data.stock WHERE branch_id = 0 OR branch_id NOT IN ( SELECT id FROM falukant_data.branch ); `); const deletedCount2 = result2[1] || 0; if (deletedCount2 > 0) { console.log(`✅ ${deletedCount2} verwaiste stock Einträge entfernt`); } // Cleanup knowledge mit ungültigen character_id oder product_id const result3 = await sequelize.query(` DELETE FROM falukant_data.knowledge WHERE character_id NOT IN ( SELECT id FROM falukant_data.character ) OR product_id NOT IN ( SELECT id FROM falukant_type.product ); `); const deletedCount3 = result3[1] || 0; if (deletedCount3 > 0) { console.log(`✅ ${deletedCount3} verwaiste knowledge Einträge entfernt`); } // Cleanup notification mit ungültigen user_id const result4 = await sequelize.query(` DELETE FROM falukant_log.notification WHERE user_id NOT IN ( SELECT id FROM falukant_data.falukant_user ); `); const deletedCount4 = result4[1] || 0; if (deletedCount4 > 0) { console.log(`✅ ${deletedCount4} verwaiste notification Einträge entfernt`); } // Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id const result5 = await sequelize.query(` DELETE FROM falukant_log.promotional_gift WHERE sender_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR recipient_character_id NOT IN ( SELECT id FROM falukant_data.character ); `); const deletedCount5 = result5[1] || 0; if (deletedCount5 > 0) { console.log(`✅ ${deletedCount5} verwaiste promotional_gift Einträge entfernt`); } // Cleanup user_house mit ungültigen house_type_id oder user_id const result6 = await sequelize.query(` DELETE FROM falukant_data.user_house WHERE house_type_id NOT IN ( SELECT id FROM falukant_type.house ) OR user_id NOT IN ( SELECT id FROM falukant_data.falukant_user ); `); const deletedCount6 = result6[1] || 0; if (deletedCount6 > 0) { console.log(`✅ ${deletedCount6} verwaiste user_house Einträge entfernt`); } // Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id const result7 = await sequelize.query(` DELETE FROM falukant_data.child_relation WHERE father_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR mother_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR child_character_id NOT IN ( SELECT id FROM falukant_data.character ); `); const deletedCount7 = result7[1] || 0; if (deletedCount7 > 0) { console.log(`✅ ${deletedCount7} verwaiste child_relation Einträge entfernt`); } if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0) { console.log("✅ Keine verwaisten Einträge gefunden"); } } catch (e) { console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e); } } console.log("Setting up associations..."); setupAssociations(); console.log("Synchronizing models..."); await syncModelsWithUpdates(models); console.log("Initializing settings..."); await initializeSettings(); console.log("Initializing types..."); await initializeTypes(); console.log("Initializing user rights..."); await initializeUserRights(); console.log("Initializing image types..."); await initializeImageTypes(); console.log("Initializing forums..."); await initializeForum(); console.log("Initializing Falukant..."); await initializeFalukant(); console.log("Creating triggers..."); await createTriggers(); console.log("Initializing chat..."); await initializeChat(); // Match3-Initialisierung NACH der Model-Synchronisation console.log("Initializing Match3..."); await initializeMatch3Data(); // Match3-Levels aktualisieren NACH der Initialisierung console.log("Updating existing Match3 levels..."); await updateExistingMatch3Levels(); console.log("Initializing Taxi..."); await initializeTaxi(); console.log('Database synchronization complete.'); } catch (error) { console.error('Unable to synchronize the database:', error); } }; // Deployment-Synchronisation (immer Schema-Updates) const syncDatabaseForDeployment = async () => { try { // WICHTIG: Bei Caching-Problemen das Script neu starten // Node.js cached ES-Module, daher müssen Models neu geladen werden console.log('📦 Lade Models neu (Node.js Module-Cache wird verwendet)...'); // Zeige den aktuellen Stage an const currentStage = process.env.STAGE || 'nicht gesetzt'; console.log(`🚀 Starte Datenbank-Synchronisation für Deployment (Stage: ${currentStage})`); console.log('✅ Deployment-Modus: Schema-Updates sind immer aktiviert'); console.log("Initializing database schemas..."); await initializeDatabase(); // Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt console.log("Pre-ensure Taxi columns (traffic_light) ..."); try { await sequelize.query(` DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile' AND column_name = 'traffic_light' ) THEN ALTER TABLE taxi.taxi_map_tile ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false; END IF; END $$; `); console.log("✅ traffic_light-Spalte ist vorhanden"); } catch (e) { console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e); } // Migration: Transport product_id und size nullable machen console.log("Making transport product_id and size nullable..."); try { await sequelize.query(` DO $$ BEGIN -- Prüfe ob product_id NOT NULL Constraint existiert IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'transport' AND column_name = 'product_id' AND is_nullable = 'NO' ) THEN ALTER TABLE falukant_data.transport ALTER COLUMN product_id DROP NOT NULL; RAISE NOTICE 'product_id NOT NULL Constraint entfernt'; END IF; -- Prüfe ob size NOT NULL Constraint existiert IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_schema = 'falukant_data' AND table_name = 'transport' AND column_name = 'size' AND is_nullable = 'NO' ) THEN ALTER TABLE falukant_data.transport ALTER COLUMN size DROP NOT NULL; RAISE NOTICE 'size NOT NULL Constraint entfernt'; END IF; END $$; `); console.log("✅ Transport product_id und size sind jetzt nullable"); } catch (e) { console.warn('⚠️ Konnte Transport-Spalten nicht nullable machen:', e?.message || e); } // Cleanup: Entferne verwaiste Einträge vor Schema-Updates console.log("Cleaning up orphaned entries..."); try { // Cleanup user_param_visibility (optimiert mit LEFT JOIN) console.log(" → Prüfe user_param_visibility..."); const result1 = await sequelize.query(` DELETE FROM community.user_param_visibility WHERE param_id NOT IN ( SELECT id FROM community.user_param ); `, { timeout: 30000 }); const deletedCount1 = result1[1] || 0; if (deletedCount1 > 0) { console.log(`✅ ${deletedCount1} verwaiste user_param_visibility Einträge entfernt`); } // Cleanup stock mit ungültigen branch_id (0 oder nicht existierend) console.log(" → Prüfe stock..."); const result2 = await sequelize.query(` DELETE FROM falukant_data.stock WHERE branch_id = 0 OR branch_id NOT IN ( SELECT id FROM falukant_data.branch ); `, { timeout: 30000 }); const deletedCount2 = result2[1] || 0; if (deletedCount2 > 0) { console.log(`✅ ${deletedCount2} verwaiste stock Einträge entfernt`); } // Cleanup knowledge mit ungültigen character_id oder product_id console.log(" → Prüfe knowledge..."); const result3 = await sequelize.query(` DELETE FROM falukant_data.knowledge WHERE character_id NOT IN ( SELECT id FROM falukant_data.character ) OR product_id NOT IN ( SELECT id FROM falukant_type.product ); `, { timeout: 30000 }); const deletedCount3 = result3[1] || 0; if (deletedCount3 > 0) { console.log(`✅ ${deletedCount3} verwaiste knowledge Einträge entfernt`); } // Cleanup notification mit ungültigen user_id console.log(" → Prüfe notification..."); const result4 = await sequelize.query(` DELETE FROM falukant_log.notification WHERE user_id NOT IN ( SELECT id FROM falukant_data.falukant_user ); `, { timeout: 30000 }); const deletedCount4 = result4[1] || 0; if (deletedCount4 > 0) { console.log(`✅ ${deletedCount4} verwaiste notification Einträge entfernt`); } // Cleanup promotional_gift mit ungültigen sender_character_id oder recipient_character_id console.log(" → Prüfe promotional_gift..."); const result5 = await sequelize.query(` DELETE FROM falukant_log.promotional_gift WHERE sender_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR recipient_character_id NOT IN ( SELECT id FROM falukant_data.character ); `, { timeout: 30000 }); const deletedCount5 = result5[1] || 0; if (deletedCount5 > 0) { console.log(`✅ ${deletedCount5} verwaiste promotional_gift Einträge entfernt`); } // Cleanup user_house mit ungültigen house_type_id oder user_id console.log(" → Prüfe user_house..."); const result6 = await sequelize.query(` DELETE FROM falukant_data.user_house WHERE house_type_id NOT IN ( SELECT id FROM falukant_type.house ) OR user_id NOT IN ( SELECT id FROM falukant_data.falukant_user ); `, { timeout: 30000 }); const deletedCount6 = result6[1] || 0; if (deletedCount6 > 0) { console.log(`✅ ${deletedCount6} verwaiste user_house Einträge entfernt`); } // Cleanup child_relation mit ungültigen father_character_id, mother_character_id oder child_character_id console.log(" → Prüfe child_relation..."); const result7 = await sequelize.query(` DELETE FROM falukant_data.child_relation WHERE father_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR mother_character_id NOT IN ( SELECT id FROM falukant_data.character ) OR child_character_id NOT IN ( SELECT id FROM falukant_data.character ); `, { timeout: 30000 }); const deletedCount7 = result7[1] || 0; if (deletedCount7 > 0) { console.log(`✅ ${deletedCount7} verwaiste child_relation Einträge entfernt`); } // Cleanup political_office mit ungültigen character_id, office_type_id oder region_id console.log(" → Prüfe political_office..."); const result8 = await sequelize.query(` DELETE FROM falukant_data.political_office WHERE character_id NOT IN ( SELECT id FROM falukant_data.character ) OR office_type_id NOT IN ( SELECT id FROM falukant_type.political_office_type ) OR region_id NOT IN ( SELECT id FROM falukant_data.region ); `, { timeout: 30000 }); const deletedCount8 = result8[1] || 0; if (deletedCount8 > 0) { console.log(`✅ ${deletedCount8} verwaiste political_office Einträge entfernt`); } // Cleanup church_office mit ungültigen character_id, office_type_id oder region_id console.log(" → Prüfe church_office..."); const result11 = await sequelize.query(` DELETE FROM falukant_data.church_office WHERE character_id NOT IN ( SELECT id FROM falukant_data.character ) OR office_type_id NOT IN ( SELECT id FROM falukant_type.church_office_type ) OR region_id NOT IN ( SELECT id FROM falukant_data.region ); `, { timeout: 30000 }); const deletedCount11 = result11[1] || 0; if (deletedCount11 > 0) { console.log(`✅ ${deletedCount11} verwaiste church_office Einträge entfernt`); } // Cleanup church_application mit ungültigen character_id, office_type_id, region_id oder supervisor_id console.log(" → Prüfe church_application..."); const result12 = await sequelize.query(` DELETE FROM falukant_data.church_application WHERE character_id NOT IN ( SELECT id FROM falukant_data.character ) OR office_type_id NOT IN ( SELECT id FROM falukant_type.church_office_type ) OR region_id NOT IN ( SELECT id FROM falukant_data.region ) OR supervisor_id NOT IN ( SELECT id FROM falukant_data.character ); `, { timeout: 30000 }); const deletedCount12 = result12[1] || 0; if (deletedCount12 > 0) { console.log(`✅ ${deletedCount12} verwaiste church_application Einträge entfernt`); } // Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt") console.log(" → Prüfe vehicle.condition..."); const result9 = await sequelize.query(` UPDATE falukant_data.vehicle SET condition = 100 WHERE condition IS NULL; `, { timeout: 30000 }); const updatedNullConditions = result9[1] || 0; if (updatedNullConditions > 0) { console.log(`✅ ${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`); } const result10 = await sequelize.query(` UPDATE falukant_data.vehicle SET condition = GREATEST(0, LEAST(100, condition)) WHERE condition < 0 OR condition > 100; `, { timeout: 30000 }); const clampedConditions = result10[1] || 0; if (clampedConditions > 0) { console.log(`✅ ${clampedConditions} vehicle.condition Werte auf 0..100 geklemmt`); } if (deletedCount1 === 0 && deletedCount2 === 0 && deletedCount3 === 0 && deletedCount4 === 0 && deletedCount5 === 0 && deletedCount6 === 0 && deletedCount7 === 0 && deletedCount8 === 0 && deletedCount11 === 0 && deletedCount12 === 0 && updatedNullConditions === 0 && clampedConditions === 0) { console.log("✅ Keine verwaisten Einträge gefunden"); } } catch (e) { console.warn('⚠️ Konnte verwaiste Einträge nicht bereinigen:', e?.message || e); } console.log("Setting up associations..."); setupAssociations(); console.log("Synchronizing models with schema updates..."); await syncModelsAlways(models); console.log("Initializing settings..."); await initializeSettings(); console.log("Initializing types..."); await initializeTypes(); console.log("Initializing user rights..."); await initializeUserRights(); console.log("Initializing image types..."); await initializeImageTypes(); console.log("Initializing forums..."); await initializeForum(); console.log("Initializing Falukant..."); await initializeFalukant(); console.log("Creating triggers..."); await createTriggers(); console.log("Initializing chat..."); await initializeChat(); // Match3-Initialisierung NACH der Model-Synchronisation UND nach der Erstellung aller Tabellen console.log("Initializing Match3..."); await initializeMatch3Data(); // Match3-Levels aktualisieren NACH der Initialisierung console.log("Updating existing Match3 levels..."); await updateExistingMatch3Levels(); console.log("Initializing Taxi..."); await initializeTaxi(); console.log('Database synchronization for deployment complete.'); } catch (error) { console.error('Unable to synchronize the database for deployment:', error); throw error; // Fehler weiterwerfen } }; export { syncDatabase, syncDatabaseForDeployment };