Files
_Assistant_Lead_Tech/knowledge/backend/patterns/prisma.md
2026-03-31 15:57:09 +02:00

9.0 KiB

title: Backend — Patterns : Prisma domain: backend bucket: patterns tags: [prisma, postgres, migration, pagination, idempotency, decimal] applies_to: [analysis, implementation, review, debug] severity: medium validated_on: 2026-03-23 source_projects: [app-template-resto, app-alexandrie]

Backend — Patterns : Prisma

Extrait de la base de connaissance Lead_tech. Voir knowledge/backend/patterns/README.md pour l'index complet.


Pattern : Soft delete et archivage explicite

  • Objectif : permettre la suppression logique sans perte immédiate de données.
  • Contexte : données métier critiques, besoins d'audit, restauration ou conformité.
  • Quand l'utiliser : dès qu'une suppression peut avoir des impacts métier ou légaux.
  • Quand l'éviter : données purement techniques ou réellement éphémères.
  • Avantage :
    • Restauration possible
    • Audit et traçabilité
    • Réduction des suppressions irréversibles
  • Limites / vigilance :
    • Complexité accrue sur les requêtes
    • Nécessite une discipline stricte (filtres par défaut)
  • Validé le : 25-01-2026
  • Contexte technique : API + DB relationnelle

Implémentation (exemple minimal)

- Champ deletedAt (nullable) ou status
- Les requêtes standards filtrent deletedAt IS NULL
- Endpoints dédiés pour restauration / purge
- Index DB tenant compte du soft delete

Checklist

  • Filtrage soft delete par défaut
  • Restauration explicite possible
  • Purge maîtrisée (cron / job)
  • Index DB adaptés
  • Tests sur cas supprimé / restauré

Pattern : Pagination robuste (cursor-based) pour les listings

  • Objectif : fournir des listings stables et performants sans incohérences entre pages.
  • Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important.
  • Quand l'utiliser : dès qu'un listing peut dépasser quelques dizaines/centaines d'items ou subir des écritures concurrentes.
  • Quand l'éviter : listes strictement petites et statiques.
  • Avantage :
    • Résultats stables malgré insertions/suppressions
    • Meilleure performance que l'offset sur gros volumes
    • Expérience client plus fiable
  • Limites / vigilance :
    • Nécessite un tri déterministe (champ + tie-breaker)
    • Complexité légèrement supérieure à offset/limit
  • Validé le : 25-01-2026
  • Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework

Implémentation (exemple minimal)

- Trier par (createdAt DESC, id DESC) (exemple)
- Le client envoie cursor = dernier (createdAt,id) reçu
- Le backend renvoie nextCursor si plus de résultats
- Ne jamais exposer de cursor implicite ou non documenté

Checklist

  • Tri déterministe (avec tie-breaker)
  • nextCursor renvoyé et documenté
  • Limite max de page (protection)
  • Index DB aligné avec le tri

Pattern : Idempotency key pour opérations sensibles

  • Objectif : empêcher les doublons lors de retries ou timeouts.
  • Contexte : création de ressources, paiements, webhooks.
  • Quand l'utiliser : toute opération non strictement en lecture.
  • Quand l'éviter : endpoints purement GET.
  • Avantage :
    • Protection contre doublons
    • Robustesse face aux retries
  • Limites / vigilance :
    • Stockage et expiration des clés à gérer
  • Validé le : 25-01-2026
  • Contexte technique : API HTTP + DB transactionnelle

Implémentation (exemple minimal)

- Client fournit Idempotency-Key
- Backend stocke la clé + résultat
- Retry retourne le résultat initial

Checklist

  • Clé obligatoire sur endpoints sensibles
  • Contrainte d'unicité côté DB
  • Comportement documenté

