# 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** ```typescript // ❌ 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** ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // ✅ 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` ```typescript // 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 ```typescript // ❌ 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