@nodefony/framework 10.0.0-alpha.9 → 10.0.0-beta.2

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.
Files changed (53) hide show
  1. package/README.md +5 -2
  2. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
  3. package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
  4. package/dist/index.js +4 -4
  5. package/dist/nodefony/controller/AdminApiController.js +4 -3
  6. package/dist/nodefony/controller/ApiKeyController.js +3 -2
  7. package/dist/nodefony/controller/OAuth2Controller.js +11 -6
  8. package/dist/nodefony/controller/SessionAuthController.js +3 -3
  9. package/dist/nodefony/controller/TokenAuthController.js +4 -4
  10. package/dist/nodefony/controller/TotpController.js +7 -5
  11. package/dist/nodefony/controller/WebAuthnController.js +4 -4
  12. package/dist/nodefony/decorators/routerDecorators.js +41 -35
  13. package/dist/nodefony/service/AdminBroker.js +2 -2
  14. package/dist/nodefony/service/IdempotencyStore.js +4 -5
  15. package/dist/nodefony/service/router.js +78 -26
  16. package/dist/nodefony/src/Controller.js +184 -78
  17. package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
  18. package/dist/nodefony/src/KernelAdminApi.js +49 -49
  19. package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
  20. package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
  21. package/dist/nodefony/src/Resolver.js +221 -65
  22. package/dist/nodefony/src/Route.js +62 -39
  23. package/dist/nodefony/src/SyslogAdminApi.js +4 -4
  24. package/dist/nodefony/src/docsReader.js +49 -39
  25. package/dist/nodefony/src/scopeCatalog.js +1 -1
  26. package/dist/nodefony/src/singletonGuard.js +76 -0
  27. package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -1
  28. package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
  29. package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
  30. package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
  31. package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
  32. package/dist/types/nodefony/controller/TokenAuthController.d.ts +2 -2
  33. package/dist/types/nodefony/controller/TotpController.d.ts +2 -1
  34. package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
  35. package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
  36. package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
  37. package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
  38. package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
  39. package/dist/types/nodefony/service/router.d.ts +33 -7
  40. package/dist/types/nodefony/src/Controller.d.ts +97 -24
  41. package/dist/types/nodefony/src/Resolver.d.ts +155 -16
  42. package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
  43. package/dist/types/nodefony/src/Route.d.ts +39 -23
  44. package/dist/types/nodefony/src/docsReader.d.ts +19 -9
  45. package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
  46. package/docs/admin.md +1 -1
  47. package/docs/controller.md +139 -121
  48. package/docs/decorateurs.md +102 -98
  49. package/docs/idempotence.md +23 -21
  50. package/docs/index.md +1 -1
  51. package/docs/routing.md +60 -53
  52. package/docs/templates.md +14 -14
  53. package/package.json +10 -11
@@ -1,25 +1,25 @@
1
- import type { ContextType, HttpError } from "@nodefony/http";
2
- import type { Injector } from "nodefony";
1
+ import type { ContextType, HttpError, IRouteResolver } from "@nodefony/http";
2
+ import type { Injector, MaybePromise } from "nodefony";
3
3
  import type { ControllerConstructor } from "../src/Route.js";
4
4
  import type { IRoute } from "./IRoute.js";
5
5
  import type { IController } from "./IController.js";
