refactor: update database setup and remove skills migration API

- Changed migration commands in DATABASE_SETUP.md to use `pnpm run sync-skills` and `pnpm run sync-teams`.
- Removed the skills migration API endpoint in route.ts, streamlining the migration process.
- Updated MIGRATION_UUID.md to reflect changes in migration steps and removed the old skills migration script.
- Added new sync scripts for skills and teams in package.json.
- Cleaned up init.sql by removing old teams data insertion and adjusted comments for clarity.
This commit is contained in:
Julien Froidefond
2025-08-21 15:21:36 +02:00
parent dad172157b
commit a4b680b092
11 changed files with 317 additions and 205 deletions

View File

@@ -140,8 +140,11 @@ docker compose up -d adminer
Une fois PostgreSQL démarré, tu peux migrer les données : Une fois PostgreSQL démarré, tu peux migrer les données :
```bash ```bash
# Migrer les skills depuis JSON vers PostgreSQL # Synchroniser les skills depuis JSON vers PostgreSQL
curl -X POST http://localhost:3000/api/skills/migrate pnpm run sync-skills
# Synchroniser les teams depuis JSON vers PostgreSQL
pnpm run sync-teams
# Vérifier dans Adminer : http://localhost:8080 # Vérifier dans Adminer : http://localhost:8080
# Ou en ligne de commande : # Ou en ligne de commande :

View File

@@ -20,8 +20,9 @@ dropdb peakskills
createdb peakskills createdb peakskills
psql -h localhost -U peakskills_user -d peakskills -f scripts/init.sql psql -h localhost -U peakskills_user -d peakskills -f scripts/init.sql
# 3. Migrer les données skills # 3. Synchroniser les données skills et teams
npm run migrate-skills pnpm run sync-skills
pnpm run sync-teams
# 4. Démarrer l'app # 4. Démarrer l'app
npm run dev npm run dev
@@ -29,19 +30,14 @@ npm run dev
### Scenario B : Migration base existante ### Scenario B : Migration base existante
```bash **⚠️ Script de migration supprimé**
# 1. Se connecter à PostgreSQL
psql -h localhost -U peakskills_user -d peakskills
# 2. Exécuter le script de migration Le script `migrate-to-uuid.sql` a été supprimé. Pour migrer une base existante :
\i scripts/migrate-to-uuid.sql
# 3. Vérifier la migration 1. **Sauvegarde** tes données importantes
SELECT id, uuid_id, first_name, last_name FROM users LIMIT 5; 2. **Supprime** l'ancienne base : `dropdb peakskills`
3. **Recrée** avec le nouveau schéma : `createdb peakskills`
# 4. Redémarrer l'app 4. **Utilise** le Scenario A ci-dessus
npm run dev
```
### 4. Nettoyer les anciennes sessions ### 4. Nettoyer les anciennes sessions
@@ -105,8 +101,7 @@ npm run dev
### Infrastructure ### Infrastructure
- `middleware.ts` → Variables UUID - `middleware.ts` → Variables UUID
- `scripts/migrate-to-uuid.sql` → Schema DB migration (pour existants) - `scripts/init.sql` → Schema DB initial avec UUIDs
- `scripts/init.sql` → Schema DB initial avec UUIDs (pour nouvelles installs)
## 🧹 Nettoyage futur ## 🧹 Nettoyage futur

View File

@@ -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 }
);
}
}

View File

@@ -23,8 +23,7 @@ export function middleware(request: NextRequest) {
if ( if (
pathname.includes("/_next/") || pathname.includes("/_next/") ||
pathname.includes("/favicon.ico") || pathname.includes("/favicon.ico") ||
pathname.includes("/public/") || pathname.includes("/public/")
pathname.includes("/api/skills/migrate")
) { ) {
return NextResponse.next(); return NextResponse.next();
} }

View File

@@ -7,7 +7,9 @@
"dev": "next dev", "dev": "next dev",
"lint": "next lint", "lint": "next lint",
"start": "next start", "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": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^7.0.0", "@fortawesome/fontawesome-svg-core": "^7.0.0",

View File

@@ -84,17 +84,6 @@ CREATE TABLE skill_evaluations (
UNIQUE(user_evaluation_id, skill_id) 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 -- Indexes for performance
CREATE INDEX idx_teams_direction ON teams(direction); CREATE INDEX idx_teams_direction ON teams(direction);
CREATE INDEX idx_skills_category_id ON skills(category_id); CREATE INDEX idx_skills_category_id ON skills(category_id);

View File

@@ -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 };

View File

@@ -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

149
scripts/sync-skills.ts Normal file
View File

@@ -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<void> {
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<string, Skill>();
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 };

148
scripts/sync-teams.ts Normal file
View File

@@ -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<void> {
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<string, Team>();
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 };

View File

@@ -24,7 +24,7 @@ export class ApiClient {
): Promise<UserEvaluation | null> { ): Promise<UserEvaluation | null> {
try { try {
let url = `${this.baseUrl}/api/evaluations`; let url = `${this.baseUrl}/api/evaluations`;
// Mode compatibilité avec profile en paramètres // Mode compatibilité avec profile en paramètres
if (profile) { if (profile) {
const params = new URLSearchParams({ 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 * Crée une nouvelle catégorie de skill
*/ */