@nodefony/framework 10.0.0-alpha.8 → 10.0.0-alpha.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.150.0 → _@oxc-project_runtime@0.151.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +5 -4
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +2 -2
- package/dist/nodefony/service/router.js +2 -2
- package/dist/nodefony/src/KernelAdminApi.js +12 -7
- package/docs/admin.md +25 -25
- package/docs/controller.md +44 -44
- package/docs/decorateurs.md +79 -79
- package/docs/idempotence.md +70 -65
- package/docs/routing.md +40 -40
- package/docs/templates.md +7 -7
- package/package.json +9 -9
package/docs/decorateurs.md
CHANGED
|
@@ -49,10 +49,10 @@ flowchart TD
|
|
|
49
49
|
|
|
50
50
|
1. **À l'import**, chaque décorateur appelle `Reflect.defineMetadata` et rend la main. Zéro route
|
|
51
51
|
créée, zéro service résolu.
|
|
52
|
-
2. **Au montage**, `controller()` (`routerDecorators.ts:
|
|
53
|
-
objets `Route` ; `controllers()` (`routerDecorators.ts:
|
|
52
|
+
2. **Au montage**, `controller()` (`routerDecorators.ts:189`) relit ces métadonnées et fabrique les
|
|
53
|
+
objets `Route` ; `controllers()` (`routerDecorators.ts:111`) accroche le contrôleur au module sur
|
|
54
54
|
le hook `onBoot` du kernel.
|
|
55
|
-
3. **À la première requête** de chaque route, `resolveActionMeta()` (`routerDecorators.ts:
|
|
55
|
+
3. **À la première requête** de chaque route, `resolveActionMeta()` (`routerDecorators.ts:1639`)
|
|
56
56
|
consolide toutes les étiquettes de l'action en **un objet figé** posé sur la route. Les requêtes
|
|
57
57
|
suivantes ne lisent plus aucune métadonnée.
|
|
58
58
|
|
|
@@ -107,7 +107,7 @@ Nodefony utilise le format **legacy** de TypeScript : `experimentalDecorators: t
|
|
|
107
107
|
- les **décorateurs de paramètre** (`@Param`, `@Body`…) — le format standard ES ne les propose pas ;
|
|
108
108
|
- l'**injection par type** du conteneur : `emitDecoratorMetadata` fait émettre au compilateur la
|
|
109
109
|
liste des types du constructeur sous la clé `design:paramtypes`, que l'injecteur relit pour
|
|
110
|
-
résoudre les dépendances sans les nommer (cf `injectable()`, `kernelDecorator.ts:
|
|
110
|
+
résoudre les dépendances sans les nommer (cf `injectable()`, `kernelDecorator.ts:135`).
|
|
111
111
|
|
|
112
112
|
Concrètement, dans une app générée par `nodefony create app`, ces deux options sont **déjà** dans le
|
|
113
113
|
`tsconfig.json`. Tu n'as rien à faire — sauf si tu pars d'un `tsconfig` à toi : sans elles, les
|
|
@@ -124,16 +124,16 @@ Trois partis pris expliquent la forme de cette surface, et un développeur qui l
|
|
|
124
124
|
jamais surprendre.
|
|
125
125
|
|
|
126
126
|
**1 — Un décorateur n'écrit QUE des métadonnées.** Aucun décorateur du framework ne contient de
|
|
127
|
-
logique de sécurité, de session ou d'idempotence. `IsGranted()` (`routerDecorators.ts:
|
|
127
|
+
logique de sécurité, de session ou d'idempotence. `IsGranted()` (`routerDecorators.ts:878`) pose une
|
|
128
128
|
clause ; c'est le `Resolver` qui appellera le moteur d'autorisation, **résolu par son nom** dans le
|
|
129
|
-
conteneur (`Resolver._enforceSecurity()`, `Resolver.ts:
|
|
129
|
+
conteneur (`Resolver._enforceSecurity()`, `Resolver.ts:672`). Pourquoi ce détour : `@nodefony/framework`
|
|
130
130
|
ne dépend **pas** de `@nodefony/security` — sans ça, les deux modules formeraient un cycle. Le prix à
|
|
131
131
|
payer est visible : une route gardée alors que le module `security` est absent renvoie **403**, pas
|
|
132
|
-
une erreur de démarrage (fail-closed, `Resolver.ts:
|
|
132
|
+
une erreur de démarrage (fail-closed, `Resolver.ts:621`).
|
|
133
133
|
|
|
134
134
|
**2 — Tout est figé une fois, puis relu en O(1).** Les métadonnées de l'action sont consolidées au
|
|
135
|
-
premier passage dans `computeActionMeta()` (`routerDecorators.ts:
|
|
136
|
-
L'objet `RouteActionMeta` (`routerDecorators.ts:
|
|
135
|
+
premier passage dans `computeActionMeta()` (`routerDecorators.ts:1595`) puis gelées sur la route.
|
|
136
|
+
L'objet `RouteActionMeta` (`routerDecorators.ts:1406`) est **partagé par toutes les requêtes** — le
|
|
137
137
|
framework ne le mute jamais, et ton code non plus. Une action non décorée obtient des champs à `null`,
|
|
138
138
|
ce qui vaut **zéro branche** dans le chemin chaud.
|
|
139
139
|
|
|
@@ -284,11 +284,11 @@ Six familles, **36 décorateurs**, un seul fichier source. Le tableau de synthè
|
|
|
284
284
|
<!-- prettier-ignore -->
|
|
285
285
|
| Décorateur | Cible | Effet | Exemple |
|
|
286
286
|
| --- | --- | --- | --- |
|
|
287
|
-
| `@controllers([…])` | **module** | Rattache des contrôleurs au module sur le hook `onBoot` ; sans lui, aucune route n'est servie (`controllers()`, `routerDecorators.ts:
|
|
288
|
-
| `@controller("/prefix")` | **classe** | Pose le préfixe d'URL **et déclenche la création des routes** de la classe (`controller()`, `routerDecorators.ts:
|
|
289
|
-
| `@route(nom, options)` | méthode | Forme complète : nom explicite, chemin, `requirements`, `defaults`, hôte (`route()`, `routerDecorators.ts:
|
|
290
|
-
| `@Domain(motif \| motifs)` | **dual** | Restreint la route (ou la classe) à un ou plusieurs vhosts ; hors domaine → **403** (`Domain()`, `routerDecorators.ts:
|
|
291
|
-
| `@Scope("singleton")` | **classe** | Une seule instance de contrôleur partagée par toutes les requêtes (`Scope()`, `routerDecorators.ts:
|
|
287
|
+
| `@controllers([…])` | **module** | Rattache des contrôleurs au module sur le hook `onBoot` ; sans lui, aucune route n'est servie (`controllers()`, `routerDecorators.ts:111`) | `@controllers([BookController])` |
|
|
288
|
+
| `@controller("/prefix")` | **classe** | Pose le préfixe d'URL **et déclenche la création des routes** de la classe (`controller()`, `routerDecorators.ts:189`) | `@controller("/api/books")` |
|
|
289
|
+
| `@route(nom, options)` | méthode | Forme complète : nom explicite, chemin, `requirements`, `defaults`, hôte (`route()`, `routerDecorators.ts:271`) | `@route("ws-echo", { path: "/echo", requirements: { methods: ["WEBSOCKET"] } })` |
|
|
290
|
+
| `@Domain(motif \| motifs)` | **dual** | Restreint la route (ou la classe) à un ou plusieurs vhosts ; hors domaine → **403** (`Domain()`, `routerDecorators.ts:664`) | `@Domain("*.cdn.example.com")` |
|
|
291
|
+
| `@Scope("singleton")` | **classe** | Une seule instance de contrôleur partagée par toutes les requêtes (`Scope()`, `routerDecorators.ts:768`) | `@Scope("singleton")` |
|
|
292
292
|
|
|
293
293
|
**`@controller` est le déclencheur.** Il relit les métadonnées posées par `@route`/`@Get`/… puis les
|
|
294
294
|
**efface** (`Reflect.deleteMetadata`, `routerDecorators.ts:135`) : une classe ne se monte qu'une
|
|
@@ -307,19 +307,19 @@ instance par requête (`ControllerScope`, `Controller.ts:144`).
|
|
|
307
307
|
|
|
308
308
|
### Méthodes HTTP
|
|
309
309
|
|
|
310
|
-
Toutes les fabriques sortent du même moule, `httpMethodDecorator()` (`routerDecorators.ts:
|
|
310
|
+
Toutes les fabriques sortent du même moule, `httpMethodDecorator()` (`routerDecorators.ts:469`) :
|
|
311
311
|
elles nomment la route automatiquement `ClasseName::methode` et posent `requirements.methods`.
|
|
312
312
|
|
|
313
313
|
| Décorateur | Méthode filtrée | Ancre | Exemple |
|
|
314
314
|
| ------------------------ | --------------- | ------------------------------------- | ------------------- |
|
|
315
|
-
| `@Get(path?, opts?)` | `GET` | `Get` (`routerDecorators.ts:
|
|
316
|
-
| `@Post(path?, opts?)` | `POST` | `Post` (`routerDecorators.ts:
|
|
317
|
-
| `@Put(path?, opts?)` | `PUT` | `Put` (`routerDecorators.ts:
|
|
318
|
-
| `@Delete(path?, opts?)` | `DELETE` | `Delete` (`routerDecorators.ts:
|
|
319
|
-
| `@Patch(path?, opts?)` | `PATCH` | `Patch` (`routerDecorators.ts:
|
|
320
|
-
| `@Options(path?, opts?)` | `OPTIONS` | `Options` (`routerDecorators.ts:
|
|
321
|
-
| `@Head(path?, opts?)` | `HEAD` | `Head` (`routerDecorators.ts:
|
|
322
|
-
| `@All(path?, opts?)` | **aucune** | `All()` (`routerDecorators.ts:
|
|
315
|
+
| `@Get(path?, opts?)` | `GET` | `Get` (`routerDecorators.ts:515`) | `@Get("/{id}")` |
|
|
316
|
+
| `@Post(path?, opts?)` | `POST` | `Post` (`routerDecorators.ts:516`) | `@Post("")` |
|
|
317
|
+
| `@Put(path?, opts?)` | `PUT` | `Put` (`routerDecorators.ts:517`) | `@Put("/{id}")` |
|
|
318
|
+
| `@Delete(path?, opts?)` | `DELETE` | `Delete` (`routerDecorators.ts:518`) | `@Delete("/{id}")` |
|
|
319
|
+
| `@Patch(path?, opts?)` | `PATCH` | `Patch` (`routerDecorators.ts:519`) | `@Patch("/{id}")` |
|
|
320
|
+
| `@Options(path?, opts?)` | `OPTIONS` | `Options` (`routerDecorators.ts:520`) | `@Options("/{id}")` |
|
|
321
|
+
| `@Head(path?, opts?)` | `HEAD` | `Head` (`routerDecorators.ts:521`) | `@Head("/{id}")` |
|
|
322
|
+
| `@All(path?, opts?)` | **aucune** | `All()` (`routerDecorators.ts:528`) | `@All("/proxy/*")` |
|
|
323
323
|
|
|
324
324
|
Deux points qu'un dev découvre sinon à ses dépens :
|
|
325
325
|
|
|
@@ -332,7 +332,7 @@ Deux points qu'un dev découvre sinon à ses dépens :
|
|
|
332
332
|
|
|
333
333
|
Le second argument accepte les options de route non redondantes — `Omit<RouteOptions, "path" | "method">`
|
|
334
334
|
(`routerDecorators.ts:338`), soit `defaults`, `requirements`, `host`, `bypassFirewall`
|
|
335
|
-
(`RouteOptions`, `Route.ts:
|
|
335
|
+
(`RouteOptions`, `Route.ts:156`) :
|
|
336
336
|
|
|
337
337
|
```typescript
|
|
338
338
|
@Get("/{page}", { defaults: { page: "1" }, requirements: { scheme: "https" } })
|
|
@@ -341,24 +341,24 @@ async index(@Param("page") page: string) { /* … */ }
|
|
|
341
341
|
|
|
342
342
|
### Paramètres — ce que l'action reçoit
|
|
343
343
|
|
|
344
|
-
Onze décorateurs, tous produits par `paramDecoratorFactory()` (`routerDecorators.ts:
|
|
344
|
+
Onze décorateurs, tous produits par `paramDecoratorFactory()` (`routerDecorators.ts:1162`) sauf
|
|
345
345
|
`@Body`, qui accepte une option supplémentaire. Chacun pose `{ source, key, index }` ; la valeur est
|
|
346
|
-
calculée par `resolveParamArg()` (`routerDecorators.ts:
|
|
346
|
+
calculée par `resolveParamArg()` (`routerDecorators.ts:1297`), une fonction **pure** — ce qui la rend
|
|
347
347
|
testable sans démarrer de serveur.
|
|
348
348
|
|
|
349
349
|
| Décorateur | Sans clé renvoie… | Avec clé renvoie… | Ancre |
|
|
350
350
|
| ------------------- | ------------------------------------ | ------------------------------------------ | -------------------------------------------- |
|
|
351
|
-
| `@Param("id")` | toutes les variables d'URL (objet) | la variable d'URL nommée | `Param` (`routerDecorators.ts:
|
|
352
|
-
| `@Query("q")` | toute la query string | un paramètre de la query string | `Query` (`routerDecorators.ts:
|
|
351
|
+
| `@Param("id")` | toutes les variables d'URL (objet) | la variable d'URL nommée | `Param` (`routerDecorators.ts:1182`) |
|
|
352
|
+
| `@Query("q")` | toute la query string | un paramètre de la query string | `Query` (`routerDecorators.ts:1183`) |
|
|
353
353
|
| `@Body("field")` | le corps parsé entier | un champ du corps parsé | `Body()` (`routerDecorators.ts:1209`) |
|
|
354
|
-
| `@Headers("x-foo")` | tous les en-têtes de requête | un en-tête (**lookup en minuscules**) | `Headers` (`routerDecorators.ts:
|
|
355
|
-
| `@Cookie("sid")` | la map des cookies | un cookie (objet `Cookie`, champ `.value`) | `Cookie` (`routerDecorators.ts:
|
|
356
|
-
| `@Session("user")` | l'objet `Session` vivant | `session.get(clé)` | `Session` (`routerDecorators.ts:
|
|
357
|
-
| `@CurrentUser()` | l'utilisateur résolu par le firewall | — | `CurrentUser` (`routerDecorators.ts:
|
|
358
|
-
| `@Req()` | la requête brute du contexte | — | `Req` (`routerDecorators.ts:
|
|
359
|
-
| `@Res()` | la réponse du contexte | — | `Res` (`routerDecorators.ts:
|
|
360
|
-
| `@UploadedFile()` | le **premier** fichier téléversé | — | `UploadedFile` (`routerDecorators.ts:
|
|
361
|
-
| `@UploadedFiles()` | tous les fichiers téléversés | — | `UploadedFiles` (`routerDecorators.ts:
|
|
354
|
+
| `@Headers("x-foo")` | tous les en-têtes de requête | un en-tête (**lookup en minuscules**) | `Headers` (`routerDecorators.ts:1246`) |
|
|
355
|
+
| `@Cookie("sid")` | la map des cookies | un cookie (objet `Cookie`, champ `.value`) | `Cookie` (`routerDecorators.ts:1247`) |
|
|
356
|
+
| `@Session("user")` | l'objet `Session` vivant | `session.get(clé)` | `Session` (`routerDecorators.ts:1248`) |
|
|
357
|
+
| `@CurrentUser()` | l'utilisateur résolu par le firewall | — | `CurrentUser` (`routerDecorators.ts:1250`) |
|
|
358
|
+
| `@Req()` | la requête brute du contexte | — | `Req` (`routerDecorators.ts:1251`) |
|
|
359
|
+
| `@Res()` | la réponse du contexte | — | `Res` (`routerDecorators.ts:1252`) |
|
|
360
|
+
| `@UploadedFile()` | le **premier** fichier téléversé | — | `UploadedFile` (`routerDecorators.ts:1253`) |
|
|
361
|
+
| `@UploadedFiles()` | tous les fichiers téléversés | — | `UploadedFiles` (`routerDecorators.ts:1254`) |
|
|
362
362
|
|
|
363
363
|
La liste des sources possibles est fermée et typée : `ParamSource` (`routerDecorators.ts:365`).
|
|
364
364
|
|
|
@@ -370,13 +370,13 @@ justificatif (mot de passe, jeton). Hors zone authentifiée, la valeur est `unde
|
|
|
370
370
|
n'authentifie rien, il expose ce qui a déjà été prouvé.
|
|
371
371
|
|
|
372
372
|
**`@Session` active la session à lui seul.** La simple présence d'un paramètre `@Session` vaut
|
|
373
|
-
déclaration d'intention : `resolveSessionIntent()` (`routerDecorators.ts:
|
|
373
|
+
déclaration d'intention : `resolveSessionIntent()` (`routerDecorators.ts:833`) la détecte et pose
|
|
374
374
|
l'intent, exactement comme `@UseSession()`. Une route sans l'un ni l'autre ne paie aucune session.
|
|
375
375
|
|
|
376
376
|
**`@Body({ stream: true })` court-circuite le parsing.** Pour un gros téléversement (vidéo,
|
|
377
377
|
sauvegarde), on injecte le **flux brut** de la requête au lieu du corps chargé en mémoire ; le
|
|
378
378
|
pipeline saute alors le parsing pour cette route, décision prise en amont par
|
|
379
|
-
`routeExpectsBodyStream()` (`routerDecorators.ts:
|
|
379
|
+
`routeExpectsBodyStream()` (`routerDecorators.ts:1379`) :
|
|
380
380
|
|
|
381
381
|
```typescript
|
|
382
382
|
@Post("/upload")
|
|
@@ -388,7 +388,7 @@ async upload(@Body({ stream: true }) stream: NodeJS.ReadableStream) {
|
|
|
388
388
|
|
|
389
389
|
> [!TIP]
|
|
390
390
|
> L'ordre d'écriture des paramètres décorés n'a aucune importance : chaque valeur est placée à son
|
|
391
|
-
> **index déclaré** par `buildParamArgs()` (`routerDecorators.ts:
|
|
391
|
+
> **index déclaré** par `buildParamArgs()` (`routerDecorators.ts:1361`), et les trous restent
|
|
392
392
|
> `undefined`. Tu peux mélanger décorés et non décorés — les non décorés reçoivent `undefined`.
|
|
393
393
|
|
|
394
394
|
#### Le corps n'est pas validé — et c'est un choix
|
|
@@ -453,14 +453,14 @@ ne s'auto-promeut pas.
|
|
|
453
453
|
| ------------------------- | ------- | ------------------------------------------------------------------------------------------ | ------------------------------------- |
|
|
454
454
|
| `@HttpCode(201)` | méthode | Fixe le statut **avant** l'exécution de l'action (`HttpCode()`, `routerDecorators.ts:548`) | `@HttpCode(204)` |
|
|
455
455
|
| `@Header("X-Foo", "bar")` | méthode | Ajoute un en-tête ; **s'empile** (plusieurs `@Header` cumulent, `routerDecorators.ts:580`) | `@Header("Cache-Control","no-store")` |
|
|
456
|
-
| `@Redirect("/url", 302)` | méthode | Redirige **si** l'action ne renvoie rien (`Redirect()`, `routerDecorators.ts:
|
|
456
|
+
| `@Redirect("/url", 302)` | méthode | Redirige **si** l'action ne renvoie rien (`Redirect()`, `routerDecorators.ts:628`) | `@Redirect("/login", 302)` |
|
|
457
457
|
|
|
458
|
-
Les deux premiers sont appliqués par `Resolver._applyResponseMeta()` (`Resolver.ts:
|
|
458
|
+
Les deux premiers sont appliqués par `Resolver._applyResponseMeta()` (`Resolver.ts:780`) **avant**
|
|
459
459
|
l'appel de l'action : ton code peut donc les écraser ensuite (`this.renderJson(data, 202)` gagne).
|
|
460
460
|
|
|
461
461
|
`@Redirect` a une subtilité utile : si l'action **retourne un objet** portant `url` (et
|
|
462
462
|
éventuellement `statusCode`), cet objet **prend le dessus** sur les valeurs du décorateur
|
|
463
|
-
(`Resolver._handleRedirect()`, `Resolver.ts:
|
|
463
|
+
(`Resolver._handleRedirect()`, `Resolver.ts:796`) — la cible peut donc être calculée à l'exécution :
|
|
464
464
|
|
|
465
465
|
```typescript
|
|
466
466
|
@Get("/go")
|
|
@@ -482,18 +482,18 @@ Sept décorateurs, **tous duals** (classe ou méthode) et **tous sans logique**
|
|
|
482
482
|
|
|
483
483
|
| Décorateur | Effet | Ancre |
|
|
484
484
|
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
|
|
485
|
-
| `@IsGranted(attr \| attrs, { subject })` | Exige un attribut (rôle `ROLE_*` ou règle métier). Tableau = **OU** ; empilés = **ET** ; refus → **403** | `IsGranted()` (`routerDecorators.ts:
|
|
486
|
-
| `@RequireScope(scope \| scopes)` | Exige un scope `api:action` d'un **jeton machine** ; no-op pour une session humaine | `RequireScope()` (`routerDecorators.ts:
|
|
487
|
-
| `@Anonymous()` | Rend l'action publique : annule l'autorisation **et** l'authentification (le « permitAll ») | `Anonymous()` (`routerDecorators.ts:
|
|
488
|
-
| `@BypassFirewall` | Court-circuite le firewall (sonde de liveness, webhook signé, endpoint de login). **Sans parenthèses** | `BypassFirewall` (`routerDecorators.ts:
|
|
489
|
-
| `@Csp({ "frame-src": [...] })` | Ajoute des directives CSP **à cette réponse** ; classe + méthode fusionnent additivement | `Csp()` (`routerDecorators.ts:
|
|
490
|
-
| `@CsrfProtect()` | Opt-**in** au jeton anti-CSRF (double-submit signé) en plus de la défense globale | `CsrfProtect` (`routerDecorators.ts:
|
|
491
|
-
| `@CsrfExempt()` | Opt-**out** de la défense CSRF **en gardant** l'authentification (webhook, POST cross-origin légitime) | `CsrfExempt` (`routerDecorators.ts:
|
|
485
|
+
| `@IsGranted(attr \| attrs, { subject })` | Exige un attribut (rôle `ROLE_*` ou règle métier). Tableau = **OU** ; empilés = **ET** ; refus → **403** | `IsGranted()` (`routerDecorators.ts:878`) |
|
|
486
|
+
| `@RequireScope(scope \| scopes)` | Exige un scope `api:action` d'un **jeton machine** ; no-op pour une session humaine | `RequireScope()` (`routerDecorators.ts:975`) |
|
|
487
|
+
| `@Anonymous()` | Rend l'action publique : annule l'autorisation **et** l'authentification (le « permitAll ») | `Anonymous()` (`routerDecorators.ts:926`) |
|
|
488
|
+
| `@BypassFirewall` | Court-circuite le firewall (sonde de liveness, webhook signé, endpoint de login). **Sans parenthèses** | `BypassFirewall` (`routerDecorators.ts:725`) |
|
|
489
|
+
| `@Csp({ "frame-src": [...] })` | Ajoute des directives CSP **à cette réponse** ; classe + méthode fusionnent additivement | `Csp()` (`routerDecorators.ts:1040`) |
|
|
490
|
+
| `@CsrfProtect()` | Opt-**in** au jeton anti-CSRF (double-submit signé) en plus de la défense globale | `CsrfProtect` (`routerDecorators.ts:1104`) |
|
|
491
|
+
| `@CsrfExempt()` | Opt-**out** de la défense CSRF **en gardant** l'authentification (webhook, POST cross-origin légitime) | `CsrfExempt` (`routerDecorators.ts:1113`) |
|
|
492
492
|
|
|
493
493
|
#### Rôles et scopes — deux axes, un seul verdict
|
|
494
494
|
|
|
495
495
|
`@IsGranted` et `@RequireScope` écrivent dans **deux jeux de métadonnées distincts**, puis
|
|
496
|
-
`computeSecurityRequirement()` (`routerDecorators.ts:
|
|
496
|
+
`computeSecurityRequirement()` (`routerDecorators.ts:1514`) les fusionne en une exigence unique dont
|
|
497
497
|
toutes les clauses sont en **ET**. Une seule chaîne d'application côté `Resolver`, deux jurés
|
|
498
498
|
différents côté `security` (le voteur de rôles, le voteur de scopes).
|
|
499
499
|
|
|
@@ -529,7 +529,7 @@ Trois façons d'ouvrir une porte, trois portées — les confondre coûte cher :
|
|
|
529
529
|
| `@CsrfExempt()` | **conservée** | **conservée** | ignorée | webhook signé, API cross-origin |
|
|
530
530
|
|
|
531
531
|
`@Anonymous()` pose en réalité **deux** marqueurs : « pas d'autorisation » et « pas de firewall »
|
|
532
|
-
(`routerDecorators.ts:
|
|
532
|
+
(`routerDecorators.ts:926-941`) — c'est un `@BypassFirewall` doublé d'une annulation des clauses
|
|
533
533
|
héritées de la classe.
|
|
534
534
|
|
|
535
535
|
> [!CAUTION]
|
|
@@ -542,8 +542,8 @@ héritées de la classe.
|
|
|
542
542
|
|
|
543
543
|
| Décorateur | Cible | Effet | Ancre |
|
|
544
544
|
| ------------------------------------ | ----- | ---------------------------------------------------------------------------------------------------------- | --------------------------------- |
|
|
545
|
-
| `@UseSession({ readOnly?, eager? })` | dual | Déclare le besoin d'une session serveur ; **méthode > classe** (`UseSession()`, `routerDecorators.ts:
|
|
546
|
-
| `@Idempotent({ required? })` | dual | Protège une mutation du double effet via `Idempotency-Key` (`Idempotent()`, `routerDecorators.ts:
|
|
545
|
+
| `@UseSession({ readOnly?, eager? })` | dual | Déclare le besoin d'une session serveur ; **méthode > classe** (`UseSession()`, `routerDecorators.ts:800`) | `@UseSession({ readOnly: true })` |
|
|
546
|
+
| `@Idempotent({ required? })` | dual | Protège une mutation du double effet via `Idempotency-Key` (`Idempotent()`, `routerDecorators.ts:1142`) | `@Idempotent()` |
|
|
547
547
|
|
|
548
548
|
**`@UseSession` est la seule façon d'ouvrir une session** (avec un paramètre `@Session`, ou la reprise
|
|
549
549
|
d'un cookie existant). Il n'existe plus de « démarrer partout » global : une route qui ne déclare rien
|
|
@@ -564,13 +564,13 @@ recherches inutiles.
|
|
|
564
564
|
|
|
565
565
|
| Décorateur | Paquet | Rôle |
|
|
566
566
|
| ----------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------- |
|
|
567
|
-
| `@services([…])` | `nodefony` | Déclare les services d'un module (`services()`, `kernelDecorator.ts:
|
|
567
|
+
| `@services([…])` | `nodefony` | Déclare les services d'un module (`services()`, `kernelDecorator.ts:53`) |
|
|
568
568
|
| `@injectable()` | `nodefony` | Rend une classe résoluble par le conteneur (`injectable()`, `kernelDecorator.ts:135`) |
|
|
569
|
-
| `@inject("nom")` | `nodefony` | Injecte un service à une position de constructeur (`inject()`, `kernelDecorator.ts:
|
|
570
|
-
| `@Inject("nom")` | `nodefony` | Idem, sur une propriété (`Inject()`, `kernelDecorator.ts:
|
|
569
|
+
| `@inject("nom")` | `nodefony` | Injecte un service à une position de constructeur (`inject()`, `kernelDecorator.ts:167`) |
|
|
570
|
+
| `@Inject("nom")` | `nodefony` | Idem, sur une propriété (`Inject()`, `kernelDecorator.ts:196`) |
|
|
571
571
|
| `@RealtimeAction("méthode")` | `@nodefony/realtime` | Expose une action JSON-RPC sur socket (`RealtimeAction()`, `realtimeDecorators.ts:101`) |
|
|
572
|
-
| `@RealtimeChannel("canal", policy)` | `@nodefony/realtime` | Déclare un canal temps réel et sa politique (`RealtimeChannel()`, `realtimeDecorators.ts:
|
|
573
|
-
| `@RealtimeInbound("méthode")` | `@nodefony/realtime` | Traite un message entrant typé (`RealtimeInbound()`, `realtimeDecorators.ts:
|
|
572
|
+
| `@RealtimeChannel("canal", policy)` | `@nodefony/realtime` | Déclare un canal temps réel et sa politique (`RealtimeChannel()`, `realtimeDecorators.ts:192`) |
|
|
573
|
+
| `@RealtimeInbound("méthode")` | `@nodefony/realtime` | Traite un message entrant typé (`RealtimeInbound()`, `realtimeDecorators.ts:231`) |
|
|
574
574
|
|
|
575
575
|
Injection et portées → [injection-portees](../../../../../docs/architecture/injection-portees.md) ·
|
|
576
576
|
socket → [realtime](../../realtime/docs/index.md).
|
|
@@ -585,7 +585,7 @@ socket → [realtime](../../realtime/docs/index.md).
|
|
|
585
585
|
## 🔌 HTTP et WebSocket — les mêmes décorateurs
|
|
586
586
|
|
|
587
587
|
Un contrôleur ne change pas de forme selon le transport : ce sont les `requirements.methods` qui
|
|
588
|
-
déclarent le canal, `WEBSOCKET` étant une pseudo-méthode du type `HTTPMethod` (`Context.ts:
|
|
588
|
+
déclarent le canal, `WEBSOCKET` étant une pseudo-méthode du type `HTTPMethod` (`Context.ts:104`).
|
|
589
589
|
|
|
590
590
|
```typescript
|
|
591
591
|
@controller("/ws/chat")
|
|
@@ -616,11 +616,11 @@ Trois faits à retenir :
|
|
|
616
616
|
- **Il n'existe pas de décorateur `@Ws`.** Le transport se déclare dans les `requirements` — via
|
|
617
617
|
`@route`, ou via `@All("/x", { requirements: { methods: ["WEBSOCKET"] } })` si tu préfères la forme
|
|
618
618
|
courte (les fabriques `@Get`/`@Post` écrasent, elles, `requirements.methods` par leur propre méthode,
|
|
619
|
-
`routerDecorators.ts:
|
|
619
|
+
`routerDecorators.ts:508`).
|
|
620
620
|
- **Les décorateurs de paramètre fonctionnent pareil.** Pour une invocation par socket, le corps de
|
|
621
621
|
la mutation voyage dans l'ALS et **prime** sur le corps HTTP (vide dans ce cas) — c'est traité dans
|
|
622
|
-
`resolveParamArg()` (`routerDecorators.ts:
|
|
623
|
-
pas celle du handshake (`Resolver._buildParamArgs()`, `Resolver.ts:
|
|
622
|
+
`resolveParamArg()` (`routerDecorators.ts:1297`), et `@Query` lit la query du chemin **invoqué**,
|
|
623
|
+
pas celle du handshake (`Resolver._buildParamArgs()`, `Resolver.ts:749`).
|
|
624
624
|
- **Les gardes s'appliquent identiquement.** `@IsGranted` protège une action joignable par socket
|
|
625
625
|
exactement comme une action HTTP : la décision est prise avant l'instanciation, quel que soit le
|
|
626
626
|
transport.
|
|
@@ -636,14 +636,14 @@ pas toutes identiques — c'est la source d'erreur n°1.
|
|
|
636
636
|
<!-- prettier-ignore -->
|
|
637
637
|
| Sujet | Règle | Ancre |
|
|
638
638
|
| --- | --- | --- |
|
|
639
|
-
| `@Domain` | option `host` de la route > méthode > classe | `controller()` (`routerDecorators.ts:
|
|
639
|
+
| `@Domain` | option `host` de la route > méthode > classe | `controller()` (`routerDecorators.ts:189`) |
|
|
640
640
|
| `@BypassFirewall` | **cumulatif** : `true` de la route, de la méthode ou de la classe suffit | `routerDecorators.ts:686` |
|
|
641
|
-
| `@UseSession` | méthode > classe (fusion des champs) | `resolveSessionIntent()` (`routerDecorators.ts:
|
|
642
|
-
| `@Idempotent` | méthode > classe | `computeIdempotent()` (`routerDecorators.ts:
|
|
643
|
-
| `@IsGranted` / `@RequireScope` | **cumul en ET** : classe **plus** méthode | `computeSecurityRequirement()` (`routerDecorators.ts:
|
|
644
|
-
| `@Anonymous` | méthode → annule tout ce que la classe a posé | `routerDecorators.ts:
|
|
645
|
-
| `@Csp` | fusion **additive** classe + méthode (sources concaténées) | `mergeCspDirectives()` (`routerDecorators.ts:
|
|
646
|
-
| `@CsrfProtect` / `@CsrfExempt` | OU logique : classe **ou** méthode suffit | `computeActionMeta()` (`routerDecorators.ts:
|
|
641
|
+
| `@UseSession` | méthode > classe (fusion des champs) | `resolveSessionIntent()` (`routerDecorators.ts:833`) |
|
|
642
|
+
| `@Idempotent` | méthode > classe | `computeIdempotent()` (`routerDecorators.ts:1576`) |
|
|
643
|
+
| `@IsGranted` / `@RequireScope` | **cumul en ET** : classe **plus** méthode | `computeSecurityRequirement()` (`routerDecorators.ts:1514`) |
|
|
644
|
+
| `@Anonymous` | méthode → annule tout ce que la classe a posé | `routerDecorators.ts:926` |
|
|
645
|
+
| `@Csp` | fusion **additive** classe + méthode (sources concaténées) | `mergeCspDirectives()` (`routerDecorators.ts:1014`) |
|
|
646
|
+
| `@CsrfProtect` / `@CsrfExempt` | OU logique : classe **ou** méthode suffit | `computeActionMeta()` (`routerDecorators.ts:1595`) |
|
|
647
647
|
| `@Header` | s'empile (plusieurs en-têtes) ; même clé → dernier écrit gagne | `Header()` (`routerDecorators.ts:580`) |
|
|
648
648
|
| `@HttpCode` | un seul par action (le dernier posé écrase) | `HttpCode()` (`routerDecorators.ts:548`) |
|
|
649
649
|
|
|
@@ -688,7 +688,7 @@ sequenceDiagram
|
|
|
688
688
|
RS->>RS: requêtes suivantes : lecture O(1), 0 Reflect
|
|
689
689
|
```
|
|
690
690
|
|
|
691
|
-
Le snapshot `RouteActionMeta` (`routerDecorators.ts:
|
|
691
|
+
Le snapshot `RouteActionMeta` (`routerDecorators.ts:1406`) regroupe **tout** ce que les décorateurs
|
|
692
692
|
ont dit de l'action :
|
|
693
693
|
|
|
694
694
|
<!-- prettier-ignore -->
|
|
@@ -706,12 +706,12 @@ ont dit de l'action :
|
|
|
706
706
|
|
|
707
707
|
Le `Resolver` consomme ce snapshot dans un ordre qui a du sens sécurité :
|
|
708
708
|
**garde d'abord, instanciation ensuite**. `security !== null` déclenche
|
|
709
|
-
`_enforceSecurity()` (`Resolver.ts:
|
|
709
|
+
`_enforceSecurity()` (`Resolver.ts:672`) **avant** `newController()` — un `403` n'instancie pas le
|
|
710
710
|
contrôleur et n'exécute pas son `initialize()`. Puis viennent les arguments
|
|
711
|
-
(`_buildParamArgs()`, `Resolver.ts:
|
|
712
|
-
(`_applyResponseMeta()`, `Resolver.ts:
|
|
711
|
+
(`_buildParamArgs()`, `Resolver.ts:749`), les métadonnées de réponse
|
|
712
|
+
(`_applyResponseMeta()`, `Resolver.ts:780`), l'action, et enfin la redirection éventuelle.
|
|
713
713
|
|
|
714
|
-
Un usage cold path mérite d'être connu : `extractActionScopes()` (`routerDecorators.ts:
|
|
714
|
+
Un usage cold path mérite d'être connu : `extractActionScopes()` (`routerDecorators.ts:1490`) parcourt
|
|
715
715
|
les routes au démarrage pour bâtir le **catalogue des scopes déclarés** — le formulaire de création
|
|
716
716
|
de clés API dans Studio propose les scopes réellement utilisés par le code, jamais une liste
|
|
717
717
|
maintenue à part.
|
|
@@ -720,10 +720,10 @@ maintenue à part.
|
|
|
720
720
|
|
|
721
721
|
Un décorateur non employé doit coûter **zéro**. C'est tenu par trois mécanismes vérifiables :
|
|
722
722
|
|
|
723
|
-
- **Lecture unique.** `resolveActionMeta()` (`routerDecorators.ts:
|
|
723
|
+
- **Lecture unique.** `resolveActionMeta()` (`routerDecorators.ts:1639`) mémorise le snapshot sur la
|
|
724
724
|
route au premier passage — ensuite, plus aucun appel `Reflect.getMetadata` ni `Object.entries` par
|
|
725
725
|
requête. Le même schéma vaut pour la détection du flux brut
|
|
726
|
-
(`routeExpectsBodyStream()`, `routerDecorators.ts:
|
|
726
|
+
(`routeExpectsBodyStream()`, `routerDecorators.ts:1379`).
|
|
727
727
|
- **`null` plutôt que structure vide.** Une action sans garde a `security: null` : le `Resolver` teste
|
|
728
728
|
un `null` et passe — ni résolution de service, ni `await`, ni allocation (`Resolver.ts:334`). Idem
|
|
729
729
|
pour `idempotent`, `cspDirectives`, `paramsMeta`.
|
|
@@ -732,14 +732,14 @@ Un décorateur non employé doit coûter **zéro**. C'est tenu par trois mécani
|
|
|
732
732
|
du processus, quelle que soit la charge. Corollaire : ne les mute jamais.
|
|
733
733
|
|
|
734
734
|
Coût résiduel côté montage seulement : la reconstruction de la pile d'appels dans `route()`
|
|
735
|
-
(`stackTrace`, `routerDecorators.ts:
|
|
735
|
+
(`stackTrace`, `routerDecorators.ts:284`) pour retrouver le fichier source. Elle a lieu **à l'import**, une fois par
|
|
736
736
|
route, jamais pendant une requête.
|
|
737
737
|
|
|
738
738
|
## 🧩 Extension — écrire son propre décorateur
|
|
739
739
|
|
|
740
740
|
Le module montre le patron à suivre : un décorateur maison **ne fait qu'écrire une métadonnée**, et
|
|
741
741
|
un point du pipeline la relit. Pour un simple drapeau dual (classe + méthode), le framework fournit
|
|
742
|
-
déjà la fabrique `booleanMarkerDecorator()` (`routerDecorators.ts:
|
|
742
|
+
déjà la fabrique `booleanMarkerDecorator()` (`routerDecorators.ts:1079`), dont `@CsrfProtect` et
|
|
743
743
|
`@CsrfExempt` sont les deux usages.
|
|
744
744
|
|
|
745
745
|
Le squelette d'un drapeau maison, en dehors du framework :
|
|
@@ -792,7 +792,7 @@ L'écran **Routes** (`/nodefony/routes`) et le point d'API `/nodefony/framework/
|
|
|
792
792
|
| Symptôme | Cause (dans le code) | Correction |
|
|
793
793
|
| --- | --- | --- |
|
|
794
794
|
| `404` sur une route pourtant décorée | Contrôleur jamais importé, ou absent de `@controllers([…])` | L'ajouter au tableau `@controllers` du module |
|
|
795
|
-
| `Action « remove » … : ce nom est RÉSERVÉ` au démarrage — ou `TS2416` au build | L'action reprend le nom d'un membre de `Controller` : la classe étend `Service`, qui expose déjà `remove(name): boolean` ([`Service.ts:
|
|
795
|
+
| `Action « remove » … : ce nom est RÉSERVÉ` au démarrage — ou `TS2416` au build | L'action reprend le nom d'un membre de `Controller` : la classe étend `Service`, qui expose déjà `remove(name): boolean` ([`Service.ts:538`](../../../../nodefony/src/Service.ts)), `set`, `get`, `clean`… Le décorateur refuse le nom avant que le conflit n'atteigne le compilateur. | Renommer l'action (`destroy`, `deleteOne`…). Le nom d'une action est libre : c'est le chemin du décorateur qui fait l'URL. |
|
|
796
796
|
| `404` après avoir déplacé `@controller` sous `@Domain` | `@controller` monte les routes ; les décorateurs lus au montage doivent être **sous** | Remettre `@controller` en **premier** (le plus haut) |
|
|
797
797
|
| Le vhost de `@Domain` classe est ignoré | `@Domain` placé **au-dessus** de `@controller` → posé trop tard | Placer `@Domain` sous `@controller` |
|
|
798
798
|
| `@BypassFirewall` n'ouvre rien | Écrit **avec** parenthèses — c'est un drapeau, pas une fabrique | `@BypassFirewall` (sans `()`) |
|