diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md index 6d76db9..73af1a3 100644 --- a/DATABASE_SETUP.md +++ b/DATABASE_SETUP.md @@ -140,8 +140,11 @@ docker compose up -d adminer Une fois PostgreSQL démarré, tu peux migrer les données : ```bash -# Migrer les skills depuis JSON vers PostgreSQL -curl -X POST http://localhost:3000/api/skills/migrate +# Synchroniser les skills depuis JSON vers PostgreSQL +pnpm run sync-skills + +# Synchroniser les teams depuis JSON vers PostgreSQL +pnpm run sync-teams # Vérifier dans Adminer : http://localhost:8080 # Ou en ligne de commande : diff --git a/MIGRATION_UUID.md b/MIGRATION_UUID.md index 2c4af9e..5764d51 100644 --- a/MIGRATION_UUID.md +++ b/MIGRATION_UUID.md @@ -20,8 +20,9 @@ dropdb peakskills createdb peakskills psql -h localhost -U peakskills_user -d peakskills -f scripts/init.sql -# 3. Migrer les données skills -npm run migrate-skills +# 3. Synchroniser les données skills et teams +pnpm run sync-skills +pnpm run sync-teams # 4. Démarrer l'app npm run dev @@ -29,19 +30,14 @@ npm run dev ### Scenario B : Migration base existante -```bash -# 1. Se connecter à PostgreSQL -psql -h localhost -U peakskills_user -d peakskills +**⚠️ Script de migration supprimé** -# 2. Exécuter le script de migration -\i scripts/migrate-to-uuid.sql +Le script `migrate-to-uuid.sql` a été supprimé. Pour migrer une base existante : -# 3. Vérifier la migration -SELECT id, uuid_id, first_name, last_name FROM users LIMIT 5; - -# 4. Redémarrer l'app -npm run dev -``` +1. **Sauvegarde** tes données importantes +2. **Supprime** l'ancienne base : `dropdb peakskills` +3. **Recrée** avec le nouveau schéma : `createdb peakskills` +4. **Utilise** le Scenario A ci-dessus ### 4. Nettoyer les anciennes sessions @@ -105,8 +101,7 @@ npm run dev ### Infrastructure - `middleware.ts` → Variables UUID -- `scripts/migrate-to-uuid.sql` → Schema DB migration (pour existants) -- `scripts/init.sql` → Schema DB initial avec UUIDs (pour nouvelles installs) +- `scripts/init.sql` → Schema DB initial avec UUIDs ## 🧹 Nettoyage futur diff --git a/app/api/skills/migrate/route.ts b/app/api/skills/migrate/route.ts deleted file mode 100644 index fa44811..0000000 --- a/app/api/skills/migrate/route.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { NextResponse } from "next/server"; -import { SkillsService } from "@/services"; -import { loadSkillCategoriesFromFiles } from "@/lib/skill-file-loader"; - -export async function POST() { - try { - console.log("🚀 Starting skills migration via API..."); - - // Load all skill categories from JSON files - const skillCategories = loadSkillCategoriesFromFiles(); - - console.log(`📊 Found ${skillCategories.length} categories`); - - const totalSkills = skillCategories.reduce( - (sum, cat) => sum + cat.skills.length, - 0 - ); - console.log(`🎯 Total skills to migrate: ${totalSkills}`); - - // Bulk insert into database - await SkillsService.bulkInsertSkillsFromJSON(skillCategories); - - console.log("✅ Skills migration completed successfully!"); - - // Verify the migration - const categoriesFromDb = await SkillsService.getSkillCategories(); - const totalSkillsInDb = categoriesFromDb.reduce( - (sum, cat) => sum + cat.skills.length, - 0 - ); - - return NextResponse.json({ - success: true, - message: "Skills migration completed successfully", - stats: { - categoriesMigrated: skillCategories.length, - skillsMigrated: totalSkills, - categoriesInDb: categoriesFromDb.length, - skillsInDb: totalSkillsInDb, - }, - }); - } catch (error) { - console.error("❌ Migration failed:", error); - return NextResponse.json( - { - error: "Failed to migrate skills", - details: error instanceof Error ? error.message : "Unknown error", - }, - { status: 500 } - ); - } -} diff --git a/middleware.ts b/middleware.ts index d7fa263..9df67eb 100644 --- a/middleware.ts +++ b/middleware.ts @@ -23,8 +23,7 @@ export function middleware(request: NextRequest) { if ( pathname.includes("/_next/") || pathname.includes("/favicon.ico") || - pathname.includes("/public/") || - pathname.includes("/api/skills/migrate") + pathname.includes("/public/") ) { return NextResponse.next(); } diff --git a/package.json b/package.json index 08faf0a..84ea69f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "next dev", "lint": "next lint", "start": "next start", - "generate-test-data": "tsx scripts/generate-test-data.ts" + "generate-test-data": "tsx scripts/generate-test-data.ts", + "sync-skills": "tsx scripts/sync-skills.ts", + "sync-teams": "tsx scripts/sync-teams.ts" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^7.0.0", diff --git a/scripts/init.sql b/scripts/init.sql index 0222be8..eda23f3 100644 --- a/scripts/init.sql +++ b/scripts/init.sql @@ -84,17 +84,6 @@ CREATE TABLE skill_evaluations ( UNIQUE(user_evaluation_id, skill_id) ); --- Insert initial teams data -INSERT INTO teams (id, name, direction) VALUES - ('frontend', 'Frontend', 'Engineering'), - ('backend', 'Backend', 'Engineering'), - ('devops', 'DevOps', 'Engineering'), - ('mobile', 'Mobile', 'Engineering'), - ('data', 'Data Science', 'Engineering'), - ('product', 'Product', 'Product'), - ('design', 'Design', 'Product'), - ('marketing', 'Marketing', 'Business'); - -- Indexes for performance CREATE INDEX idx_teams_direction ON teams(direction); CREATE INDEX idx_skills_category_id ON skills(category_id); diff --git a/scripts/migrate-skills.ts b/scripts/migrate-skills.ts deleted file mode 100644 index 2ba96d7..0000000 --- a/scripts/migrate-skills.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { SkillsService } from "../services/skills-service"; -import { loadSkillCategoriesFromFiles } from "../lib/skill-file-loader"; - -async function migrateSkillsToDatabase() { - console.log("🚀 Starting skills migration..."); - - try { - // Load all skill categories from JSON files - const skillCategories = loadSkillCategoriesFromFiles(); - - console.log(`📊 Found ${skillCategories.length} categories`); - - const totalSkills = skillCategories.reduce( - (sum, cat) => sum + cat.skills.length, - 0 - ); - console.log(`🎯 Total skills to migrate: ${totalSkills}`); - - // Bulk insert into database - await SkillsService.bulkInsertSkillsFromJSON(skillCategories); - - console.log("✅ Skills migration completed successfully!"); - - // Verify the migration - const categoriesFromDb = await SkillsService.getSkillCategories(); - console.log( - `✨ Verification: ${categoriesFromDb.length} categories in database` - ); - - const totalSkillsInDb = categoriesFromDb.reduce( - (sum, cat) => sum + cat.skills.length, - 0 - ); - console.log(`✨ Verification: ${totalSkillsInDb} skills in database`); - } catch (error) { - console.error("❌ Migration failed:", error); - process.exit(1); - } -} - -// Run if called directly -if (require.main === module) { - migrateSkillsToDatabase() - .then(() => { - console.log("🎉 Migration script completed"); - process.exit(0); - }) - .catch((error) => { - console.error("💥 Migration script failed:", error); - process.exit(1); - }); -} - -export { migrateSkillsToDatabase }; diff --git a/scripts/migrate-to-uuid.sql b/scripts/migrate-to-uuid.sql deleted file mode 100644 index 664f5d8..0000000 --- a/scripts/migrate-to-uuid.sql +++ /dev/null @@ -1,40 +0,0 @@ --- Migration script: Replace sequential user IDs with UUIDs for security --- This prevents enumeration attacks and improves security - --- Step 1: Enable UUID extension if not already enabled -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - --- Step 2: Add new UUID column to users table -ALTER TABLE users ADD COLUMN uuid_id UUID DEFAULT uuid_generate_v4(); - --- Step 3: Update all existing users to have UUIDs (they will be auto-generated) -UPDATE users SET uuid_id = uuid_generate_v4() WHERE uuid_id IS NULL; - --- Step 4: Make UUID column NOT NULL -ALTER TABLE users ALTER COLUMN uuid_id SET NOT NULL; - --- Step 5: Add new UUID column to user_evaluations table -ALTER TABLE user_evaluations ADD COLUMN user_uuid UUID; - --- Step 6: Update user_evaluations to use the new UUIDs -UPDATE user_evaluations -SET user_uuid = users.uuid_id -FROM users -WHERE user_evaluations.user_id = users.id; - --- Step 7: Make user_uuid NOT NULL -ALTER TABLE user_evaluations ALTER COLUMN user_uuid SET NOT NULL; - --- Step 8: Add new UUID column to skill_evaluations (via user_evaluations) --- No direct change needed as skill_evaluations references user_evaluations.id - --- Step 9: Create unique constraint on UUID -ALTER TABLE users ADD CONSTRAINT users_uuid_unique UNIQUE (uuid_id); - --- Step 10: Add unique constraint and foreign key for user_evaluations -ALTER TABLE user_evaluations ADD CONSTRAINT user_evaluations_user_uuid_unique UNIQUE (user_uuid); -ALTER TABLE user_evaluations ADD CONSTRAINT fk_user_evaluations_user_uuid - FOREIGN KEY (user_uuid) REFERENCES users(uuid_id); - --- Note: The actual switchover will be done in the application code --- The old id columns will be kept temporarily for backward compatibility diff --git a/scripts/sync-skills.ts b/scripts/sync-skills.ts new file mode 100644 index 0000000..07c8dd4 --- /dev/null +++ b/scripts/sync-skills.ts @@ -0,0 +1,149 @@ +#!/usr/bin/env tsx + +import { SkillsService } from "../services/skills-service"; +import { loadSkillCategoriesFromFiles } from "../lib/skill-file-loader"; +import { SkillCategory, Skill } from "../lib/types"; + +interface SyncStats { + categoriesProcessed: number; + skillsProcessed: number; + newSkills: number; + updatedSkills: number; + skippedSkills: number; +} + +/** + * Synchronise les skills depuis les fichiers JSON vers la base de données + * - Préserve les skills existantes (ne modifie que description/image) + * - Ajoute uniquement les nouvelles skills + * - Ne supprime jamais de skills + */ +async function syncSkillsToDatabase(): Promise { + try { + console.log("🚀 Démarrage de la synchronisation des skills..."); + + // Charger les skills depuis les fichiers JSON + const skillCategoriesFromFiles = loadSkillCategoriesFromFiles(); + console.log(`📁 ${skillCategoriesFromFiles.length} catégories trouvées dans les fichiers`); + + // Récupérer les skills existantes de la base de données + const existingCategories = await SkillsService.getSkillCategories(); + console.log(`💾 ${existingCategories.length} catégories existantes en base`); + + // Créer un map des skills existantes pour une recherche rapide + const existingSkillsMap = new Map(); + existingCategories.forEach(category => { + category.skills.forEach(skill => { + existingSkillsMap.set(skill.id, skill); + }); + }); + + console.log(`🔍 ${existingSkillsMap.size} skills existantes détectées`); + + const stats: SyncStats = { + categoriesProcessed: 0, + skillsProcessed: 0, + newSkills: 0, + updatedSkills: 0, + skippedSkills: 0 + }; + + // Synchroniser chaque catégorie + for (const categoryFromFile of skillCategoriesFromFiles) { + console.log(`\n📂 Traitement de la catégorie: ${categoryFromFile.category}`); + + const categoryId = categoryFromFile.category.toLowerCase(); + + // Vérifier si la catégorie existe, sinon la créer + const existingCategory = existingCategories.find(cat => + cat.category.toLowerCase() === categoryFromFile.category.toLowerCase() + ); + + if (!existingCategory) { + console.log(` ➕ Création de la nouvelle catégorie: ${categoryFromFile.category}`); + await SkillsService.createSkillCategory({ + id: categoryId, + name: categoryFromFile.category, + icon: categoryFromFile.icon + }); + } + + // Synchroniser les skills de cette catégorie + for (const skillFromFile of categoryFromFile.skills) { + const existingSkill = existingSkillsMap.get(skillFromFile.id); + + if (!existingSkill) { + // Nouvelle skill - l'ajouter + console.log(` ➕ Nouvelle skill: ${skillFromFile.name} (${skillFromFile.id})`); + await SkillsService.createSkill({ + id: skillFromFile.id, + name: skillFromFile.name, + description: skillFromFile.description, + icon: skillFromFile.icon, + categoryId: categoryId, + links: skillFromFile.links || [] + }); + stats.newSkills++; + } else { + // Skill existante - vérifier s'il faut mettre à jour description/icon/links + const needsUpdate = + existingSkill.description !== skillFromFile.description || + existingSkill.icon !== skillFromFile.icon || + JSON.stringify(existingSkill.links?.sort()) !== JSON.stringify(skillFromFile.links?.sort()); + + if (needsUpdate) { + console.log(` 🔄 Mise à jour de la skill: ${skillFromFile.name} (${skillFromFile.id})`); + // Utiliser la méthode bulkInsert avec une seule catégorie/skill pour la mise à jour + await SkillsService.bulkInsertSkillsFromJSON([{ + category: categoryFromFile.category, + icon: categoryFromFile.icon, + skills: [skillFromFile] + }]); + stats.updatedSkills++; + } else { + console.log(` ✅ Skill inchangée: ${skillFromFile.name} (${skillFromFile.id})`); + stats.skippedSkills++; + } + } + + stats.skillsProcessed++; + } + + stats.categoriesProcessed++; + } + + // Afficher les statistiques finales + console.log("\n📊 Statistiques de synchronisation:"); + console.log(` • Catégories traitées: ${stats.categoriesProcessed}`); + console.log(` • Skills traitées: ${stats.skillsProcessed}`); + console.log(` • Nouvelles skills: ${stats.newSkills}`); + console.log(` • Skills mises à jour: ${stats.updatedSkills}`); + console.log(` • Skills inchangées: ${stats.skippedSkills}`); + + // Vérification finale + const finalCategories = await SkillsService.getSkillCategories(); + const totalSkillsInDb = finalCategories.reduce((sum, cat) => sum + cat.skills.length, 0); + + console.log(`\n✅ Synchronisation terminée avec succès!`); + console.log(`💾 Total skills en base: ${totalSkillsInDb}`); + + } catch (error) { + console.error("❌ Erreur lors de la synchronisation:", error); + process.exit(1); + } +} + +// Exécuter le script si appelé directement +if (require.main === module) { + syncSkillsToDatabase() + .then(() => { + console.log("🎉 Script terminé avec succès"); + process.exit(0); + }) + .catch((error) => { + console.error("💥 Erreur fatale:", error); + process.exit(1); + }); +} + +export { syncSkillsToDatabase }; diff --git a/scripts/sync-teams.ts b/scripts/sync-teams.ts new file mode 100644 index 0000000..b75f971 --- /dev/null +++ b/scripts/sync-teams.ts @@ -0,0 +1,148 @@ +#!/usr/bin/env tsx + +import * as fs from "fs"; +import * as path from "path"; +import { TeamsService } from "../services/teams-service"; +import { Team } from "../lib/types"; + +interface TeamsData { + teams: Team[]; +} + +interface SyncStats { + teamsProcessed: number; + newTeams: number; + updatedTeams: number; + skippedTeams: number; +} + +/** + * Charge les teams depuis le fichier JSON + */ +function loadTeamsFromFile(): Team[] { + try { + const teamsFilePath = path.join(process.cwd(), "data", "teams.json"); + const fileContent = fs.readFileSync(teamsFilePath, "utf-8"); + const teamsData: TeamsData = JSON.parse(fileContent); + + console.log(`📁 ${teamsData.teams.length} teams trouvées dans le fichier`); + return teamsData.teams; + } catch (error) { + console.error("❌ Erreur lors du chargement du fichier teams.json:", error); + throw error; + } +} + +/** + * Synchronise les teams depuis le fichier JSON vers la base de données + * - Préserve les teams existantes (ne modifie que name/direction si changé) + * - Ajoute uniquement les nouvelles teams + * - Ne supprime jamais de teams + */ +async function syncTeamsToDatabase(): Promise { + try { + console.log("🚀 Démarrage de la synchronisation des teams..."); + + // Charger les teams depuis le fichier JSON + const teamsFromFile = loadTeamsFromFile(); + + // Récupérer les teams existantes de la base de données + const existingTeams = await TeamsService.getTeams(); + console.log(`💾 ${existingTeams.length} teams existantes en base`); + + // Créer un map des teams existantes pour une recherche rapide + const existingTeamsMap = new Map(); + existingTeams.forEach((team) => { + existingTeamsMap.set(team.id, team); + }); + + const stats: SyncStats = { + teamsProcessed: 0, + newTeams: 0, + updatedTeams: 0, + skippedTeams: 0, + }; + + // Synchroniser chaque team + for (const teamFromFile of teamsFromFile) { + const existingTeam = existingTeamsMap.get(teamFromFile.id); + + if (!existingTeam) { + // Nouvelle team - l'ajouter + console.log( + `➕ Nouvelle team: ${teamFromFile.name} (${teamFromFile.id})` + ); + await TeamsService.createTeam({ + id: teamFromFile.id, + name: teamFromFile.name, + direction: teamFromFile.direction, + }); + stats.newTeams++; + } else { + // Team existante - vérifier s'il faut mettre à jour + const needsUpdate = + existingTeam.name !== teamFromFile.name || + existingTeam.direction !== teamFromFile.direction; + + if (needsUpdate) { + console.log( + `🔄 Mise à jour de la team: ${teamFromFile.name} (${teamFromFile.id})` + ); + console.log( + ` • Name: "${existingTeam.name}" → "${teamFromFile.name}"` + ); + console.log( + ` • Direction: "${existingTeam.direction}" → "${teamFromFile.direction}"` + ); + + await TeamsService.updateTeam(teamFromFile.id, { + name: teamFromFile.name, + direction: teamFromFile.direction, + }); + stats.updatedTeams++; + } else { + console.log( + `✅ Team inchangée: ${teamFromFile.name} (${teamFromFile.id})` + ); + stats.skippedTeams++; + } + } + + stats.teamsProcessed++; + } + + // Afficher les statistiques finales + console.log("\n📊 Statistiques de synchronisation:"); + console.log(` • Teams traitées: ${stats.teamsProcessed}`); + console.log(` • Nouvelles teams: ${stats.newTeams}`); + console.log(` • Teams mises à jour: ${stats.updatedTeams}`); + console.log(` • Teams inchangées: ${stats.skippedTeams}`); + + // Vérification finale + const finalTeams = await TeamsService.getTeams(); + console.log(`\n✅ Synchronisation terminée avec succès!`); + console.log(`💾 Total teams en base: ${finalTeams.length}`); + + // Afficher les directions disponibles + const directions = await TeamsService.getDirections(); + console.log(`🎯 Directions disponibles: ${directions.join(", ")}`); + } catch (error) { + console.error("❌ Erreur lors de la synchronisation:", error); + process.exit(1); + } +} + +// Exécuter le script si appelé directement +if (require.main === module) { + syncTeamsToDatabase() + .then(() => { + console.log("🎉 Script terminé avec succès"); + process.exit(0); + }) + .catch((error) => { + console.error("💥 Erreur fatale:", error); + process.exit(1); + }); +} + +export { syncTeamsToDatabase }; diff --git a/services/api-client.ts b/services/api-client.ts index 2e2ea06..b29e795 100644 --- a/services/api-client.ts +++ b/services/api-client.ts @@ -24,7 +24,7 @@ export class ApiClient { ): Promise { try { let url = `${this.baseUrl}/api/evaluations`; - + // Mode compatibilité avec profile en paramètres if (profile) { const params = new URLSearchParams({ @@ -383,33 +383,6 @@ export class ApiClient { } } - /** - * Migre les skills depuis JSON vers PostgreSQL - */ - async migrateSkills(): Promise<{ - success: boolean; - stats?: any; - error?: string; - }> { - try { - const response = await fetch(`${this.baseUrl}/api/skills/migrate`, { - method: "POST", - }); - - if (!response.ok) { - throw new Error("Erreur lors de la migration des skills"); - } - - return await response.json(); - } catch (error) { - console.error("Erreur lors de la migration des skills:", error); - return { - success: false, - error: error instanceof Error ? error.message : "Unknown error", - }; - } - } - /** * Crée une nouvelle catégorie de skill */