490 lines
15 KiB
TypeScript
490 lines
15 KiB
TypeScript
#!/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();
|