mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-04-06 21:41:42 +02:00
Capitalisation complète — app-alexandrie & app-template-resto (23-03-2026)
Intègre ~50 entrées depuis 95_a_capitaliser.md vers les fichiers validés :
- backend risques : +15 (GET sans authz, TOCTOU tenantId, TTL UTC, AdminRoleGuard, P3014...)
- backend patterns : P2002 amendé (create+update) + 10 nouveaux (Decimal, URL safe, EN enforcement...)
- frontend risques : +21 (defaultValue/key, useTransition global, consent state, Tailwind invalide...)
- frontend patterns : +6 (click-to-load, toggle optimiste, Server Action retourne entité...)
- debug/postmortem : export{fn} ne crée pas de binding local
95_a_capitaliser.md remis à l'état initial vide.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ Ce fichier contient **uniquement** des patterns back-end :
|
||||
|
||||
Objectif : éviter de réinventer la roue et réduire le temps de debug.
|
||||
|
||||
Dernière mise à jour : 20-03-2026
|
||||
Dernière mise à jour : 23-03-2026
|
||||
|
||||
---
|
||||
|
||||
@@ -30,7 +30,7 @@ Dernière mise à jour : 20-03-2026
|
||||
- [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court)
|
||||
- [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid)
|
||||
- [Restauration d’achats Stripe en 3 étapes](#pattern-restauration-achats-stripe)
|
||||
- [Mapping explicite de `P2002` Prisma sur update de champ unique](#pattern-prisma-p2002-update-unique)
|
||||
- [Mapping explicite de `P2002` Prisma sur create/update de champ unique](#pattern-prisma-p2002-update-unique)
|
||||
- [Autorisation interne minimale sans RBAC complet](#pattern-autorisation-interne-minimale)
|
||||
- [Anti-énumération sur endpoints auth liés à un email](#pattern-anti-enumeration-auth-email)
|
||||
- [Token à usage unique — génération, hash et invalidation atomique](#pattern-token-usage-unique)
|
||||
@@ -42,6 +42,15 @@ Dernière mise à jour : 20-03-2026
|
||||
- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques)
|
||||
- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier)
|
||||
- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique)
|
||||
- [Filtrage des règles métier dans le service, pas dans le repository](#pattern-filtrage-metier-service)
|
||||
- [Sérialiser les champs `Decimal` Prisma en string au niveau du repository](#pattern-decimal-prisma-serialisation)
|
||||
- [Extraire les helpers de résolution tenant dans un module partagé dédié](#pattern-helper-tenant-module-partage)
|
||||
- [Helper centralisé d'activation de features tenant-scoped](#pattern-helper-feature-flag-tenant)
|
||||
- [Réutiliser un champ existant plutôt que créer un modèle dédié en V1](#pattern-reutiliser-champ-existant-v1)
|
||||
- [Valider le protocole d'une URL externe avant de la passer à un lien public](#pattern-validation-url-externe)
|
||||
- [Utilitaires purs : extraire dans un module sans `server-only`](#pattern-utilitaires-purs-module-partage)
|
||||
- [EN enforcement optionnel par tenant (toggle + publish gate)](#pattern-en-enforcement-tenant)
|
||||
- [Prisma — Migration manuelle sans shadow DB (P3014)](#pattern-prisma-migration-manuelle-p3014)
|
||||
|
||||
---
|
||||
|
||||
@@ -686,11 +695,11 @@ handlePackWebhookEvent(event): PackWebhookResult | null
|
||||
|
||||
<a id="pattern-prisma-p2002-update-unique"></a>
|
||||
|
||||
## Pattern : mapping explicite de `P2002` Prisma sur update de champ unique
|
||||
## 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 : `update` Prisma sur un champ `@unique` alimenté par une source externe ou concurrente.
|
||||
- Quand l’utiliser : dès qu’un champ unique peut être mis à jour après création.
|
||||
- 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
|
||||
@@ -708,10 +717,28 @@ handlePackWebhookEvent(event): PackWebhookResult | null
|
||||
- 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 updates sensibles
|
||||
- Code d’erreur métier stable
|
||||
- `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
|
||||
|
||||
---
|
||||
@@ -1149,3 +1176,260 @@ if (count !== null && count > QUOTA_MAX) {
|
||||
- [ ] 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)
|
||||
|
||||
---
|
||||
|
||||
<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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<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-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-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
|
||||
|
||||
---
|
||||
|
||||
<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-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`)
|
||||
|
||||
---
|
||||
|
||||
<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.
|
||||
|
||||
Reference in New Issue
Block a user