mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-05-18 08:18:15 +02:00
capitalisation: intégration ~60 entrées RL799_V2 (triage 2026-05-02)
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>
This commit is contained in:
@@ -441,3 +441,181 @@ Checklist minimale après `prisma migrate resolve --applied` :
|
||||
- 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
|
||||
|
||||
Reference in New Issue
Block a user