mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Refonte Structure
This commit is contained in:
18
knowledge/backend/patterns/README.md
Normal file
18
knowledge/backend/patterns/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Backend — Patterns validés — Index
|
||||
|
||||
Patterns backend testés et validés en conditions réelles.
|
||||
|
||||
Avant toute proposition backend, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
|
||||
|
||||
---
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `auth.md` | Auth, sessions, tokens, erreurs API, corrélation | Format erreur standardisé, middleware requestId, anti-énumération, token usage unique, autorisation interne, opérations atomiques |
|
||||
| `contracts.md` | Contrats API, Zod, error codes, HTTP sémantique | Contracts-First/Zod-Infer/No-DTO, error codes comme contrat, HTTP 200 payload métier |
|
||||
| `prisma.md` | Prisma, DB, migrations, pagination | Soft delete, pagination cursor, idempotency key, P2002 unique, Decimal sérialisation, migration manuelle P3014, filtrage métier dans service |
|
||||
| `stripe.md` | Stripe, paiements, webhooks entrants, subscriptions | Provider-Strategy, metadata subscription_data, parsing webhook unique, restauration achats, Trial vs Paid |
|
||||
| `nestjs.md` | NestJS, guards, Redis, quotas | Guard global APP_GUARD, RedisHealthService cache court, quota INCR+EXPIREAT atomique |
|
||||
| `multi-tenant.md` | Multi-tenant, isolation, feature flags | 403 vs 404, repository tenant-aware, tenantId dans updates, helper tenant partagé, feature flag tenant, EN enforcement |
|
||||
| `nextjs.md` | Next.js App Router, Server Actions, isolation | Runtime-only logique pure, server-only isolation, utilitaires purs sans server-only, réutiliser champ V1, validation URL externe |
|
||||
| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents |
|
||||
79
knowledge/backend/patterns/async.md
Normal file
79
knowledge/backend/patterns/async.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Backend — Patterns : Async
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-execution-asynchrone-taches-longues"></a>
|
||||
|
||||
## Pattern : Exécution asynchrone des tâches longues (queue + outbox light)
|
||||
|
||||
- Objectif : sortir les opérations longues ou fragiles du chemin request/response.
|
||||
- Contexte : envoi d'emails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
|
||||
- Quand l'utiliser : dès qu'une opération peut dépasser la latence acceptable ou dépendre d'un service externe.
|
||||
- Quand l'éviter : opérations réellement instantanées et sans dépendances externes.
|
||||
- Avantage :
|
||||
- API plus rapide et plus fiable
|
||||
- Retries maîtrisés
|
||||
- Meilleure résilience aux pannes externes
|
||||
- Limites / vigilance :
|
||||
- Demande une discipline stricte sur l'idempotence
|
||||
- Nécessite une stratégie minimale de dead-letter ou d'alerting
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend agnostique + DB transactionnelle + worker
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- API écrit un job ou event en DB dans la transaction métier
|
||||
- Worker lit les jobs en attente et exécute
|
||||
- Retries avec backoff + compteur
|
||||
- Statut FAILED ou dead-letter + alerte
|
||||
- Idempotence par clé métier ou idempotency key
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Job créé dans une transaction (évite les pertes)
|
||||
- Retries et backoff définis
|
||||
- Dead-letter ou statut FAILED visible
|
||||
- Idempotence garantie
|
||||
- Logs corrélés (requestId/traceId)
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-webhooks-sortants-robustes-idempotents"></a>
|
||||
|
||||
## Pattern : Webhooks sortants robustes et idempotents
|
||||
|
||||
- Objectif : garantir des intégrations fiables avec des systèmes externes.
|
||||
- Contexte : notifications, synchronisations, événements métier sortants.
|
||||
- Quand l'utiliser : dès qu'un événement doit être transmis à un tiers.
|
||||
- Quand l'éviter : intégrations strictement synchrones et internes.
|
||||
- Avantage :
|
||||
- Tolérance aux pannes réseau
|
||||
- Retries maîtrisés
|
||||
- Observabilité des échecs
|
||||
- Limites / vigilance :
|
||||
- Gestion des retries et du volume
|
||||
- Nécessite une idempotence côté consommateur
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend + HTTP + worker/queue
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Événement persisté (outbox) en DB
|
||||
- Envoi asynchrone via worker
|
||||
- Retries avec backoff
|
||||
- Signature du payload (HMAC)
|
||||
- Idempotency key dans le header
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Payload signé et vérifiable
|
||||
- Retries + backoff définis
|
||||
- Dead-letter ou statut FAILED visible
|
||||
- Idempotence documentée
|
||||
- Logs corrélés (requestId/traceId)
|
||||
230
knowledge/backend/patterns/auth.md
Normal file
230
knowledge/backend/patterns/auth.md
Normal file
@@ -0,0 +1,230 @@
|
||||
# Backend — Patterns : Auth
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-format-derreur-api-standardise"></a>
|
||||
|
||||
## Pattern : Format d'erreur API standardisé
|
||||
|
||||
- Objectif : fournir des erreurs prévisibles, exploitables et cohérentes pour tous les clients.
|
||||
- Contexte : API consommée par front-end, automatisations ou intégrations externes.
|
||||
- Quand l'utiliser : dès qu'une API est exposée à autre chose qu'un usage interne trivial.
|
||||
- Quand l'éviter : jamais.
|
||||
- Avantage :
|
||||
- Debug plus rapide
|
||||
- UX maîtrisée côté client
|
||||
- Observabilité améliorée
|
||||
- Limites / vigilance :
|
||||
- Discipline requise pour éviter les formats ad hoc
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP agnostique
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "USER_NOT_FOUND",
|
||||
"message": "Utilisateur introuvable",
|
||||
"requestId": "abc-123"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Codes HTTP cohérents (4xx / 5xx)
|
||||
- Codes d'erreur applicatifs stables
|
||||
- Message utilisateur non technique
|
||||
- requestId présent
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-middleware-correlation-requestid-traceid"></a>
|
||||
|
||||
## Pattern : Middleware de corrélation (requestId / traceId)
|
||||
|
||||
- Objectif : relier chaque requête aux logs et erreurs associées.
|
||||
- Contexte : toute API ou service exposé.
|
||||
- Quand l'utiliser : systématiquement en production.
|
||||
- Quand l'éviter : jamais.
|
||||
- Avantage :
|
||||
- MTTR réduit drastiquement
|
||||
- Debug cross-services possible
|
||||
- Limites / vigilance :
|
||||
- Doit être propagé partout (logs, erreurs, appels sortants)
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : Backend agnostique (HTTP)
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Générer un requestId à l'entrée si absent
|
||||
- Le propager dans le contexte de requête
|
||||
- L'inclure dans chaque log et réponse d'erreur
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- requestId généré ou repris d'un header existant
|
||||
- Présent dans tous les logs
|
||||
- Présent dans les erreurs retournées
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-anti-enumeration-auth-email"></a>
|
||||
## Pattern : Anti-énumération sur endpoints auth liés à un email
|
||||
|
||||
- Objectif : empêcher qu'un endpoint auth révèle si un compte existe, n'existe pas ou n'est pas éligible.
|
||||
- Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part d'un email utilisateur.
|
||||
- Quand l'utiliser : dès qu'une requête auth touche un identifiant de type email.
|
||||
- Quand l'éviter : jamais sur une surface exposée.
|
||||
- Avantage :
|
||||
- réduit la fuite d'information sur les comptes existants
|
||||
- homogénéise les réponses côté client
|
||||
- se combine bien avec les garde-fous anti-abus
|
||||
- Limites / vigilance :
|
||||
- ne protège pas seul contre le brute-force, à combiner avec du rate-limiting
|
||||
- les logs internes doivent conserver la vraie cause sans l'exposer au client
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js / auth applicative / API HTTP
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- retourner la même réponse HTTP 200 qu'un compte existe ou non
|
||||
- ne jamais distinguer "email inconnu", "email connu" ou "compte OAuth-only" dans la réponse
|
||||
- journaliser la cause réelle côté serveur
|
||||
- ajouter un rate-limiting basé sur email + IP
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Réponse client uniforme pour les cas compte connu/inconnu/non éligible
|
||||
- Aucune fuite d'existence dans le message ou le code d'erreur
|
||||
- Rate-limiting présent sur les endpoints exposés
|
||||
- Logs internes exploitables
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-token-usage-unique"></a>
|
||||
## Pattern : Token à usage unique — génération, hash et invalidation atomique
|
||||
|
||||
- Objectif : standardiser la création et la consommation de tokens sensibles sans stocker de secret brut en base.
|
||||
- Contexte : invitation, reset de mot de passe, vérification d'email, lien magique ou tout token one-shot.
|
||||
- Quand l'utiliser : pour tout token à usage unique transmis à l'utilisateur.
|
||||
- Quand l'éviter : sessions longues ou secrets devant être relus en clair côté serveur.
|
||||
- Avantage :
|
||||
- réduit l'impact d'une fuite de base
|
||||
- garde des tokens URL-safe
|
||||
- favorise une consommation atomique et réutilisable
|
||||
- Limites / vigilance :
|
||||
- la consommation doit rester atomique
|
||||
- la politique d'expiration doit être explicite
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js `crypto` / Prisma / email ou URL signée
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- générer le token avec `crypto.randomBytes(32).toString("base64url")`
|
||||
- stocker uniquement le hash SHA-256 du token en base
|
||||
- transmettre le token brut uniquement via URL ou email
|
||||
- recalculer le hash côté serveur lors de la consommation
|
||||
- invalider le token dans une transaction atomique après usage
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Token brut jamais persisté en base
|
||||
- Hash recalculé côté serveur pour la vérification
|
||||
- Expiration explicite
|
||||
- Invalidation atomique après consommation
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-autorisation-interne-minimale"></a>
|
||||
|
||||
## Pattern : Autorisation interne minimale sans RBAC complet
|
||||
|
||||
- Objectif : sécuriser une capacité interne sensible sans ouvrir trop tôt un chantier RBAC complet.
|
||||
- Contexte : application avec peu de rôles, besoin ponctuel d'une capacité admin ou opérateur clairement identifiée.
|
||||
- Quand l'utiliser : quand une story métier demande un pouvoir interne limité mais réel.
|
||||
- Quand l'éviter : si les permissions deviennent nombreuses, hiérarchiques ou contextuelles.
|
||||
- Avantage :
|
||||
- sécurisation rapide et lisible d'une capacité sensible
|
||||
- source de vérité backend explicite
|
||||
- chemin d'évolution propre vers un RBAC plus complet
|
||||
- Limites / vigilance :
|
||||
- ne pas laisser proliférer des rôles ad hoc non gouvernés
|
||||
- ne remplace pas un vrai modèle de permissions si le domaine grossit
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : NestJS / auth par session ou JWT / API métier interne
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN)
|
||||
- propager ce rôle dans la session ou le token d'auth
|
||||
- créer un décorateur + guard dédiés pour la capacité sensible
|
||||
- interdire les booléens front, emails hardcodés ou `if` dispersés dans les contrôleurs
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Le rôle vit dans la source de vérité backend
|
||||
- Le rôle est propagé dans le mécanisme d'auth existant
|
||||
- Les endpoints sensibles passent par un guard dédié
|
||||
- Aucun contrôle d'accès critique n'est piloté par le front
|
||||
- Le passage à RBAC reste possible sans casser le contrat existant
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-auth-operations-atomiques"></a>
|
||||
## Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes
|
||||
|
||||
- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent.
|
||||
- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session.
|
||||
- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence.
|
||||
- Quand l'éviter : opérations de lecture pure.
|
||||
- Avantage :
|
||||
- pas de token valide après reset de mot de passe si l'opération est interrompue
|
||||
- suppression de session idempotente (P2025 absorbé silencieusement)
|
||||
- comportement prévisible même en cas de retry ou de concurrence
|
||||
- Limites / vigilance :
|
||||
- `$transaction` Prisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Node.js / Prisma / auth par session ou token
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// consumePasswordReset — atomique dans une transaction
|
||||
await prisma.$transaction([
|
||||
prisma.passwordResetToken.update({
|
||||
where: { tokenHash },
|
||||
data: { consumedAt: new Date() },
|
||||
}),
|
||||
prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { passwordHash: newHash },
|
||||
}),
|
||||
prisma.session.deleteMany({ where: { userId } }),
|
||||
]);
|
||||
|
||||
// Suppression de session — idempotente (P2025 absorbé)
|
||||
try {
|
||||
await prisma.session.delete({ where: { sessionToken } });
|
||||
} catch (err) {
|
||||
if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK
|
||||
}
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Toute opération hash + update + delete dans une `$transaction`
|
||||
- [ ] `P2025` absorbé silencieusement sur les suppressions de session
|
||||
- [ ] Effets de bord hors transaction documentés (cookie, email)
|
||||
- [ ] Tests couvrant le cas d'une session déjà expirée
|
||||
138
knowledge/backend/patterns/contracts.md
Normal file
138
knowledge/backend/patterns/contracts.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Backend — Patterns : Contracts
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-contracts-first-zod-infer-no-dto"></a>
|
||||
|
||||
## Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)
|
||||
|
||||
- Objectif : avoir une seule source de vérité pour les contrats d'interface entre API et client, sans redéfinition manuelle de types.
|
||||
- Contexte : monorepo TypeScript avec un package partagé (`packages/contracts` ou équivalent), consommé par le backend et le front/mobile.
|
||||
- Quand l'utiliser : dès qu'une API est consommée par un client TypeScript dans le même repo.
|
||||
- Quand l'éviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence.
|
||||
- Avantage :
|
||||
- Zéro drift entre contrat et implémentation
|
||||
- Types TypeScript gratuits via `z.infer<>` — aucune réécriture
|
||||
- Changement de contrat = erreur de compilation immédiate côté client
|
||||
- Mocks de tests alignés automatiquement
|
||||
- Limites / vigilance :
|
||||
- Ne pas mettre de logique métier dans `packages/contracts` (IO only)
|
||||
- Attention aux dépendances circulaires si le package grossit
|
||||
- Validé le : 07-03-2026
|
||||
- Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// packages/contracts/src/auth/auth.schemas.ts
|
||||
export const RegisterRequestSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
});
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>; // type GRATUIT
|
||||
|
||||
// packages/contracts/src/index.ts
|
||||
export * from './auth/auth.schemas';
|
||||
export * from './errors/error-code';
|
||||
|
||||
// apps/api/src/modules/auth/auth.controller.ts
|
||||
import type { RegisterRequest } from '@monrepo/contracts';
|
||||
// + ZodValidationPipe → validation automatique, zéro DTO manuel
|
||||
|
||||
// apps/mobile/src/domains/auth/auth.store.ts
|
||||
import type { RegisterRequest } from '@monrepo/contracts';
|
||||
// même type, même schéma, zéro duplication
|
||||
```
|
||||
|
||||
### Structure cible du package contracts
|
||||
|
||||
```
|
||||
packages/contracts/src/
|
||||
auth/auth.schemas.ts ← request/response auth
|
||||
users/users.schemas.ts ← request/response users
|
||||
billing/billing.schemas.ts ← request/response billing (Epic suivant)
|
||||
errors/error-code.ts ← enum codes d'erreur stables
|
||||
http/envelopes.ts ← { data, meta } / { error, meta }
|
||||
index.ts ← re-export tout
|
||||
```
|
||||
|
||||
### Ce qui appartient à contracts
|
||||
|
||||
- Schémas Zod request/response
|
||||
- Types inférés (`z.infer<>`)
|
||||
- Codes d'erreur applicatifs stables
|
||||
- Enums et constantes partagées (ex : liste officielle de sujets/topics)
|
||||
|
||||
### Ce qui n'appartient PAS à contracts
|
||||
|
||||
- Logique métier
|
||||
- Modules/services/guards framework (NestJS, etc.)
|
||||
- State management client (Zustand, Redux, etc.)
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Zéro DTO manuel dans l'API — uniquement `z.infer<typeof Schema>`
|
||||
- [ ] `ZodValidationPipe` global ou par endpoint pour la validation d'entrée
|
||||
- [ ] Constantes partagées (enums, listes) dans contracts, jamais dupliquées
|
||||
- [ ] Mocks de tests importent les types depuis contracts
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-contracts-error-codes"></a>
|
||||
|
||||
## Pattern : Contracts-First — error codes comme contrat obligatoire
|
||||
|
||||
- Objectif : maintenir les codes d'erreur API dans `packages/contracts` pour éviter les clients stringly-typed.
|
||||
- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`.
|
||||
- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après.
|
||||
- Risque si ignoré : clients qui testent des strings hardcodées au lieu d'importer l'enum → drift silencieux.
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : TypeScript / NestJS + Expo (React Native)
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit
|
||||
- [ ] Clients importent l'enum, pas une string littérale
|
||||
- [ ] PR review : vérifier `error-code.ts` à chaque ajout d'endpoint d'erreur
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-http-200-payload-metier"></a>
|
||||
## Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès
|
||||
|
||||
- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
|
||||
- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
|
||||
- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
|
||||
- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
|
||||
- Avantage :
|
||||
- pas de gestion d'exception côté client mobile pour des états courants
|
||||
- rendu conditionnel (paywall, teaser, empty) piloté par le payload
|
||||
- log serveur propre — 4xx réservés aux erreurs techniques/sécurité
|
||||
- Limites / vigilance :
|
||||
- ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
|
||||
- Validé le : 20-03-2026
|
||||
- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// GET /community/forums
|
||||
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
|
||||
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }
|
||||
|
||||
// ❌ Anti-pattern
|
||||
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });
|
||||
|
||||
// ✅ Pattern correct
|
||||
return res.status(200).json({
|
||||
data: { forums: [], paywallRequired: true },
|
||||
meta: { total: 0 },
|
||||
});
|
||||
```
|
||||
|
||||
### Règle
|
||||
|
||||
- **4xx** = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
|
||||
- **200 + flag métier** = état métier normal que le client doit interpréter pour le rendu
|
||||
188
knowledge/backend/patterns/multi-tenant.md
Normal file
188
knowledge/backend/patterns/multi-tenant.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# Backend — Patterns : Multi-tenant
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-guardrails-multi-tenant-403-404"></a>
|
||||
## Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique
|
||||
|
||||
- Objectif : éviter les fuites d'information inter-tenant tout en gardant une sémantique d'erreur claire.
|
||||
- Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
|
||||
- Quand l'utiliser : dès qu'une vérification d'appartenance tenant peut soit refuser explicitement l'accès, soit masquer l'existence d'une ressource.
|
||||
- Quand l'éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite.
|
||||
- Avantage :
|
||||
- clarifie la convention de sécurité
|
||||
- évite les réponses incohérentes selon les modules
|
||||
- facilite les tests d'isolation tenant
|
||||
- Limites / vigilance :
|
||||
- la convention doit être documentée et appliquée partout
|
||||
- un mauvais choix entre 403 et 404 peut révéler une information sensible
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : API multi-tenant / HTTP / services métier
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l'accès refusé
|
||||
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l'existence d'une ressource d'un autre tenant
|
||||
- documenter la convention dans le module
|
||||
- couvrir les deux sémantiques par des tests dédiés
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Convention 403 vs 404 documentée
|
||||
- Helpers distincts selon la sémantique métier
|
||||
- Aucune fuite d'existence cross-tenant sur les ressources métier
|
||||
- Tests dédiés sur les deux comportements
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-repository-tenant-aware"></a>
|
||||
## Pattern : Repository tenant-aware — `tenantId` obligatoire dans la signature
|
||||
|
||||
- Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant.
|
||||
- Contexte : repositories ou services d'accès aux données sur ressources tenant-scoped.
|
||||
- Quand l'utiliser : dès qu'un domaine métier est massivement filtré par tenant.
|
||||
- Quand l'éviter : domaines réellement globaux ou méthodes volontairement cross-tenant.
|
||||
- Avantage :
|
||||
- force le scoping dès la signature TypeScript
|
||||
- réduit les oublis de filtre tenant dans les call sites
|
||||
- rend les exceptions cross-tenant visibles
|
||||
- Limites / vigilance :
|
||||
- les exceptions cross-tenant doivent être rares et documentées explicitement
|
||||
- ne dispense pas d'un second garde-fou dans les mutations sensibles
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : TypeScript / Prisma / architecture repository
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire
|
||||
- les méthodes réellement cross-tenant sont nommées et documentées comme exception
|
||||
- les call sites Prisma directs sur ces domaines sont interdits ou supprimés
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `tenantId` obligatoire sur les méthodes tenant-scoped
|
||||
- Exceptions cross-tenant documentées
|
||||
- Appels directs concurrents à Prisma supprimés
|
||||
- Tests sur scoping tenant au niveau repository
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-tenantid-dans-updates"></a>
|
||||
## Pattern : Défense en profondeur — inclure `tenantId` dans les updates
|
||||
|
||||
- Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont.
|
||||
- Contexte : `update` ou `updateMany` sur une ressource tenant-scoped.
|
||||
- Quand l'utiliser : dès qu'une mutation dépend d'un `id` reçu ou résolu dans un flux multi-tenant.
|
||||
- Quand l'éviter : ressources globales non liées à un tenant.
|
||||
- Avantage :
|
||||
- ajoute une seconde barrière côté base
|
||||
- réduit l'impact d'un call site mal scopé
|
||||
- rend la mutation plus sûre sans complexité forte
|
||||
- Limites / vigilance :
|
||||
- ne remplace pas le scoping en lecture ni la vérification d'autorisation
|
||||
- suppose que `tenantId` soit disponible au moment de la mutation
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Prisma / multi-tenant / mutations métier
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped
|
||||
- appliquer la même règle sur `updateMany` et opérations de révocation
|
||||
- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `tenantId` présent dans les clauses `where` des updates sensibles
|
||||
- Pas de mutation tenant-scoped basée sur `id` seul
|
||||
- Revue explicite des exceptions documentées
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-helper-tenant-module-partage"></a>
|
||||
## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié
|
||||
|
||||
- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
|
||||
- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
|
||||
- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
|
||||
- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Next.js / TypeScript — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// ✅ src/server/tenant/resolvePublicTenant.ts
|
||||
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }
|
||||
|
||||
// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
|
||||
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-helper-feature-flag-tenant"></a>
|
||||
## Pattern : Helper centralisé d'activation de features tenant-scoped
|
||||
|
||||
- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
|
||||
- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
|
||||
- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
|
||||
- Avantage :
|
||||
- helper pur et testable sans I/O
|
||||
- comportement par défaut sain (`null`/`undefined` → tout activé)
|
||||
- composants de navigation et pages importent ce helper, jamais Prisma directement
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Next.js App Router / TypeScript — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// src/server/public/publicPagesConfig.ts
|
||||
export function isPublicPageEnabled(
|
||||
config: PublicPagesConfigRecord | null | undefined,
|
||||
pageKey: PublicPageKey
|
||||
): boolean {
|
||||
if (!config) return true; // config absente = tout activé par défaut
|
||||
return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
|
||||
}
|
||||
```
|
||||
|
||||
**Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-en-enforcement-tenant"></a>
|
||||
## Pattern : EN enforcement optionnel par tenant (toggle + publish gate)
|
||||
|
||||
- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
|
||||
- Contexte : app multi-tenant avec internationalisation optionnelle.
|
||||
- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
|
||||
- Validé le : 21-03-2026
|
||||
- Contexte technique : Prisma / Next.js App Router — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// 1. Modèle Tenant
|
||||
enableEn Boolean @default(false)
|
||||
|
||||
// 2. Vérification dans chaque action mutante (create/update)
|
||||
const { enableEn } = await getEnConfig(tenantId);
|
||||
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });
|
||||
|
||||
// 3. Gate publish — vérification de complétude
|
||||
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
|
||||
// Exclut : isSystem:true, tenantId:null, isVisible:false
|
||||
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });
|
||||
```
|
||||
|
||||
**Règles :**
|
||||
- `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication)
|
||||
- `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`)
|
||||
138
knowledge/backend/patterns/nestjs.md
Normal file
138
knowledge/backend/patterns/nestjs.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Backend — Patterns : NestJS
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-guard-global-nestjs"></a>
|
||||
|
||||
## Pattern : Guard global NestJS — ordre d'enregistrement et décorateurs de bypass
|
||||
|
||||
- Objectif : protéger tous les endpoints par défaut, avec un mécanisme explicite pour les exceptions.
|
||||
- Contexte : API NestJS avec plusieurs guards globaux (authn, authz, feature flags...).
|
||||
- Quand l'utiliser : dès qu'on a 2+ guards globaux dont l'un dépend du résultat de l'autre.
|
||||
- Quand l'éviter : si un seul guard suffit.
|
||||
- Avantage :
|
||||
- Sécurité par défaut (opt-out, pas opt-in)
|
||||
- Ordre d'exécution garanti et explicite
|
||||
- Bypass documenté et traçable via décorateurs
|
||||
- Limites / vigilance :
|
||||
- L'ordre des `APP_GUARD` dans `providers[]` est l'ordre d'exécution — ne pas inverser
|
||||
- Exporter le service depuis son module si injecté dans un guard global d'un autre module
|
||||
- Validé le : 07-03-2026
|
||||
- Contexte technique : NestJS v10+
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// app.module.ts
|
||||
providers: [
|
||||
{ provide: APP_GUARD, useClass: AuthGuard }, // 1er : peuple request.user
|
||||
{ provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user
|
||||
{ provide: APP_GUARD, useClass: EntitlementsGuard }, // 3ème : lit request.user + entitlements
|
||||
]
|
||||
|
||||
// skip-auth.decorator.ts
|
||||
export const SKIP_AUTH = 'skipAuth';
|
||||
export const SkipAuth = () => SetMetadata(SKIP_AUTH, true);
|
||||
|
||||
// auth.guard.ts
|
||||
const skip = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH, [
|
||||
context.getHandler(),
|
||||
context.getClass(), // permet @SkipAuth() au niveau classe
|
||||
]);
|
||||
if (skip) return true;
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] AuthGuard enregistré en premier dans `providers[]`
|
||||
- [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule
|
||||
- [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs)
|
||||
- [ ] Tests unitaires sur le guard avec reflector mocké
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-redis-health-cache-court"></a>
|
||||
|
||||
## Pattern : RedisHealthService avec cache interne court
|
||||
|
||||
- Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête.
|
||||
- Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision d'écriture.
|
||||
- Quand l'utiliser : quand plusieurs requêtes concurrentes doivent consulter l'état Redis.
|
||||
- Quand l'éviter : si Redis n'est pas consulté dans le chemin request/response.
|
||||
- Avantage :
|
||||
- réduit fortement le flood de `PING`
|
||||
- garde un signal d'état suffisamment frais
|
||||
- Limites / vigilance :
|
||||
- la fenêtre de cache doit rester courte
|
||||
- l'état initial doit être explicite et assumé
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : NestJS / Redis
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Mémoriser lastStatus et lastCheck
|
||||
- Si le dernier check a moins de 5s, retourner l'état en cache
|
||||
- Sinon exécuter un vrai PING et mettre le cache à jour
|
||||
- Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Cache court documenté
|
||||
- Pas de ping Redis à chaque requête
|
||||
- Comportement initial explicite
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-quota-redis-atomique"></a>
|
||||
## Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline)
|
||||
|
||||
- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline.
|
||||
- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles).
|
||||
- Quand l'utiliser : toute limite d'action journalière avec Redis disponible.
|
||||
- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation).
|
||||
- Avantage :
|
||||
- atomicité garantie : `INCR + EXPIREAT` dans un pipeline `MULTI/EXEC`
|
||||
- pas de clé sans TTL même en cas de deux requêtes simultanées (`count === 1` concurrent)
|
||||
- mode dégradé explicite si Redis down (`count === null` → permissif)
|
||||
- Limites / vigilance :
|
||||
- compensation `incrBy(-1)` en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification
|
||||
- la fenêtre expire à minuit UTC, pas à minuit local
|
||||
- Validé le : 20-03-2026
|
||||
- Contexte technique : Redis / NestJS / app-alexandrie story 4.2
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// RedisService — méthode dédiée
|
||||
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
|
||||
const pipeline = this.client.multi();
|
||||
pipeline.incr(key);
|
||||
pipeline.expireAt(key, Math.floor(expireAtMs / 1000));
|
||||
const results = await pipeline.exec();
|
||||
return results[0] as number; // valeur post-INCR
|
||||
}
|
||||
|
||||
// Service métier
|
||||
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC
|
||||
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
|
||||
const quotaKey = `app:quota:post:${userId}:${today}`;
|
||||
const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime());
|
||||
|
||||
if (count !== null && count > QUOTA_MAX) {
|
||||
await redis.incrBy(quotaKey, -1); // compensation
|
||||
throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS);
|
||||
}
|
||||
// count === null → Redis down → mode dégradé permissif
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] Vérifier le quota AVANT la création en DB
|
||||
- [ ] `INCR + EXPIREAT` dans un pipeline atomique
|
||||
- [ ] Mode dégradé permissif si `count === null` (Redis down)
|
||||
- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC)
|
||||
- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent)
|
||||
166
knowledge/backend/patterns/nextjs.md
Normal file
166
knowledge/backend/patterns/nextjs.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# Backend — Patterns : Next.js
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-nextjs-runtime-only-logique-pure-testable"></a>
|
||||
## Pattern : Next.js runtime-only — orchestration en bord et logique pure testable
|
||||
|
||||
- Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d'orchestration.
|
||||
- Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node.
|
||||
- Quand l'utiliser : dès qu'un flux serveur mélange APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`, `server-only`) et logique métier réutilisable.
|
||||
- Quand l'éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément.
|
||||
- Avantage :
|
||||
- garde la logique métier importable dans un runner Node standard
|
||||
- évite que `server-only` contamine des modules purs
|
||||
- facilite les tests unitaires sans mocks lourds du runtime Next.js
|
||||
- clarifie la responsabilité des Server Actions et handlers serveur
|
||||
- Limites / vigilance :
|
||||
- demande une discipline de découpage
|
||||
- peut introduire une indirection inutile si la logique extraite est réellement triviale
|
||||
- les frontières d'injection doivent rester simples pour éviter un excès d'abstraction
|
||||
- Validé le : 19-03-2026
|
||||
- Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js
|
||||
- garder la Server Action, route handler ou module email comme couche d'orchestration fine
|
||||
- extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only`
|
||||
- injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.)
|
||||
- tester unitairement le module pur dans le runner Node ; tester l'orchestrateur plus légèrement
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `server-only` absent des modules de logique pure
|
||||
- APIs Next.js runtime-only limitées aux couches d'entrée
|
||||
- Logique métier principale testable sans runtime Next.js
|
||||
- Dépendances injectées explicitement quand utile
|
||||
- Server Action ou handler fin et lisible
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-nextjs-server-only-isolation"></a>
|
||||
## Pattern : Next.js server-only & Server Actions — règles d'isolation
|
||||
|
||||
- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires.
|
||||
- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif.
|
||||
- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js.
|
||||
- Quand l'éviter : modules purement UI côté client.
|
||||
- Avantage :
|
||||
- logique pure testable sans friction (runner Node natif)
|
||||
- Server Action fine et lisible — orchestration uniquement
|
||||
- `server-only` explicite et intentionnel, pas par habitude
|
||||
- Limites / vigilance :
|
||||
- ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js
|
||||
- Validé le : 16-03-2026
|
||||
- Contexte technique : Next.js App Router / Node.js test runner
|
||||
|
||||
### Règles
|
||||
|
||||
```txt
|
||||
- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime
|
||||
(cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure
|
||||
- Logique pure extraite dans un module injectable sans `server-only` :
|
||||
deleteSession({ prismaClient, sessionToken })
|
||||
→ testable avec le runner Node sans friction
|
||||
- Server Action = orchestration mince, elle appelle les modules purs injectés
|
||||
et gère les dépendances Next.js runtime uniquement
|
||||
- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé,
|
||||
sans import nodemailer / server-only
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] `server-only` absent des repositories et modules de logique pure
|
||||
- [ ] Server Action ≤ 10 lignes, délègue au module pur injectable
|
||||
- [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale
|
||||
- [ ] La logique pure ne dépend pas du runtime pour être exécutée
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-utilitaires-purs-module-partage"></a>
|
||||
## Pattern : Utilitaires purs — extraire dans un module sans `server-only`
|
||||
|
||||
- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction.
|
||||
- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`.
|
||||
- Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests.
|
||||
- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle.
|
||||
- Validé le : 21-03-2026
|
||||
- Contexte technique : Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```
|
||||
src/server/menuAdmin/
|
||||
allergensRepository.ts ← import { slugify } from "./slugify"
|
||||
slugify.ts ← export function slugify() {} // pas de "server-only"
|
||||
|
||||
tests/
|
||||
allergens-admin.test.ts ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-reutiliser-champ-existant-v1"></a>
|
||||
## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1
|
||||
|
||||
- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple.
|
||||
- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique).
|
||||
- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations.
|
||||
- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes.
|
||||
- Avantage : zéro migration supplémentaire, zéro scope creep
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js — app-template-resto
|
||||
|
||||
### Règle
|
||||
|
||||
```txt
|
||||
- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit
|
||||
- Un champ optionnel dans le modèle le plus proche est suffisant en V1
|
||||
- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-validation-url-externe"></a>
|
||||
## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public
|
||||
|
||||
- Objectif : prévenir les injections `javascript:` et URLs malformées dans les `<a href>` ou `<img src>` publics.
|
||||
- Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML.
|
||||
- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML.
|
||||
- Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
function isSafeUrl(url: string): boolean {
|
||||
try {
|
||||
const { protocol } = new URL(url);
|
||||
return protocol === "https:" || protocol === "http:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation complète en service/repository
|
||||
if (mediaUrl) {
|
||||
try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
|
||||
if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
|
||||
throw new HttpError("URL doit commencer par https://.", { status: 400 });
|
||||
if (mediaUrl.length > 500)
|
||||
throw new HttpError("URL trop longue.", { status: 400 });
|
||||
}
|
||||
// Retourner null si invalide — le composant gère l'absence d'URL
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- [ ] 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
|
||||
247
knowledge/backend/patterns/prisma.md
Normal file
247
knowledge/backend/patterns/prisma.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# Backend — Patterns : Prisma
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-soft-delete-archivage-explicite"></a>
|
||||
|
||||
## Pattern : Soft delete et archivage explicite
|
||||
|
||||
- Objectif : permettre la suppression logique sans perte immédiate de données.
|
||||
- Contexte : données métier critiques, besoins d'audit, restauration ou conformité.
|
||||
- Quand l'utiliser : dès qu'une suppression peut avoir des impacts métier ou légaux.
|
||||
- Quand l'éviter : données purement techniques ou réellement éphémères.
|
||||
- Avantage :
|
||||
- Restauration possible
|
||||
- Audit et traçabilité
|
||||
- Réduction des suppressions irréversibles
|
||||
- Limites / vigilance :
|
||||
- Complexité accrue sur les requêtes
|
||||
- Nécessite une discipline stricte (filtres par défaut)
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API + DB relationnelle
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Champ deletedAt (nullable) ou status
|
||||
- Les requêtes standards filtrent deletedAt IS NULL
|
||||
- Endpoints dédiés pour restauration / purge
|
||||
- Index DB tenant compte du soft delete
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Filtrage soft delete par défaut
|
||||
- Restauration explicite possible
|
||||
- Purge maîtrisée (cron / job)
|
||||
- Index DB adaptés
|
||||
- Tests sur cas supprimé / restauré
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-pagination-robuste-cursor-based"></a>
|
||||
|
||||
## Pattern : Pagination robuste (cursor-based) pour les listings
|
||||
|
||||
- Objectif : fournir des listings stables et performants sans incohérences entre pages.
|
||||
- Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important.
|
||||
- Quand l'utiliser : dès qu'un listing peut dépasser quelques dizaines/centaines d'items ou subir des écritures concurrentes.
|
||||
- Quand l'éviter : listes strictement petites et statiques.
|
||||
- Avantage :
|
||||
- Résultats stables malgré insertions/suppressions
|
||||
- Meilleure performance que l'offset sur gros volumes
|
||||
- Expérience client plus fiable
|
||||
- Limites / vigilance :
|
||||
- Nécessite un tri déterministe (champ + tie-breaker)
|
||||
- Complexité légèrement supérieure à offset/limit
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Trier par (createdAt DESC, id DESC) (exemple)
|
||||
- Le client envoie cursor = dernier (createdAt,id) reçu
|
||||
- Le backend renvoie nextCursor si plus de résultats
|
||||
- Ne jamais exposer de cursor implicite ou non documenté
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Tri déterministe (avec tie-breaker)
|
||||
- nextCursor renvoyé et documenté
|
||||
- Limite max de page (protection)
|
||||
- Index DB aligné avec le tri
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-idempotency-key-operations-sensibles"></a>
|
||||
|
||||
## Pattern : Idempotency key pour opérations sensibles
|
||||
|
||||
- Objectif : empêcher les doublons lors de retries ou timeouts.
|
||||
- Contexte : création de ressources, paiements, webhooks.
|
||||
- Quand l'utiliser : toute opération non strictement en lecture.
|
||||
- Quand l'éviter : endpoints purement GET.
|
||||
- Avantage :
|
||||
- Protection contre doublons
|
||||
- Robustesse face aux retries
|
||||
- Limites / vigilance :
|
||||
- Stockage et expiration des clés à gérer
|
||||
- Validé le : 25-01-2026
|
||||
- Contexte technique : API HTTP + DB transactionnelle
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Client fournit Idempotency-Key
|
||||
- Backend stocke la clé + résultat
|
||||
- Retry retourne le résultat initial
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Clé obligatoire sur endpoints sensibles
|
||||
- Contrainte d'unicité côté DB
|
||||
- Comportement documenté
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-prisma-p2002-update-unique"></a>
|
||||
|
||||
## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique
|
||||
|
||||
- Objectif : transformer un conflit d'unicité prévisible en erreur métier exploitable plutôt qu'en 500 opaque.
|
||||
- Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check.
|
||||
- Quand l'utiliser : dès qu'un champ unique peut entrer en collision — à la création ET à la modification.
|
||||
- Quand l'éviter : jamais si le champ peut réellement entrer en collision.
|
||||
- Avantage :
|
||||
- réponse client stable
|
||||
- diagnostic métier plus rapide
|
||||
- Limites / vigilance :
|
||||
- le mapping doit rester cohérent avec le format d'erreur API standardisé
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Prisma / PostgreSQL / NestJS
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Catch explicite de PrismaClientKnownRequestError code P2002
|
||||
- Mapping vers une erreur métier stable
|
||||
- Conserver requestId et format d'erreur standardisé
|
||||
```
|
||||
|
||||
### Implémentation (exemple complet)
|
||||
|
||||
```typescript
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
try {
|
||||
await prisma.item.create({ data: { ... } });
|
||||
// ou: await prisma.item.update({ where: { id }, data: { ... } });
|
||||
} catch (err) {
|
||||
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
|
||||
throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
**Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. S'applique à `create`, `update`, `updateMany`, `upsert`.
|
||||
|
||||
### Checklist
|
||||
|
||||
- `P2002` intercepté sur les creates ET les updates sensibles
|
||||
- Code d'erreur métier stable (409 Conflict)
|
||||
- Pas de 500 générique sur conflit prévisible
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-decimal-prisma-serialisation"></a>
|
||||
## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository
|
||||
|
||||
- Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses.
|
||||
- Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action.
|
||||
- Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories.
|
||||
- Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner.
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js — app-template-resto
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// Repository — convertir avant de retourner
|
||||
return {
|
||||
...dish,
|
||||
price: dish.price?.toString() ?? null, // Decimal → string
|
||||
};
|
||||
|
||||
// DTO public
|
||||
type DishDto = {
|
||||
price: string | null; // pas Decimal
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-prisma-migration-manuelle-p3014"></a>
|
||||
## Pattern : Prisma — Migration manuelle sans shadow DB (P3014)
|
||||
|
||||
- Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes).
|
||||
- Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser.
|
||||
- Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`.
|
||||
- Risque si ignoré : blocage complet de la migration sur env managé.
|
||||
- Validé le : 23-03-2026
|
||||
- Contexte technique : Prisma v7+ — app-alexandrie / Supabase
|
||||
|
||||
### Implémentation
|
||||
|
||||
```bash
|
||||
# 1. Écrire le SQL manuellement
|
||||
mkdir -p prisma/migrations/<timestamp>_<nom>
|
||||
# Créer migration.sql à la main
|
||||
|
||||
# 2. Appliquer le SQL directement en DB
|
||||
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql
|
||||
|
||||
# 3. Marquer la migration comme appliquée dans _prisma_migrations
|
||||
npx prisma migrate resolve --applied <timestamp>_<nom>
|
||||
|
||||
# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts
|
||||
```
|
||||
|
||||
**Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-filtrage-metier-service"></a>
|
||||
## Pattern : Filtrage des règles métier dans le service, pas dans le repository
|
||||
|
||||
- Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service).
|
||||
- Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin).
|
||||
- Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel.
|
||||
- Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`.
|
||||
- Avantage :
|
||||
- la règle est testable unitairement sans Prisma (mock de données brutes)
|
||||
- la requête DB reste simple et stable entre contextes
|
||||
- les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
|
||||
- Validé le : 17-03-2026
|
||||
- Contexte technique : Prisma / Node.js / Next.js — app-template-resto
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// Repository — charge tout ce qui est candidat
|
||||
async findCategories(tenantId: string) {
|
||||
return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible
|
||||
}
|
||||
|
||||
// Service — applique la règle métier et mappe vers DTO
|
||||
const raw = await repo.findCategories(tenantId);
|
||||
return raw.filter(c => c.isVisible).map(toPublicDto);
|
||||
|
||||
// Admin : même repo, filtre différent dans le service admin
|
||||
return raw.map(toAdminDto); // retourne tout, visible ou non
|
||||
```
|
||||
160
knowledge/backend/patterns/stripe.md
Normal file
160
knowledge/backend/patterns/stripe.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Backend — Patterns : Stripe
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-provider-strategy-integrations-tierces"></a>
|
||||
|
||||
## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet
|
||||
|
||||
- Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers.
|
||||
- Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…).
|
||||
- Quand l'utiliser : dès qu'un service applicatif dépend d'un SDK tiers (et plus encore s'il y a des webhooks).
|
||||
- Quand l'éviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle.
|
||||
- Avantage :
|
||||
- Testabilité : mock du provider, pas du SDK
|
||||
- Remplacement du prestataire sans refactor "en cascade"
|
||||
- Responsabilités claires : provider = "parle Stripe", service = "parle domaine"
|
||||
- Limites / vigilance :
|
||||
- L'interface doit exposer des **types normalisés** (pas de types Stripe)
|
||||
- Le provider gère aussi les webhooks : validation signature, parsing event, mapping
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```typescript
|
||||
// billing-provider.interface.ts (pas d'import Stripe)
|
||||
export type BillingPlan = 'MONTHLY' | 'ANNUAL';
|
||||
|
||||
export type BillingWebhookResult = {
|
||||
userId: string;
|
||||
externalId: string;
|
||||
plan: BillingPlan;
|
||||
status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED';
|
||||
currentPeriodEnd: Date | null;
|
||||
};
|
||||
|
||||
export interface BillingProvider {
|
||||
createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>;
|
||||
cancelSubscription(externalId: string): Promise<void>;
|
||||
handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
|
||||
}
|
||||
|
||||
// billing.service.ts (domaine uniquement)
|
||||
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
|
||||
const result = await this.billingProvider.handleWebhook(rawBody, signature);
|
||||
if (!result) return;
|
||||
await this.prisma.subscription.upsert({ /* données normalisées */ });
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-stripe-subscription-metadata"></a>
|
||||
|
||||
## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session
|
||||
|
||||
- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`.
|
||||
- Contexte : intégration Stripe Checkout avec webhooks abonnement.
|
||||
- Quand l'utiliser : systématiquement dès qu'on crée une Checkout Session liée à une Subscription.
|
||||
- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod.
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : Stripe API v17+ / NestJS
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
stripe.checkout.sessions.create({
|
||||
metadata: { userId }, // pour checkout.session.completed
|
||||
subscription_data: { metadata: { userId } }, // pour customer.subscription.*
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-webhook-parsing-unique"></a>
|
||||
|
||||
## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`)
|
||||
|
||||
- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs.
|
||||
- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…).
|
||||
- Quand l'utiliser : dès qu'on a 2+ handlers webhook sur le même endpoint.
|
||||
- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO).
|
||||
- Validé le : 09-03-2026
|
||||
- Contexte technique : Stripe / NestJS
|
||||
|
||||
### Implémentation
|
||||
|
||||
```typescript
|
||||
// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque
|
||||
// 2. Extracteurs purs, sans effet de bord :
|
||||
handleSubscriptionWebhookEvent(event): WebhookResult | null
|
||||
handlePackWebhookEvent(event): PackWebhookResult | null
|
||||
// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-restauration-achats-stripe"></a>
|
||||
|
||||
## Pattern : restauration d'achats Stripe en 3 étapes
|
||||
|
||||
- Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre d'une hypothèse fragile.
|
||||
- Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé.
|
||||
- Quand l'utiliser : dès qu'un utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation.
|
||||
- Quand l'éviter : si l'état Stripe n'est pas la source de vérité.
|
||||
- Avantage :
|
||||
- rend la réconciliation explicite
|
||||
- supporte retries et restaurations tardives
|
||||
- Limites / vigilance :
|
||||
- la pagination Stripe et l'idempotence d'écriture restent obligatoires
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Stripe API / backend Node/NestJS
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent)
|
||||
2. Reconstruction de l'état Stripe utile au domaine
|
||||
3. Réconciliation et écritures locales idempotentes
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- `stripeCustomerId` persistant côté app
|
||||
- Réconciliation explicite documentée
|
||||
- Upsert ou écriture idempotente
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-subscription-trial-vs-paid"></a>
|
||||
|
||||
## Pattern : Sémantique explicite `Trial` vs `Paid` dans Subscription
|
||||
|
||||
- Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de l'abonnement payant actif.
|
||||
- Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai.
|
||||
- Quand l'utiliser : dès qu'un même enregistrement supporte trial et abonnement payant.
|
||||
- Quand l'éviter : si trial et abonnement payant sont modélisés par des entités distinctes.
|
||||
- Avantage :
|
||||
- évite les incohérences silencieuses dans les guards
|
||||
- rend les fixtures et mocks e2e cohérents avec la règle métier
|
||||
- Limites / vigilance :
|
||||
- toute logique `isActive` doit préciser si elle signifie "trial ou paid" ou "paid only"
|
||||
- Validé le : 10-03-2026
|
||||
- Contexte technique : Backend agnostique / modèle d'abonnement
|
||||
|
||||
### Implémentation (exemple minimal)
|
||||
|
||||
```txt
|
||||
- Un abonnement payant actif n'est pas seulement status = ACTIVE
|
||||
- Il doit aussi avoir trialEndsAt = null
|
||||
- Les fixtures et mocks e2e d'un abonnement payant fixent toujours trialEndsAt: null
|
||||
```
|
||||
|
||||
### Checklist
|
||||
|
||||
- Règle métier explicitée
|
||||
- Guards alignés sur la sémantique choisie
|
||||
- Fixtures et seeds cohérents
|
||||
Reference in New Issue
Block a user