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:
@@ -77,3 +77,177 @@
|
||||
- Dead-letter ou statut FAILED visible
|
||||
- Idempotence documentée
|
||||
- Logs corrélés (requestId/traceId)
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-hooks-fire-and-forget-creation-db"></a>
|
||||
## Pattern : Hooks fire-and-forget après création DB critique
|
||||
|
||||
- Objectif : déclencher des hooks secondaires (mail accusé réception, notification, invalidation cache) après une création DB sans bloquer la réponse HTTP au client.
|
||||
- Contexte : endpoint POST qui crée une ressource en DB et déclenche en cascade des hooks impliquant des appels réseau (Resend, FCM, Redis cache).
|
||||
- Quand l'utiliser : hooks **rapides** (< 1-2 s) qui peuvent vivre dans le même process que la requête HTTP.
|
||||
- Quand l'éviter : tâches lourdes (génération PDF, batch envoi sur 100 destinataires) — utiliser un vrai job queue (BullMQ, pg-boss).
|
||||
- Avantage :
|
||||
- la 201 part dès la création DB (l'AC critique de la route)
|
||||
- chaque hook logge ses propres échecs sans bloquer le caller
|
||||
- `Promise.allSettled` détaché → robustesse même si un hook futur ajoute un comportement async
|
||||
- Limites / vigilance :
|
||||
- dans Next.js 15+, préférer `after()` (cf. `knowledge/backend/patterns/nextjs.md`) qui garantit l'exécution post-réponse même en serverless
|
||||
- `Promise.all` reject au premier échec — `allSettled` attend toutes les promesses
|
||||
- tests : poll DB borné (`waitForX`) plutôt que `setTimeout(50)` (cf. `knowledge/backend/patterns/tests.md`)
|
||||
- Validé le : 30-04-2026
|
||||
- Contexte technique : Node.js — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ✅ La 201 part dès la création DB ; les hooks tournent en parallèle
|
||||
const created = await prisma.registration.create({ data });
|
||||
|
||||
// Promise.allSettled détaché : ne reject jamais, on capture quand même
|
||||
// au cas où le service de log lui-même bug
|
||||
void Promise.allSettled([
|
||||
sendAcknowledgmentMail(data.email),
|
||||
notifyObservers(created.id),
|
||||
invalidateCache(`stats:${data.scope}`),
|
||||
]).catch((err) => {
|
||||
logger.error({ type: 'hooks', event: 'unexpected_error', err: String(err) });
|
||||
});
|
||||
|
||||
return jsonResponse(201, { data: created });
|
||||
```
|
||||
|
||||
### Règles d'utilisation
|
||||
|
||||
1. **L'AC critique doit être atteint avant** : la création DB doit réussir (await) — c'est le seul résultat que le client attend.
|
||||
2. **Chaque hook doit logger ses propres échecs** : le service mail doit avoir son propre `logger.error` sur status=failed. Le `.catch()` du `Promise.allSettled` est un filet, pas le canal d'audit primaire.
|
||||
3. **`Promise.allSettled` (pas `Promise.all`)** : robuste si un hook futur ajoute un comportement asynchrone derrière.
|
||||
4. **Côté tests** : helper `waitForX` polling-borné plutôt que `setTimeout(N)` arbitraire.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-fanout-notification-grade-plancher"></a>
|
||||
## Pattern : Notification fanout fire-and-forget avec filtre grade plancher
|
||||
|
||||
- Objectif : notifier N destinataires éligibles (filtrage par grade plancher) après une mutation, sans bloquer la réponse HTTP et sans rollback de la création principale si la notif échoue.
|
||||
- Contexte : action métier qui crée une ressource + doit notifier les membres dont le grade ≥ grade plancher de la ressource (`SOIREE_CANCELLED` à tous les membres, `COMMUNICATION_PUBLISHED` aux membres de grade ≥ X, etc.).
|
||||
- Quand l'utiliser : fanout multi-rôles avec filtrage métier sur le profil destinataire.
|
||||
- Quand l'éviter : si la notif est critique (la ressource ne doit pas exister sans notif) — utiliser une transaction.
|
||||
- Avantage :
|
||||
- seuil monotone `gradeRank(member) >= gradeRank(resource)` aligné sur les filtres `list*` consommateurs
|
||||
- exclusion du créateur via `id: { not: userId }` pour éviter de se notifier soi-même
|
||||
- log explicite sur `catch` du fire-and-forget — pas de perte silencieuse
|
||||
- Limites / vigilance :
|
||||
- pas de transaction avec la création principale : best-effort, dégradation acceptable
|
||||
- le `linkUrl` doit être rôle-aware (cf. `knowledge/backend/risques/general.md` risque-notif-linkurl-non-role-aware)
|
||||
- Validé le : 23-04-2026
|
||||
- Contexte technique : Prisma — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
const createResourceNotifications = async (input: {
|
||||
resourceId: string;
|
||||
grade: string; // plancher (seuil monotone)
|
||||
excludeUserId?: string;
|
||||
}): Promise<void> => {
|
||||
const thresholdRank = gradeRank(input.grade);
|
||||
|
||||
const recipients = await prisma.user.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
role: { in: [...ROLES_ALL_ACTIVE] },
|
||||
id: input.excludeUserId ? { not: input.excludeUserId } : undefined,
|
||||
profile: { is: {} },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
role: true, // pour linkUrl rôle-aware si multi-rôles
|
||||
profile: { select: { grade: true } },
|
||||
},
|
||||
});
|
||||
|
||||
const eligibleIds = recipients
|
||||
.filter((r) => {
|
||||
const g = r.profile?.grade;
|
||||
if (!g) return false;
|
||||
return gradeRank(g) >= thresholdRank;
|
||||
})
|
||||
.map((r) => r.id);
|
||||
|
||||
if (eligibleIds.length === 0) return;
|
||||
|
||||
await prisma.notification.createMany({
|
||||
data: eligibleIds.map((recipientId) => ({
|
||||
type: NotificationType.RESOURCE_CREATED,
|
||||
recipientId,
|
||||
// …
|
||||
linkUrl: ..., // rôle-aware si nécessaire
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
// Côté handler
|
||||
try {
|
||||
const resource = await createResource({ ... });
|
||||
logAction(userId, 'resource:create', ...);
|
||||
|
||||
// Fire-and-forget
|
||||
void createResourceNotifications({
|
||||
resourceId: resource.id,
|
||||
...minimumDataForNotif,
|
||||
}).catch((err) => {
|
||||
console.error('[resource:create] notification fanout failed:', err);
|
||||
});
|
||||
|
||||
return jsonResponse(201, { data: serialize(resource) });
|
||||
} catch {
|
||||
return errorResponse(500, ...);
|
||||
}
|
||||
```
|
||||
|
||||
### Pourquoi un seuil monotone
|
||||
|
||||
`gradeRank(member) >= gradeRank(resource)` = "à partir du grade X", aligné sur les filtres `list*` consommateurs. Évite les sélections non-contiguës (A+M sans C) qui sont pénibles à représenter.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-auto-purge-fenetre-temporelle-sql"></a>
|
||||
## Pattern : Auto-purge côté vue via fenêtre temporelle SQL
|
||||
|
||||
- Objectif : faire porter la rétention courte par le filtre de lecture plutôt que par un cron de purge réelle, quand une donnée a deux publics avec des besoins de rétention différents.
|
||||
- Contexte : donnée consultée à long terme côté admin/historique mais utile uniquement sur fenêtre courte côté consommateur final (membre lambda).
|
||||
- Quand l'utiliser : 2 publics, rétention courte côté consommateur, rétention longue côté admin, volumétrie raisonnable.
|
||||
- Quand l'éviter :
|
||||
- volumétrie très élevée (millions de rows) — finir par un vrai archivage si le volume explose
|
||||
- RGPD / obligations légales de suppression — il faut **vraiment** supprimer la donnée, pas la masquer
|
||||
- données avec coût de stockage significatif (PDF, blobs, logs verbeux) — purge réelle + archivage externe
|
||||
- Avantage :
|
||||
- pas de cron à écrire, déployer, monitorer
|
||||
- zéro risque de purge destructive : la donnée reste en DB
|
||||
- rétention courte est **déclarative** (paramètre de query), pas cachée dans un job planifié
|
||||
- l'admin conserve l'accès complet via un autre endpoint
|
||||
- Limites / vigilance :
|
||||
- index sur `createdAt` indispensable dès que la table grossit
|
||||
- Validé le : 23-04-2026
|
||||
- Contexte technique : Prisma / Postgres — RL799_V2
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
export const listRecentXxxForMember = async (
|
||||
...filters,
|
||||
sinceDays = 30,
|
||||
) => {
|
||||
const since = new Date(Date.now() - sinceDays * 24 * 60 * 60 * 1000);
|
||||
return prisma.xxx.findMany({
|
||||
where: {
|
||||
...filters,
|
||||
createdAt: { gte: since },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
L'admin garde un endpoint distinct sans le filtre temporel pour l'accès historique complet.
|
||||
|
||||
Reference in New Issue
Block a user