NeahNew/MISSION_CREATION_WORKFLOW_DETAILED.md
2026-01-04 13:12:51 +01:00

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.id est généré automatiquement (UUID)
  • Le champ logo est null pour 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-read pour 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 uploaderId est 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 :

  1. Leantime : Projet de gestion de projet
  2. Outline : Collection de documentation
  3. RocketChat : Canal de communication
  4. Gitea : Repository Git
  5. 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 :

  1. Erreur lors de l'upload du logo → Suppression du logo
  2. Erreur lors de l'upload d'un attachment → Suppression de tous les fichiers
  3. Erreur lors de la vérification Minio → Suppression de tous les fichiers
  4. 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 :

  1. prisma.mission.create()

    • Crée la mission avec tous les champs
    • Génère un UUID pour id
    • logo = null initialement
  2. prisma.missionUser.createMany()

    • Crée tous les gardiens et volontaires en une requête
    • Utilise createMany() pour performance
  3. prisma.mission.update() (si logo)

    • Met à jour le champ logo avec le chemin Minio
  4. prisma.attachment.create() (pour chaque attachment)

    • Créé en parallèle avec Promise.all()
    • Un enregistrement par fichier

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 :

  1. Logo (si présent)

    • Bucket : missions
    • Key : {missionId}/logo.png
    • Path stocké : missions/{missionId}/logo.png
  2. Attachments (si présents, en parallèle)

    • Bucket : missions
    • Key : {missionId}/attachments/{filename}
    • Path stocké : missions/{missionId}/attachments/{filename}
  3. Vérifications (après uploads)

    • HeadObjectCommand pour 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) :

  1. Création projet Leantime
  2. Création collection Outline
  3. Création canal RocketChat
  4. Création repository Gitea
  5. 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)
  • HeadObjectCommand pour 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 :

  1. Début du workflow : === Mission Creation Started ===
  2. Création Prisma : Mission created successfully
  3. Upload Minio : Logo upload successful / Attachment upload successful
  4. Vérification : verifyFileExists() logs
  5. N8N : === Starting N8N Workflow === / N8N Workflow Result
  6. 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