Foundation Builder Docs
Architecture

Repositories et implémentations

Comprendre les repositories et leurs implémentations dans l'architecture hexagonale de Foundation Builder

Repositories

Les repositories constituent la couche d'accès aux données et aux services externes. Ils implémentent les ports de sortie (out) et encapsulent toutes les interactions avec les systèmes externes, garantissant que la logique métier reste indépendante des détails techniques.


Rôle des repositories

Abstraction des dépendances externes

Les repositories abstraient complètement les détails techniques des systèmes externes. Ils transforment les objets métier en formats compatibles avec les APIs externes et vice versa, garantissant que la logique métier reste indépendante des spécificités techniques.

Interface uniforme

Chaque repository implémente une interface définie dans les ports de sortie, fournissant une interface uniforme pour accéder aux données, peu importe la technologie sous-jacente utilisée.

Gestion des erreurs

Les repositories gèrent la conversion des erreurs spécifiques des systèmes externes en erreurs métier compréhensibles, et mappent les réponses externes vers les modèles métier de l'application.


Structure des repositories

Implémentation des ports

Chaque repository implémente un port de sortie spécifique :

export class StripePaymentRepositoryImpl implements PaymentRepository {
  private readonly stripe: Stripe | null = null;

  public constructor() {
    if (process.env.STRIPE_SECRET_KEY) {
      this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
        apiVersion: "2025-07-30.basil",
      });
    }
  }

  public async createPaymentIntent(request: PaymentRequest): Promise<PaymentIntent> {
    this.ensureStripeAvailable();
    const paymentIntent = await this.stripe!.paymentIntents.create({
      amount: request.amount,
      currency: request.currency,
      description: request.description,
      metadata: request.metadata,
      automatic_payment_methods: { enabled: true },
    });
    return this.mapStripePaymentIntent(paymentIntent);
  }
}

Configuration des services externes

Les repositories gèrent la configuration et l'initialisation des services externes :

export class ResendEmailRepositoryImpl implements EmailRepository {
  private readonly resend: Resend | null = null;

  public constructor() {
    const apiKey = process.env.RESEND_API_KEY;
    if (apiKey) {
      this.resend = new Resend(apiKey);
    }
  }

  private ensureResendAvailable(): void {
    if (!this.resend) {
      throw new Error("Resend n'est pas configuré. Vérifiez que RESEND_API_KEY est définie.");
    }
  }
}

Transformation des données

Mapping vers les modèles métier

Les repositories convertissent les réponses des systèmes externes vers les modèles métier de l'application :

private mapStripePaymentIntent(paymentIntent: Stripe.PaymentIntent): PaymentIntent {
  if (!paymentIntent.client_secret) {
    throw new Error("Le client_secret du PaymentIntent est manquant");
  }
  return {
    id: paymentIntent.id,
    clientSecret: paymentIntent.client_secret,
    amount: paymentIntent.amount,
    currency: paymentIntent.currency,
    status: paymentIntent.status,
    metadata: paymentIntent.metadata ?? undefined,
  };
}

Validation des données

Les repositories valident les données reçues des systèmes externes avant de les transformer :

private firebaseUserToUser(firebaseUser: FirebaseUser | null): User | null {
  if (!firebaseUser) return null;
  return {
    id: firebaseUser.uid,
    email: firebaseUser.email || "",
    displayName: firebaseUser.displayName || "",
    photoURL: firebaseUser.photoURL || "",
    purchasedReports: 0,
    updatedAt: new Date().toISOString(),
    usedReports: 0,
  };
}

Gestion des erreurs

Conversion des erreurs

Les repositories convertissent les erreurs spécifiques des systèmes externes en erreurs métier compréhensibles :

private logStripeError(context: string, error: unknown): void {
  if (error instanceof Error) {
    console.error(`Erreur lors de la ${context}:`, error.message);
    return;
  }
  console.error(`Erreur inconnue lors de la ${context}:`, error);
}

Gestion des erreurs de configuration

Les repositories gèrent les erreurs de configuration des services externes :

private ensureStripeAvailable(): void {
  if (!this.stripe) {
    throw new Error("Stripe n'est pas configuré. Vérifiez que STRIPE_SECRET_KEY est définie.");
  }
}

Flexibilité d'implémentation

Implémentations multiples

Chaque repository peut avoir plusieurs implémentations pour un même port. Par exemple, le repository d'authentification peut être implémenté avec Firebase, Supabase, ou Better Auth.

Le choix de l'implémentation se fait au niveau du conteneur d'injection de dépendances, permettant une configuration flexible selon l'environnement ou les besoins du projet.

Changer d'implémentation pour une dépendance externe ne nécessite que de modifier l'implémentation du repository correspondant, sans toucher à la logique métier.


Avantages des repositories

Indépendance technologique

Les repositories permettent de changer d'implémentation sans impacter la logique métier. Par exemple, passer de Firebase à Supabase ne nécessite que de modifier l'implémentation du repository correspondant.

Les repositories facilitent les tests en permettant d'utiliser des mocks pour les dépendances externes.

La séparation claire des responsabilités rend le code plus facile à comprendre et à maintenir.

L'ajout de nouvelles fonctionnalités se fait de manière modulaire en étendant les repositories existants ou en créant de nouveaux repositories.