mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
Triage du 95_a_capitaliser.md (~75 propositions) : - 60 entrées intégrées dans knowledge/ (backend, frontend, workflow) - 4 nouveaux fichiers : backend/patterns/tests.md, backend/risques/tests.md, frontend/patterns/general.md, workflow/patterns/general.md - 6 doublons rejetés - Mise à jour des READMEs index pour refléter les nouvelles entrées - 95_a_capitaliser.md restauré à sa structure initiale - 40_decisions_et_archi.md : décision mono-tenant déployable vs SaaS multi-tenant - 90_debug_et_postmortem.md : sub-agents Write indisponible, effet iceberg CI, prisma migrate diffs cosmétiques Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
622 lines
23 KiB
Markdown
622 lines
23 KiB
Markdown
---
|
|
title: Backend — Risques & vigilance : Prisma
|
|
domain: backend
|
|
bucket: risques
|
|
tags: [prisma, transactions, tenant, schema, race-condition]
|
|
applies_to: [implementation, review, debug, architecture]
|
|
severity: high
|
|
validated_on: 2026-04-07
|
|
source_projects: [app-template-resto, app-alexandrie, RL799_V2]
|
|
---
|
|
|
|
# Backend — Risques & vigilance : Prisma
|
|
|
|
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-unique-nullable"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-transaction-toctou-tenantid"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-or-tenantid-null"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-nextorder-hors-transaction"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-tenantid-sans-fk-relation"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-schema-divergence-spec-story"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-prismaservice-getter-manquant"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-init-module-build"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-jest-clearallmocks-imbrique"></a>
|
|
## `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
|
|
|
|
---
|
|
|
|
<a id="risque-cursor-pagination-opaque"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-enum-like-string-prisma"></a>
|
|
## 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
|
|
|
|
---
|
|
|
|
<a id="risque-migration-manuelle-hors-git"></a>
|
|
## Migration appliquée manuellement hors git
|
|
|
|
### Risques
|
|
|
|
- Une migration Prisma appliquée via DDL direct + `migrate resolve --applied` produit un fichier `migration.sql` qui peut rester untracked dans git
|
|
- Quiconque clone le repo et lance `prisma migrate deploy` n'a pas la migration
|
|
|
|
### Symptômes
|
|
|
|
- `??` (untracked) dans `git status` sur le dossier `prisma/migrations/`
|
|
- `prisma migrate status` rapporte un drift entre le schéma et l'état de la DB
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
Checklist minimale après `prisma migrate resolve --applied` :
|
|
- `git status` → vérifier que `prisma/migrations/<nom>/migration.sql` est présent et tracké
|
|
- `git add prisma/migrations/<nom>/` si untracked
|
|
- Valider que `prisma migrate status` rapporte la migration comme appliquée sans drift
|
|
|
|
- Contexte technique : Prisma / migrations manuelles — RL799_V2 02-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-relation-1-1-sans-unique"></a>
|
|
## Relation 1:1 métier sans contrainte `@unique` en DB
|
|
|
|
### Risques
|
|
|
|
- Un mapping métier 1:1 (ex: planche tracée → document généré) implémenté avec un simple index + `findFirst` crée un risque de backlinks non déterministes en cas d'incohérence de données
|
|
|
|
### Symptômes
|
|
|
|
- Champ de référence nullable seulement indexé, puis lecture via `findFirst`
|
|
- Le code suppose l'unicité mais la base ne l'impose pas
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Poser une contrainte `@unique` (nullable) sur la référence quand la relation métier est 1:1
|
|
- Préférer `findUnique` / lecture déterministe
|
|
- **Signal review** : si le code suppose l'unicité, la base doit l'imposer explicitement
|
|
|
|
- Contexte technique : Prisma / contraintes DB — RL799_V2 06-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-catch-all-silencieux-repository"></a>
|
|
## Catch-all silencieux dans les repositories Prisma
|
|
|
|
### Risques
|
|
|
|
- Un `try { prisma.update(...); return true } catch { return false }` dans un repository masque TOUTES les erreurs Prisma (connexion perdue, timeout, contrainte violée) derrière une réponse "not found"
|
|
- Le service appelant ne peut pas distinguer un échec technique d'une absence de données
|
|
|
|
### Symptômes
|
|
|
|
- Le service retourne 404 alors que la DB est down, ou 404 alors qu'une contrainte FK est violée
|
|
- Le monitoring ne voit aucune erreur 500
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Dans les repositories Prisma, catcher uniquement `Prisma.PrismaClientKnownRequestError` avec le code spécifique attendu (`P2025` pour record not found, `P2002` pour unique constraint)
|
|
- Re-throw toute autre erreur pour qu'elle remonte en 500 dans le handler
|
|
- **Signal review** : `catch {` ou `catch (e) {` sans vérification de `e.code` dans un repository Prisma
|
|
|
|
- Contexte technique : Prisma / error handling — RL799_V2 08-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-filtre-lecture-ecriture-desaligne"></a>
|
|
## Filtre de lecture appliqué mais filtre d'écriture oublié
|
|
|
|
### Risques
|
|
- Mutation (`updateMany`/`deleteMany`) affectant des lignes hors périmètre autorisé.
|
|
|
|
### Symptômes
|
|
- Le listing semble correct, mais les opérations d'écriture touchent des données invisibles pour l'utilisateur.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Aligner strictement les prédicats lecture/écriture sur les mêmes dimensions métier (ex: grade, tenant, statut).
|
|
- Factoriser le filtre dans un helper partagé côté service/repository.
|
|
|
|
- Contexte technique : Prisma / filtres métier — RL799_V2 09-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-deletemany-sans-partition"></a>
|
|
## `deleteMany` partiel sans clé de partition métier
|
|
|
|
### Risques
|
|
- Suppression transversale de données d'autres partitions (ex: grade, segment, scope logique).
|
|
|
|
### Symptômes
|
|
- Comportement correct tant que le frontend envoie un payload complet, puis corruption lors d'un refactor/concurrence.
|
|
|
|
### Bonnes pratiques / mitigations
|
|
- Inclure toutes les dimensions de partition dans les clauses `deleteMany`/`updateMany`.
|
|
- Ajouter des tests ciblés sur payload partiel et concurrence logique.
|
|
|
|
- Contexte technique : Prisma / partition logique — RL799_V2 09-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-capture-pre-updatemany-race-window"></a>
|
|
## Capture pré-`updateMany` sans transaction — race window silencieuse
|
|
|
|
### Risques
|
|
|
|
- `findUnique` + `updateMany` non atomiques : entre les deux, un autre process peut modifier le champ capturé. L'audit log ment (enregistre une `previousValue` qui n'était plus la valeur courante au moment de l'écriture). Notif envoyée au mauvais target.
|
|
- Si l'`updateMany` ne filtre que sur `status` sans inclure la valeur attendue, il peut écraser une nouvelle valeur sans erreur
|
|
|
|
### Symptômes
|
|
|
|
```typescript
|
|
// ❌ Race window entre les deux requêtes
|
|
const before = await prisma.entity.findUnique({ where: { id }, select: { x: true } });
|
|
// … un autre process modifie entity.x ici …
|
|
const after = await prisma.entity.updateMany({
|
|
where: { id, status: 'X' },
|
|
data: { status: 'Y', x: null },
|
|
});
|
|
audit.log({ previousX: before.x }); // ← MENT
|
|
notify(before.x); // ← mauvais target
|
|
```
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
**Solution 1 — `SELECT ... FOR UPDATE` dans une transaction** (cf. `pattern-revocation-atomique-etat-transversal` dans `patterns/prisma.md`) :
|
|
|
|
```typescript
|
|
let previousValue: T | null = null;
|
|
await prisma.$transaction(async (tx) => {
|
|
const locked = await tx.$queryRaw<Array<{ x: T }>>`
|
|
SELECT x FROM "entities" WHERE id = ${id} FOR UPDATE
|
|
`;
|
|
if (locked.length === 0) return;
|
|
previousValue = locked[0].x;
|
|
await tx.entity.updateMany({ where: { id, ... }, data: { ... } });
|
|
});
|
|
```
|
|
|
|
**Solution 2 — `WHERE` qui inclut la valeur attendue** (CAS-light, sans transaction) :
|
|
|
|
```typescript
|
|
const updated = await prisma.entity.updateMany({
|
|
where: { id, status: 'X', x: expectedX }, // ← guard sur la valeur
|
|
data: { ... },
|
|
});
|
|
if (updated.count === 0) {
|
|
// soit déjà transitionné, soit x a changé — relire et décider
|
|
}
|
|
```
|
|
|
|
### Détecteur mental
|
|
|
|
Si tu écris :
|
|
|
|
```typescript
|
|
const before = await prisma.X.findUnique(...);
|
|
await prisma.X.updateMany(...);
|
|
// … tu utilises before.<champ> dans l'audit ou la notif
|
|
```
|
|
|
|
→ **Stop**. Tu as une race. Soit `before.<champ>` n'a pas changé entre les deux (et alors pourquoi le capturer ?), soit il a pu changer (et tu mens).
|
|
|
|
- Contexte technique : Prisma / concurrence — RL799_V2 27-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-slugs-metier-user-id"></a>
|
|
## Slugs métier comme `User.id` — schémas Zod laxistes obligés
|
|
|
|
### Risques
|
|
|
|
- Un `User.id` en `String` libre (slug lisible côté seed, UUID `@default(uuid())` côté invitations) empêche toute rigidification Zod `.uuid()` sur les champs `userId` côté API
|
|
- Couplage tests/seed invisible : des dizaines de tests hardcodent `'membre-m05'` côté input, sans contrat explicite. Tout renommage du seed casse la suite en cascade sans warning compilateur
|
|
- Drift silencieux : deux populations d'ids coexistent en base, validation impossible à uniformiser
|
|
|
|
### Symptômes
|
|
|
|
- `prisma.user.create({ data: { id: '<texte-lisible>', ... } })` dans un fichier de seed
|
|
- Schéma Zod avec `z.string().min(1).max(128)` là où on voudrait `z.string().uuid()`
|
|
- Test qui référence `userId: 'membre-m05'` en argument d'une requête API
|
|
- Commentaire "l'ID n'est pas forcément un UUID, on accepte toute chaîne" → dette déguisée
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
- Décider tôt : soit `@default(uuid())` côté Prisma partout, soit IDs structurés documentés avec une regex stricte (`^[a-z]+-[a-z0-9]+$`) publiée dans un helper shared (`isValidUserId`)
|
|
- **Ne jamais mélanger** : si le seed utilise des slugs et les comptes produits utilisent des UUIDs, les schémas Zod sont condamnés à être laxistes
|
|
- Migration : utiliser un UUID v5 déterministe (`seedUserId(slug)`) — cf. `pattern-uuid-v5-deterministe-seed` dans `patterns/prisma.md`
|
|
- Test d'invariant post-seed obligatoire (cf. pattern dédié)
|
|
- Si migration en cours de route : prévoir un script qui propage sur **toutes** les FKs (`audit_logs.user_id`, `notifications.recipient_id`, `refresh_tokens.user_id`, etc.)
|
|
|
|
- Contexte technique : Prisma / Zod — RL799_V2 22-04-2026
|
|
|
|
---
|
|
|
|
<a id="risque-prisma-where-relation-every-vide"></a>
|
|
## `where: { relation: { every: ... } }` trivialement vrai sur relation vide
|
|
|
|
### Risques
|
|
|
|
- La clause `every` sur une relation est **trivialement vraie** quand la relation est vide. Sans coupler avec `some: {}`, on capture aussi les rows qui n'ont aucune entrée liée — risque de purge à tort sur les nouvelles entités
|
|
|
|
### Symptômes
|
|
|
|
```typescript
|
|
// ❌ Faux : capture aussi les profiles SANS aucune VR liée
|
|
const orphans = await prisma.visitorProfile.findMany({
|
|
where: {
|
|
lastSeenAt: { lt: cutoff },
|
|
registrations: { every: { status: 'rejected' } }, // vacuously true si pas de VR
|
|
},
|
|
});
|
|
```
|
|
|
|
- Test "purge orphelins après 30 j" qui supprime un profile fraîchement créé
|
|
- Tests qui passent sur des fixtures avec relations existantes mais cassent dès qu'une entité sans relation est créée
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
// ✅ Correct : exige au moins une VR liée ET toutes rejected
|
|
where: {
|
|
lastSeenAt: { lt: cutoff },
|
|
registrations: {
|
|
some: {}, // au moins une VR existe
|
|
every: { status: 'rejected' }, // toutes rejected
|
|
},
|
|
},
|
|
```
|
|
|
|
**Règle générale** : à chaque fois qu'on cherche « toutes les X de Y sont Z », vérifier si Y peut avoir 0 X. Si oui, ajouter `some: {}` pour exclure le cas vide.
|
|
|
|
- Contexte technique : Prisma — RL799_V2 01-05-2026
|
|
|
|
---
|
|
|
|
<a id="risque-resolvedburl-template-based-testing"></a>
|
|
## `resolveDbUrl()` testing template-based — préserver le DSN explicite vers la template
|
|
|
|
### Risques
|
|
|
|
- Un helper `resolveDbUrl()` qui force `pathname='/<projet>_test'` quand `NODE_ENV=test` écrase un DSN appelant qui pointe explicitement vers `<projet>_test_template`
|
|
- Le bootstrap template (`runPrisma(['db', 'seed'])` en sub-process avec `DB_URL=...test_template + NODE_ENV=test`) écrit dans la mauvaise DB ou échoue avec "database does not exist"
|
|
|
|
### Symptômes
|
|
|
|
- `bootstrapTemplate échec "pnpm prisma db seed" (exit 1)`
|
|
- Tests vitest échouent ensuite avec `Database <projet>_test does not exist on the database server`
|
|
- Sub-process de seed qui logge un DSN différent de celui passé en `env`
|
|
|
|
### Bonnes pratiques / mitigations
|
|
|
|
```typescript
|
|
const resolveDbUrl = (): string | undefined => {
|
|
const url = process.env.DB_URL;
|
|
if (!url) return url;
|
|
if (process.env.NODE_ENV !== 'test') return url;
|
|
|
|
try {
|
|
const parsed = new URL(url);
|
|
// Exception : préserver le DSN si déjà sur la template
|
|
// (cas bootstrap migrate/seed, sinon le seed pointe sur <projet>_test inexistante)
|
|
if (parsed.pathname === '/<projet>_test_template') {
|
|
return url;
|
|
}
|
|
parsed.pathname = '/<projet>_test';
|
|
return parsed.toString();
|
|
} catch {
|
|
return url;
|
|
}
|
|
};
|
|
```
|
|
|
|
**Règle générale** : toute stratégie template-based doit auditer le chemin du `DB_URL` à travers les sub-processes de bootstrap. Le bootstrap ouvre une connexion sur la template, mais le seed transitif exécuté via un sub-process peut être sujet à des transformations agressives du DSN qui le redirigent ailleurs.
|
|
|
|
- Contexte technique : Prisma / template database / Vitest — RL799_V2 01-05-2026
|