@nodefony/framework 10.0.0-alpha.7 → 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.
@@ -45,11 +45,11 @@ flowchart LR
45
45
 
46
46
  Trois idées à retenir :
47
47
 
48
- 1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:112`). Tu récupères
48
+ 1. **Tu hérites de `Service`** — `Controller` étend `Service` (`Controller.ts:146`). Tu récupères
49
49
  donc gratuitement le container (`this.get()`), les logs (`this.log()`) et les événements.
50
50
  2. **Tu ne construis rien toi-même** — le `Resolver` instancie ta classe via l'injecteur
51
- (`Resolver.newController()`, `Resolver.ts:236`), jamais un `new` direct.
52
- 3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:697`) traduit la
51
+ (`Resolver.newController()`, `Resolver.ts:254`), jamais un `new` direct.
52
+ 3. **Ton `return` EST la réponse** — `Resolver.returnController()` (`Resolver.ts:827`) traduit la
53
53
  valeur retournée : objet → JSON, string → corps brut, `void` → « j'ai répondu moi-même ».
54
54
 
55
55
  ## 📖 Lexique
@@ -196,14 +196,14 @@ Le tableau ci-dessous donne la séquence exacte, avec l'ancre qui la prouve :
196
196
  | # | Étape | Où |
197
197
  | --- | --------------------------------------- | --------------------------------------------------- |
198
198
  | 1 | Appariement de la route | `router.resolve()` (`http-kernel.ts:1324`) |
199
- | 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1334`) |
199
+ | 2 | En-têtes de sécurité applicatifs | `applySecurityHeaders()` (`http-kernel.ts:1381`) |
200
200
  | 3 | Parse du corps (sauf `@Body({stream})`) | `http-kernel.ts:1316` |
201
201
  | 4 | Armement de la route (sans instance) | `prepareFrontController()` (`http-kernel.ts:767`) |
202
- | 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1290`) |
203
- | 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1131`) |
204
- | 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1301`) |
202
+ | 5 | CSRF | `firewall.enforceCsrf()` (`http-kernel.ts:1479`) |
203
+ | 6 | Session (reprise ou ouverture) | `HttpKernel.startSession()` (`http-kernel.ts:1152`) |
204
+ | 7 | Firewall — **authentification** | `firewall.handleSecurity()` (`http-kernel.ts:1490`) |
205
205
  | 8 | Autorisation `@IsGranted` | `Resolver.executeAction()` (`Resolver.ts:334`) |
206
- | 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:313`) |
206
+ | 9 | **Instanciation DI + `initialize()`** | `Resolver.executeAction()` (`Resolver.ts:335`) |
207
207
  | 10 | **Ton action** | `controller[methodKey]()` (`Resolver.ts:382`) |
208
208
 
209
209
  > [!IMPORTANT]
@@ -257,7 +257,7 @@ Si ton `initialize()` lève, l'exception remonte le pipeline et sort en réponse
257
257
  Chaque étape est chronométrée sous le nom d'une **phase**, lisible dans la debug bar et le profileur :
258
258
  `resolve` · `initialize` (DI + ton hook) · `parse` · `firewall` · `action` · `render` · `send`.
259
259
  La phase `initialize` existe précisément pour que le temps passé dans ton hook et dans la résolution
260
- DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:281`).
260
+ DI **soit imputé à quelqu'un** au lieu de disparaître dans le bloc `action` (`Resolver.ts:100`).
261
261
 
262
262
  ## 🔌 HTTP et WebSocket — le même contrôleur
263
263
 
