mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user