Files
yourpart3/backend/utils/syncDatabase.js
Torsten Schulz (local) b6a4607e60 Implement vocab course and grammar exercise features in backend and frontend
- Added new course management functionalities in VocabController, including creating, updating, and deleting courses and lessons.
- Implemented enrollment and progress tracking for courses, along with grammar exercise creation and management.
- Updated database schema to include tables for courses, lessons, enrollments, and grammar exercises.
- Enhanced frontend with new routes and views for course listing and details, including internationalization support for course-related texts.
- Improved user experience by adding navigation to courses from the main vocab trainer view.
2026-01-19 10:58:53 +01:00

784 lines
37 KiB
JavaScript

// 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,
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_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_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
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`);
}
// Cleanup political_office mit ungültigen character_id, office_type_id oder region_id
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
);
`);
const deletedCount8 = result8[1] || 0;
if (deletedCount8 > 0) {
console.log(`${deletedCount8} verwaiste political_office Einträge entfernt`);
}
// Cleanup vehicle.condition: Legacy-Nulls + Range clamp (UI zeigt sonst "Unbekannt")
const result9 = await sequelize.query(`
UPDATE falukant_data.vehicle
SET condition = 100
WHERE condition IS NULL;
`);
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;
`);
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 && 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 };