mirror of
https://github.com/MaksTinyWorkshop/_Assistant_Lead_Tech
synced 2026-06-28 01:53:40 +02:00
capitalisation: triage 95_a_capitaliser + création domaine infra
Triage des 27 propositions du buffer de capitalisation (skill capitalisation-triage), avec vérification des doublons contre la base. Intégré dans knowledge/ (23 entrées): - backend: redis (compensation incrBy non-atomique), nestjs (injection cassée sous tsx watch; guard write mode dégradé), async (test rollback pipeline multi-fichiers), contracts (idempotence POST), auth (disclosure comptes soft-deleted), prisma (index partial soft-delete), llm-providers (nouveau: OAuth vs API key, prompt caching). - frontend: tests (garde-fous parking Later), navigation (fichiers non-route sous src/app Expo Router), general (type client vs payload backend), state (fallback catch-all mapping DB→UI). - workflow: story-tracking (statut BMAD vs narratif obsolète). - product: general (nouveau: doc feature store sans UI). - infra: NOUVEAU DOMAINE (traefik, tailscale, docker, docker-networking, reverse-proxy-paths, sidecar tailscale) + 00_INDEX.md. Autres: - 90_debug_et_postmortem.md: post-mortem réseau Docker partagé hors compose. - Rejeté 3 doublons (types enum contracts, getter PrismaService, $transaction). - Buffer 95_a_capitaliser.md purgé et restauré à son état initial. - _projects.conf: MAJ statuts epics + ajout app-rl799.
This commit is contained in:
@@ -9,12 +9,13 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript
|
||||
| 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 |
|
||||
| `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, idempotence POST = retour ressource existante |
|
||||
| `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, hooks fire-and-forget après création DB, fanout notification avec filtre grade, auto-purge fenêtre temporelle SQL |
|
||||
| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents, hooks fire-and-forget après création DB, fanout notification avec filtre grade, auto-purge fenêtre temporelle SQL, test de rollback pipeline multi-fichiers atomique |
|
||||
| `general.md` | Architecture générale, helpers, RBAC | Helper auth centralisé enrichissable, ordre canonique des gates HTTP, délégation agrégat → endpoint agrégé, anti-énumération DELETE 204, lazy init memoizé, cap LRU par-user, convention dot-notation audit, whitelist explicite audit, singleton DB config, invalidation cache avant mutation, pipeline CI/CD GitHub Actions → VPS |
|
||||
| `tests.md` | Tests d'intégration DB, isolation, atomicité | `cleanup.track()` LIFO, `globalSetup` purge, template database Postgres, helper `waitForX()` polling-borné, test d'atomicité transaction, convention `describe()` 2 niveaux, refactor itératif d'un fichier monolithe |
|
||||
| `llm-providers.md` | Fournisseurs LLM, auth, coûts | OAuth consumer ≠ API key, prompt caching system prompt stable, budget cap fournisseur |
|
||||
|
||||
@@ -251,3 +251,30 @@ export const listRecentXxxForMember = async (
|
||||
```
|
||||
|
||||
L'admin garde un endpoint distinct sans le filtre temporel pour l'accès historique complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-test-rollback-pipeline-multi-fichiers"></a>
|
||||
## Pattern : Test de rollback pour pipelines multi-fichiers atomiques
|
||||
|
||||
- Objectif : garantir qu'un pipeline qui écrit N fichiers avec rollback nettoie réellement l'état partiel quand le Nème échoue.
|
||||
- Contexte : opération qui persiste plusieurs artefacts (ex : variantes/dérivés d'une image, fichiers d'un export) en gardant la liste des chemins déjà écrits pour pouvoir les supprimer en cas d'échec.
|
||||
- Quand l'utiliser : tout pipeline « tout ou rien » sur N écritures de fichiers avec compensation manuelle (pas de transaction native).
|
||||
- Quand l'éviter : écriture d'un seul fichier, ou stockage qui offre une vraie transactionnalité.
|
||||
- Avantage :
|
||||
- couvre le cas limite réel (échec en cours de pipeline) plutôt que le seul chemin nominal
|
||||
- détecte une compensation incomplète (fichiers orphelins) ou excessive (suppression d'un fichier non écrit par ce pipeline)
|
||||
- Limites / vigilance :
|
||||
- le test doit être mis à jour quand N change (ajout d'un variant) pour couvrir le nouveau cas limite
|
||||
- Validé le : 03-04-2026
|
||||
- Contexte technique : Node.js / écriture fichiers — app-template-resto story 4.3
|
||||
|
||||
### Règle
|
||||
|
||||
Tout pipeline qui écrit N fichiers avec rollback (suppression des déjà-écrits si un échec survient) doit avoir un test unitaire couvrant le cas **« N-1 fichiers écrits + le Nème échoue »**. Ce test vérifie :
|
||||
|
||||
- que les N-1 fichiers déjà écrits sont **exactement** supprimés (ni plus, ni moins) ;
|
||||
- que la phase de finalisation (`finalize()` ou équivalent) n'est **pas** appelée ;
|
||||
- que l'erreur est bien propagée à l'appelant.
|
||||
|
||||
Quand le nombre d'artefacts change (ajout d'un variant), mettre à jour ce test pour couvrir le nouveau N.
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
title: Backend — Patterns : Contracts
|
||||
domain: backend
|
||||
bucket: patterns
|
||||
tags: [contracts, zod, api, error-codes, monorepo]
|
||||
tags: [contracts, zod, api, error-codes, monorepo, idempotence, http-semantics]
|
||||
applies_to: [analysis, implementation, review, architecture]
|
||||
severity: high
|
||||
validated_on: 2026-04-07
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [app-alexandrie, RL799_V2]
|
||||
---
|
||||
|
||||
@@ -150,6 +150,34 @@ return res.status(200).json({
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-idempotence-post-ressource-existante"></a>
|
||||
## Pattern : Idempotence POST = retour de la ressource existante
|
||||
|
||||
- Objectif : garantir qu'un POST dont l'AC demande l'idempotence produit le **même résultat quel que soit le nombre d'appels**.
|
||||
- Contexte : endpoint de création où une duplication est détectable (ressource déjà créée pour le même sujet, ex : demande d'export, déclenchement de job unique). Complément du pattern « HTTP 200 + payload métier » ci-dessus, appliqué au cas duplication.
|
||||
- Quand l'utiliser : POST déclaré idempotent (retries client, double-tap mobile, rejeu réseau).
|
||||
- Quand l'éviter : création stricte où une duplication est une vraie erreur métier que l'appelant doit traiter explicitement.
|
||||
- Validé le : 13-04-2026
|
||||
- Contexte technique : NestJS / contrat API — app-alexandrie story 9.2
|
||||
|
||||
### Règle
|
||||
|
||||
Un POST idempotent **retourne la ressource existante** (HTTP 200 / 201) quand une duplication est détectée, sans lever d'erreur. Un `409 CONFLICT` est un comportement de **blocage**, pas d'idempotence : il force l'appelant à gérer un cas d'erreur et casse le contrat « N appels = 1 résultat ».
|
||||
|
||||
```typescript
|
||||
// ✅ IDEMPOTENT — retourne la ressource existante
|
||||
if (existing) return this.serialize(existing);
|
||||
|
||||
// ❌ NON-IDEMPOTENT — lève une erreur de blocage
|
||||
if (existing) throw new HttpException({ error: { code: 'ALREADY_EXISTS' } }, 409);
|
||||
```
|
||||
|
||||
### Côté client
|
||||
|
||||
Dédupliquer par `id` avant d'insérer dans la liste/store local, pour éviter les doublons en cas de race condition (deux requêtes parallèles recevant la même ressource).
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-coherence-result-repository"></a>
|
||||
## Pattern : Cohérence du pattern Result dans un repository
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
title: "Backend — Patterns : Fournisseurs LLM"
|
||||
domain: backend
|
||||
bucket: patterns
|
||||
tags: [llm, anthropic, openai, api-key, oauth, prompt-caching, couts]
|
||||
applies_to: [analysis, implementation, architecture]
|
||||
severity: medium
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [_Assistant_Cuisine]
|
||||
---
|
||||
|
||||
# Backend — Patterns : Fournisseurs LLM
|
||||
|
||||
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-oauth-consumer-vs-api-key"></a>
|
||||
## OAuth abonnement consumer ≠ API key — ne pas mélanger les deux usages
|
||||
|
||||
Tous les fournisseurs LLM (Anthropic, OpenAI, Google) séparent strictement deux mondes :
|
||||
|
||||
| Type | Auth | Usage prévu | Coût |
|
||||
|---|---|---|---|
|
||||
| **Abonnement consumer** (Claude Pro/Max, ChatGPT Plus) | OAuth via app officielle | usage interactif personnel via clients officiels (desktop, web, plugin IDE) | forfait mensuel |
|
||||
| **API platform** | API key | usage programmatique, multi-users, services tiers, prod | pay-per-token |
|
||||
|
||||
### Règle
|
||||
|
||||
- Ne **jamais** utiliser les credentials OAuth d'un abonnement consumer pour servir une application à plusieurs utilisateurs. Anthropic l'interdit explicitement (ToS) ; les autres le tolèrent moins ouvertement mais le principe est identique. Risque concret : bannissement du compte si détecté.
|
||||
- Pour toute application applicative (chatbot, intégration produit), prendre une **API key séparée**, indépendante de l'abonnement consumer.
|
||||
|
||||
---
|
||||
|
||||
<a id="pattern-prompt-caching-system-prompt-stable"></a>
|
||||
## Prompt caching obligatoire dès qu'un gros system prompt est stable
|
||||
|
||||
Tout système ayant un system prompt volumineux et stable (knowledge base, format de sortie, persona) DOIT activer le prompt caching côté fournisseur. La portion cachée est facturée à une fraction du tarif normal (ordre de grandeur : ~10 %), ce qui divise le coût input par 5 à 10 sur un usage où le system prompt domine.
|
||||
|
||||
```ts
|
||||
// OpenAI : promptCacheKey via provider metadata
|
||||
streamText({
|
||||
model: openai("gpt-4.1-mini"),
|
||||
system: bigStaticPrompt,
|
||||
experimental_providerMetadata: {
|
||||
openai: { promptCacheKey: "myapp-system-v1" },
|
||||
},
|
||||
});
|
||||
|
||||
// Anthropic : marquage explicite des blocs cacheables dans messages (cache_control)
|
||||
```
|
||||
|
||||
### Conséquences sur les coûts
|
||||
|
||||
- Le **coût marginal** d'un nouvel utilisateur est quasi nul tant que le system prompt domine le volume de tokens.
|
||||
- Pour un usage léger (quelques conversations/jour, prompt caching actif), prévoir un budget mensuel de l'ordre de quelques euros.
|
||||
- Toujours poser un **budget cap** côté console fournisseur pour éviter toute dérive.
|
||||
|
||||
---
|
||||
@@ -8,12 +8,12 @@ Avant toute proposition backend, identifie le fichier dont le nom et la descript
|
||||
|
||||
| Fichier | Domaine | Entrées clés |
|
||||
|---------|---------|--------------|
|
||||
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact |
|
||||
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e, champ absent JWT, email login vs contact, disclosure comptes soft-deleted dans login() |
|
||||
| `contracts.md` | Contrats, validation, codes erreur | Contrats implicites, erreurs non standardisées, duplication constantes, schema orphelin, code erreur générique 409, ForbiddenException pour validation, process.env direct, statut métier non propagé |
|
||||
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique |
|
||||
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé, enum-like String, migration manuelle hors git, relation 1:1 sans unique, index partial soft-delete (perf) |
|
||||
| `stripe.md` | Stripe, paiements, webhooks, subscriptions | billing_cycle_anchor vs current_period_end, list() sans has_more, concurrence trial→payant, non-idempotence, 200 pendant processing |
|
||||
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète |
|
||||
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h |
|
||||
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète, guard multi-statut READ_METHODS, bootstrap OK mais injection cassée (tsx watch), guard écriture mode dégradé bloque le support |
|
||||
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h, compensation incrBy non-atomique (quota fantôme) |
|
||||
| `nextjs.md` | Next.js, build, routing | Prisma init au chargement module, server-only dans repositories, redirect boucle infinie feature flags, dossiers `_*` exclus du routing App Router |
|
||||
| `general.md` | Observabilité, migrations, performance, architecture | Observabilité insuffisante, migrations non reproductibles, upsert N+1, authorize-after-fetch, valeur sentinelle DTO, idempotence endpoint, fichier orphelin, mélange Date UTC/locale, champ fantôme Zod, catch vide, params non validés, cast TS brut, chevauchement temporel, TOCTOU, biais agrégation, couplage types erreur, service HTTP-aware, count sans filtre, env top-level, dérive DTO liste vs détail, notification linkUrl rôle-aware, matrice documentée vs code, format `User.id` mixte, Web Push topic > 32 chars, lib npm types non embarqués, form HTML POST dans un mail, env vars frontend-facing fail-fast |
|
||||
| `tests.md` | Isolation des tests d'intégration | `vi.stubEnv` sans restauration, `maxWorkers: 1` masque l'isolation, flakiness inter-fichiers DB partagée |
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
title: Backend — Risques & vigilance : Auth
|
||||
domain: backend
|
||||
bucket: risques
|
||||
tags: [auth, guards, request-user, sessions, admin]
|
||||
tags: [auth, guards, request-user, sessions, admin, enumeration, soft-delete]
|
||||
applies_to: [implementation, review, debug]
|
||||
severity: high
|
||||
validated_on: 2026-04-07
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [app-alexandrie, RL799_V2]
|
||||
---
|
||||
|
||||
@@ -458,4 +458,40 @@ Quand on prévoit de supprimer un flag auth profondément câblé :
|
||||
|
||||
**Anti-pattern** : déprécier en douceur en gardant le flag avec un commentaire `// @deprecated` sans supprimer les usages. Le code mort s'accumule, les futurs devs hésitent à le nettoyer ("pourquoi c'est encore là ?"), la dépréciation ne se finit jamais.
|
||||
|
||||
- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026
|
||||
- Contexte technique : auth / refactor schema — RL799_V2 28-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-disclosure-comptes-soft-deleted-login"></a>
|
||||
## Information disclosure sur comptes soft-deleted dans `login()`
|
||||
|
||||
### Risques
|
||||
|
||||
- Retourner un code d'erreur distinct (ex : `ACCOUNT_DELETED`) pour les comptes supprimés dans `login()` permet l'**énumération** : un attaquant distingue « compte supprimé » de « identifiants invalides » par email.
|
||||
- Complément du pattern anti-énumération (cf. `pattern-...anti-énumération` dans `patterns/auth.md`) appliqué au cas soft-delete.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `login()` lève `ACCOUNT_DELETED` au lieu de `INVALID_CREDENTIALS` pour un compte soft-deleted
|
||||
- L'existence (et le statut) d'un compte fuite via la réponse d'authentification
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ DANGEREUX — révèle l'existence d'un compte supprimé
|
||||
if (user.deletedAt !== null) {
|
||||
throw new UnauthorizedException({ error: { code: 'ACCOUNT_DELETED' } });
|
||||
}
|
||||
|
||||
// ✅ CORRECT — même code que des identifiants invalides
|
||||
if (user.deletedAt !== null) {
|
||||
throw new UnauthorizedException({
|
||||
error: { code: 'INVALID_CREDENTIALS', message: 'Email ou mot de passe invalide.' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : dans `login()`, toujours répondre `INVALID_CREDENTIALS` pour un compte soft-deleted — jamais un code spécifique.
|
||||
- **Nuance** : un code `ACCOUNT_DELETED` reste acceptable dans un flux `exchange()` OAuth, où le provider a déjà confirmé l'identité (pas d'énumération possible côté attaquant).
|
||||
|
||||
- Contexte technique : auth / soft-delete / anti-énumération — app-alexandrie 13-04-2026
|
||||
@@ -2,10 +2,10 @@
|
||||
title: Backend — Risques & vigilance : NestJS
|
||||
domain: backend
|
||||
bucket: risques
|
||||
tags: [nestjs, controllers, guards, providers, review]
|
||||
tags: [nestjs, controllers, guards, providers, review, injection, tsx-watch, mode-degrade]
|
||||
applies_to: [implementation, review, debug]
|
||||
severity: high
|
||||
validated_on: 2026-03-30
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [app-alexandrie]
|
||||
---
|
||||
|
||||
@@ -144,3 +144,50 @@ if (user.sessionStatus === 'BLOCKED') throw new HttpException(...);
|
||||
- **Règle** : définir une whitelist explicite des writes autorisés même en état bloqué.
|
||||
|
||||
- Contexte technique : NestJS / guard statut — app-alexandrie 30-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-bootstrap-ok-injection-cassee-tsx-watch"></a>
|
||||
## Bootstrap NestJS OK mais injection runtime cassée sous `tsx watch`
|
||||
|
||||
### Risques
|
||||
|
||||
- En dev, un serveur NestJS peut afficher `Nest application successfully started` et exposer ses routes, tout en cassant à la **première requête** : des dépendances injectées (`Reflector`, services consommés par les guards globaux) arrivent à `undefined` au runtime.
|
||||
- Le symptôme initial ressemble à un problème réseau/mobile alors que la cause réelle est l'injection backend.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `GET /` ou une route d'auth répond `500` alors que le bootstrap est nominal
|
||||
- Stack runtime du type `Cannot read properties of undefined (reading 'getAllAndOverride')`
|
||||
- Les guards globaux (`AuthGuard`, guards basés sur `Reflector`) sont les premiers à tomber
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Ne pas conclure trop vite à un problème réseau dès que le port écoute : traiter « le port écoute » et « les requêtes fonctionnent » comme **deux validations distinctes**.
|
||||
- Tester immédiatement `GET /`, l'endpoint OpenAPI et une route publique métier, puis lire la stack runtime **après le premier hit** (pas seulement les logs de bootstrap).
|
||||
- Si un lanceur `tsx watch` est utilisé avec NestJS, vérifier explicitement la compatibilité avec l'injection runtime ; en cas de doute, expliciter les injections critiques avec `@Inject(...)` sur guards et services exposés dès le premier hit.
|
||||
|
||||
- Contexte technique : NestJS / tsx watch / injection — app-alexandrie 01-04-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-writemodeguard-bloque-endpoints-support"></a>
|
||||
## Guard d'écriture en mode dégradé bloque les endpoints de support
|
||||
|
||||
### Risques
|
||||
|
||||
- Un guard global qui refuse toutes les écritures (`POST/PUT/PATCH/DELETE`) quand un service critique est down (ex : Redis) bloque aussi les endpoints dont l'utilité est **maximale en panne** : soumission de ticket de support, feedback, remontée de logs d'erreur client.
|
||||
- Cas typique : un endpoint de ticketing (`POST /support/...`) devient inaccessible précisément au moment où l'utilisateur veut signaler le dysfonctionnement qu'il subit.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Soumission de ticket de support impossible quand le service de quota/cache est indisponible
|
||||
- Le guard rejette des routes qui devraient rester ouvertes en mode dégradé
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
- Lors de l'ajout d'un guard d'écriture en mode dégradé, **recenser explicitement les endpoints critiques qui doivent rester disponibles** : support, feedback, logs d'erreur client.
|
||||
- Whitelister ces préfixes dès la création du guard — pas lors de la code review d'une story ultérieure. Exemple de whitelist : `/auth`, `/billing/.../webhooks`, `/support`.
|
||||
- **Why** : un service de ticketing bloqué quand le backend dysfonctionne est contre-productif — l'utilisateur ne peut pas signaler le problème qu'il est en train de subir.
|
||||
|
||||
- Contexte technique : NestJS / guard mode dégradé — app-alexandrie 01-04-2026
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
title: Backend — Risques & vigilance : Prisma
|
||||
domain: backend
|
||||
bucket: risques
|
||||
tags: [prisma, transactions, tenant, schema, race-condition]
|
||||
tags: [prisma, transactions, tenant, schema, race-condition, index, soft-delete, performance]
|
||||
applies_to: [implementation, review, debug, architecture]
|
||||
severity: high
|
||||
validated_on: 2026-04-07
|
||||
validated_on: 2026-06-25
|
||||
source_projects: [app-template-resto, app-alexandrie, RL799_V2]
|
||||
---
|
||||
|
||||
@@ -619,3 +619,33 @@ const resolveDbUrl = (): string | undefined => {
|
||||
**Règle générale** : toute stratégie template-based doit auditer le chemin du `DB_URL` à travers les sub-processes de bootstrap. Le bootstrap ouvre une connexion sur la template, mais le seed transitif exécuté via un sub-process peut être sujet à des transformations agressives du DSN qui le redirigent ailleurs.
|
||||
|
||||
- Contexte technique : Prisma / template database / Vitest — RL799_V2 01-05-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-index-partial-soft-delete-perf"></a>
|
||||
## Index partial sur colonnes soft-delete (nuance perf)
|
||||
|
||||
### Risques
|
||||
|
||||
- Un index plein sur une colonne soft-delete nullable (`deleted_at`) indexe **tous les NULL**, c'est-à-dire la quasi-totalité des lignes (les comptes actifs). L'index est volumineux et peu sélectif pour les requêtes qui ne ciblent que les lignes supprimées.
|
||||
- Distinct de l'index **unique** partiel déjà documenté (cf. `risque-prisma-unique-nullable`, qui traite de l'idempotence) : ici l'enjeu est purement la **performance / le coût d'indexation**.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- `CREATE INDEX ... ON ("deleted_at")` sans clause `WHERE` sur une table où >99 % des lignes ont `deleted_at IS NULL`
|
||||
- Index gros pour un bénéfice quasi nul sur les requêtes « lister les comptes supprimés »
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```sql
|
||||
-- ❌ Index plein — indexe la masse des NULL (lignes actives)
|
||||
CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at");
|
||||
|
||||
-- ✅ Index partial — n'indexe que les lignes effectivement supprimées
|
||||
CREATE INDEX "users_deleted_at_idx" ON "users"("deleted_at")
|
||||
WHERE "deleted_at" IS NOT NULL;
|
||||
```
|
||||
|
||||
- **Règle** : pour une colonne soft-delete nullable à majorité `NULL`, préférer un index partial `WHERE deleted_at IS NOT NULL`.
|
||||
|
||||
- Contexte technique : Prisma / PostgreSQL / index partial — app-alexandrie 13-04-2026
|
||||
|
||||
@@ -103,3 +103,41 @@ endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
|
||||
- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review
|
||||
|
||||
- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026
|
||||
|
||||
---
|
||||
|
||||
<a id="risque-compensation-incrby-non-atomique"></a>
|
||||
## Compensation `incrBy(-1)` non-atomique après dépassement de quota
|
||||
|
||||
### Risques
|
||||
|
||||
- Pattern courant de quota Redis : `INCR` → vérifier `> limit` → si oui, décrémenter (`incrBy(-1)`) et lever 429. La compensation n'est PAS atomique avec le check.
|
||||
- Si Redis devient indisponible (flap) entre l'incrément et la compensation, l'appel de décrément échoue silencieusement et retourne `null`. Le compteur reste incrémenté → quota fantôme jusqu'à l'expiration de la clé.
|
||||
|
||||
### Symptômes
|
||||
|
||||
- Utilisateur bloqué par le quota alors qu'il n'a pas atteint la limite réelle
|
||||
- Aucune erreur visible : la valeur de retour `null` du décrément n'est pas vérifiée
|
||||
- Comportement intermittent corrélé aux instabilités Redis
|
||||
|
||||
### Bonnes pratiques / mitigations
|
||||
|
||||
```typescript
|
||||
// ❌ Compensation non vérifiée — échec silencieux possible
|
||||
await redis.incr(key);
|
||||
if (current > limit) {
|
||||
await redis.incrBy(key, -1); // retourne null si Redis flap → compensation perdue
|
||||
throw quotaExceeded();
|
||||
}
|
||||
|
||||
// ✅ Vérifier le retour de la compensation et loguer
|
||||
const compensated = await redis.incrBy(key, -1);
|
||||
if (compensated === null) {
|
||||
logger.error({ type: 'quota', event: 'compensation_failed', key });
|
||||
}
|
||||
```
|
||||
|
||||
- **Règle** : toujours vérifier le retour du décrément de compensation et loguer explicitement si `null`. Documenter ce choix dans les Dev Notes de la story (comportement intentionnel vs bug).
|
||||
- **Solution robuste** : encapsuler incrément + compensation conditionnelle dans le même pipeline `MULTI/EXEC` ou un script Lua (atomicité garantie), au prix d'une complexité plus élevée.
|
||||
|
||||
- Contexte technique : Redis / NestJS — app-alexandrie 01-04-2026
|
||||
|
||||
Reference in New Issue
Block a user