6
- export interface IResolver {
7
- injector?: Injector | null;
6
+ export interface IResolver extends IRouteResolver {
7
+ injector?: Injector | null | undefined;
8
8
  controller: ControllerConstructor | null;
9
- actionName?: string;
10
- action?: (...args: unknown[]) => unknown;
9
+ actionName?: string | undefined;
10
+ action?: ((...args: unknown[]) => unknown) | undefined;
11
11
  context: ContextType;
12
12
  readonly route: IRoute | null;
13
13
  resolve: boolean;
14
14
  variables: unknown[];
15
- exception?: HttpError | Error | null;
15
+ exception?: HttpError | Error | null | undefined;
16
16
  acceptedProtocol: string | null;
17
17
  bypassFirewall: boolean;
18
18
  match(route: IRoute, context: ContextType, cleanPath?: string): unknown;
19
19
  getMatchedParams(): Record<string, unknown>;
20
20
  parsePathernController(name: string): void;
21
21
  getAction(name: string): ((...args: unknown[]) => unknown) | null;
22
- newController(context?: ContextType): Promise<IController | object>;
23
- callController(data?: unknown[], reload?: boolean): Promise<unknown>;
24
- returnController(result: unknown): Promise<unknown>;
22
+ newController(context?: ContextType): MaybePromise<IController | object>;
23
+ callController(data?: unknown[], reload?: boolean): MaybePromise<unknown>;
24
+ returnController(result: unknown): MaybePromise<unknown>;
25
25
  }
@@ -1,22 +1,22 @@
1
- import type { HTTPMethod, SchemeType, ContextType } from "@nodefony/http";
1
+ import type { HTTPMethod, SchemeType, ContextType, IResolvedRoute } from "@nodefony/http";
2
2
  import type { ControllerConstructor, RouteRequirements } from "../src/Route.js";
3
- export interface IRoute {
3
+ export interface IRoute extends IResolvedRoute {
4
4
  name: string;
5
- path?: string;
6
- controller?: ControllerConstructor;
7
- classMethod?: string;
8
- prefix?: string;
9
- method?: HTTPMethod;
10
- schemes?: SchemeType;
11
- pattern?: RegExp;
5
+ path?: string | undefined;
6
+ controller?: ControllerConstructor | undefined;
7
+ classMethod?: string | undefined;
8
+ prefix?: string | undefined;
9
+ method?: HTTPMethod | undefined;
10
+ schemes?: SchemeType | undefined;
11
+ pattern?: RegExp | undefined;
12
12
  variables: unknown[];
13
13
  defaults: Partial<Record<string, unknown>>;
14
14
  requirements: Partial<RouteRequirements>;
15
- hash?: string;
16
- host?: string | string[];
17
- hostRegexp?: RegExp[];
15
+ hash?: string | undefined;
16
+ host?: string | string[] | undefined;
17
+ hostRegexp?: RegExp[] | undefined;
18
18
  bypassFirewall: boolean;
19
- filePath?: string;
19
+ filePath?: string | undefined;
20
20
  match(context: ContextType, cleanPath?: string): unknown[] | null | undefined;
21
21
  compile(): RegExp;
22
22
  toString(): string;
@@ -15,11 +15,11 @@ declare class AdminBroker extends Service implements IAdminBroker {
15
15
  readonly apiSegment = "api";
16
16
  readonly defaultRole = "ROLE_NODEFONY_ADMIN";
17
17
  /** Producteurs enregistrés, indexés par namespace. */
18
- private producers;
18
+ private readonly producers;
19
19
  /** Routes résolues, indexées par nom de route (dispatch O(1)). */
20
- private byRouteName;
20
+ private readonly byRouteName;
21
21
  /** Module framework propriétaire — requis pour `Router.setController`. */
22
- private frameworkModule;
22
+ private readonly frameworkModule;
23
23
  /** Vrai une fois `mountAll()` exécuté (idempotence + verrou de register). */
24
24
  private mounted;
25
25
  constructor(module: Module);
@@ -1,26 +1,35 @@
1
1
  import { Service, Module } from "nodefony";
2
+ import type { MaybePromise } from "nodefony";
2
3
  import Route, { RouteOptions } from "../src/Route.js";
3
4
  import { ContextType } from "@nodefony/http";
5
+ import type { IRequestRouter } from "@nodefony/http";
4
6
  import Resolver from "../src/Resolver.js";
5
7
  import Controller from "../src/Controller.js";
6
8
  export type TypeController<T> = new (...args: any[]) => T;
7
- declare class Router extends Service {
9
+ declare class Router extends Service implements IRequestRouter {
8
10
  static routes: Route[];
9
11
  routes: Route[];
10
12
  private singletonControllers;
11
13
  constructor(module: Module);
12
14
  /**
13
15
  * Retourne l'instance singleton d'une classe controller `@Scope("singleton")`,
14
- * en la créant au premier appel via `create`. On cache la **promesse** (pas
15
- * l'instance) : N requêtes concurrentes pendant la création (`initialize()`
16
- * async) attendent le MÊME travail — jamais deux instances (race de création
17
- * éliminée structurellement).
16
+ * en la créant au premier appel via `create`.
17
+ *
18
+ * Tant que la création est en cours (`initialize()` async), c'est la
19
+ * **promesse** qui est en cache : N requêtes concurrentes attendent le MÊME
20
+ * travail — jamais deux instances (race de création éliminée
21
+ * structurellement). Dès que l'instance est prête, elle REMPLACE la promesse :
22
+ * les requêtes suivantes la reçoivent directement, au lieu d'attendre à
23
+ * chaque fois une promesse déjà tenue (#505).
18
24
  *
19
25
  * @param ctor - la classe controller (clé du cache).
20
26
  * @param create - fabrique exécutée une seule fois (instantiate + initialize).
21
- * @returns la promesse de l'instance partagée.
27
+ * @returns l'instance partagée quand elle existe — ou que `create` la rend
28
+ * sans rien attendre —, sinon la promesse de sa création.
29
+ * @throws ce que `create` lève de façon synchrone ; rien n'est alors mis en
30
+ * cache, la requête suivante recrée.
22
31
  */
23
- getSingletonController(ctor: TypeController<Controller>, create: () => Promise<Controller>): Promise<Controller>;
32
+ getSingletonController<T extends object>(ctor: new (...args: never[]) => T, create: () => MaybePromise<T>): MaybePromise<T>;
24
33
  /**
25
34
  * Résout une route pour un contexte donné.
26
35
  *
@@ -42,6 +51,23 @@ declare class Router extends Service {
42
51
  setRoute(): void;
43
52
  removeRoutes(name: string): void;
44
53
  static createRoute(name: string, obj: RouteOptions): Route;
54
+ /**
55
+ * Enregistre une classe contrôleur pour un module — l'entonnoir UNIQUE : le
56
+ * décorateur `@controllers` y passe, comme les contrôleurs internes que le
57
+ * framework enregistre directement (plan d'administration, OAuth,
58
+ * WebAuthn…).
59
+ *
60
+ * C'est donc ICI que se refusent, au démarrage, les fautes de déclaration
61
+ * qu'un contrôleur ne révélerait qu'à sa première requête : une dépendance
62
+ * captive (un singleton qui détient un service de portée `request`), et une
63
+ * portée que sa classe de base n'admet pas (`Controller.assertScope`).
64
+ *
65
+ * @param myconstructor - la classe contrôleur.
66
+ * @param module - le module qui la porte.
67
+ * @returns la classe, rangée sous `module:Classe`.
68
+ * @throws BootConfigurationError sur une dépendance captive ou une portée
69
+ * refusée — fatale dans tous les environnements.
70
+ */
45
71
  static setController(myconstructor: TypeController<Controller>, module: Module): TypeController<Controller>;
46
72
  /**
47
73
  * Retourne les routes enregistrées pour un controller donné — utilisé par
@@ -1,4 +1,5 @@
1
1
  import { Service, Module, FileClass } from "nodefony";
2
+ import type { MaybePromise } from "nodefony";
2
3
  import type { IController } from "../interfaces/index.js";
3
4
  import Route from "./Route.js";
4
5
  import { contextRequest, HTTPMethod, HttpResponse, Session, ContextType, Http2Response, WebsocketResponse } from "@nodefony/http";
@@ -32,40 +33,61 @@ export declare function parseByteRange(range: string, length: number): {
32
33
  end: number;
33
34
  } | "unsatisfiable" | null;
34
35
  /**
35
- * Scope d'instanciation d'un controller (V4.3).
36
+ * Portée d'instanciation d'un contrôleur.
36
37
  *
37
- * - `"request"` (défaut) : une instance par requête — l'état per-request peut
38
- * vivre sur `this` (legacy sûr, zéro breaking).
39
- * - `"singleton"` (opt-in via `@Scope`) : UNE instance partagée par toutes les
40
- * requêtes — réservé aux controllers **stateless** (état uniquement via
41
- * arguments décorés + ALS). Un champ mutable par requête sur `this` y serait
42
- * une data race silencieuse entre requêtes concurrentes.
38
+ * - `"singleton"` (le défaut) : UNE instance par classe, construite à la
39
+ * première requête et partagée par toutes les suivantes, concurrentes
40
+ * comprises. Le contrôleur ne porte que du CODE : l'état de la requête
41
+ * arrive par les arguments décorés (`@Param`, `@Query`, `@Body`…) et par les
42
+ * helpers, qui retrouvent la requête courante dans l'ALS. Écrire un état de
43
+ * requête sur `this` le ferait fuir vers la requête suivante — les
44
+ * accesseurs d'état le refusent, et le développement refuse toute écriture
45
+ * sur un champ (cf `singletonGuard.ts`).
46
+ * - `"request"` (option explicite, `@Scope("request")`) : une instance par
47
+ * requête — par CONNEXION en WebSocket. Pour un contrôleur qui injecte un
48
+ * service de portée `request` à son constructeur, prépare chaque requête
49
+ * dans `initialize()`, ou garde un état sur `this`.
43
50
  */
44
51
  export type ControllerScope = "request" | "singleton";
45
52
  declare class Controller extends Service implements IController {
46
53
  #private;
47
54
  static prefix: string;
48
55
  /**
49
- * Scope d'instanciation de la classe — `"request"` par défaut, `"singleton"`
50
- * posé par le décorateur `@Scope` (statique hérité, lu via `new.target` au
51
- * constructor et par le Resolver : 0 Reflect). Cf {@link ControllerScope}.
56
+ * Portée d'instanciation de la classe — `"singleton"` par défaut,
57
+ * `"request"` posé par le décorateur `@Scope` (statique hérité, lu via
58
+ * `new.target` au constructeur et par le Resolver : 0 Reflect).
59
+ * Cf {@link ControllerScope}.
52
60
  */
53
61
  static scope: ControllerScope;
54
- module?: Module;
55
- template?: Eta | null;
56
62
  /**
57
- * Contexte transport courant. Per-request : champ posé par `setContext`
58
- * (constructor) — coût d'accès inchangé. Singleton stateless (V4.3) : champ
59
- * jamais posé → lecture de l'ALS `RequestContext` (le `HttpKernel` y place
60
- * le contexte à l'entrée du scope, V4.1) — chaque appel de helper retrouve
61
- * LA requête en cours, jamais celle d'une requête concurrente.
63
+ * Vérifie que la portée de la classe est admise par sa lignée. Appelée à
64
+ * l'enregistrement du contrôleur (`Router.setController`), donc au
65
+ * démarrage.
66
+ *
67
+ * N'impose rien par défaut. Une classe de base qui n'admet qu'une portée la
68
+ * fait respecter ici — `RealtimeController`, lié à SA connexion par
69
+ * construction, refuse le singleton.
70
+ *
71
+ * @throws BootConfigurationError quand la portée déclarée est refusée.
72
+ */
73
+ static assertScope(): void;
74
+ module?: Module | undefined;
75
+ template?: Eta | null | undefined;
76
+ /**
77
+ * Contexte transport courant. Par requête : champ posé par `setContext`
78
+ * (constructeur). Singleton : champ jamais posé → lecture de l'ALS
79
+ * `RequestContext`, où le `HttpKernel` place le contexte à l'entrée de la
80
+ * requête — chaque helper retrouve LA requête en cours, jamais une requête
81
+ * concurrente.
62
82
  */
63
83
  get context(): ContextType | undefined;
64
84
  set context(context: ContextType | undefined);
65
85
  /**
66
- * Route matchée. Per-request : posée par le Resolver via `setRoute`.
67
- * Sans champ (singleton) : dérive du Resolver de la requête courante
68
- * (`context.resolver`), donc toujours la route de CETTE requête.
86
+ * Route de l'appel en cours. Par requête : posée par le Resolver
87
+ * (`setRoute`). Singleton : celle du résolveur de l'APPEL quand le pont
88
+ * `api.request` en a posé un dans l'ALS (clé `resolver` — un résolveur par
89
+ * appel, jamais rangé sur le contexte de la connexion), sinon celle du
90
+ * contexte.
69
91
  */
70
92
  get route(): Route | null;
71
93
  get request(): contextRequest;
@@ -74,8 +96,14 @@ declare class Controller extends Service implements IController {
74
96
  set response(response: HttpResponse | Http2Response | WebsocketResponse | null);
75
97
  get method(): HTTPMethod | undefined;
76
98
  set method(method: HTTPMethod | undefined);
99
+ /**
100
+ * Query string de l'appel en cours — pour un singleton invoqué par le pont
101
+ * `api.request`, celle du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas
102
+ * celle du handshake de la connexion. Une seule lecture de l'ALS.
103
+ */
77
104
  get queryGet(): Record<string, unknown>;
78
105
  set queryGet(value: Record<string, unknown>);
106
+ /** Comme {@link queryGet}, pour la query fusionnée (`query`). */
79
107
  get query(): Record<string, unknown>;
80
108
  set query(value: Record<string, unknown>);
81
109
  get queryFile(): unknown[];
@@ -101,11 +129,38 @@ declare class Controller extends Service implements IController {
101
129
  */
102
130
  get session(): Session | null;
103
131
  constructor(name: string, context: ContextType);
132
+ /**
133
+ * Lie l'instance au contexte de SA requête — contrôleur par requête
134
+ * seulement : tout l'état de la requête (`request`, `response`, `method`,
135
+ * `query*`) en dérive ensuite, toujours frais.
136
+ *
137
+ * @param context - le contexte de la requête servie par cette instance.
138
+ * @throws TypeError sur un singleton, qui sert toutes les requêtes.
139
+ */
104
140
  setContext(context: ContextType): void;
105
141
  setContextJson(encoding?: BufferEncoding): void | undefined;
106
142
  setContextHtml(encoding?: BufferEncoding): void | undefined;
107
- render(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: Record<string, string | number>): Promise<Http2Response | HttpResponse>;
108
- renderResponse(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: OutgoingHttpHeaders): Promise<Http2Response | HttpResponse> | Promise<WebsocketResponse>;
143
+ /**
144
+ * Rend une donnée sur la réponse de la requête courante (JSON, HTML ou
145
+ * texte selon son type).
146
+ *
147
+ * Synchrone quand l'envoi l'est — réponse unique, sans session à sauver ni
148
+ * hook d'envoi (#505). Un `await` reste valable dans tous les cas ; un
149
+ * `.then` chaîné ne l'est plus : la valeur n'est une promesse que quand
150
+ * l'envoi attend.
151
+ *
152
+ * @returns la réponse, ou sa promesse.
153
+ */
154
+ render(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: Record<string, string | number>): MaybePromise<Http2Response | HttpResponse>;
155
+ /**
156
+ * Envoie une donnée déjà sérialisée sur le transport de la requête (HTTP ou
157
+ * WebSocket), après statut et en-têtes éventuels.
158
+ *
159
+ * Synchrone quand l'envoi l'est (#505) — cf {@link render}.
160
+ *
161
+ * @returns la réponse, ou sa promesse.
162
+ */
163
+ renderResponse(data: unknown, encoding?: BufferEncoding, status?: string | number, headers?: OutgoingHttpHeaders): MaybePromise<Http2Response | HttpResponse | WebsocketResponse>;
109
164
  renderView(path: string | FileClass, param?: Record<string, unknown>, status?: string | number, headers?: Record<string, string | number>): Promise<Http2Response | HttpResponse | WebsocketResponse>;
110
165
  /**
111
166
  * Injecte les helpers frontend (`frontendTags`/`frontendDocument`) dans les
@@ -114,14 +169,32 @@ declare class Controller extends Service implements IController {
114
169
  * valeurs fournies par l'action priment (spread `param` en dernier).
115
170
  */
116
171
  private withFrontendLocals;
117
- renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): Promise<Http2Response | HttpResponse | WebsocketResponse>;
172
+ /**
173
+ * Sérialise en JSON puis envoie ({@link renderResponse}). Synchrone quand
174
+ * l'envoi l'est (#505).
175
+ *
176
+ * @returns la réponse, ou sa promesse.
177
+ * @throws de façon SYNCHRONE ce que lève `JSON.stringify` (cycle, BigInt).
178
+ */
179
+ renderJson(obj: unknown, status?: string | number, headers?: OutgoingHttpHeaders): MaybePromise<Http2Response | HttpResponse | WebsocketResponse>;
180
+ /**
181
+ * Pose la route de la requête courante sur l'instance (per-request).
182
+ *
183
+ * `null` est accepté : un re-routage sans route matchée (forward interne)
184
+ * efface la précédente au lieu de la laisser traîner.
185
+ *
186
+ * @param route - route matchée, ou `null`.
187
+ * @returns la route posée.
188
+ * @throws TypeError sur un singleton : sa route se lit dans l'appel en cours.
189
+ */
118
190
  setRoute(route: Route): Route;
191
+ setRoute(route: Route | null): Route | null;
119
192
  getSession(): Session | undefined | null;
120
193
  redirect(url: string, status?: string | number, headers?: Record<string, string | number>): void;
121
194
  getFlashBag(key: string): unknown;
122
195
  setFlashBag(key: string, value: unknown): unknown;
123
196
  addFlash(key: string, value: unknown): unknown;
124
- forward(name: string, param?: unknown[]): Promise<unknown>;
197
+ forward(name: string, param?: unknown[]): unknown;
125
198
  /**
126
199
  * @deprecated Bloque l'event-loop (`fs.lstatSync` via `new FileClass`).
127
200
  * Utiliser {@link getFileAsync} dans tout pipeline. Conservé pour compat.
@@ -1,13 +1,15 @@
1
1
  import { Injector } from "nodefony";
2
+ import type { MaybePromise } from "nodefony";
2
3
  import type { IResolver } from "../interfaces/index.js";
3
4
  import { HttpError, ContextType } from "@nodefony/http";
4
5
  import Route, { ControllerConstructor } from "./Route.js";
5
6
  import Controller from "./Controller.js";
6
7
  import { type RouteActionMeta, type RedirectMeta } from "../decorators/routerDecorators.js";
7
8
  /**
8
- * Interface-marqueur du hook **per-request** d'un {@link Controller} : `initialize`
9
- * est appelé par le {@link Resolver} à CHAQUE requête, avant l'action (hot path —
10
- * jamais gardé/borné, contrairement au boot des services). Distinct du hook de boot
9
+ * Interface-marqueur du hook d'initialisation d'un {@link Controller} : `initialize`
10
+ * est appelé par le {@link Resolver} à la création de l'instance, avant l'action —
11
+ * UNE fois pour un singleton (défaut), à CHAQUE requête sous `@Scope("request")`
12
+ * (jamais gardé/borné, contrairement au boot des services). Distinct du hook de boot
11
13
  * `ServiceWithInit` (singleton, 1× au démarrage).
12
14
  *
13
15
  * @remarks Signature alignée sur l'appel réel `controller.initialize()` (sans arg) ;
@@ -16,16 +18,25 @@ import { type RouteActionMeta, type RedirectMeta } from "../decorators/routerDec
16
18
  export interface ControllerWithInitialize {
17
19
  initialize(): Promise<this>;
18
20
  }
21
+ /**
22
+ * Ce qu'une action a rendu, AVANT rendu sur le transport : sa valeur brute
23
+ * (éventuellement une promesse, que le rendu démêle) et la redirection
24
+ * déclarée par `@Redirect`.
25
+ */
26
+ type ActionOutcome = {
27
+ result: unknown;
28
+ redirectMeta: RedirectMeta | undefined;
29
+ };
19
30
  declare class Resolver implements IResolver {
20
- injector?: Injector | null;
31
+ injector?: Injector | null | undefined;
21
32
  controller: ControllerConstructor | null;
22
- actionName?: string;
23
- action?: (...args: unknown[]) => unknown;
33
+ actionName?: string | undefined;
34
+ action?: ((...args: unknown[]) => unknown) | undefined;
24
35
  context: ContextType;
25
36
  route: Route | null;
26
37
  resolve: boolean;
27
38
  variables: unknown[];
28
- exception?: HttpError | Error | null;
39
+ exception?: HttpError | Error | null | undefined;
29
40
  acceptedProtocol: string | null;
30
41
  bypassFirewall: boolean;
31
42
  /**
@@ -60,7 +71,7 @@ declare class Resolver implements IResolver {
60
71
  */
61
72
  messageInvocation: boolean;
62
73
  constructor(context: ContextType);
63
- match(route: Route, context: ContextType, cleanPath?: string): ((string | null)[] & Record<string, unknown>) | null | undefined;
74
+ match(route: Route, context: ContextType, cleanPath?: string, literal?: boolean): ((string | null)[] & Record<string, unknown>) | null | undefined;
64
75
  /**
65
76
  * Snapshot per-requête des variables de route matchées (`{name}` → valeur,
66
77
  * + wildcard `*` éventuel). Construit depuis les valeurs de CETTE requête
@@ -71,17 +82,66 @@ declare class Resolver implements IResolver {
71
82
  * `Context.setMetaData()` pour exposer `msg.nodefony.route.variablesMap`.
72
83
  */
73
84
  getMatchedParams(): Record<string, unknown>;
85
+ /**
86
+ * Associe chaque nom de variable de la route à la valeur capturée au match.
87
+ *
88
+ * @returns la table nom → valeur (vide sans route)
89
+ */
90
+ private _zipRouteParams;
74
91
  parsePathernController(name: string): void;
75
92
  getAction(name: string): ((...args: unknown[]) => unknown) | null;
76
- newController(context?: ContextType): Promise<Controller>;
93
+ /**
94
+ * L'instance du controller de la route, posée sur le container du contexte.
95
+ *
96
+ * Singleton (le défaut) : l'instance partagée du cache du Router — rendue
97
+ * DIRECTEMENT une fois créée, sans promesse à attendre (#505). Portée
98
+ * `request` : une instance neuve, synchrone tant que son `initialize()` ne
99
+ * rend pas de promesse.
100
+ *
101
+ * @param context - le contexte à injecter (celui du résolveur par défaut).
102
+ * @returns l'instance, ou la promesse de l'instance quand sa création attend
103
+ * (`initialize()` asynchrone).
104
+ * @throws de façon SYNCHRONE si la route n'a pas de controller, ou si sa
105
+ * construction lève ; un `initialize()` qui échoue rompt la promesse rendue.
106
+ */
107
+ newController(context?: ContextType): MaybePromise<Controller>;
108
+ /**
109
+ * Pose le pointeur `"controller"` sur le container du contexte : un message
110
+ * WS suivant ou un forward (`reload`) retrouve l'instance par ce chemin.
111
+ *
112
+ * SAUF pour une invocation par message (pont `api.request`) : le container
113
+ * est alors celui de la CONNEXION, et son pointeur désigne le contrôleur qui
114
+ * la sert (le hub). L'écraser par la cible de l'appel faisait échouer le test
115
+ * `instanceof` au message suivant — le hub était RÉINSTANCIÉ (DI +
116
+ * `initialize()`) à chaque frame qui suivait un `api.request`.
117
+ *
118
+ * @returns le controller reçu, pour enchaîner.
119
+ */
120
+ private _pinController;
77
121
  /**
78
122
  * Instancie la classe controller résolue (DI) + hooks de création : `module`
79
123
  * (constante de classe, shadow d'instance posé 1× ici — plus de write par
80
124
  * requête dans `executeAction`) puis `initialize()`. Pour un singleton,
81
125
  * `initialize()` n'est donc appelé qu'UNE fois, à la création (sémantique
82
126
  * boot) — le per-request y lit l'ALS s'il a besoin de la requête.
127
+ *
128
+ * @returns le controller prêt ; sa promesse seulement quand `initialize()`
129
+ * en rend une.
130
+ * @throws de façon SYNCHRONE ce que lèvent la DI, le constructeur ou un
131
+ * `initialize()` synchrone ; un `initialize()` asynchrone qui échoue rompt
132
+ * la promesse rendue.
83
133
  */
84
134
  private _createController;
135
+ /**
136
+ * Filet de DÉVELOPPEMENT : un singleton sert toutes les requêtes, donc une
137
+ * écriture sur l'un de ses champs, à partir d'ici, fuirait vers la suivante.
138
+ * Posé APRÈS `initialize()`, qui prépare légitimement un état de démarrage.
139
+ * Jamais hors développement : les champs y deviennent des accesseurs, un coût
140
+ * que la production ne paie pas.
141
+ *
142
+ * @returns le controller reçu, pour enchaîner.
143
+ */
144
+ private _guardSingleton;
85
145
  /**
86
146
  * Exécute l'action résolue et retourne sa **valeur brute**, SANS la rendre sur
87
147
  * le transport (pas de `returnController`/`send`). Découple « exécuter → valeur »
@@ -90,15 +150,69 @@ declare class Resolver implements IResolver {
90
150
  * (`{ id, result }`, champ GraphQL…). Le pipeline HTTP/WS normal passe par
91
151
  * {@link callController} (= `executeAction` + rendu).
92
152
  *
153
+ * Synchrone tant que rien n'est à attendre (#505) : la garde d'autorisation
154
+ * n'est attendue que sur une route gardée, l'instance que si elle est à
155
+ * créer. La valeur de l'action est rendue BRUTE — une promesse si l'action
156
+ * est asynchrone ; c'est le rendu qui la démêle.
157
+ *
93
158
  * @param data - args supplémentaires (message WS brut legacy) concaténés aux variables de route.
94
159
  * @param reload - force `newController()` (le container peut déjà porter un AUTRE controller).
95
- * @returns la valeur retournée par l'action + son `RedirectMeta` éventuel.
160
+ * @returns la valeur retournée par l'action + son `RedirectMeta` éventuel —
161
+ * ou la promesse de ce couple quand la garde ou la création attendent.
162
+ * @throws de façon SYNCHRONE ce que lève une action synchrone (ou l'absence
163
+ * d'action) ; un refus d'autorisation rompt la promesse rendue.
96
164
  */
97
- executeAction(data?: unknown[], reload?: boolean, metaArg?: RouteActionMeta): Promise<{
98
- result: unknown;
99
- redirectMeta: RedirectMeta | undefined;
100
- }>;
101
- callController(data?: unknown[], reload?: boolean): Promise<unknown>;
165
+ executeAction(data?: unknown[], reload?: boolean, metaArg?: RouteActionMeta): MaybePromise<ActionOutcome>;
166
+ /**
167
+ * Les gardes d'autorisation d'une action : `@IsGranted`/`@RequireScope`, puis
168
+ * le filet de la ZONE quand aucune garde n'a décidé du RÔLE.
169
+ *
170
+ * ⚠️ Vrai du trajet HTTP SEULEMENT. Le kernel y arme la route sans instancier
171
+ * (`http-kernel.ts` `prepareFrontController`), donc ce court-circuit est celui
172
+ * de bout en bout. En **WebSocket**, le controller est instancié au HANDSHAKE
173
+ * — avant qu'une frame ne soit gardée : sur ce transport, un refus n'évite
174
+ * ni la DI ni `initialize()`, qui ont déjà tourné.
175
+ *
176
+ * @returns `undefined` quand la route n'est pas gardée (le cas nominal : ni
177
+ * lookup, ni attente) ; sinon la promesse du verdict, rompue par un refus.
178
+ */
179
+ private _enforceActionSecurity;
180
+ /**
181
+ * Filet de la ZONE : une route dont personne n'a décidé le RÔLE hérite de
182
+ * celui de sa zone du firewall, au lieu d'être ouverte à tout compte
183
+ * authentifié.
184
+ *
185
+ * @returns `undefined` sur l'immense majorité des routes (aucune zone, ou
186
+ * zone sans rôle) ; sinon la promesse du verdict.
187
+ */
188
+ private _enforceAreaSecurity;
189
+ /**
190
+ * L'instance qui sert l'action — celle déjà posée sur le container quand
191
+ * c'est la bonne classe, sinon une création —, puis l'action elle-même.
192
+ * N'attend que la création, et seulement quand elle attend.
193
+ */
194
+ private _acquireAndInvoke;
195
+ /**
196
+ * Appelle l'action sur l'instance : état de requête (portée `request`
197
+ * seulement), arguments, méta de réponse (`@HttpCode`, `@Header`), puis
198
+ * l'appel — synchrone, sa valeur rendue brute.
199
+ *
200
+ * @throws de façon SYNCHRONE ce que lève l'action, ou l'absence d'action.
201
+ */
202
+ private _invokeAction;
203
+ /**
204
+ * Exécute l'action PUIS rend sa valeur sur le transport.
205
+ *
206
+ * Synchrone de bout en bout quand rien n'attend — route non gardée,
207
+ * controller déjà créé, action synchrone, rendu synchrone (#505).
208
+ *
209
+ * @param data - args supplémentaires (message WS brut legacy).
210
+ * @param reload - force une nouvelle instance (forward).
211
+ * @returns le résultat du rendu, ou sa promesse dès qu'une étape attend.
212
+ * @throws de façon SYNCHRONE ce que lève une étape synchrone (action,
213
+ * construction) ; les échecs asynchrones rompent la promesse rendue.
214
+ */
215
+ callController(data?: unknown[], reload?: boolean): MaybePromise<unknown>;
102
216
  /**
103
217
  * Exécute l'action AVEC la porte d'idempotence mais SANS rendu
104
218
  * (`returnController`) — le chemin du **pont `api.request`** (WS) : la valeur
@@ -196,8 +310,33 @@ declare class Resolver implements IResolver {
196
310
  * partagée ne porte aucune response).
197
311
  */
198
312
  private _applyResponseMeta;
313
+ /**
314
+ * Applique `@Redirect` à la valeur de l'action, puis la rend.
315
+ *
316
+ * Sans `@Redirect` (le cas nominal), rend directement. Avec, la valeur est
317
+ * attendue seulement si c'est une promesse — une action synchrone qui rend
318
+ * `undefined` redirige sans suspension.
319
+ *
320
+ * @returns le résultat du rendu, ou sa promesse.
321
+ */
199
322
  private _handleRedirect;
200
- returnController(result: unknown): Promise<unknown>;
323
+ /** Suite de {@link _handleRedirect}, la valeur de l'action connue. */
324
+ private _redirectOrReturn;
325
+ /**
326
+ * Rend la valeur d'une action sur le transport, selon sa nature : promesse
327
+ * (démêlée puis re-rendue), texte, Buffer, réponse déjà construite, scalaire
328
+ * ou objet (auto-JSON), rien (l'action envoie elle-même).
329
+ *
330
+ * Pas `async` : l'envelopper dans une promesse de plus ne changeait rien au
331
+ * rendu, et coûtait une suspension à chaque requête (#505). La valeur n'est
332
+ * attendue que si c'en est une.
333
+ *
334
+ * @param result - la valeur rendue par l'action.
335
+ * @returns ce que rend l'envoi (souvent sa promesse), ou `undefined` quand il
336
+ * n'y a rien à envoyer.
337
+ * @throws de façon SYNCHRONE ce que lève un envoi synchrone.
338
+ */
339
+ returnController(result: unknown): MaybePromise<unknown>;
201
340
  /** Statut courant de la réponse (0 si le transport n'en porte pas). */
202
341
  private getResponseStatus;
203
342
  }
@@ -1,7 +1,7 @@
1
1
  import Controller from "./Controller.js";
2
2
  import type { ControllerScope } from "./Controller.js";
3
3
  import type { ContextType } from "@nodefony/http";
4
- import type { IPage, IPageQuery } from "nodefony";
4
+ import type { IPage, IPageQuery, PartialInput } from "nodefony";
5
5
  /**
6
6
  * Contrat structurel du service de ressource consommé par un
7
7
  * {@link ResourceController} — aligné sur `AbstractCrudService`
@@ -31,7 +31,7 @@ export interface IResourceReadOptions {
31
31
  * par la caster, et le tri se perd en silence — la pagination devient alors
32
32
  * fausse par intermittence, ce qui est pire qu'absente.
33
33
  */
34
- order?: Array<[string, "ASC" | "DESC"]>;
34
+ order?: Array<[string, "ASC" | "DESC"]> | undefined;
35
35
  /** Associations à charger avec l'enregistrement. */
36
36
  relations?: string[];
37
37
  }
@@ -62,8 +62,8 @@ export interface IResourceService<T = unknown> {
62
62
  * de `find`.
63
63
  */
64
64
  findPage?(page: IResourcePageQuery): Promise<IPage<T>> | IPage<T>;
65
- create?(data: Partial<T>): Promise<T> | T;
66
- updateOne?(criteria: Record<string, unknown>, data: Partial<T>): Promise<T | null> | T | null;
65
+ create?(data: PartialInput<T>): Promise<T> | T;
66
+ updateOne?(criteria: Record<string, unknown>, data: PartialInput<T>): Promise<T | null> | T | null;
67
67
  delete?(criteria: Record<string, unknown>): Promise<number> | number;
68
68
  }
69
69
  /**
@@ -162,9 +162,9 @@ declare class ResourceController<T = unknown> extends Controller {
162
162
  */
163
163
  protected getResource(id: string, options?: IResourceReadOptions): Promise<T | null>;
164
164
  /** Crée une entité — 501 si la ressource est read-only. */
165
- protected createResource(data: Partial<T>): Promise<T>;
165
+ protected createResource(data: PartialInput<T>): Promise<T>;
166
166
  /** Met à jour une entité ciblée par critères — 501 si non supporté. */
167
- protected updateResource(criteria: Record<string, unknown>, data: Partial<T>): Promise<T | null>;
167
+ protected updateResource(criteria: Record<string, unknown>, data: PartialInput<T>): Promise<T | null>;
168
168
  /** Supprime par critères (nombre d'entités touchées) — 501 si non supporté. */
169
169
  protected removeResource(criteria: Record<string, unknown>): Promise<number>;
170
170
  }