feat(match3): Erweiterung der Match3-Admin-Funktionalitäten und -Modelle

- Implementierung neuer Endpunkte für die Verwaltung von Match3-Kampagnen, Levels, Objectives und Tile-Typen im Admin-Bereich.
- Anpassung der Admin-Services zur Unterstützung von Benutzerberechtigungen und Fehlerbehandlung.
- Einführung von neuen Modellen und Assoziationen für Match3-Levels und Tile-Typen in der Datenbank.
- Verbesserung der Internationalisierung für Match3-spezifische Texte in Deutsch und Englisch.
- Aktualisierung der Frontend-Routen und -Komponenten zur Verwaltung von Match3-Inhalten.
This commit is contained in:
Torsten Schulz (local)
2025-08-23 06:00:29 +02:00
parent 3eb7ae4e93
commit e168adeb51
40 changed files with 6474 additions and 1007 deletions

View File

@@ -0,0 +1,61 @@
import { sequelize } from './sequelize.js';
async function checkRightsTable() {
try {
console.log('🔍 Überprüfe den aktuellen Zustand der chat.rights Tabelle...');
// Überprüfe die Constraints der chat.rights Tabelle
const rightsConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
AND tc.table_name = 'rights'
ORDER BY tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
rightsConstraints.forEach(constraint => {
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
// Überprüfe speziell die UNIQUE Constraints auf der tr-Spalte
const trUniqueConstraints = rightsConstraints.filter(c =>
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
);
console.log(`\n🎯 UNIQUE Constraints auf der tr-Spalte: ${trUniqueConstraints.length}`);
if (trUniqueConstraints.length === 1) {
console.log('✅ Perfekt! Es gibt nur noch einen UNIQUE Constraint auf der tr-Spalte.');
} else if (trUniqueConstraints.length === 0) {
console.log('⚠️ Es gibt keinen UNIQUE Constraint auf der tr-Spalte!');
} else {
console.log(`❌ Es gibt immer noch ${trUniqueConstraints.length} UNIQUE Constraints auf der tr-Spalte.`);
}
console.log('\n✅ Überprüfung abgeschlossen');
} catch (error) {
console.error('❌ Fehler bei der Überprüfung:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
checkRightsTable()
.then(() => {
console.log('🎯 Überprüfung der chat.rights Tabelle abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Überprüfung fehlgeschlagen:', error);
process.exit(1);
});
}
export default checkRightsTable;

View File

@@ -0,0 +1,234 @@
import { sequelize } from './sequelize.js';
/**
* Bereinigt doppelte Constraints und Indexe in der Datenbank
* Dies sollte nur einmal ausgeführt werden, um bestehende Probleme zu beheben
*/
async function cleanupDatabaseConstraints() {
try {
console.log('🧹 Starte Bereinigung der Datenbank-Constraints...');
// 1. Doppelte UNIQUE Constraints entfernen
console.log('🔍 Suche nach doppelten UNIQUE Constraints...');
const duplicateUniqueConstraints = await sequelize.query(`
SELECT
tc.table_schema,
tc.table_name,
kcu.column_name,
COUNT(*) as constraint_count,
array_agg(tc.constraint_name) as constraint_names
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.constraint_type = 'UNIQUE'
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
GROUP BY tc.table_schema, tc.table_name, kcu.column_name
HAVING COUNT(*) > 1
ORDER BY tc.table_name, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateUniqueConstraints.length} Spalten mit doppelten UNIQUE Constraints`);
// Entferne doppelte UNIQUE Constraints
for (const duplicate of duplicateUniqueConstraints) {
console.log(`🗑️ Entferne doppelte UNIQUE Constraints für ${duplicate.table_schema}.${duplicate.table_name}.${duplicate.column_name}`);
// Behalte den ersten Constraint, entferne die anderen
const constraintNames = duplicate.constraint_names;
for (let i = 1; i < constraintNames.length; i++) {
const constraintName = constraintNames[i];
try {
await sequelize.query(`
ALTER TABLE ${duplicate.table_schema}.${duplicate.table_name}
DROP CONSTRAINT IF EXISTS "${constraintName}"
`);
console.log(` ✅ Entfernt: ${constraintName}`);
} catch (error) {
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
}
}
}
// 2. Doppelte CHECK Constraints entfernen (korrigierte Abfrage)
console.log('🔍 Suche nach doppelten CHECK Constraints...');
try {
const duplicateCheckConstraints = await sequelize.query(`
SELECT
ttc.table_schema,
ttc.table_name,
tc.constraint_name,
tc.check_clause
FROM information_schema.check_constraints tc
JOIN information_schema.table_constraints ttc
ON tc.constraint_name = ttc.constraint_name
WHERE ttc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
ORDER BY ttc.table_name, tc.constraint_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateCheckConstraints.length} CHECK Constraints`);
} catch (error) {
console.log(`⚠️ Konnte CHECK Constraints nicht abfragen: ${error.message}`);
}
// 3. Doppelte Foreign Key Constraints entfernen
console.log('🔍 Suche nach doppelten Foreign Key Constraints...');
const duplicateFKs = await sequelize.query(`
SELECT
tc.constraint_name,
tc.table_name,
tc.table_schema,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_schema IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
ORDER BY tc.table_name, tc.constraint_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateFKs.length} Foreign Key Constraints`);
// 4. Doppelte Indexe entfernen
console.log('🔍 Suche nach doppelten Indexen...');
const duplicateIndexes = await sequelize.query(`
SELECT
schemaname,
tablename,
indexname,
indexdef
FROM pg_indexes
WHERE schemaname IN ('match3', 'community', 'falukant_data', 'falukant_type', 'falukant_predefine', 'falukant_log', 'chat', 'forum', 'logs', 'type', 'service')
AND indexname LIKE '%_index_%'
ORDER BY tablename, indexname;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Gefunden: ${duplicateIndexes.length} potenziell doppelte Indexe`);
// 5. Spezifische Match3-Constraints prüfen
console.log('🔍 Prüfe Match3-spezifische Constraints...');
const match3Constraints = await sequelize.query(`
SELECT
constraint_name,
table_name,
constraint_type
FROM information_schema.table_constraints
WHERE table_schema = 'match3'
ORDER BY table_name, constraint_type;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Match3 Constraints: ${match3Constraints.length} gefunden`);
match3Constraints.forEach(constraint => {
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name})`);
});
// 6. Spezifische Chat-Constraints prüfen (da das Problem dort auftritt)
console.log('🔍 Prüfe Chat-spezifische Constraints...');
try {
const chatConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.table_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
ORDER BY tc.table_name, tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Constraints: ${chatConstraints.length} gefunden`);
chatConstraints.forEach(constraint => {
console.log(` - ${constraint.table_name}: ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
} catch (error) {
console.log(`⚠️ Konnte Chat Constraints nicht abfragen: ${error.message}`);
}
// 7. Spezifische Überprüfung der chat.rights Tabelle
console.log('🔍 Spezielle Überprüfung der chat.rights Tabelle...');
try {
const rightsConstraints = await sequelize.query(`
SELECT
tc.constraint_name,
tc.constraint_type,
kcu.column_name
FROM information_schema.table_constraints tc
LEFT JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'chat'
AND tc.table_name = 'rights'
ORDER BY tc.constraint_type, kcu.column_name;
`, { type: sequelize.QueryTypes.SELECT });
console.log(`📊 Chat Rights Constraints: ${rightsConstraints.length} gefunden`);
rightsConstraints.forEach(constraint => {
console.log(` - ${constraint.constraint_type} (${constraint.constraint_name}) auf Spalte: ${constraint.column_name}`);
});
// Entferne alle doppelten UNIQUE Constraints auf der tr-Spalte
const trUniqueConstraints = rightsConstraints.filter(c =>
c.constraint_type === 'UNIQUE' && c.column_name === 'tr'
);
if (trUniqueConstraints.length > 1) {
console.log(`🗑️ Entferne ${trUniqueConstraints.length - 1} doppelte UNIQUE Constraints auf chat.rights.tr`);
// Behalte den ersten, entferne die anderen
for (let i = 1; i < trUniqueConstraints.length; i++) {
const constraintName = trUniqueConstraints[i].constraint_name;
try {
await sequelize.query(`
ALTER TABLE chat.rights
DROP CONSTRAINT IF EXISTS "${constraintName}"
`);
console.log(` ✅ Entfernt: ${constraintName}`);
} catch (error) {
console.log(` ⚠️ Konnte nicht entfernen: ${constraintName} - ${error.message}`);
}
}
}
} catch (error) {
console.log(`⚠️ Konnte chat.rights Constraints nicht abfragen: ${error.message}`);
}
// 8. Empfehlungen ausgeben
console.log('\n💡 Empfehlungen:');
console.log('1. Überprüfe die oben gelisteten Constraints auf Duplikate');
console.log('2. Verwende updateSchema() nur bei expliziten Schema-Änderungen');
console.log('3. Normale syncModels() läuft jetzt ohne alter: true');
console.log('4. Bei Problemen: Manuelle Bereinigung der doppelten Constraints');
console.log('✅ Datenbank-Constraint-Bereinigung abgeschlossen');
} catch (error) {
console.error('❌ Fehler bei der Constraint-Bereinigung:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
cleanupDatabaseConstraints()
.then(() => {
console.log('🎯 Datenbank-Constraint-Bereinigung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Datenbank-Constraint-Bereinigung fehlgeschlagen:', error);
process.exit(1);
});
}
export default cleanupDatabaseConstraints;

View File

@@ -1,56 +1,133 @@
import { sequelize } from './sequelize.js';
import Match3Campaign from '../models/match3/campaign.js';
import Match3Level from '../models/match3/level.js';
import Match3Objective from '../models/match3/objective.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
export const initializeMatch3Data = async () => {
/**
* Initialisiert die Match3-Daten in der Datenbank
*/
async function initializeMatch3Data() {
try {
console.log('🎯 Initialisiere Match3-Daten...');
// Prüfe ob bereits Daten vorhanden sind
const existingCampaigns = await Match3Campaign.count();
if (existingCampaigns > 0) {
console.log('Match3 data already exists, skipping initialization');
console.log('Match3-Daten bereits vorhanden, überspringe Initialisierung');
return;
}
console.log('Initializing Match3 data...');
// Lösche existierende Level und erstelle sie neu
console.log('🔄 Lösche existierende Level...');
await Match3Level.destroy({ where: { campaignId: campaign.id } });
console.log('✅ Existierende Level gelöscht');
// Erstelle erste Kampagne
console.log('🎯 Erstelle neue Level...');
// Erstelle Kampagne
const campaign = await Match3Campaign.create({
name: 'Juwelen-Meister',
description: 'Meistere die Kunst des Juwelen-Matchings',
isActive: true,
order: 1
description: 'Meistere die Kunst des Juwelen-Matchings mit einzigartigen Level-Formen',
isActive: true
});
// Erstelle erste Level
console.log('✅ Kampagne erstellt:', campaign.name);
// Erstelle Level 1: Einfaches 5x5 Feld
const level1 = await Match3Level.create({
campaignId: campaign.id,
name: 'Der Anfang',
description: 'Lerne die Grundlagen des Spiels',
description: 'Lerne die Grundlagen mit einem einfachen 5x5 Feld',
order: 1,
boardSize: 6,
boardLayout: 'xxxxx\nxxxxx\nxxxxx\nxxxxx\nxxxxx',
boardWidth: 5,
boardHeight: 5,
tileTypes: ['gem', 'star', 'heart'],
moveLimit: 15,
isActive: true
});
// Erstelle Level 2: 7x6 Feld
const level2 = await Match3Level.create({
campaignId: campaign.id,
name: 'Erste Herausforderung',
description: 'Erweitere deine Fähigkeiten',
description: 'Ein größeres 7x6 Feld stellt dich vor neue Herausforderungen',
order: 2,
boardSize: 7,
boardLayout: 'xxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx\nxxxxxxx',
boardWidth: 7,
boardHeight: 6,
tileTypes: ['gem', 'star', 'heart', 'diamond'],
moveLimit: 20,
isActive: true
});
// Erstelle Level 3: L-Form mit festen Gems
const level3 = await Match3Level.create({
campaignId: campaign.id,
name: 'Spielzug',
description: 'Sei ein Profi',
order: 3,
boardLayout: 'xxxxx\nxooxx\nxxxgx\nxxxxx\nxxxgx',
boardWidth: 5,
boardHeight: 5,
tileTypes: ['gem', 'star', 'heart', 'diamond'],
moveLimit: 15,
isActive: true
});
// Erstelle Level 4: H-Form
const level4 = await Match3Level.create({
campaignId: campaign.id,
name: 'H-Form',
description: 'Eine H-Form mit vielen Ecken und Kanten',
order: 4,
boardLayout: 'xxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx\nxxxxxoooxxxxx',
boardWidth: 13,
boardHeight: 13,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon'],
moveLimit: 30,
isActive: true
});
// Erstelle Level 5: Diamant-Form
const level5 = await Match3Level.create({
campaignId: campaign.id,
name: 'Diamant-Form',
description: 'Eine elegante Diamant-Form für Fortgeschrittene',
order: 5,
boardLayout: 'oooxxxooo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\nooxxxxxxoo\noooxxxooo',
boardWidth: 9,
boardHeight: 10,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star'],
moveLimit: 35,
isActive: true
});
// Erstelle Level 6: Spiral-Form
const level6 = await Match3Level.create({
campaignId: campaign.id,
name: 'Spiral-Form',
description: 'Eine komplexe Spiral-Form für Meister',
order: 6,
boardLayout: 'xxxxxxxxxxxxx\nxooooooooooox\nxoxxxxxxxxxox\nxoxoooooooxox\nxoxoxxxxxoxox\nxoxoxoooxoxox\nxoxoxoxoxoxox\nxoxoxoooxoxox\nxoxoxxxxxoxox\nxoxoooooooxox\nxoxxxxxxxxxox\nxooooooooooox\nxxxxxxxxxxxxx',
boardWidth: 13,
boardHeight: 13,
tileTypes: ['gem', 'star', 'heart', 'diamond', 'crown', 'moon', 'star', 'crystal'],
moveLimit: 40,
isActive: true
});
console.log('✅ Alle Level erstellt');
// Erstelle Objectives für Level 1
await Match3Objective.bulkCreate([
{
levelId: level1.id,
type: 'score',
description: 'Sammle 100 Punkte',
target: 100,
description: 'Sammle 150 Punkte',
target: 150,
operator: '>=',
order: 1,
isRequired: true
@@ -58,8 +135,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level1.id,
type: 'matches',
description: 'Mache 3 Matches',
target: 3,
description: 'Mache 5 Matches',
target: 5,
operator: '>=',
order: 2,
isRequired: true
@@ -71,8 +148,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level2.id,
type: 'score',
description: 'Sammle 200 Punkte',
target: 200,
description: 'Sammle 250 Punkte',
target: 250,
operator: '>=',
order: 1,
isRequired: true
@@ -80,8 +157,8 @@ export const initializeMatch3Data = async () => {
{
levelId: level2.id,
type: 'matches',
description: 'Mache 5 Matches',
target: 5,
description: 'Mache 8 Matches',
target: 8,
operator: '>=',
order: 2,
isRequired: true
@@ -97,8 +174,137 @@ export const initializeMatch3Data = async () => {
}
]);
console.log('Match3 data initialized successfully');
// Erstelle Objectives für Level 3
await Match3Objective.bulkCreate([
{
levelId: level3.id,
type: 'score',
description: 'Sammle 400 Punkte',
target: 400,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level3.id,
type: 'matches',
description: 'Mache 12 Matches',
target: 12,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level3.id,
type: 'moves',
description: 'Verwende weniger als 25 Züge',
target: 25,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 4
await Match3Objective.bulkCreate([
{
levelId: level4.id,
type: 'score',
description: 'Sammle 600 Punkte',
target: 600,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level4.id,
type: 'matches',
description: 'Mache 15 Matches',
target: 15,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level4.id,
type: 'moves',
description: 'Verwende weniger als 30 Züge',
target: 30,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 5
await Match3Objective.bulkCreate([
{
levelId: level5.id,
type: 'score',
description: 'Sammle 800 Punkte',
target: 800,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level5.id,
type: 'matches',
description: 'Mache 18 Matches',
target: 18,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level5.id,
type: 'moves',
description: 'Verwende weniger als 35 Züge',
target: 35,
operator: '<=',
order: 3,
isRequired: true
}
]);
// Erstelle Objectives für Level 6
await Match3Objective.bulkCreate([
{
levelId: level6.id,
type: 'score',
description: 'Sammle 1000 Punkte',
target: 1000,
operator: '>=',
order: 1,
isRequired: true
},
{
levelId: level6.id,
type: 'matches',
description: 'Mache 25 Matches',
target: 25,
operator: '>=',
order: 2,
isRequired: true
},
{
levelId: level6.id,
type: 'moves',
description: 'Verwende weniger als 40 Züge',
target: 40,
operator: '<=',
order: 3,
isRequired: true
}
]);
console.log('✅ Alle Objectives erstellt');
console.log('🎯 Match3-Daten erfolgreich initialisiert');
} catch (error) {
console.error('Error initializing Match3 data:', error);
console.error('❌ Fehler beim Initialisieren der Match3-Daten:', error);
throw error;
}
};
}
export default initializeMatch3Data;

View File

@@ -0,0 +1,119 @@
import { sequelize } from './sequelize.js';
import Match3TileType from '../models/match3/tileType.js';
async function initializeMatch3TileTypes() {
try {
console.log('🚀 Initialisiere Match3 Tile-Typen...');
// Synchronisiere das TileType-Modell
await Match3TileType.sync({ alter: true });
console.log('✅ TileType-Modell synchronisiert');
// Standard-Tile-Typen definieren
const defaultTileTypes = [
{
name: 'gem',
displayName: 'Juwel',
symbol: '💎',
color: '#ff6b6b',
rarity: 'common',
points: 10
},
{
name: 'star',
displayName: 'Stern',
symbol: '⭐',
color: '#feca57',
rarity: 'common',
points: 15
},
{
name: 'heart',
displayName: 'Herz',
symbol: '❤️',
color: '#ff9ff3',
rarity: 'common',
points: 12
},
{
name: 'diamond',
displayName: 'Diamant',
symbol: '🔷',
color: '#54a0ff',
rarity: 'uncommon',
points: 20
},
{
name: 'circle',
displayName: 'Kreis',
symbol: '⭕',
color: '#5f27cd',
rarity: 'uncommon',
points: 18
},
{
name: 'square',
displayName: 'Quadrat',
symbol: '🟦',
color: '#00d2d3',
rarity: 'rare',
points: 25
},
{
name: 'crown',
displayName: 'Krone',
symbol: '👑',
color: '#ff9f43',
rarity: 'epic',
points: 35
},
{
name: 'rainbow',
displayName: 'Regenbogen',
symbol: '🌈',
color: '#ff6348',
rarity: 'legendary',
points: 50
}
];
// Tile-Typen erstellen oder aktualisieren
for (const tileType of defaultTileTypes) {
const [tileTypeInstance, created] = await Match3TileType.findOrCreate({
where: { name: tileType.name },
defaults: tileType
});
if (created) {
console.log(`✅ Tile-Typ "${tileType.displayName}" erstellt`);
} else {
// Aktualisiere bestehende Tile-Typen
await Match3TileType.update(tileType, {
where: { name: tileType.name }
});
console.log(`🔄 Tile-Typ "${tileType.displayName}" aktualisiert`);
}
}
console.log('🎉 Alle Match3 Tile-Typen erfolgreich initialisiert!');
} catch (error) {
console.error('❌ Fehler beim Initialisieren der Match3 Tile-Typen:', error);
throw error;
}
}
// Führe die Initialisierung aus, wenn das Skript direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
initializeMatch3TileTypes()
.then(() => {
console.log('✅ Tile-Typen-Initialisierung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler bei der Tile-Typen-Initialisierung:', error);
process.exit(1);
});
}
export default initializeMatch3TileTypes;

View File

@@ -34,6 +34,10 @@ const initializeUserRights = async() => {
where: { title: "developer"},
defaults: { title: "developer"}
});
await UserRightType.findOrCreate({
where: { title: "match3"},
defaults: { title: "match3"}
});
};
export default initializeUserRights;

View File

@@ -7,7 +7,9 @@ const sequelize = new Sequelize(process.env.DB_NAME, process.env.DB_USER, proces
host: process.env.DB_HOST,
dialect: 'postgres',
define: {
timestamps: false
timestamps: false,
underscored: true, // WICHTIG: Alle Datenbankfelder im snake_case Format
freezeTableName: true // Verhindert Pluralisierung der Tabellennamen
},
});
@@ -32,9 +34,290 @@ const initializeDatabase = async () => {
};
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) => {
console.log('🔍 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 = :schemaName
ORDER BY table_name
`, {
replacements: { 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 = :schemaName
AND table_name = :tableName
);
`, {
replacements: { schemaName, tableName: 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 = :schemaName
AND table_name = :tableName
ORDER BY ordinal_position
`, {
replacements: { 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) => {
const type = attribute.type;
if (type instanceof sequelize.DataTypes.INTEGER) return 'integer';
if (type instanceof sequelize.DataTypes.STRING) return 'character varying';
if (type instanceof sequelize.DataTypes.TEXT) return 'text';
if (type instanceof sequelize.DataTypes.BOOLEAN) return 'boolean';
if (type instanceof sequelize.DataTypes.DATE) return 'timestamp without time zone';
if (type instanceof sequelize.DataTypes.JSON) return 'json';
if (type instanceof sequelize.DataTypes.DECIMAL) return 'numeric';
// Fallback
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) {
@@ -70,4 +353,4 @@ async function updateFalukantUserMoney(falukantUserId, moneyChange, activity, ch
}
}
export { sequelize, initializeDatabase, syncModels, updateFalukantUserMoney };
export { sequelize, initializeDatabase, syncModels, syncModelsWithUpdates, updateSchema, updateFalukantUserMoney };

View File

@@ -1,6 +1,6 @@
// syncDatabase.js
import { initializeDatabase, syncModels } from './sequelize.js';
import { initializeDatabase, syncModelsWithUpdates } from './sequelize.js';
import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js';
@@ -11,7 +11,8 @@ 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 initializeMatch3Data from './initializeMatch3.js';
import updateExistingMatch3Levels from './updateExistingMatch3Levels.js';
const syncDatabase = async () => {
try {
@@ -19,7 +20,7 @@ const syncDatabase = async () => {
await initializeDatabase();
console.log("Synchronizing models...");
await syncModels(models);
await syncModelsWithUpdates(models);
console.log("Setting up associations...");
setupAssociations();
@@ -48,6 +49,10 @@ const syncDatabase = async () => {
console.log("Initializing chat...");
await initializeChat();
// Match3-Initialisierung NACH der Model-Synchronisation
console.log("Updating existing Match3 levels...");
await updateExistingMatch3Levels();
console.log("Initializing Match3...");
await initializeMatch3Data();

View File

@@ -0,0 +1,74 @@
import { sequelize } from './sequelize.js';
import Match3Level from '../models/match3/level.js';
/**
* Aktualisiert existierende Match3-Level mit Standard-Layouts
* und neuen Feldern
*/
async function updateExistingMatch3Levels() {
try {
console.log('🔧 Aktualisiere existierende Match3-Level...');
// Finde alle existierenden Level ohne boardLayout
const existingLevels = await Match3Level.findAll({
where: {
boardLayout: null
}
});
console.log(`📊 Gefunden: ${existingLevels.length} Level ohne boardLayout`);
if (existingLevels.length === 0) {
console.log('✅ Alle Level haben bereits boardLayout');
return;
}
// Aktualisiere jeden Level mit Standard-Layout
for (const level of existingLevels) {
const oldBoardSize = level.boardSize || 6;
// Erstelle Standard-Layout basierend auf alter boardSize
let boardLayout = '';
for (let i = 0; i < oldBoardSize; i++) {
for (let j = 0; j < oldBoardSize; j++) {
boardLayout += 'x';
}
if (i < oldBoardSize - 1) boardLayout += '\n';
}
// Aktualisiere den Level mit allen neuen Feldern
await level.update({
boardLayout: boardLayout,
boardWidth: oldBoardSize,
boardHeight: oldBoardSize,
// Stelle sicher, dass alle erforderlichen Felder gesetzt sind
tileTypes: level.tileTypes || ['gem', 'star', 'heart'],
moveLimit: level.moveLimit || 20,
isActive: level.isActive !== undefined ? level.isActive : true
});
console.log(`🔧 Level ${level.id} aktualisiert: ${oldBoardSize}x${oldBoardSize} → alle neuen Felder gesetzt`);
}
console.log('✅ Alle existierenden Level wurden aktualisiert');
} catch (error) {
console.error('❌ Fehler beim Aktualisieren der Match3-Level:', error);
throw error;
}
}
// Führe das Skript aus, wenn es direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
updateExistingMatch3Levels()
.then(() => {
console.log('🎯 Match3-Level-Update abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('💥 Match3-Level-Update fehlgeschlagen:', error);
process.exit(1);
});
}
export default updateExistingMatch3Levels;

View File

@@ -0,0 +1,115 @@
import { sequelize } from './sequelize.js';
import Match3Level from '../models/match3/level.js';
import Match3TileType from '../models/match3/tileType.js';
import Match3LevelTileType from '../models/match3/levelTileType.js';
/**
* Aktualisiert bestehende Match3 Level mit den neuen Tile-Typen
*/
async function updateMatch3LevelsWithTileTypes() {
try {
console.log('🔄 Aktualisiere Match3 Level mit neuen Tile-Typen...');
// Synchronisiere die neuen Modelle
await Match3TileType.sync({ alter: true });
await Match3LevelTileType.sync({ alter: true });
console.log('✅ Neue Modelle synchronisiert');
// Hole alle Level
const levels = await Match3Level.findAll();
console.log(`📊 ${levels.length} Level gefunden`);
// Hole alle verfügbaren Tile-Typen
const tileTypes = await Match3TileType.findAll();
console.log(`🎯 ${tileTypes.length} Tile-Typen verfügbar`);
// Erstelle eine Mapping-Tabelle für die alten tileTypes Arrays
const tileTypeMapping = {
'gem': tileTypes.find(t => t.name === 'gem'),
'star': tileTypes.find(t => t.name === 'star'),
'heart': tileTypes.find(t => t.name === 'heart'),
'diamond': tileTypes.find(t => t.name === 'diamond'),
'circle': tileTypes.find(t => t.name === 'circle'),
'square': tileTypes.find(t => t.name === 'square'),
'crown': tileTypes.find(t => t.name === 'crown'),
'rainbow': tileTypes.find(t => t.name === 'rainbow')
};
// Gehe durch alle Level und erstelle Verknüpfungen
for (const level of levels) {
console.log(`🔄 Verarbeite Level ${level.order}: ${level.name}`);
// Lösche bestehende Verknüpfungen für dieses Level
await Match3LevelTileType.destroy({
where: { levelId: level.id }
});
// Verwende die alten tileTypes Arrays als Fallback
let levelTileTypes = [];
if (level.tileTypes && Array.isArray(level.tileTypes)) {
levelTileTypes = level.tileTypes;
} else {
// Fallback: Verwende Standard-Tile-Typen basierend auf der Level-Nummer
switch (level.order) {
case 1:
levelTileTypes = ['gem', 'star', 'heart'];
break;
case 2:
levelTileTypes = ['gem', 'star', 'heart', 'diamond'];
break;
case 3:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle'];
break;
case 4:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square'];
break;
case 5:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown'];
break;
case 6:
levelTileTypes = ['gem', 'star', 'heart', 'diamond', 'circle', 'square', 'crown', 'rainbow'];
break;
default:
levelTileTypes = ['gem', 'star', 'heart'];
}
}
// Erstelle Verknüpfungen für jeden Tile-Typ
for (const tileTypeName of levelTileTypes) {
const tileType = tileTypeMapping[tileTypeName];
if (tileType) {
await Match3LevelTileType.create({
levelId: level.id,
tileTypeId: tileType.id,
weight: 1, // Standard-Gewichtung
isActive: true
});
console.log(` ✅ Verknüpft mit ${tileType.displayName}`);
} else {
console.warn(` ⚠️ Tile-Typ "${tileTypeName}" nicht gefunden`);
}
}
}
console.log('🎉 Alle Match3 Level erfolgreich mit Tile-Typen verknüpft!');
} catch (error) {
console.error('❌ Fehler beim Aktualisieren der Match3 Level:', error);
throw error;
}
}
// Führe die Aktualisierung aus, wenn das Skript direkt aufgerufen wird
if (import.meta.url === `file://${process.argv[1]}`) {
updateMatch3LevelsWithTileTypes()
.then(() => {
console.log('✅ Level-Tile-Typ-Aktualisierung abgeschlossen');
process.exit(0);
})
.catch((error) => {
console.error('❌ Fehler bei der Level-Tile-Typ-Aktualisierung:', error);
process.exit(1);
});
}
export default updateMatch3LevelsWithTileTypes;