29 KiB
Workflow Détaillé : Création de Mission - Prisma, Minio et N8N
📋 Vue d'Ensemble
Ce document trace chaque étape du workflow de création de mission, depuis le formulaire frontend jusqu'aux intégrations externes via N8N, en passant par Prisma (base de données) et Minio (stockage de fichiers).
🔄 Flux Complet - Vue d'Ensemble
┌─────────────────────────────────────────────────────────────┐
│ 1. FRONTEND - MissionsAdminPanel │
│ - Validation des champs │
│ - Préparation des données (base64 pour fichiers) │
│ - POST /api/missions │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. BACKEND - POST /api/missions │
│ ├─ Authentification (NextAuth) │
│ ├─ Validation des champs requis │
│ ├─ STEP 1: Prisma.mission.create() │
│ ├─ STEP 2: Prisma.missionUser.createMany() │
│ ├─ STEP 3: Upload Logo → Minio │
│ ├─ STEP 4: Upload Attachments → Minio │
│ ├─ STEP 5: Vérification fichiers Minio │
│ └─ STEP 6: N8N Workflow Trigger │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. N8N WORKFLOW │
│ - Création Leantime Project │
│ - Création Outline Collection │
│ - Création RocketChat Channel │
│ - Création Gitea Repository │
│ - Création Penpot Project │
└─────────────────────────────────────────────────────────────┘
📝 ÉTAPE 1 : Frontend - Préparation des Données
Fichier : components/missions/missions-admin-panel.tsx
1.1 Validation (validateMission())
Lignes 369-397
const validateMission = () => {
const requiredFields = {
name: !!missionData.name,
oddScope: Array.isArray(missionData.oddScope) && missionData.oddScope.length > 0,
niveau: !!missionData.niveau,
intention: !!missionData.intention,
missionType: !!missionData.missionType,
donneurDOrdre: !!missionData.donneurDOrdre,
projection: !!missionData.projection,
participation: !!missionData.participation,
gardiens: gardienDuTemps !== null &&
gardienDeLaParole !== null &&
gardienDeLaMemoire !== null
};
// Vérifie que tous les champs requis sont remplis
// Retourne false si un champ manque
}
Champs requis :
- ✅
name: Nom de la mission - ✅
oddScope: Array avec au moins 1 ODD - ✅
niveau: A/B/C/S - ✅
intention: Description - ✅
missionType: remote/onsite/hybrid - ✅
donneurDOrdre: individual/group/organization - ✅
projection: short/medium/long - ✅
participation: volontaire/cooptation - ✅
gardiens: Les 3 gardiens doivent être assignés
1.2 Préparation des Données (handleSubmitMission())
Lignes 400-460
const handleSubmitMission = async () => {
// 1. Validation
if (!validateMission()) return;
// 2. Préparation des gardiens
const guardians = {
"gardien-temps": gardienDuTemps,
"gardien-parole": gardienDeLaParole,
"gardien-memoire": gardienDeLaMemoire
};
// 3. Construction de l'objet de soumission
const missionSubmitData = {
...missionData, // name, oddScope, niveau, intention, etc.
services: selectedServices, // ["Gite", "ArtLab", "Calcul"]
profils: selectedProfils, // ["DataIntelligence", "Expression", ...]
guardians, // { "gardien-temps": userId, ... }
volunteers: volontaires, // [userId1, userId2, ...]
logo: missionData.logo // { data: "data:image/png;base64,...", name, type }
};
// 4. Envoi à l'API
const response = await fetch('/api/missions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(missionSubmitData)
});
}
Format du logo (si présent) :
logo: {
data: "data:image/png;base64,iVBORw0KGgoAAAANS...", // Base64 avec préfixe
name: "logo.png",
type: "image/png"
}
Format des attachments (si présents) :
attachments: [
{
data: "data:application/pdf;base64,JVBERi0xLjQKJe...",
name: "document.pdf",
type: "application/pdf"
}
]
🗄️ ÉTAPE 2 : Backend - POST /api/missions
Fichier : app/api/missions/route.ts
2.1 Authentification et Validation
Lignes 205-224
export async function POST(request: Request) {
// 1. Vérification de l'authentification
const { authorized, userId } = await checkAuth(request);
if (!authorized || !userId) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// 2. Parsing du body
const body = await request.json();
// 3. Validation minimale (name et oddScope requis)
if (!body.name || !body.oddScope) {
return NextResponse.json({
error: 'Missing required fields',
missingFields: ['name', 'oddScope'].filter(field => !body[field])
}, { status: 400 });
}
}
2.2 STEP 1 : Création de la Mission en Base de Données
Lignes 226-248
// Préparation des données pour Prisma
const missionData = {
name: body.name,
oddScope: body.oddScope, // Array de strings
niveau: body.niveau,
intention: body.intention,
missionType: body.missionType,
donneurDOrdre: body.donneurDOrdre,
projection: body.projection,
services: body.services, // Array de strings
profils: body.profils, // Array de strings
participation: body.participation,
creatorId: userId, // ID de l'utilisateur connecté
logo: null, // Sera mis à jour après upload
};
// Création en base de données
const mission = await prisma.mission.create({
data: missionData
});
// Résultat : mission.id est maintenant disponible
// Exemple : mission.id = "abc-123-def-456"
Schéma Prisma :
model Mission {
id String @id @default(uuid())
name String
logo String? // null pour l'instant
oddScope String[]
niveau String
intention String
missionType String
donneurDOrdre String
projection String
services String[]
participation String?
profils String[]
creatorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
creator User @relation(...)
missionUsers MissionUser[]
attachments Attachment[]
}
Points importants :
- ✅ La mission est créée AVANT l'upload des fichiers
- ✅ Le
mission.idest généré automatiquement (UUID) - ✅ Le champ
logoestnullpour l'instant - ✅ Tous les champs sont sauvegardés sauf les fichiers
2.3 STEP 2 : Création des MissionUsers (Gardiens + Volontaires)
Lignes 250-283
// Préparation du tableau de MissionUsers
const missionUsers = [];
// 2.1 Ajout des gardiens
if (body.guardians) {
for (const [role, guardianId] of Object.entries(body.guardians)) {
if (guardianId) {
missionUsers.push({
missionId: mission.id, // ID de la mission créée
userId: guardianId, // ID de l'utilisateur gardien
role: role // "gardien-temps", "gardien-parole", "gardien-memoire"
});
}
}
}
// 2.2 Ajout des volontaires
if (body.volunteers && body.volunteers.length > 0) {
for (const volunteerId of body.volunteers) {
missionUsers.push({
missionId: mission.id,
userId: volunteerId,
role: 'volontaire'
});
}
}
// 2.3 Création en batch dans Prisma
if (missionUsers.length > 0) {
await prisma.missionUser.createMany({
data: missionUsers
});
}
Schéma Prisma MissionUser :
model MissionUser {
id String @id @default(uuid())
role String // 'gardien-temps', 'gardien-parole', 'gardien-memoire', 'volontaire'
missionId String
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
mission Mission @relation(...)
user User @relation(...)
@@unique([missionId, userId, role]) // Un utilisateur ne peut avoir qu'un rôle par mission
}
Exemple de données créées :
missionUsers = [
{ missionId: "abc-123", userId: "user-1", role: "gardien-temps" },
{ missionId: "abc-123", userId: "user-2", role: "gardien-parole" },
{ missionId: "abc-123", userId: "user-3", role: "gardien-memoire" },
{ missionId: "abc-123", userId: "user-4", role: "volontaire" },
{ missionId: "abc-123", userId: "user-5", role: "volontaire" }
]
Points importants :
- ✅ Utilisation de
createMany()pour performance (1 requête au lieu de N) - ✅ Contrainte unique : un utilisateur ne peut avoir qu'un rôle par mission
- ✅ Les gardiens sont obligatoires, les volontaires optionnels
2.4 STEP 3 : Upload du Logo vers Minio
Lignes 285-310
let logoPath = null;
if (body.logo?.data) {
try {
// 3.1 Conversion base64 → Buffer → File
const base64Data = body.logo.data.split(',')[1]; // Retire "data:image/png;base64,"
const buffer = Buffer.from(base64Data, 'base64');
const file = new File(
[buffer],
body.logo.name || 'logo.png',
{ type: body.logo.type || 'image/png' }
);
// 3.2 Upload vers Minio
const { filePath } = await uploadMissionLogo(userId, mission.id, file);
logoPath = filePath; // Ex: "missions/abc-123/logo.png"
uploadedFiles.push({ type: 'logo', path: filePath });
// 3.3 Mise à jour de la mission avec le chemin du logo
await prisma.mission.update({
where: { id: mission.id },
data: { logo: filePath }
});
} catch (uploadError) {
throw new Error('Failed to upload logo');
}
}
Fonction uploadMissionLogo() - lib/mission-uploads.ts :
Lignes 96-145
export async function uploadMissionLogo(
userId: string,
missionId: string,
file: File
): Promise<{ filePath: string }> {
// 1. Génération du chemin
const fileExtension = file.name.substring(file.name.lastIndexOf('.'));
const filePath = getMissionLogoPath(userId, missionId, fileExtension);
// Résultat : "missions/{missionId}/logo.png"
// 2. Conversion pour Minio (retire le préfixe "missions/")
const minioPath = filePath.replace(/^missions\//, '');
// Résultat : "{missionId}/logo.png"
// 3. Conversion File → Buffer
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 4. Upload vers Minio via S3 SDK
await s3Client.send(new PutObjectCommand({
Bucket: 'missions', // Bucket Minio
Key: minioPath, // "{missionId}/logo.png"
Body: buffer, // Contenu du fichier
ContentType: file.type, // "image/png"
ACL: 'public-read' // Accès public en lecture
}));
return { filePath }; // Retourne le chemin complet avec préfixe
}
Configuration Minio :
// lib/mission-uploads.ts
const s3Client = new S3Client({
region: 'us-east-1',
endpoint: 'https://dome-api.slm-lab.net', // Endpoint Minio
credentials: {
accessKeyId: '4aBT4CMb7JIMMyUtp4Pl',
secretAccessKey: 'HGn39XhCIlqOjmDVzRK9MED2Fci2rYvDDgbLFElg'
},
forcePathStyle: true // Requis pour MinIO
});
Structure dans Minio :
Bucket: missions
└── {missionId}/
└── logo.png
Points importants :
- ✅ Le logo est uploadé APRÈS la création de la mission (pour avoir le missionId)
- ✅ Le chemin est mis à jour dans Prisma après l'upload
- ✅ Le fichier est stocké avec ACL
public-readpour accès public - ✅ Le chemin stocké inclut le préfixe
missions/pour cohérence
2.5 STEP 4 : Upload des Attachments vers Minio
Lignes 312-343
if (body.attachments && body.attachments.length > 0) {
try {
// 4.1 Traitement parallèle de tous les attachments
const attachmentPromises = body.attachments.map(async (attachment: any) => {
// Conversion base64 → Buffer → File
const base64Data = attachment.data.split(',')[1];
const buffer = Buffer.from(base64Data, 'base64');
const file = new File(
[buffer],
attachment.name || 'attachment',
{ type: attachment.type || 'application/octet-stream' }
);
// 4.2 Upload vers Minio
const { filePath, filename, fileType, fileSize } =
await uploadMissionAttachment(userId, mission.id, file);
uploadedFiles.push({ type: 'attachment', path: filePath });
// 4.3 Création de l'enregistrement Attachment en base
return prisma.attachment.create({
data: {
missionId: mission.id,
filename, // "document.pdf"
filePath, // "missions/abc-123/attachments/document.pdf"
fileType, // "application/pdf"
fileSize, // 123456 (bytes)
uploaderId: userId
}
});
});
// 4.4 Attente de tous les uploads (parallèle)
await Promise.all(attachmentPromises);
} catch (attachmentError) {
throw new Error('Failed to upload attachments');
}
}
Fonction uploadMissionAttachment() - lib/mission-uploads.ts :
Lignes 148-210
export async function uploadMissionAttachment(
userId: string,
missionId: string,
file: File
): Promise<{
filename: string;
filePath: string;
fileType: string;
fileSize: number;
}> {
// 1. Génération du chemin
const filePath = getMissionAttachmentPath(userId, missionId, file.name);
// Résultat : "missions/{missionId}/attachments/{filename}"
// 2. Conversion pour Minio
const minioPath = filePath.replace(/^missions\//, '');
// Résultat : "{missionId}/attachments/{filename}"
// 3. Conversion File → Buffer
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 4. Upload vers Minio
await s3Client.send(new PutObjectCommand({
Bucket: 'missions',
Key: minioPath,
Body: buffer,
ContentType: file.type,
ACL: 'public-read'
}));
return {
filename: file.name,
filePath, // Chemin complet avec préfixe
fileType: file.type,
fileSize: file.size
};
}
Schéma Prisma Attachment :
model Attachment {
id String @id @default(uuid())
filename String // "document.pdf"
filePath String // "missions/{missionId}/attachments/{filename}"
fileType String // "application/pdf"
fileSize Int // 123456
missionId String
uploaderId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
mission Mission @relation(...)
uploader User @relation(...)
}
Structure dans Minio :
Bucket: missions
└── {missionId}/
├── logo.png
└── attachments/
├── document1.pdf
├── document2.docx
└── image.jpg
Points importants :
- ✅ Uploads en parallèle avec
Promise.all()pour performance - ✅ Chaque attachment crée un enregistrement Prisma séparé
- ✅ Le
uploaderIdest l'utilisateur qui a créé la mission - ✅ Les fichiers sont stockés avec ACL
public-read
2.6 STEP 5 : Vérification des Fichiers dans Minio
Lignes 345-365
// 5.1 Vérification du logo
if (logoPath) {
const logoExists = await verifyFileExists(logoPath);
if (!logoExists) {
throw new Error('Logo file not found in Minio');
}
}
// 5.2 Vérification des attachments
if (body.attachments?.length > 0) {
const attachmentVerifications = uploadedFiles
.filter(f => f.type === 'attachment')
.map(f => verifyFileExists(f.path));
const attachmentResults = await Promise.all(attachmentVerifications);
if (attachmentResults.some(exists => !exists)) {
throw new Error('One or more attachment files not found in Minio');
}
}
Fonction verifyFileExists() - Lignes 191-202
async function verifyFileExists(filePath: string): Promise<boolean> {
try {
await s3Client.send(new HeadObjectCommand({
Bucket: 'missions',
Key: filePath.replace('missions/', '') // Retire le préfixe
}));
return true; // Fichier existe
} catch (error) {
return false; // Fichier n'existe pas
}
}
Points importants :
- ✅ Vérification AVANT de déclencher N8N
- ✅ Utilise
HeadObjectCommand(légère, ne télécharge pas le fichier) - ✅ Si un fichier manque, le workflow s'arrête avec erreur
2.7 STEP 6 : Déclenchement du Workflow N8N
Lignes 367-393
// 6.1 Préparation des données pour N8N
const n8nData = {
...body, // Toutes les données de la mission
creatorId: userId, // ID du créateur
logoPath: logoPath, // Chemin du logo (ou null)
config: {
N8N_API_KEY: process.env.N8N_API_KEY,
MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL
}
};
// 6.2 Déclenchement du workflow
const n8nService = new N8nService();
const workflowResult = await n8nService.triggerMissionCreation(n8nData);
// 6.3 Vérification du résultat
if (!workflowResult.success) {
throw new Error(workflowResult.error || 'N8N workflow failed');
}
// 6.4 Retour succès
return NextResponse.json({
success: true,
mission,
message: 'Mission created successfully with all integrations'
});
🔗 ÉTAPE 3 : Service N8N
Fichier : lib/services/n8n-service.ts
3.1 Configuration
Lignes 3-17
export class N8nService {
private webhookUrl: string;
private rollbackWebhookUrl: string;
private apiKey: string;
constructor() {
this.webhookUrl = process.env.N8N_WEBHOOK_URL ||
'https://brain.slm-lab.net/webhook/mission-created';
this.rollbackWebhookUrl = process.env.N8N_ROLLBACK_WEBHOOK_URL ||
'https://brain.slm-lab.net/webhook/mission-rollback';
this.apiKey = process.env.N8N_API_KEY || '';
}
}
3.2 Nettoyage et Validation des Données
Lignes 19-49
async triggerMissionCreation(data: any): Promise<any> {
// Nettoyage des données
const cleanData = {
name: data.name,
oddScope: Array.isArray(data.oddScope) ? data.oddScope : [data.oddScope],
niveau: data.niveau || 'default',
intention: data.intention?.trim() || '',
missionType: data.missionType || 'default',
donneurDOrdre: data.donneurDOrdre || 'default',
projection: data.projection || 'default',
services: Array.isArray(data.services) ? data.services : [],
participation: data.participation || 'default',
profils: Array.isArray(data.profils) ? data.profils : [],
guardians: data.guardians || {},
volunteers: Array.isArray(data.volunteers) ? data.volunteers : [],
creatorId: data.creatorId,
config: {
...data.config,
N8N_API_KEY: this.apiKey,
MISSION_API_URL: process.env.NEXT_PUBLIC_API_URL || 'https://api.slm-lab.net/api'
}
};
}
Points importants :
- ✅ Normalisation des arrays (assure qu'ils sont bien des arrays)
- ✅ Valeurs par défaut pour éviter les undefined
- ✅ Trim de l'intention pour retirer les espaces
- ✅ Conservation de la config avec les clés API
3.3 Envoi au Webhook N8N
Lignes 73-96
const response = await fetch(this.webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey // Authentification
},
body: JSON.stringify(cleanData)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
3.4 Traitement de la Réponse
Lignes 98-143
const responseText = await response.text();
try {
const result = JSON.parse(responseText);
// Détection d'erreurs partielles
if (result.error || result.message?.includes('failed')) {
const errorMessage = result.message || result.error;
const failedServices = {
gitRepo: errorMessage.includes('Git repository creation failed'),
leantimeProject: errorMessage.includes('Leantime project creation failed'),
docCollection: errorMessage.includes('Documentation collection creation failed'),
rocketChatChannel: errorMessage.includes('RocketChat channel creation failed')
};
// Retourne succès avec erreurs partielles
return {
success: true,
results: { ...result, failedServices }
};
}
return { success: true, results: result };
} catch (parseError) {
// Si la réponse n'est pas JSON, considère comme succès
return {
success: true,
results: {
logoUrl: null,
leantimeProjectId: null,
outlineCollectionId: null,
rocketChatChannelId: null,
giteaRepositoryUrl: null
}
};
}
Format de réponse attendu de N8N :
{
"leantimeProjectId": "project-123",
"outlineCollectionId": "collection-456",
"rocketChatChannelId": "channel-789",
"giteaRepositoryUrl": "https://git.slm-lab.net/mission-abc",
"penpotProjectId": "penpot-xyz"
}
Intégrations créées par N8N :
- Leantime : Projet de gestion de projet
- Outline : Collection de documentation
- RocketChat : Canal de communication
- Gitea : Repository Git
- Penpot : Projet de design
Points importants :
- ✅ Gestion des erreurs partielles (certains services peuvent échouer)
- ✅ Si la réponse n'est pas JSON, considère comme succès (workflow déclenché)
- ✅ Les IDs retournés ne sont PAS sauvegardés en base (TODO)
🧹 Gestion des Erreurs et Cleanup
Fichier : app/api/missions/route.ts - Lignes 398-418
catch (error) {
console.error('Error in mission creation:', error);
// Cleanup: Suppression de tous les fichiers uploadés
for (const file of uploadedFiles) {
try {
await s3Client.send(new DeleteObjectCommand({
Bucket: 'missions',
Key: file.path.replace('missions/', '')
}));
console.log('Cleaned up file:', file.path);
} catch (cleanupError) {
console.error('Error cleaning up file:', file.path, cleanupError);
}
}
return NextResponse.json({
error: 'Failed to create mission',
details: error instanceof Error ? error.message : String(error)
}, { status: 500 });
}
Scénarios de cleanup :
- ✅ Erreur lors de l'upload du logo → Suppression du logo
- ✅ Erreur lors de l'upload d'un attachment → Suppression de tous les fichiers
- ✅ Erreur lors de la vérification Minio → Suppression de tous les fichiers
- ✅ Erreur N8N → Suppression de tous les fichiers
Points importants :
- ✅ La mission reste en base même en cas d'erreur (orphan)
- ✅ Les MissionUsers restent en base même en cas d'erreur
- ✅ Seuls les fichiers Minio sont nettoyés
- ⚠️ TODO : Rollback complet (suppression mission + users si erreur)
📊 Résumé des Opérations Prisma
Requêtes Prisma dans l'ordre d'exécution :
-
prisma.mission.create()- Crée la mission avec tous les champs
- Génère un UUID pour
id logo=nullinitialement
-
prisma.missionUser.createMany()- Crée tous les gardiens et volontaires en une requête
- Utilise
createMany()pour performance
-
prisma.mission.update()(si logo)- Met à jour le champ
logoavec le chemin Minio
- Met à jour le champ
-
prisma.attachment.create()(pour chaque attachment)- Créé en parallèle avec
Promise.all() - Un enregistrement par fichier
- Créé en parallèle avec
Total : 1 + 1 + (0 ou 1) + N requêtes Prisma
- Minimum : 2 requêtes (mission + users)
- Avec logo : 3 requêtes
- Avec N attachments : 3 + N requêtes
📦 Résumé des Opérations Minio
Uploads Minio dans l'ordre d'exécution :
-
Logo (si présent)
- Bucket :
missions - Key :
{missionId}/logo.png - Path stocké :
missions/{missionId}/logo.png
- Bucket :
-
Attachments (si présents, en parallèle)
- Bucket :
missions - Key :
{missionId}/attachments/{filename} - Path stocké :
missions/{missionId}/attachments/{filename}
- Bucket :
-
Vérifications (après uploads)
HeadObjectCommandpour chaque fichier- Vérifie l'existence avant N8N
Total : 1 + N uploads + (1 + N) vérifications
🔄 Résumé du Workflow N8N
Données envoyées à N8N :
{
name: "Mission Example",
oddScope: ["odd-3"],
niveau: "a",
intention: "Description...",
missionType: "remote",
donneurDOrdre: "individual",
projection: "short",
services: ["Gite", "ArtLab"],
participation: "volontaire",
profils: ["DataIntelligence", "Expression"],
guardians: {
"gardien-temps": "user-1",
"gardien-parole": "user-2",
"gardien-memoire": "user-3"
},
volunteers: ["user-4", "user-5"],
creatorId: "user-creator",
logoPath: "missions/abc-123/logo.png",
config: {
N8N_API_KEY: "...",
MISSION_API_URL: "https://api.slm-lab.net/api"
}
}
Actions N8N (workflow interne) :
- Création projet Leantime
- Création collection Outline
- Création canal RocketChat
- Création repository Gitea
- Création projet Penpot
Note : Les IDs retournés ne sont PAS sauvegardés en base actuellement.
⚠️ Points d'Attention et TODOs
1. Sauvegarde des IDs N8N
Problème : Les IDs retournés par N8N (leantimeProjectId, etc.) ne sont pas sauvegardés en base.
Solution proposée :
// Après le workflow N8N
if (workflowResult.success && workflowResult.results) {
await prisma.mission.update({
where: { id: mission.id },
data: {
leantimeProjectId: workflowResult.results.leantimeProjectId,
outlineCollectionId: workflowResult.results.outlineCollectionId,
rocketChatChannelId: workflowResult.results.rocketChatChannelId,
giteaRepositoryUrl: workflowResult.results.giteaRepositoryUrl,
penpotProjectId: workflowResult.results.penpotProjectId
}
});
}
2. Rollback Complet
Problème : En cas d'erreur, la mission reste en base (orphan).
Solution proposée :
catch (error) {
// Suppression de la mission et des relations
await prisma.mission.delete({ where: { id: mission.id } });
// Les MissionUsers et Attachments seront supprimés en cascade
// Nettoyage Minio...
}
3. Credentials Minio Hardcodés
Problème : Les credentials Minio sont hardcodés dans lib/mission-uploads.ts.
Solution : Déplacer vers variables d'environnement.
4. Gestion des Erreurs N8N Partielles
Problème : Si certains services N8N échouent, on continue quand même.
Solution : Décider si on continue ou on rollback selon la criticité.
📈 Performance et Optimisations
Optimisations actuelles :
- ✅
createMany()pour MissionUsers (1 requête au lieu de N) - ✅
Promise.all()pour les attachments (parallèle) - ✅
HeadObjectCommandpour vérification (léger)
Optimisations possibles :
- 🔄 Transaction Prisma pour atomicité
- 🔄 Batch upload Minio (si supporté)
- 🔄 Retry logic pour N8N
- 🔄 Cache des vérifications Minio
🔍 Debugging et Logs
Points de logging importants :
- Début du workflow :
=== Mission Creation Started === - Création Prisma :
Mission created successfully - Upload Minio :
Logo upload successful/Attachment upload successful - Vérification :
verifyFileExists()logs - N8N :
=== Starting N8N Workflow ===/N8N Workflow Result - Erreurs : Tous les catch blocks loggent les erreurs
Commandes de debug :
# Voir les logs du serveur
npm run dev # Logs dans la console
# Vérifier Minio
# Accéder à https://dome-api.slm-lab.net
# Bucket: missions
# Vérifier Prisma
npx prisma studio # Interface graphique
Document généré le : $(date) Version : 1.0 Auteur : Analyse complète du codebase