Files
_Assistant_Lead_Tech/knowledge/backend/risques/prisma.md
MaksTinyWorkshop fc0bec0e2b capitalisation: intégrer 12 entrées depuis app-alexandrie et app-template-resto
- backend/risques/nestjs : guard multi-statut READ_METHODS avant statut
- backend/patterns/nestjs : fusionner lastSeenAt dans la réconciliation
- backend/risques/contracts : pas de process.env dans services/helpers
- backend/risques/nextjs : self-request Server Action + EXDEV atomic write
- backend/risques/prisma : champ enum-like stocké en String
- frontend/risques/general : Alert.prompt iOS-only
- frontend/risques/tests : 3 anti-patterns (helpers copiés, test indirect, test façade)
- workflow/risques/story-tracking : 2 entrées (hors périmètre, File List approximative)
- skill capitalisation-triage : nouveau format de rapport (tableaux par domaine)
- 95_a_capitaliser.md : purgé
2026-03-31 14:47:42 +02:00

12 KiB

Backend — Risques & vigilance : Prisma

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


PostgreSQL / Prisma : @unique sur champ nullable (idempotence cassée)

Risques

  • Doublons en base malgré un "unique" attendu (PostgreSQL autorise plusieurs NULL dans un index UNIQUE)
  • Upserts non idempotents si la clé peut être null (where: { externalId: null } crée plusieurs lignes)

Symptômes

  • Plusieurs enregistrements "équivalents" avec externalId = NULL
  • Rejouer un webhook / retry réseau crée une nouvelle ligne au lieu d'upsert

Bonnes pratiques / mitigations

  • Toute clé utilisée dans un where d'upsert doit être non-nullable
  • Si un identifiant externe peut légitimement être null, ne pas l'utiliser comme clé d'idempotence : choisir une autre clé unique non-nullable

Prisma $transaction : fenêtres TOCTOU (check hors transaction)

Risques

  • Un pre-check + une $transaction avec un update non sécurisé crée une fenêtre TOCTOU
  • Deux appels concurrents peuvent tous deux passer le check et agir simultanément
  • En multi-tenant : un bug upstream peut permettre une écriture cross-tenant malgré le guard applicatif

Symptômes

  • Double action sur un état booléen (ex : double mise en vitrine) si le check n'est pas dans la transaction
  • Écriture sur une ressource d'un autre tenant possible en race condition

Bonnes pratiques / mitigations

Cas 1 — Multi-tenant : inclure tenantId dans chaque écriture

// ❌ Anti-pattern — check OK mais écriture sans tenantId
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
await prisma.$transaction(
  ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
);

// ✅ Défense en profondeur — tenantId dans chaque écriture
await prisma.$transaction(
  ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
);
  • Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure tenantId dans le WHERE, même dans une transaction précédée d'un check
  • Utiliser updateMany/deleteMany pour inclure tenantId sans exception si 0 lignes

Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction

// ❌ Anti-pattern : check d'état hors transaction
if (resource.isActive) throw ...;
await prisma.$transaction(async (tx) => {
  // resource.isActive a pu changer entre-temps
  return tx.resource.update(...);
});

// ✅ Pattern correct : check ET update dans la transaction
await prisma.$transaction(async (tx) => {
  const current = await tx.resource.findUnique({ where: { id } });
  if (current?.isActive) throw ...;        // re-check atomique
  const count = await tx.resource.count(...);
  if (count >= LIMIT) throw ...;
  return tx.resource.update(...);
});
  • Règle : tout guard métier de type "déjà fait / plafond atteint" doit être vérifié à l'intérieur de la transaction, pas avant

  • Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 ; NestJS / Prisma — app-alexandrie 23-03-2026


Prisma OR multi-tenant : tenantId: null manquant sur la branche système

Risques

  • Sur un modèle à tenantId nullable distinguant ressources "système" et "tenant", un filtre { isSystem: true } sans tenantId: null expose des ressources corrompues à tous les tenants

Symptômes

  • Un tag isSystem: true avec tenantId non-null est exposé à tous les tenants
  • Bug de sécurité difficile à détecter car le comportement nominal semble correct

Bonnes pratiques / mitigations

// ❌ Trop permissif
OR: [{ isSystem: true }, { tenantId, isSystem: false }]

// ✅ Défense en profondeur — double condition sur la branche système
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
  • Règle : sur tout modèle tenantId? (nullable) + flag isSystem/isGlobal/isPublic, la branche "ressource publique" du filtre OR doit toujours inclure tenantId: null

  • Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026


Calcul de nextOrder hors transaction (race condition sortOrder)

Risques

  • Deux requêtes concurrentes obtiennent le même MAX(sortOrder) et créent deux entités avec le même sortOrder

Symptômes

  • Deux items avec le même sortOrder dans la même catégorie/scope
  • Bug aléatoire selon la charge — invisible en dev, présent en prod

Bonnes pratiques / mitigations

// ✅ Calcul dans la transaction interactive
return prisma.$transaction(async (tx) => {
  const maxOrder = await tx.entity.aggregate({
    where: { tenantId, scopeId },
    _max: { sortOrder: true },
  });
  const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
  return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
});
  • Règle : ne jamais calculer maxOrder hors de la transaction qui crée l'entité

  • Contexte technique : Prisma / transactions — app-template-resto 21-03-2026


Champ tenantId sans FK ni relation Prisma vers Tenant

Risques

  • Un tenantId TEXT NOT NULL sans relation Prisma ne génère aucune FK en DB
  • L'isolation multi-tenant n'est pas enforced au niveau base de données

