#!/usr/bin/env tsx /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Script de migration des données SQLite vers PostgreSQL * * Usage dans le container: * docker-compose exec got-app sh -c "cd /app && pnpm dlx tsx scripts/migrate-sqlite-to-postgres.ts" * * Variables d'environnement: * SQLITE_DB_PATH - Chemin vers le fichier SQLite (défaut: /app/data/dev.db) * DATABASE_URL - URL de connexion PostgreSQL (défaut: depuis env) */ import { PrismaPg } from "@prisma/adapter-pg"; import { Pool } from "pg"; import Database from "better-sqlite3"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; import { existsSync } from "fs"; // Résoudre le chemin vers le module Prisma const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const projectRoot = join(__dirname, ".."); const SQLITE_DB_PATH = process.env.SQLITE_DB_PATH || "/app/data/dev.db"; const POSTGRES_URL = process.env.DATABASE_URL; if (!POSTGRES_URL) { console.error("❌ DATABASE_URL est requis"); process.exit(1); } // Variables globales pour les clients (initialisées dans main) let sqliteDb: Database.Database; let pgPool: Pool; let prismaPG: any; // PrismaClient - typé dynamiquement interface MigrationStats { users: number; userPreferences: number; events: number; eventRegistrations: number; eventFeedbacks: number; sitePreferences: number; challenges: number; errors: number; } const stats: MigrationStats = { users: 0, userPreferences: 0, events: 0, eventRegistrations: 0, eventFeedbacks: 0, sitePreferences: 0, challenges: 0, errors: 0, }; function readSQLite(table: string): any[] { try { const rows = sqliteDb.prepare(`SELECT * FROM "${table}"`).all() as any[]; return rows; } catch (error) { console.error(`Erreur lecture table ${table}:`, error); return []; } } async function migrateUsers() { console.log("\n📦 Migration des Users..."); const users = readSQLite("User"); for (const user of users) { try { await prismaPG.user.upsert({ where: { id: user.id }, update: { email: user.email, password: user.password, username: user.username, role: user.role, score: user.score, level: user.level, hp: user.hp, maxHp: user.maxHp, xp: user.xp, maxXp: user.maxXp, avatar: user.avatar, bio: user.bio, characterClass: user.characterClass, createdAt: new Date(user.createdAt), updatedAt: new Date(user.updatedAt), }, create: { id: user.id, email: user.email, password: user.password, username: user.username, role: user.role, score: user.score, level: user.level, hp: user.hp, maxHp: user.maxHp, xp: user.xp, maxXp: user.maxXp, avatar: user.avatar, bio: user.bio, characterClass: user.characterClass, createdAt: new Date(user.createdAt), updatedAt: new Date(user.updatedAt), }, }); stats.users++; process.stdout.write( `\r ✅ ${stats.users}/${users.length} users migrés` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur user ${user.id}:`, error); } } console.log(`\n ✅ ${stats.users} users migrés avec succès`); } async function migrateUserPreferences() { console.log("\n📦 Migration des UserPreferences..."); const preferences = readSQLite("UserPreferences"); for (const pref of preferences) { try { await prismaPG.userPreferences.upsert({ where: { id: pref.id }, update: { userId: pref.userId, homeBackground: pref.homeBackground, eventsBackground: pref.eventsBackground, leaderboardBackground: pref.leaderboardBackground, theme: pref.theme, createdAt: new Date(pref.createdAt), updatedAt: new Date(pref.updatedAt), }, create: { id: pref.id, userId: pref.userId, homeBackground: pref.homeBackground, eventsBackground: pref.eventsBackground, leaderboardBackground: pref.leaderboardBackground, theme: pref.theme, createdAt: new Date(pref.createdAt), updatedAt: new Date(pref.updatedAt), }, }); stats.userPreferences++; process.stdout.write( `\r ✅ ${stats.userPreferences}/${preferences.length} préférences migrées` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur préférence ${pref.id}:`, error); } } console.log( `\n ✅ ${stats.userPreferences} préférences migrées avec succès` ); } async function migrateEvents() { console.log("\n📦 Migration des Events..."); const events = readSQLite("Event"); for (const event of events) { try { await prismaPG.event.upsert({ where: { id: event.id }, update: { date: new Date(event.date), name: event.name, description: event.description, type: event.type, room: event.room, time: event.time, maxPlaces: event.maxPlaces, createdAt: new Date(event.createdAt), updatedAt: new Date(event.updatedAt), }, create: { id: event.id, date: new Date(event.date), name: event.name, description: event.description, type: event.type, room: event.room, time: event.time, maxPlaces: event.maxPlaces, createdAt: new Date(event.createdAt), updatedAt: new Date(event.updatedAt), }, }); stats.events++; process.stdout.write( `\r ✅ ${stats.events}/${events.length} événements migrés` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur event ${event.id}:`, error); } } console.log(`\n ✅ ${stats.events} événements migrés avec succès`); } async function migrateEventRegistrations() { console.log("\n📦 Migration des EventRegistrations..."); const registrations = readSQLite("EventRegistration"); for (const reg of registrations) { try { await prismaPG.eventRegistration.upsert({ where: { userId_eventId: { userId: reg.userId, eventId: reg.eventId, }, }, update: { createdAt: new Date(reg.createdAt), }, create: { id: reg.id, userId: reg.userId, eventId: reg.eventId, createdAt: new Date(reg.createdAt), }, }); stats.eventRegistrations++; process.stdout.write( `\r ✅ ${stats.eventRegistrations}/${registrations.length} inscriptions migrées` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur registration ${reg.id}:`, error); } } console.log( `\n ✅ ${stats.eventRegistrations} inscriptions migrées avec succès` ); } async function migrateEventFeedbacks() { console.log("\n📦 Migration des EventFeedbacks..."); const feedbacks = readSQLite("EventFeedback"); for (const feedback of feedbacks) { try { await prismaPG.eventFeedback.upsert({ where: { userId_eventId: { userId: feedback.userId, eventId: feedback.eventId, }, }, update: { rating: feedback.rating, comment: feedback.comment, isRead: feedback.isRead ? true : false, createdAt: new Date(feedback.createdAt), updatedAt: new Date(feedback.updatedAt), }, create: { id: feedback.id, userId: feedback.userId, eventId: feedback.eventId, rating: feedback.rating, comment: feedback.comment, isRead: feedback.isRead ? true : false, createdAt: new Date(feedback.createdAt), updatedAt: new Date(feedback.updatedAt), }, }); stats.eventFeedbacks++; process.stdout.write( `\r ✅ ${stats.eventFeedbacks}/${feedbacks.length} feedbacks migrés` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur feedback ${feedback.id}:`, error); } } console.log(`\n ✅ ${stats.eventFeedbacks} feedbacks migrés avec succès`); } async function migrateSitePreferences() { console.log("\n📦 Migration des SitePreferences..."); const sitePrefs = readSQLite("SitePreferences"); for (const pref of sitePrefs) { try { await prismaPG.sitePreferences.upsert({ where: { id: pref.id }, update: { homeBackground: pref.homeBackground, eventsBackground: pref.eventsBackground, leaderboardBackground: pref.leaderboardBackground, challengesBackground: pref.challengesBackground, eventRegistrationPoints: pref.eventRegistrationPoints, eventFeedbackPoints: pref.eventFeedbackPoints, createdAt: new Date(pref.createdAt), updatedAt: new Date(pref.updatedAt), }, create: { id: pref.id, homeBackground: pref.homeBackground, eventsBackground: pref.eventsBackground, leaderboardBackground: pref.leaderboardBackground, challengesBackground: pref.challengesBackground, eventRegistrationPoints: pref.eventRegistrationPoints, eventFeedbackPoints: pref.eventFeedbackPoints, createdAt: new Date(pref.createdAt), updatedAt: new Date(pref.updatedAt), }, }); stats.sitePreferences++; } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur site preferences ${pref.id}:`, error); } } console.log( ` ✅ ${stats.sitePreferences} préférences site migrées avec succès` ); } async function migrateChallenges() { console.log("\n📦 Migration des Challenges..."); const challenges = readSQLite("Challenge"); for (const challenge of challenges) { try { await prismaPG.challenge.upsert({ where: { id: challenge.id }, update: { challengerId: challenge.challengerId, challengedId: challenge.challengedId, title: challenge.title, description: challenge.description, pointsReward: challenge.pointsReward, status: challenge.status, adminId: challenge.adminId, adminComment: challenge.adminComment, winnerId: challenge.winnerId, createdAt: new Date(challenge.createdAt), acceptedAt: challenge.acceptedAt ? new Date(challenge.acceptedAt) : null, completedAt: challenge.completedAt ? new Date(challenge.completedAt) : null, updatedAt: new Date(challenge.updatedAt), }, create: { id: challenge.id, challengerId: challenge.challengerId, challengedId: challenge.challengedId, title: challenge.title, description: challenge.description, pointsReward: challenge.pointsReward, status: challenge.status, adminId: challenge.adminId, adminComment: challenge.adminComment, winnerId: challenge.winnerId, createdAt: new Date(challenge.createdAt), acceptedAt: challenge.acceptedAt ? new Date(challenge.acceptedAt) : null, completedAt: challenge.completedAt ? new Date(challenge.completedAt) : null, updatedAt: new Date(challenge.updatedAt), }, }); stats.challenges++; process.stdout.write( `\r ✅ ${stats.challenges}/${challenges.length} défis migrés` ); } catch (error) { stats.errors++; console.error(`\n ❌ Erreur sur challenge ${challenge.id}:`, error); } } console.log(`\n ✅ ${stats.challenges} défis migrés avec succès`); } async function main() { console.log("🚀 Démarrage de la migration SQLite → PostgreSQL"); console.log(`📂 SQLite: ${SQLITE_DB_PATH}`); console.log(`🐘 PostgreSQL: ${POSTGRES_URL?.replace(/:[^:@]+@/, ":****@")}`); try { // Import dynamique du PrismaClient depuis la racine du projet const prismaModule = await import( join(projectRoot, "prisma/generated/prisma/client") ); const { PrismaClient } = prismaModule; // Vérifier que le fichier SQLite existe if (!existsSync(SQLITE_DB_PATH)) { console.error(`\n❌ Le fichier SQLite n'existe pas: ${SQLITE_DB_PATH}`); console.error(`\n💡 Vérifications:`); console.error(` 1. Le volume est-il monté dans docker-compose.yml ?`); console.error(` 2. Le fichier existe-t-il sur l'hôte ?`); console.error(` 3. Le chemin est-il correct ?`); console.error(`\n Pour vérifier dans le container:`); console.error(` docker-compose exec got-app ls -la /app/data/`); throw new Error(`Fichier SQLite introuvable: ${SQLITE_DB_PATH}`); } console.log(` ✅ Fichier SQLite trouvé: ${SQLITE_DB_PATH}`); // Initialiser les clients sqliteDb = new Database(SQLITE_DB_PATH, { readonly: true }); pgPool = new Pool({ connectionString: POSTGRES_URL, }); const pgAdapter = new PrismaPg(pgPool); prismaPG = new PrismaClient({ adapter: pgAdapter, log: ["error"], }); // Vérifier les connexions console.log("\n🔍 Vérification des connexions..."); if (!sqliteDb.open) { throw new Error("Impossible d'ouvrir la base SQLite"); } console.log(" ✅ SQLite connecté"); await prismaPG.$connect(); console.log(" ✅ PostgreSQL connecté"); // Migration dans l'ordre des dépendances await migrateUsers(); await migrateUserPreferences(); await migrateEvents(); await migrateEventRegistrations(); await migrateEventFeedbacks(); await migrateSitePreferences(); await migrateChallenges(); // Résumé console.log("\n" + "=".repeat(50)); console.log("📊 Résumé de la migration:"); console.log("=".repeat(50)); console.log(` Users: ${stats.users}`); console.log(` UserPreferences: ${stats.userPreferences}`); console.log(` Events: ${stats.events}`); console.log(` EventRegistrations: ${stats.eventRegistrations}`); console.log(` EventFeedbacks: ${stats.eventFeedbacks}`); console.log(` SitePreferences: ${stats.sitePreferences}`); console.log(` Challenges: ${stats.challenges}`); console.log(` Erreurs: ${stats.errors}`); console.log("=".repeat(50)); if (stats.errors > 0) { console.log( "\n⚠️ Certaines erreurs sont survenues. Vérifiez les logs ci-dessus." ); process.exit(1); } else { console.log("\n✅ Migration terminée avec succès!"); } } catch (error) { console.error("\n❌ Erreur fatale:", error); process.exit(1); } finally { if (prismaPG) { await prismaPG.$disconnect(); } if (sqliteDb) { sqliteDb.close(); } if (pgPool) { await pgPool.end(); } } } main();