@@ -300,7 +300,7 @@ class ChatController extends Controller {
300
300
  | `initialize()` | À chaque requête | **Une seule fois**, au handshake |
301
301
 
302
302
  La réutilisation de l'instance vient du cache posé sur le container du contexte
303
- (`Resolver.newController()`, `Resolver.ts:232`) : le contexte WS étant partagé par la connexion, le
303
+ (`Resolver.newController()`, `Resolver.ts:254`) : le contexte WS étant partagé par la connexion, le
304
304
  contrôleur l'est aussi. Un garde-fou vérifie que l'instance cachée est bien de la classe de la route
305
305
  courante et la reconstruit sinon (`Resolver.ts:344-347`) — sans quoi un message invoquant une autre
306
306
  action se tromperait d'objet.
@@ -310,9 +310,9 @@ action se tromperait d'objet.
310
310
  > frame 2 — pratique pour un état de conversation, piège si tu comptais sur une instance neuve. En
311
311
  > HTTP, l'inverse : chaque requête repart d'une instance vierge.
312
312
 
313
- Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1702`)
313
+ Côté WebSocket, l'ordre est encore plus marqué : `HttpKernel.onConnect()` (`http-kernel.ts:1711`)
314
314
  appelle `handleFrontController()` (donc `initialize()`) **avant** `startSession()`
315
- (`http-kernel.ts:1131`), avant l'acceptation de la socket, et avant le firewall
315
+ (`http-kernel.ts:1152`), avant l'acceptation de la socket, et avant le firewall
316
316
  (`http-kernel.ts:1457`).
317
317
 
318
318
  ## 🧠 D'où viennent `request`, `response`, `session`
@@ -322,16 +322,16 @@ sont des accesseurs qui dérivent du contexte **vivant**, selon le motif `champ
322
322
 
323
323
  | Raccourci | Ce que tu obtiens | Ancre |
324
324
  | ---------------- | -------------------------------------------------- | ------------------- |
325
- | `this.context` | Le contexte transport de la requête courante | `Controller.ts:146` |
325
+ | `this.context` | Le contexte transport de la requête courante | `Controller.ts:180` |
326
326
  | `this.route` | La route matchée | `Controller.ts:158` |
327
327
  | `this.request` | La requête (HTTP, HTTP/2 ou WS) | `Controller.ts:162` |
328
- | `this.response` | La réponse du transport | `Controller.ts:169` |
328
+ | `this.response` | La réponse du transport | `Controller.ts:203` |
329
329
  | `this.method` | La méthode HTTP (ou `WEBSOCKET`) | `Controller.ts:212` |
330
330
  | `this.queryGet` | Les paramètres de la query string | `Controller.ts:221` |
331
331
  | `this.queryPost` | Le corps parsé | `Controller.ts:248` |
332
332
  | `this.body` | Le corps parsé — alias de `queryPost` | `Controller.ts:248` |
333
333
  | `this.queryFile` | Les fichiers uploadés | `Controller.ts:239` |
334
- | `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:229` |
334
+ | `this.session` | La session **ou `null`** si elle n'est pas activée | `Controller.ts:279` |
335
335
 
336
336
  Pourquoi des accesseurs plutôt que des champs recopiés au constructeur : **la fraîcheur et le coût**.
337
337
  Une valeur recopiée vieillit dès que le pipeline modifie le contexte, et recopier quatre structures
@@ -340,7 +340,7 @@ gratuitement.
340
340
 
341
341
  ### La session est **lazy** — elle n'existe que si tu la demandes
342
342
 
343
- `this.session` est un simple getter sur `context.session` (`Controller.ts:229`). Il n'y a **pas** de
343
+ `this.session` est un simple getter sur `context.session` (`Controller.ts:279`). Il n'y a **pas** de
344
344
  `startSession()` à appeler depuis un contrôleur : l'activation se déclare sur la route, avec
345
345
  `@UseSession()` (ou un paramètre `@Session()`, qui vaut déclaration implicite), et le pipeline
346
346
  l'exécute à son point unique. Sans déclaration et sans cookie de session entrant, **aucune session
@@ -387,26 +387,26 @@ Ce que ça change, concrètement :
387
387
 
388
388
  ## 🧰 Répondre — ce que ton `return` déclenche
389
389
 
390
- Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:697`). Il regarde le **type**
390
+ Le traducteur unique est `Resolver.returnController()` (`Resolver.ts:827`). Il regarde le **type**
391
391
  de ce que tu as retourné :
392
392
 
393
393
  <!-- prettier-ignore -->
394
394
  | Tu retournes… | Ce qui se passe | Ancre |
395
395
  | --- | --- | --- |
396
- | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:700-710` |
396
+ | Une `Promise` / un thenable | Déballée puis re-traitée (récursif) | `Resolver.ts:830-840` |
397
397
  | Une `string` | Envoyée telle quelle en corps | `Resolver.ts:711` |
398
- | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:760` |
398
+ | Un objet simple ou un tableau | **Auto-JSON** : `application/json` + sérialisation | `Resolver.ts:870` |
399
399
  | Un `number` / un `boolean` | Auto-JSON scalaire (RFC 8259 §2 : `42`, `true` sont des documents valides) | `Resolver.ts:734` |
400
- | Un `Buffer` | Envoyé brut | `Resolver.ts:723` |
401
- | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:716` |
402
- | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:855`) |
403
- | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:801` |
404
- | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:770-777` |
400
+ | Un `Buffer` | Envoyé brut | `Resolver.ts:854` |
401
+ | Une `Response` (via un `render*`) | Retournée telle quelle — l'envoi a déjà eu lieu | `Resolver.ts:581` |
402
+ | `void`/`null` **et** statut 204/205/304 | Réponse **vide envoyée** (RFC 9110 : ces statuts n'ont pas de corps) | `NO_BODY_STATUS` (`Resolver.ts:948`) |
403
+ | `void`/`null` avec tout autre statut | `waitAsync` : « l'action enverra plus tard » | `Resolver.ts:932` |
404
+ | Une instance de classe (entité ORM, DTO) | **Non sérialisée** → `waitAsync` (le teardown avertit du blocage) | `Resolver.ts:906-913` |
405
405
 
406
406
  > [!WARNING]
407
407
  > **Le piège n° 1 : `return null` sur un statut à corps.** Le framework l'interprète comme « je
408
408
  > répondrai moi-même » et attend — jusqu'au timeout. La distinction se fait sur le **statut** :
409
- > `NO_BODY_STATUS` (`Resolver.ts:855`) contient 204, 205 et 304. Donc un `@Delete` qui fait
409
+ > `NO_BODY_STATUS` (`Resolver.ts:948`) contient 204, 205 et 304. Donc un `@Delete` qui fait
410
410
  > `@HttpCode(204)` puis `return null` répond bien 204 vide ; le même `return null` sans `@HttpCode`
411
411
  > laisse la requête pendue.
412
412
 
@@ -419,12 +419,12 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
419
419
 
420
420
  | Helper | Pour… | Ancre |
421
421
  | -------------------------------------------- | -------------------------------------------------------- | ------------------- |
422
- | `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:379` |
422
+ | `renderJson(obj, status?, headers?)` | JSON explicite avec statut/en-têtes | `Controller.ts:481` |
423
423
  | `render(data, encoding?, status?, headers?)` | Envoyer un corps quelconque via le contexte | `Controller.ts:377` |
424
424
  | `renderView(path, params, status?)` | Rendre un template **Eta** (avec les helpers frontend) | `Controller.ts:410` |
425
425
  | `renderResponse(data, encoding?, …)` | Poser statut + en-têtes, puis envoyer | `Controller.ts:392` |
426
- | `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:382` |
427
- | `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:432` |
426
+ | `redirect(url, status?, headers?)` | Rediriger | `Controller.ts:500` |
427
+ | `forward("module:controller:action")` | Déléguer à une autre action **sans** aller-retour réseau | `Controller.ts:534` |
428
428
  | `setContextJson()` / `setContextHtml()` | Choisir le type de contenu avant d'envoyer | `Controller.ts:319` |
429
429
 
430
430
  `renderView()` mesure sa propre phase `render` et injecte automatiquement les aides frontend
@@ -437,7 +437,7 @@ Quand tu veux piloter l'envoi plutôt que retourner une valeur :
437
437
  > [!TIP]
438
438
  > **Redirection : le code par défaut est 302** (Found), pas 301. Un statut absent ou hors de la liste
439
439
  > RFC 9110 §15.4 (301, 302, 303, 307, 308) retombe sur 302 avec un log d'avertissement
440
- > (`Response.redirect()`, `Response.ts:595`). Un 301 par défaut piégeait : les navigateurs le mettent
440
+ > (`Response.redirect()`, `Response.ts:611`). Un 301 par défaut piégeait : les navigateurs le mettent
441
441
  > en cache de façon quasi irréversible.
442
442
 
443
443
  ## 📁 Servir un fichier — téléchargement et flux média
@@ -446,7 +446,7 @@ Deux besoins distincts, deux helpers.
446
446
 
447
447
  ### Téléchargement — `renderFileDownload()`
448
448
 
449
- `renderFileDownload(file, options?, headers?)` (`Controller.ts:473`) pose
449
+ `renderFileDownload(file, options?, headers?)` (`Controller.ts:625`) pose
450
450
  `Content-Disposition: attachment`, `Content-Length`, le type MIME du fichier, puis délègue au moteur
451
451
  de flux. Le fichier est résolu **sans bloquer l'event loop** (`getFileAsync()`, `Controller.ts:586`) ;
452
452
  la variante synchrone `getFile()` existe encore mais est marquée obsolète — elle appelle `lstatSync`
@@ -454,7 +454,7 @@ et gèle le process le temps du stat.
454
454
 
455
455
  ### Lecture en continu — `renderMediaStream()`
456
456
 
457
- `renderMediaStream(file, headers?, options?)` (`Controller.ts:609`) implémente les **requêtes par
457
+ `renderMediaStream(file, headers?, options?)` (`Controller.ts:780`) implémente les **requêtes par
458
458
  plage** (RFC 9110 §14), ce qui permet à un lecteur vidéo de sauter dans le flux :
459
459
 
460
460
  | Le client envoie… | Réponse |
@@ -474,7 +474,7 @@ donc testable sans serveur.
474
474
  **nettoyage** : le flux est ouvert avec `autoClose: false`, et un client qui raccroche en plein
475
475
  téléchargement laisserait sinon un descripteur de fichier ouvert et une promesse pendue à jamais. Un
476
476
  écouteur `close` sur la réponse détruit le flux, ce qui déclenche la fermeture du descripteur et
477
- résout la promesse (`Controller.ts:553-558`), puis se retire lui-même (`Controller.ts:574`). Un
477
+ résout la promesse (`Controller.ts:729-734`), puis se retire lui-même (`Controller.ts:746`). Un
478
478
  téléchargement interrompu ne coûte donc **rien** en ressource retenue.
479
479
 
480
480
  ## ⚠️ Erreurs — lever, rendre, observer
@@ -489,11 +489,11 @@ throw new nodefonyError("Article introuvable", 404); // statut porté par l'erre
489
489
  throw new HttpError("Not Found", 404, this.context); // variante enrichie du contexte
490
490
  ```
491
491
 
492
- L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:874`), qui délègue la mise en
492
+ L'exception remonte jusqu'à `HttpKernel.onError()` (`http-kernel.ts:879`), qui délègue la mise en
493
493
  forme au rendeur d'erreurs. Ce qui en sort :
494
494
 
495
495
  - **statut normalisé** — un code absent (ou l'ancien quirk `200`) devient **500**
496
- (`normalizeHttpStatus()`, `error-renderer.ts:355`) ;
496
+ (`normalizeHttpStatus()`, `error-renderer.ts:642`) ;
497
497
  - **corps structuré** : `{ code, message, result: null, error: {…}, nodefony: {…} }`, l'enveloppe
498
498
  `nodefony` portant l'environnement, l'URL et l'**identifiant de requête** — de quoi retrouver la
499
499
  trace complète dans les logs ;
@@ -501,13 +501,13 @@ forme au rendeur d'erreurs. Ce qui en sort :
501
501
  ne tente pas de rendre — il journalise et s'arrête (`http-kernel.ts:770-775`).
502
502
 
503
503
  En **WebSocket**, il n'y a pas de statut : l'erreur devient un **code de fermeture** RFC 6455
504
- (`renderWebsocket()`, `error-renderer.ts:393`) — 401/403 → 1008 (violation de politique),
504
+ (`renderWebsocket()`, `error-renderer.ts:518`) — 401/403 → 1008 (violation de politique),
505
505
  5xx → 1011 (erreur interne), le reste → 4004 (plage privée). Si la socket n'est pas encore acceptée,
506
506
  c'est un **rejet** de handshake.
507
507
 
508
508
  > [!NOTE]
509
509
  > Les erreurs de ton action remontent **seules** : le Resolver n'enveloppe pas l'appel dans un
510
- > `try/catch` inutile (`Resolver.ts:405-406`). Inutile d'attraper pour re-lever — sauf si tu veux
510
+ > `try/catch` inutile (`Resolver.ts:444-445`). Inutile d'attraper pour re-lever — sauf si tu veux
511
511
  > vraiment traduire l'erreur en un autre statut.
512
512
 
513
513
  ## 🧩 Services injectés — trois façons
@@ -521,7 +521,7 @@ explicite :
521
521
  const catalog = this.get<CatalogService>("catalog"); // null si absent ou container nettoyé
522
522
  ```
523
523
 
524
- `Service.get()` (`Service.ts:472`) est une **façade sûre** : elle retourne `null` au lieu de lever si
524
+ `Service.get()` (`Service.ts:518`) est une **façade sûre** : elle retourne `null` au lieu de lever si
525
525
  le container a déjà été détaché. C'est le style à privilégier dans `initialize()`.
526
526
 
527
527
  ### 2. Injection par le constructeur — `@inject`
@@ -568,7 +568,7 @@ code du framework applique — et attend de toi — les règles suivantes :
568
568
  (`Controller.ts:574`).
569
569
  - **Métadonnées d'action figées** : `@HttpCode`, `@Header`, les paramètres décorés et l'intention de
570
570
  session sont calculés **une fois** par route puis mémorisés, au lieu d'être relus par `Reflect` à
571
- chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:402`).
571
+ chaque requête (`resolveActionMeta()` appelé en `Resolver.ts:469`).
572
572
  - **Gardes payées seulement si présentes** : sans `@IsGranted`, la vérification d'autorisation est
573
573
  un test de nullité (`Resolver.ts:334`) — 0 lookup, 0 `await`, 0 allocation.
574
574
  - **Ta part du contrat** : pas de structure allouée « au cas où » dans le constructeur ni dans
@@ -578,13 +578,13 @@ code du framework applique — et attend de toi — les règles suivantes :
578
578
 
579
579
  | Domaine | Norme | Comment le code s'y conforme |
580
580
  | -------------------------------- | ------------------------ | -------------------------------------------------------------- |
581
- | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:855`) |
581
+ | Statuts sans corps (204/205/304) | RFC 9110 §15.3.5/§15.4.5 | `NO_BODY_STATUS` (`Resolver.ts:948`) |
582
582
  | Requêtes par plage | RFC 9110 §14.1.2, §14.2 | `parseByteRange()` (`Controller.ts:107`) |