Symptômes

  • Migration SQL sans ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"
  • Prisma ne génère pas de FK automatiquement sans @relation déclarée

Bonnes pratiques / mitigations

Tout modèle tenant-scoped doit avoir les trois :

  1. tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) dans le modèle Prisma
  2. La relation inverse dans Tenant (ex: menuCategories MenuCategory[])
  3. La FK correspondante dans la migration SQL
  • Checklist review : vérifier systématiquement que les nouveaux modèles respectent ce guardrail

  • Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026


Divergence schéma Prisma / spec story (champ déclaré mais absent)

Risques

  • Une tâche de story cochée implique un champ (ex: consumedAt, tokenHash) qui n'existe pas dans schema.prisma
  • Le code compile ou passe en review sans que le champ soit réellement présent en DB

Symptômes

  • Erreur à l'exécution sur un champ inexistant malgré une story marquée "done"
  • schema.prisma ne contient pas le champ mentionné dans les tâches

Bonnes pratiques / mitigations

  • Avant de marquer une tâche , croiser avec schema.prisma pour confirmer que le champ existe réellement

  • Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier

  • Contexte technique : Prisma / app-template-resto — 16-03-2026


PrismaService — getter explicite manquant sur nouveau modèle

Risques

  • L'ajout d'un modèle dans schema.prisma sans son getter dans PrismaService casse le typecheck
  • Erreur silencieuse si les modules sont peu typés

Symptômes

  • Property 'forum' does not exist on type 'PrismaService' à la compilation
  • Module fonctionnel sur le PrismaClient direct mais cassé via PrismaService

Bonnes pratiques / mitigations

Tout ajout de modèle Prisma = deux actions :

  1. Ajouter le modèle dans schema.prisma
  2. Ajouter le getter dans prisma.service.ts
// apps/api/src/infra/prisma/prisma.service.ts
get forum() {
  return this.client.forum;
}
  • Checklist review : à chaque nouvelle migration Prisma, vérifier que prisma.service.ts est mis à jour.
  • Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026

Prisma initialisé au chargement de module — casse le build Next.js

Risques

  • Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si DATABASE_URL n'est pas disponible dans l'environnement de build

Symptômes

  • PrismaClientInitializationError ou Error: Environment variable not found: DATABASE_URL au next build
  • L'app tourne en dev mais le build CI échoue

Bonnes pratiques / mitigations

  • Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier

  • Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB

  • Ne jamais instancier new PrismaClient() au top-level d'un module importé par Next.js

  • Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026


jest.clearAllMocks() dans des beforeEach imbriqués avec mocks Prisma

Risques

  • Remise à zéro d'un setup attendu par un scope de test plus profond
  • Tests verts ou rouges pour de mauvaises raisons
  • Forte difficulté à comprendre l'état réel des mocks

Symptômes

  • Comportement différent selon l'ordre ou le niveau d'imbrication des describe
  • Mocks Prisma "perdus" entre deux tests
  • Corrections locales qui cassent d'autres blocs de tests

Bonnes pratiques / mitigations

  • Centraliser la stratégie de reset des mocks
  • Éviter les clearAllMocks() concurrents à plusieurs niveaux de nesting
  • Préférer un setup explicite et local par scénario quand les mocks Prisma sont structurants
  • Contexte technique : Jest / Prisma / tests NestJS — 10-03-2026

Cursor de pagination opaque — validation manquante (500 au lieu de 400)

Risques

  • Un cursor base64url+JSON non validé crash en HTTP 500 si malformé ou corrompu
  • Exposé à des attaques par input malveillant sur les endpoints paginés publics ou semi-publics

Symptômes

  • JSON.parse ou décodage base64 lève une exception non catchée → 500 en prod
  • Les logs montrent une stack trace sur un endpoint paginé avec un cursor externe

Bonnes pratiques / mitigations

// ❌ DANGEREUX — crash 500 si cursor corrompu
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());

// ✅ CORRECT — validation avec code d'erreur sémantique
let decoded = null;
if (cursor) {
  try {
    decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
    if (!decoded.createdAt || !decoded.id) throw new Error('Champs manquants');
  } catch {
    throw new BadRequestException({ error: { code: 'INVALID_CURSOR', message: 'Cursor de pagination invalide.' } });
  }
}
  • Règle : ajouter un test unitaire "cursor invalide → 400" sur tout endpoint paginé par cursor

  • Contexte technique : NestJS / pagination — app-alexandrie 24-03-2026


Champ enum-like stocké en String Prisma — perte de contrainte DB et typage dégradé

Risques

  • Aucune contrainte en base sur les valeurs acceptées — insertion de valeurs invalides possible sans erreur DB.
  • Cast manuel as EnumType dans le service masque l'absence de validation Prisma.

Symptômes

  • as SomeEnum dans un service ou repository sur un champ qui provient de la DB
  • Getter get model(): any dans PrismaService pour contourner le typage

Bonnes pratiques / mitigations

  1. Tout champ à valeurs finies doit être déclaré avec un enum Prisma dès la création du modèle — jamais en String.
  2. Si un modèle existant utilise String, créer une migration de conversion : ALTER COLUMN ... TYPE "EnumType" USING ...::"EnumType".
  3. Signal review : tout cast as EnumType sur une valeur issue de Prisma = dette à corriger immédiatement.
  • Contexte technique : Prisma / PostgreSQL — app-alexandrie 31-03-2026