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:
@@ -164,3 +164,124 @@ if (mediaUrl) {
|
||||
- [ ] Validation format (`new URL()`) + protocole + longueur max
|
||||
- [ ] Retourner `null` si invalide, jamais passer la string brute
|
||||
- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-nextjs-after-fanout-post-reponse"></a>
|
||||
## Pattern : Next.js `after()` pour fanout post-réponse
|
||||
|
||||
- Objectif : exécuter du travail accessoire (notif push, audit, webhook) APRÈS avoir renvoyé la réponse HTTP, sans bloquer la latence métier ni risquer la fermeture lambda du fire-and-forget naïf.
|
||||
- Contexte : route Next.js 15+ (App Router) qui termine une mutation métier (ex : `prisma.$transaction`) et veut déclencher un effet de bord non critique.
|
||||
- Quand l'utiliser : effet de bord best-effort qui ne doit JAMAIS bloquer la réponse principale (push, log audit asynchrone, webhook sortant).
|
||||
- Quand l'éviter : effet de bord qui DOIT réussir avant de répondre 200 (validation transactionnelle, écriture critique).
|
||||
- Avantage :
|
||||
- réponse HTTP immédiate (latence métier préservée)
|
||||
- le runtime Node attend la fin du callback avant de fermer le process — pas de fire-and-forget orphelin
|
||||
- les exceptions du callback sont attrapables localement, ne propagent pas au caller
|
||||
- Limites / vigilance :
|
||||
- **JAMAIS placer `after()` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...) })`** : si la transaction rollback, le callback `after()` reste planifié et s'exécute sur des données qui n'existent pas en DB
|
||||
- construire le payload SYNCHRONE avant `after()` et attraper les erreurs là, sinon une exception dans le callback async devient silencieuse
|
||||
- comportement en serverless (Vercel, Lambda) vs Node standalone peut différer — tester en cible
|
||||
- Validé le : 28-04-2026
|
||||
- Contexte technique : Next.js 15+ App Router — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
import { after } from 'next/server';
|
||||
|
||||
export const notifyConvocationPublished = async (
|
||||
tenueId: string,
|
||||
recipientIds: string[],
|
||||
): Promise<void> => {
|
||||
// 1. Mutation métier (transactionnelle)
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.notification.createMany({ data: ... });
|
||||
});
|
||||
|
||||
// 2. Construction payload SYNCHRONE — toute exception attrapée ici
|
||||
let payload: PushPayload;
|
||||
try {
|
||||
payload = buildPushPayload(tenueId);
|
||||
} catch (err) {
|
||||
log.warn('payload build failed', { err });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Hook après la transaction — JAMAIS dedans
|
||||
after(async () => {
|
||||
try {
|
||||
await pushService.sendPushToUsers(recipientIds, payload);
|
||||
} catch (err) {
|
||||
log.warn('fanout failed', { err }); // jamais throw vers le caller
|
||||
}
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### Anti-patterns
|
||||
|
||||
- ❌ `await pushService.sendPushToUsers(...)` dans le service métier (bloque la latence + propage les erreurs)
|
||||
- ❌ `after(() => { ... })` à l'intérieur d'une `prisma.$transaction(async tx => { ... after(...); })`
|
||||
- ❌ Construire le payload DANS le callback `after()` async — une exception y devient silencieuse
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `after()` appelé APRÈS le `await prisma.$transaction(...)`, jamais à l'intérieur
|
||||
- [ ] Payload construit synchrone avant `after()`, exceptions attrapées localement
|
||||
- [ ] Le callback `after()` n'a pas le droit de throw (best-effort wrapping)
|
||||
- [ ] La route métier renvoie 2xx même si le fanout échoue
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-gate-agir-au-nom-de"></a>
|
||||
## Pattern : Gate "agir au nom de X" (3 étages : rôle → type/scope → cible actif)
|
||||
|
||||
- Objectif : valider correctement une autorisation d'override d'attribution (`uploadedByOverride`, `createdByOverride`) pour qu'un rôle élevé puisse agir "au nom de" un autre user, sans laisser de faille.
|
||||
- Contexte : endpoint API qui accepte un override d'attribution (archiviste upload pour un Frère, admin crée une entité pour X).
|
||||
- Quand l'utiliser : tout endpoint avec un override d'attribution sensible.
|
||||
- Quand l'éviter : si la délégation est implicite et déjà couverte par un guard centralisé.
|
||||
- Avantage :
|
||||
- validation strictement séquentielle et défensive — chaque étage a son code HTTP propre
|
||||
- le check "actif" combine toutes les dimensions disponibles (pas un seul flag)
|
||||
- Limites / vigilance :
|
||||
- ordre impératif : rôle EN PREMIER (plus sensible). Un membre lambda envoyant un payload avec override doit recevoir 403 même si la cible est valide
|
||||
- Validé le : 20-04-2026
|
||||
- Contexte technique : Next.js / API HTTP — RL799_V2
|
||||
|
||||
### Les 3 étages
|
||||
|
||||
1. **Rôle** : le user courant a-t-il la capacité ? Sinon **403 FORBIDDEN**.
|
||||
2. **Type/scope** : l'override est-il pertinent pour ce type d'entité ? Sinon **400 VALIDATION_ERROR**.
|
||||
3. **Cible** : la cible existe-t-elle ET est-elle active ? Sinon **400 VALIDATION_ERROR** si introuvable OU inactive OU démissionnée OU décédée.
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
let effectiveUploadedBy = currentUser.id;
|
||||
if (metadata.uploadedByOverride) {
|
||||
// Étage 1 : rôle
|
||||
if (userRole !== 'archiviste' && userRole !== 'admin') {
|
||||
return errorResponse(403, 'FORBIDDEN', '...');
|
||||
}
|
||||
// Étage 2 : type/scope
|
||||
if (metadata.type !== 'planche') {
|
||||
return errorResponse(400, 'VALIDATION_ERROR', '...');
|
||||
}
|
||||
// Étage 3 : cible actif (toutes les dimensions disponibles)
|
||||
const targetUser = await getUserById(metadata.uploadedByOverride);
|
||||
const isInactive =
|
||||
!targetUser
|
||||
|| !targetUser.isActive
|
||||
|| !!targetUser.profile?.resignedAt
|
||||
|| !!targetUser.profile?.deceasedAt;
|
||||
if (isInactive) {
|
||||
return errorResponse(400, 'VALIDATION_ERROR', '...');
|
||||
}
|
||||
effectiveUploadedBy = metadata.uploadedByOverride;
|
||||
}
|
||||
```
|
||||
|
||||
### Pourquoi vérifier toutes les dimensions
|
||||
|
||||
Un user peut être `isActive: true` dans le système mais avoir une `resignedAt` antérieure (désactivation non-synchrone). Le check "actif" doit combiner **toutes** les dimensions disponibles du modèle, pas un seul flag.
|
||||
|
||||
Reference in New Issue
Block a user