docs(knowledge): capitalisation backend — intégration du triage local (mai-juin 2026)

Triage et intégration des propositions backend du buffer 95_a_capitaliser.md
(lot local RL799_V2 + app-alexandrie, mai-juin 2026), distinct de la capitalisation
remote antérieure (triage 2026-05-02).

~73 entrées intégrées sur knowledge/backend/, dont :
- patterns/auth.md : série "membrane d'auth fédérée BFF/OIDC" (9 patterns) + jose algo whitelist
- patterns/prisma.md : recette fusionnée "Migration String/Int → enum" (backfill + Cas A/B/C),
  row réactivable, endpoint replace atomique, updateMany conditionnel, etc.
- risques/general.md : 19 risques (epoch s vs ms, keepAliveTimeout=0, upsert+filtre liste,
  fail-safe catch-all, retrait asymétrique front/back, anti-énumération rate-limit, etc.)
- patterns/general, async, nestjs, contracts, tests + risques/auth, contracts, prisma, redis, stripe, tests
- compléments d'entrées existantes (authorize-after-fetch, P3014, cursor opaque, DI swc, Stripe v20...)
- README patterns/risques mis à jour

Doublons internes corrigés en relecture (suppression-champ .map() → general seul ;
e2e DB-based → tests.md seul). Doublons hors backend / entrées projet / rejets non intégrés.
Source 95_a_capitaliser.md non purgée à ce stade (purge en fin de capitalisation complète).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MaksTinyWorkshop
2026-06-25 11:25:02 +02:00
parent ef24d85d57
commit f1b783407a
18 changed files with 2896 additions and 24 deletions
+164
View File
@@ -176,3 +176,167 @@ private async reconcileSessionStatus(session, now = new Date()) {
});
}
```
---
<a id="pattern-ressource-paire-non-ordonnee"></a>
## Pattern : Ressource « paire non ordonnée » — adresser par les participants, pas par l'id
- Objectif : éviter un endpoint « créer la ressource » séparé avant le premier écrit, et la race condition associée, pour une ressource dont l'identité est dérivable d'une paire d'acteurs.
- Contexte : DM 1:1, match, follow réciproque, partage 1:1 — toute ressource « paire non ordonnée » où l'id est déductible des deux participants.
- Quand l'utiliser : dès que le premier écrit doit pouvoir créer la ressource implicitement (premier message à un utilisateur, première interaction).
- Quand l'éviter : ressource avec identité propre (commande, document) qui existe indépendamment des participants.
- Avantage :
- 1 seul appel API au lieu de 2 (UX + perf)
- pas de race condition : contrainte unique Prisma sur la paire ordonnée applicative (`userA < userB`)
- l'endpoint GET par id reste légitime pour la pagination des fils existants
- Limites / vigilance :
- appliquer les vérifications d'éligibilité/quota AVANT le `findUnique` pour ne pas leak l'existence de la ressource
- l'upsert doit se faire en transaction (création conversation + création message)
- Validé le : 13-05-2026
- Contexte technique : NestJS / Prisma — app-alexandrie story 10.2
### Implémentation (exemple minimal)
```txt
POST /messaging/conversations/with/:targetUserId/messages
```
Le service, après contrôles d'éligibilité/quota :
1. `findUnique({ where: { userAId_userBId: orderPair(currentUser, target) } })`
2. Si absent → `create` la conversation
3. `create` le message dans la même `$transaction`
Adresser par l'identité des deux participants (ou le seul participant cible, l'autre étant `req.user`), jamais par l'id de la ressource. Faire un upsert idempotent côté serveur en transaction.
### Checklist
- [ ] Endpoint d'écriture adressé par les participants, pas par l'id de la ressource
- [ ] Contrainte unique Prisma sur la paire ordonnée applicative (`userA < userB`)
- [ ] Vérifications d'éligibilité/quota AVANT le `findUnique` (anti-leak d'existence)
- [ ] Upsert conversation + message dans une même `$transaction`
- [ ] Endpoint GET par id conservé pour la pagination
---
<a id="pattern-batched-eligibility-decoration"></a>
## Pattern : Batched eligibility / decoration — ne jamais calculer dans `.map(async)`
- Objectif : éviter le N+1 caché quand chaque row d'une liste doit être décorée d'un flag calculé par une logique partagée (éligibilité, ACL, entitlements).
- Contexte : `listX` qui ajoute un flag dérivé (`isReadOnly`, `canEdit`...) calculé par row à partir de queries internes.
- Quand l'utiliser : tout listing qui décore N rows d'un flag dépendant de queries (DB ou cache) par row.
- Quand l'éviter : flag purement synchrone dérivable de la row elle-même (pas de query).
- Avantage :
- passe de N×K requêtes à un nombre constant de queries bulk
- lookup O(1) via `Set` / `Map` au moment de la décoration
- Limites / vigilance :
- le cache (ex : entitlements 60 s) n'amortit pas la première fenêtre froide
- itérer la page **synchroniquement** après le chargement bulk — pas de `await` dans la boucle de décoration
- Validé le : 13-05-2026
- Contexte technique : NestJS / Prisma — app-alexandrie story 10.2 DM messaging
### Anti-pattern (N+1)
```typescript
// ❌ N appels indépendants → N×K requêtes (K = queries internes de getEligibility)
const items = await Promise.all(
page.map(async (row) => {
const eligibility = await this.getEligibilityForExistingConversation(
currentUserId, peerIdFromRow(row),
);
return { ...row, isReadOnly: eligibility.isReadOnly };
}),
);
```
### Pattern (batched)
1. Extraire les IDs cibles : `const peerIds = page.map(peerIdFromRow)`.
2. Charger TOUTES les dépendances en bulk :
- `await entitlements.getEntitlementsForUser(currentUserId)` (1 fois)
- `await Promise.all(peerIds.map(id => entitlements.getEntitlementsForUser(id)))` (parallèle, cache amortit)
- `await prisma.follow.findMany({ where: { OR: [{ followerId: currentUserId, followingId: { in: peerIds } }, { followerId: { in: peerIds }, followingId: currentUserId }] } })` (1 seule requête)
3. Construire des `Set` / `Map` pour lookup O(1).
4. Itérer la page **synchroniquement** et décorer chaque row.
### Règle
Tout flag décoratif calculé par row qui dépend d'une logique partagée DOIT être factorisé en une méthode `compute<Flag>ForBatch(peerIds[])` retournant `Map<peerId, value>`.
---
<a id="pattern-providers-externes-injection-token"></a>
## Pattern : Providers externes via injection par token (jamais `new XxxProvider()`)
- Objectif : rendre les providers externes mockables, swappables par env, et conformes au contrat DI Nest.
- Contexte : service Nest qui dépend d'un système externe (email, billing, push, OAuth IdP, storage, geocoding...).
- Quand l'utiliser : tout provider qui touche un système externe.
- Quand l'éviter : services purement applicatifs (`UsersService`, `CommunityService`) — injectés par classe directement.
- Avantage :
- mock propre en test (`useValue`)
- rotation d'implémentation par env sans rebuild
- arbre DI complet : le module déclare la dépendance
- Limites / vigilance :
- un `new XxxProvider()` au constructeur s'exécute même quand un test injecte un mock du service → tests cassés
- Validé le : 20-05-2026
- Contexte technique : NestJS — app-alexandrie story infra-5
### Anti-pattern
```typescript
// ❌ instancié au constructeur : non mockable, non swappable, contrat DI incomplet
export class AuthService {
private readonly emailProvider = new NoopEmailProvider();
private readonly authProvider = new GoogleAuthProvider();
constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}
}
// Conséquences observées (avant fix) : AuthService 5 fails, billing 26 fails, notifications 2 fails.
```
### Pattern correct
```typescript
// 1. Token + interface
export const EMAIL_PROVIDER = Symbol('EMAIL_PROVIDER');
export interface EmailProvider {
sendVerificationEmail(email: string, token: string): Promise<void>;
}
// 2. Implémentation @Injectable()
@Injectable()
export class NoopEmailProvider implements EmailProvider { /* ... */ }
// 3. Binding dans le module
@Module({
providers: [
AuthService,
{ provide: EMAIL_PROVIDER, useClass: NoopEmailProvider },
],
})
export class AuthModule {}
// 4. Injection par token
@Injectable()
export class AuthService {
constructor(
@Inject(EMAIL_PROVIDER) private readonly emailProvider: EmailProvider,
) {}
}
// 5. Mock en test
const module = await Test.createTestingModule({
providers: [
AuthService,
{ provide: EMAIL_PROVIDER, useValue: { sendVerificationEmail: jest.fn() } },
],
}).compile();
```
### Checklist
- [ ] Token (`Symbol`) + interface dans un fichier dédié
- [ ] Implémentation `@Injectable()` qui implémente l'interface
- [ ] Binding `{ provide: TOKEN, useClass: Impl }` dans le module
- [ ] Injection via `@Inject(TOKEN)` au constructeur
- [ ] Aucun `new XxxProvider()` dans un service métier