583
583
  | Plage insatisfiable → 416 | RFC 9110 §15.5.17 | `renderResponse()` avec 416 (`Controller.ts:392`) |
584
584
  | Redirections | RFC 9110 §15.4 | Liste blanche + repli 302 (`Response.ts:534`) |
585
- | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:760`), vérifié par le banc `auto-json` |
585
+ | Média JSON sans `charset` | RFC 8259 §11 | Auto-JSON (`Resolver.ts:892`), vérifié par le banc `auto-json` |
586
586
  | Scalaire JSON de premier niveau | RFC 8259 §2 | `number`/`boolean` rendus (`Resolver.ts:734`) |
587
- | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:393`) |
587
+ | Codes de fermeture WebSocket | RFC 6455 §7.4 | `renderWebsocket()` (`error-renderer.ts:518`) |
588
588
 
589
589
  ## 📡 Observabilité — Studio
590
590
 
@@ -601,11 +601,11 @@ code du framework applique — et attend de toi — les règles suivantes :
601
601
  <!-- prettier-ignore -->
602
602
  | Symptôme | Cause (dans le code) | Correction |
603
603
  | --- | --- | --- |
604
- | La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:801`) | Retourner une valeur, ou poser `@HttpCode(204)` |
605
- | Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:775`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
604
+ | La requête pend puis expire, alors que l'action a bien tourné | `return null`/`undefined` avec un statut à corps → `waitAsync` (`Resolver.ts:906`) | Retourner une valeur, ou poser `@HttpCode(204)` |
605
+ | Réponse vide alors qu'on retourne une entité ORM | Instance de classe **non** sérialisée → `waitAsync` (`Resolver.ts:906`) | Retourner un objet simple, ou `renderJson(entity.toJSON())` |
606
606
  | `Route Action not found` | L'action porte un nom déjà utilisé par un membre de `Controller` | Renommer : `session`, `request`, `response`, `context`, `route`, `method`, `query*`, `get`, `set`, `render*`, `redirect`, `forward` sont réservés |
607
607
  | `this.session` est `null` dans `initialize()` | La session est activée **après** (`http-kernel.ts:1142`) | Lire la session dans l'action, pas dans le hook |
608
- | Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1294`) | Déplacer l'effet de bord dans l'action |
608
+ | Effet de bord exécuté pour une requête finalement 401 | `initialize()` tourne avant `firewall.handleSecurity()` (`http-kernel.ts:1490`) | Déplacer l'effet de bord dans l'action |
609
609
  | Redirection permanente non voulue | Un statut invalide retombe sur 302, un `301` explicite reste 301 | Passer le code voulu : `this.redirect(url, 302)` |
610
610
  | WS : l'état d'une frame « bave » sur la suivante | L'instance est partagée par toute la connexion (`Resolver.ts:262`) | Réinitialiser l'état en tête d'action, ou le porter par message |
611
611
  | WS : l'action n'est jamais appelée | Route sans transport `WEBSOCKET` déclaré | `requirements: { methods: ["WEBSOCKET"] }` |