Pattern : mapping explicite de P2002 Prisma sur create/update de champ unique

  • Objectif : transformer un conflit d'unicité prévisible en erreur métier exploitable plutôt qu'en 500 opaque.
  • Contexte : create, update ou upsert Prisma sur un champ @unique alimenté par une source externe, concurrente, ou après un pre-check.
  • Quand l'utiliser : dès qu'un champ unique peut entrer en collision — à la création ET à la modification.
  • Quand l'éviter : jamais si le champ peut réellement entrer en collision.
  • Avantage :
    • réponse client stable
    • diagnostic métier plus rapide
  • Limites / vigilance :
    • le mapping doit rester cohérent avec le format d'erreur API standardisé
  • Validé le : 10-03-2026
  • Contexte technique : Prisma / PostgreSQL / NestJS

Implémentation (exemple minimal)

- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format d'erreur standardisé

Implémentation (exemple complet)

import { Prisma } from "@prisma/client";

try {
  await prisma.item.create({ data: { ... } });
  // ou: await prisma.item.update({ where: { id }, data: { ... } });
} catch (err) {
  if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
    throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
  }
  throw err;
}

Important : un pre-check applicatif (findUnique avant create) ne suffit pas contre les race conditions. Le try/catch P2002 est le seul garde-fou fiable. S'applique à create, update, updateMany, upsert.

Checklist

  • P2002 intercepté sur les creates ET les updates sensibles
  • Code d'erreur métier stable (409 Conflict)
  • Pas de 500 générique sur conflit prévisible

Pattern : Sérialiser les champs Decimal Prisma en string au niveau du repository

  • Objectif : éviter que les objets Decimal Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses.
  • Contexte : tout champ Decimal en Prisma (ex: price) retourné via API ou Server Action.
  • Quand l'utiliser : systématiquement sur tout champ Decimal dans les repositories.
  • Risque si ignoré : Decimal n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner.
  • Validé le : 17-03-2026
  • Contexte technique : Prisma / Node.js — app-template-resto

Implémentation

// Repository — convertir avant de retourner
return {
  ...dish,
  price: dish.price?.toString() ?? null, // Decimal → string
};

// DTO public
type DishDto = {
  price: string | null; // pas Decimal
};

Pattern : Prisma — Migration manuelle sans shadow DB (P3014)

  • Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes).
  • Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser.
  • Quand l'utiliser : quand prisma migrate dev échoue avec P3014 Prisma Migrate could not create the shadow database.
  • Risque si ignoré : blocage complet de la migration sur env managé.
  • Validé le : 23-03-2026
  • Contexte technique : Prisma v7+ — app-alexandrie / Supabase

Implémentation

# 1. Écrire le SQL manuellement
mkdir -p prisma/migrations/<timestamp>_<nom>
# Créer migration.sql à la main

# 2. Appliquer le SQL directement en DB
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql

# 3. Marquer la migration comme appliquée dans _prisma_migrations
npx prisma migrate resolve --applied <timestamp>_<nom>

# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts

Ne pas utiliser prisma db push en production — il ne versionne pas les migrations.


Pattern : Filtrage des règles métier dans le service, pas dans le repository

  • Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service).
  • Contexte : entités publiques avec règles de filtrage (isVisible, isActive), qui varient selon le contexte appelant (public vs admin).
  • Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel.
  • Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le where.
  • Avantage :
    • la règle est testable unitairement sans Prisma (mock de données brutes)
    • la requête DB reste simple et stable entre contextes
    • les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
  • Validé le : 17-03-2026
  • Contexte technique : Prisma / Node.js / Next.js — app-template-resto

Implémentation (exemple minimal)

// Repository — charge tout ce qui est candidat
async findCategories(tenantId: string) {
  return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible
}

// Service — applique la règle métier et mappe vers DTO
const raw = await repo.findCategories(tenantId);
return raw.filter(c => c.isVisible).map(toPublicDto);

// Admin : même repo, filtre différent dans le service admin
return raw.map(toAdminDto); // retourne tout, visible ou non