@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.
- package/README.md +5 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.151.0 → _@oxc-project_runtime@0.152.0}/helpers/esm/decorateMetadata.js +1 -1
- package/dist/index.js +4 -4
- package/dist/nodefony/controller/AdminApiController.js +4 -3
- package/dist/nodefony/controller/ApiKeyController.js +3 -2
- package/dist/nodefony/controller/OAuth2Controller.js +11 -6
- package/dist/nodefony/controller/SessionAuthController.js +3 -3
- package/dist/nodefony/controller/TokenAuthController.js +4 -4
- package/dist/nodefony/controller/TotpController.js +7 -5
- package/dist/nodefony/controller/WebAuthnController.js +4 -4
- package/dist/nodefony/decorators/routerDecorators.js +41 -35
- package/dist/nodefony/service/AdminBroker.js +2 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -5
- package/dist/nodefony/service/router.js +78 -26
- package/dist/nodefony/src/Controller.js +184 -78
- package/dist/nodefony/src/FrameworkAdminApi.js +19 -6
- package/dist/nodefony/src/KernelAdminApi.js +49 -49
- package/dist/nodefony/src/PlaygroundAdminApi.js +3 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +2 -2
- package/dist/nodefony/src/Resolver.js +221 -65
- package/dist/nodefony/src/Route.js +62 -39
- package/dist/nodefony/src/SyslogAdminApi.js +4 -4
- package/dist/nodefony/src/docsReader.js +49 -39
- package/dist/nodefony/src/scopeCatalog.js +1 -1
- package/dist/nodefony/src/singletonGuard.js +76 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -1
- package/dist/types/nodefony/controller/BenchController.d.ts +2 -2
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +3 -3
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +1 -1
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +2 -1
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +2 -2
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -1
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +49 -43
- package/dist/types/nodefony/interfaces/IController.d.ts +9 -7
- package/dist/types/nodefony/interfaces/IResolver.d.ts +10 -10
- package/dist/types/nodefony/interfaces/IRoute.d.ts +13 -13
- package/dist/types/nodefony/service/AdminBroker.d.ts +3 -3
- package/dist/types/nodefony/service/router.d.ts +33 -7
- package/dist/types/nodefony/src/Controller.d.ts +97 -24
- package/dist/types/nodefony/src/Resolver.d.ts +155 -16
- package/dist/types/nodefony/src/ResourceController.d.ts +6 -6
- package/dist/types/nodefony/src/Route.d.ts +39 -23
- package/dist/types/nodefony/src/docsReader.d.ts +19 -9
- package/dist/types/nodefony/src/singletonGuard.d.ts +53 -0
- package/docs/admin.md +1 -1
- package/docs/controller.md +139 -121
- package/docs/decorateurs.md +102 -98
- package/docs/idempotence.md +23 -21
- package/docs/index.md +1 -1
- package/docs/routing.md +60 -53
- package/docs/templates.md +14 -14
- 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):
|
|
23
|
-
callController(data?: unknown[], reload?: boolean):
|
|
24
|
-
returnController(result: 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`.
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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:
|
|
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
|
-
*
|
|
36
|
+
* Portée d'instanciation d'un contrôleur.
|
|
36
37
|
*
|
|
37
|
-
* - `"
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
50
|
-
* posé par le décorateur `@Scope` (statique hérité, lu via
|
|
51
|
-
*
|
|
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
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
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
|
|
67
|
-
*
|
|
68
|
-
*
|
|
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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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[]):
|
|
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
|
|
9
|
-
* est appelé par le {@link Resolver} à
|
|
10
|
-
*
|
|
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
|
-
|
|
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):
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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:
|
|
66
|
-
updateOne?(criteria: Record<string, unknown>, data:
|
|
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:
|
|
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:
|
|
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
|
}
|