Files
yourpart3/backend/utils/syncDatabase.js

1085 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// syncDatabase.js
import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js';
// Helper: Query mit Timeout
const queryWithTimeout = async (query, timeoutMs = 30000, description = 'Query') => {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs);
});
try {
const result = await Promise.race([
sequelize.query(query),
timeoutPromise
]);
return result;
} catch (error) {
if (error.message.includes('Timeout')) {
console.warn(`⚠️ ${description} hat Timeout nach ${timeoutMs}ms - überspringe...`);
return [null, 0]; // Return empty result
}
throw error;
}
};
// Helper: Retry wrapper for transient pool/connection issues
const runWithRetry = async (fn, { retries = 3, delayMs = 2000, description = 'operation' } = {}) => {
let lastError;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
const isAcquireTimeout = error?.name === 'SequelizeConnectionAcquireTimeoutError'
|| error?.message?.includes('ConnectionAcquireTimeoutError')
|| error?.message?.includes('Operation timeout');
if (!isAcquireTimeout || attempt === retries) {
throw error;
}
console.warn(`⚠️ ${description} fehlgeschlagen (AcquireTimeout). Retry ${attempt}/${retries} in ${delayMs}ms...`);
await new Promise(resolve => setTimeout(resolve, delayMs));
}
}
throw lastError;
};
// Helper: Prüft ob Tabelle existiert
const tableExists = async (schema, tableName) => {
try {
const result = await sequelize.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = '${schema}'
AND table_name = '${tableName}'
);
`, { type: sequelize.QueryTypes.SELECT });
return result[0]?.exists || false;
} catch (error) {
return false;
}
};
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';
import initializeWidgetTypes from './initializeWidgetTypes.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();
// Dashboard: Widget-Typen-Tabelle (mögliche Widgets)
console.log("Ensuring widget_type table exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS type.widget_type (
id SERIAL PRIMARY KEY,
label VARCHAR(255) NOT NULL,
endpoint VARCHAR(255) NOT NULL,
description VARCHAR(255),
order_id INTEGER NOT NULL DEFAULT 0
);
`);
} catch (e) {
console.warn('⚠️ Konnte type.widget_type nicht anlegen:', e?.message || e);
}
// Dashboard: Benutzer-Konfiguration (Widgets pro User)
console.log("Ensuring user_dashboard table exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS community.user_dashboard (
user_id INTEGER NOT NULL PRIMARY KEY,
config JSONB NOT NULL DEFAULT '{"widgets":[]}'::jsonb,
CONSTRAINT user_dashboard_user_fk
FOREIGN KEY (user_id)
REFERENCES community."user"(id)
ON DELETE CASCADE
);
`);
} catch (e) {
console.warn('⚠️ Konnte community.user_dashboard nicht anlegen:', e?.message || e);
}
// 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);
}
// Migration: ChurchApplication supervisor_id nullable machen (kritisch für Funktionalität)
console.log("Making church_application supervisor_id nullable...");
try {
await sequelize.query(`
DO $$
BEGIN
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'church_application'
AND column_name = 'supervisor_id'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.church_application
ALTER COLUMN supervisor_id DROP NOT NULL;
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
END IF;
END
$$;
`);
console.log("✅ church_application supervisor_id ist jetzt nullable");
} catch (e) {
console.warn('⚠️ Konnte church_application supervisor_id nicht nullable machen:', e?.message || e);
}
// Relationship-/Marriage-Proposal-Änderungen loggen (keine Einträge löschen; ohne db:migrate)
console.log("Ensuring relationship change log (falukant) exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.relationship_change_log (
id serial PRIMARY KEY,
changed_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
table_name character varying(64) NOT NULL,
operation character varying(16) NOT NULL,
record_id integer,
payload_old jsonb,
payload_new jsonb
);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_change_log_changed_at_idx
ON falukant_log.relationship_change_log (changed_at);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS relationship_change_log_table_operation_idx
ON falukant_log.relationship_change_log (table_name, operation);
`);
await sequelize.query(`
CREATE OR REPLACE FUNCTION falukant_log.log_relationship_change()
RETURNS TRIGGER AS $$
DECLARE
v_record_id INTEGER;
v_payload_old JSONB;
v_payload_new JSONB;
BEGIN
IF TG_OP = 'INSERT' THEN
v_record_id := NEW.id;
v_payload_old := NULL;
v_payload_new := to_jsonb(NEW);
ELSIF TG_OP = 'UPDATE' THEN
v_record_id := NEW.id;
v_payload_old := to_jsonb(OLD);
v_payload_new := to_jsonb(NEW);
ELSIF TG_OP = 'DELETE' THEN
v_record_id := OLD.id;
v_payload_old := to_jsonb(OLD);
v_payload_new := NULL;
END IF;
INSERT INTO falukant_log.relationship_change_log (
table_name, operation, record_id, payload_old, payload_new
) VALUES (
TG_TABLE_NAME, TG_OP, v_record_id, v_payload_old, v_payload_new
);
IF TG_OP = 'DELETE' THEN RETURN OLD; ELSE RETURN NEW; END IF;
END;
$$ LANGUAGE plpgsql;
`);
await sequelize.query(`
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.relationship;
CREATE TRIGGER trg_log_relationship_change
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.relationship
FOR EACH ROW
EXECUTE FUNCTION falukant_log.log_relationship_change();
`);
await sequelize.query(`
DROP TRIGGER IF EXISTS trg_log_relationship_change ON falukant_data.marriage_proposals;
CREATE TRIGGER trg_log_relationship_change
AFTER INSERT OR UPDATE OR DELETE ON falukant_data.marriage_proposals
FOR EACH ROW
EXECUTE FUNCTION falukant_log.log_relationship_change();
`);
console.log("✅ relationship_change_log und Trigger sind vorhanden.");
} catch (e) {
console.warn('⚠️ relationship_change_log/Trigger konnten nicht sichergestellt werden:', e?.message || e);
}
// Preishistorie für Produkte (Zeitreihe) nur Schema/Struktur, noch ohne Logik
console.log("Ensuring falukant_log.product_price_history exists...");
try {
await sequelize.query(`
CREATE TABLE IF NOT EXISTS falukant_log.product_price_history (
id serial PRIMARY KEY,
product_id integer NOT NULL,
region_id integer NOT NULL,
price numeric(12,2) NOT NULL,
recorded_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
`);
await sequelize.query(`
CREATE INDEX IF NOT EXISTS product_price_history_product_region_recorded_idx
ON falukant_log.product_price_history (product_id, region_id, recorded_at);
`);
console.log("✅ product_price_history ist vorhanden.");
} catch (e) {
console.warn('⚠️ product_price_history konnte nicht sichergestellt werden:', 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 runWithRetry(
() => initializeSettings(),
{ retries: 3, delayMs: 2000, description: '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("Initializing widget types...");
await initializeWidgetTypes();
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);
}
// Migration: ChurchApplication supervisor_id nullable machen
console.log("Making church_application supervisor_id nullable...");
try {
await sequelize.query(`
DO $$
BEGIN
-- Prüfe ob supervisor_id NOT NULL Constraint existiert
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'falukant_data'
AND table_name = 'church_application'
AND column_name = 'supervisor_id'
AND is_nullable = 'NO'
) THEN
ALTER TABLE falukant_data.church_application
ALTER COLUMN supervisor_id DROP NOT NULL;
RAISE NOTICE 'supervisor_id NOT NULL Constraint entfernt';
END IF;
END
$$;
`);
console.log("✅ church_application supervisor_id ist jetzt nullable");
} catch (e) {
console.warn('⚠️ Konnte church_application supervisor_id 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 queryWithTimeout(`
DELETE FROM community.user_param_visibility
WHERE param_id NOT IN (
SELECT id FROM community.user_param
);
`, 30000, 'user_param_visibility cleanup');
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 queryWithTimeout(`
DELETE FROM falukant_data.stock
WHERE branch_id = 0 OR branch_id NOT IN (
SELECT id FROM falukant_data.branch
);
`, 30000, 'stock cleanup');
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 queryWithTimeout(`
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
);
`, 30000, 'knowledge cleanup');
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 queryWithTimeout(`
DELETE FROM falukant_log.notification
WHERE user_id NOT IN (
SELECT id FROM falukant_data.falukant_user
);
`, 30000, 'notification cleanup');
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 queryWithTimeout(`
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
);
`, 30000, 'promotional_gift cleanup');
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 queryWithTimeout(`
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
);
`, 30000, 'user_house cleanup');
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 queryWithTimeout(`
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
);
`, 30000, 'child_relation cleanup');
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 queryWithTimeout(`
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
);
`, 30000, 'political_office cleanup');
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 (nur wenn Tabelle existiert)
if (await tableExists('falukant_data', 'church_office')) {
console.log(" → Prüfe church_office...");
const result11 = await queryWithTimeout(`
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
);
`, 30000, 'church_office cleanup');
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 (nur wenn Tabelle existiert)
if (await tableExists('falukant_data', 'church_application')) {
console.log(" → Prüfe church_application...");
const result12 = await queryWithTimeout(`
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 IS NOT NULL AND supervisor_id NOT IN (
SELECT id FROM falukant_data.character
));
`, 30000, 'church_application cleanup');
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 queryWithTimeout(`
UPDATE falukant_data.vehicle
SET condition = 100
WHERE condition IS NULL;
`, 30000, 'vehicle condition NULL update');
const updatedNullConditions = result9[1] || 0;
if (updatedNullConditions > 0) {
console.log(`${updatedNullConditions} vehicle.condition NULL → 100 gesetzt`);
}
const result10 = await queryWithTimeout(`
UPDATE falukant_data.vehicle
SET condition = GREATEST(0, LEAST(100, condition))
WHERE condition < 0 OR condition > 100;
`, 30000, 'vehicle condition clamp');
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("Initializing widget types...");
await initializeWidgetTypes();
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 };