@nodefony/framework 10.0.0-beta.3 → 10.0.0-beta.4
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 +4 -0
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +9 -18
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +38 -7
- package/dist/nodefony/controller/ApiKeyController.js +16 -8
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +4 -11
- package/dist/nodefony/controller/OAuth2Controller.js +3 -4
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +13 -4
- package/dist/nodefony/controller/TokenAuthController.js +3 -4
- package/dist/nodefony/controller/TotpController.js +2 -3
- package/dist/nodefony/controller/WebAuthnController.js +2 -3
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +155 -53
- package/dist/nodefony/service/AdminBroker.js +4 -6
- package/dist/nodefony/service/Eta.js +1 -2
- package/dist/nodefony/service/IdempotencyStore.js +4 -6
- package/dist/nodefony/service/router.js +49 -15
- package/dist/nodefony/src/Controller.js +22 -39
- package/dist/nodefony/src/FrameworkAdminApi.js +31 -10
- package/dist/nodefony/src/KernelAdminApi.js +7 -9
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +66 -34
- package/dist/nodefony/src/ResourceController.js +2 -3
- package/dist/nodefony/src/Route.js +2 -3
- package/dist/nodefony/src/SecurityApiController.js +1 -2
- package/dist/nodefony/src/SyslogAdminApi.js +1 -2
- package/dist/nodefony/src/Template.js +1 -2
- package/dist/nodefony/src/configMutation.js +1 -2
- package/dist/nodefony/src/docsReader.js +1 -2
- package/dist/nodefony/src/idempotency.js +1 -2
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +2 -3
- package/dist/nodefony/src/requireAuthentication.js +1 -2
- package/dist/nodefony/src/routeSchema.js +81 -0
- package/dist/nodefony/src/scopeCatalog.js +1 -2
- package/dist/nodefony/src/singletonGuard.js +1 -2
- package/dist/nodefony/src/syslogFilters.js +1 -2
- package/dist/types/index.d.ts +5 -10
- package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +7 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +133 -32
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/service/router.d.ts +27 -1
- package/dist/types/nodefony/src/Controller.d.ts +14 -16
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +25 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
- package/dist/types/nodefony/src/Route.d.ts +3 -0
- package/dist/types/nodefony/src/loginPage.d.ts +1 -1
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/docs/admin.md +80 -80
- package/docs/controller.md +108 -109
- package/docs/decorateurs.md +208 -161
- package/docs/idempotence.md +133 -133
- package/docs/routing.md +90 -89
- package/docs/templates.md +55 -55
- package/package.json +8 -11
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { assertRouteSchemaShape } from "../src/routeSchema.js";
|
|
1
2
|
import Controller from "../src/Controller.js";
|
|
2
3
|
import router_default from "../service/router.js";
|
|
3
|
-
import { RequestContext } from "nodefony";
|
|
4
|
+
import { BootConfigurationError, RequestContext } from "nodefony";
|
|
4
5
|
import "reflect-metadata";
|
|
5
|
-
|
|
6
|
+
// nodefony/decorators/routerDecorators.ts
|
|
6
7
|
const metadataKey = "routes:definitions";
|
|
7
8
|
/**
|
|
8
9
|
* Noms déjà portés par `Controller` — méthodes ET accesseurs, les siens comme
|
|
@@ -134,7 +135,7 @@ function controller(prefix) {
|
|
|
134
135
|
const classBypass = Reflect.getMetadata(BYPASS_FIREWALL_CLASS_METADATA, mycontroller);
|
|
135
136
|
if (methodBypass === true || classBypass === true) options.bypassFirewall = true;
|
|
136
137
|
}
|
|
137
|
-
if (options.path
|
|
138
|
+
if (options.path === "*") {
|
|
138
139
|
hasMagic = {
|
|
139
140
|
options,
|
|
140
141
|
name
|
|
@@ -149,26 +150,19 @@ function controller(prefix) {
|
|
|
149
150
|
return mycontroller;
|
|
150
151
|
};
|
|
151
152
|
}
|
|
152
|
-
/**
|
|
153
|
-
* Crée une route avec le nom et les options spécifiés.
|
|
154
|
-
*
|
|
155
|
-
* @param name - Le nom de la route.
|
|
156
|
-
* @param options - Les options de la route.
|
|
157
|
-
* @returns Un décorateur de méthode qui peut être utilisé pour annoter une méthode de contrôleur.
|
|
158
|
-
*
|
|
159
|
-
* @example
|
|
160
|
-
* \@route("myroute", {
|
|
161
|
-
* path: "/add/{name}",
|
|
162
|
-
* method: ["GET", "POST"],
|
|
163
|
-
* defaults: { name: "john" },
|
|
164
|
-
* })
|
|
165
|
-
* method(name: string) {
|
|
166
|
-
* return this.renderJson({ name });
|
|
167
|
-
* }
|
|
168
|
-
*/
|
|
169
153
|
function route(name, options) {
|
|
154
|
+
return defineRoute(name, options);
|
|
155
|
+
}
|
|
156
|
+
/** L'implémentation de {@link route}, sans les surcharges de typage. */
|
|
157
|
+
function defineRoute(name, options) {
|
|
170
158
|
return function(target, propertyKey, descriptor) {
|
|
171
159
|
assertActionNameFree(target, propertyKey);
|
|
160
|
+
if (options.schema !== void 0) {
|
|
161
|
+
const previous = Reflect.getOwnMetadata(ROUTE_SCHEMA_METADATA, target, propertyKey);
|
|
162
|
+
if (previous !== void 0 && previous !== options.schema) throw new BootConfigurationError(`${target.constructor.name}.${propertyKey} : deux routes de la même action déclarent des schémas différents — une action n'a qu'une entrée, déclarer le schéma une seule fois.`);
|
|
163
|
+
Reflect.defineMetadata(ROUTE_SCHEMA_METADATA, options.schema, target, propertyKey);
|
|
164
|
+
}
|
|
165
|
+
if (options.bodyStream === true) Reflect.defineMetadata(BODY_STREAM_METADATA, true, target, propertyKey);
|
|
172
166
|
const className = target.constructor.name;
|
|
173
167
|
const classMethod = propertyKey;
|
|
174
168
|
const prefix = options.prefix || null;
|
|
@@ -211,6 +205,10 @@ function extractControllerFilePath(stackTrace) {
|
|
|
211
205
|
const HTTP_CODE_METADATA = "route:httpCode";
|
|
212
206
|
const HEADERS_METADATA = "route:responseHeaders";
|
|
213
207
|
const REDIRECT_METADATA = "route:redirect";
|
|
208
|
+
/** Schéma d'entrée d'une action (`schema` d'une option de route) — par méthode. */
|
|
209
|
+
const ROUTE_SCHEMA_METADATA = "route:schema";
|
|
210
|
+
/** `bodyStream: true` d'une option de route — par méthode. */
|
|
211
|
+
const BODY_STREAM_METADATA = "route:bodyStream";
|
|
214
212
|
const PARAM_ARGS_METADATA = "route:paramArgs";
|
|
215
213
|
const DOMAIN_CLASS_METADATA = "route:domainClass";
|
|
216
214
|
const DOMAIN_METHOD_METADATA = "route:domainMethod";
|
|
@@ -232,7 +230,7 @@ function httpMethodDecorator(methods) {
|
|
|
232
230
|
const declaredMethods = options.requirements?.methods;
|
|
233
231
|
const added = declaredMethods === void 0 ? [] : Array.isArray(declaredMethods) ? declaredMethods : [declaredMethods];
|
|
234
232
|
const fusion = added.length > 0 ? [.../* @__PURE__ */ new Set([...methods, ...added])] : methods;
|
|
235
|
-
return
|
|
233
|
+
return defineRoute(name, {
|
|
236
234
|
...options,
|
|
237
235
|
path,
|
|
238
236
|
requirements: {
|
|
@@ -255,14 +253,14 @@ const Head = httpMethodDecorator(["HEAD"]);
|
|
|
255
253
|
* HTTP (équivalent NestJS `@All()`). N'émet aucun requirement `methods`, donc
|
|
256
254
|
* `Route.matchRequirements` ne lève jamais 405 sur la méthode.
|
|
257
255
|
*/
|
|
258
|
-
|
|
256
|
+
const All = function(path = "", options = {}) {
|
|
259
257
|
return function(target, propertyKey, descriptor) {
|
|
260
|
-
return
|
|
258
|
+
return defineRoute(`${target.constructor.name}::${propertyKey}`, {
|
|
261
259
|
...options,
|
|
262
260
|
path
|
|
263
261
|
})(target, propertyKey, descriptor);
|
|
264
262
|
};
|
|
265
|
-
}
|
|
263
|
+
};
|
|
266
264
|
/**
|
|
267
265
|
* Fixe le code de statut HTTP de la réponse d'une action.
|
|
268
266
|
*
|
|
@@ -722,26 +720,18 @@ const Query = paramDecoratorFactory("query");
|
|
|
722
720
|
*
|
|
723
721
|
* ## Le corps n'est PAS validé — le type écrit ici ne promet rien
|
|
724
722
|
*
|
|
725
|
-
* `@Body()` injecte le corps **tel qu'il a été
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
729
|
-
* taxerait toute requête à paramètres décorés).
|
|
723
|
+
* `@Body()` injecte le corps **tel qu'il a été décodé** (objet JSON ou de
|
|
724
|
+
* formulaire, chaîne pour `text/*`, octets `Uint8Array` pour tout autre type) :
|
|
725
|
+
* `@Body() dto: CreateOrder` compile, mais rien ne garantit qu'un `CreateOrder`
|
|
726
|
+
* soit arrivé.
|
|
730
727
|
*
|
|
731
|
-
*
|
|
728
|
+
* Pour un contrat vérifié, déclarer l'option de route `schema` (ADR-0018) : le
|
|
729
|
+
* corps est validé AVANT l'action — 422 qui nomme chaque champ —, sur HTTP comme
|
|
730
|
+
* sur le pont WebSocket, et l'action le reçoit typé dans `input.body`. Une action
|
|
731
|
+
* à schéma ne prend aucun décorateur de paramètre.
|
|
732
732
|
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
* (unicité en base) y est possible — et ils gardent REST, WebSocket **et** la
|
|
736
|
-
* CLI d'un seul geste. C'est ce que génère `nodefony create entity`.
|
|
737
|
-
* - **Un cas isolé** → `schema.parse(body)` en première ligne de l'action, sur le
|
|
738
|
-
* modèle d'`assertPageQuery`. Rien d'autre à écrire : une `ZodError` qui remonte
|
|
739
|
-
* devient un **422** (RFC 9110 §15.5.21) portant `error.fields` — quel champ,
|
|
740
|
-
* quel message, quelle règle.
|
|
741
|
-
*
|
|
742
|
-
* Et typer le paramètre avec `z.infer<typeof createXSchema>` plutôt qu'avec
|
|
743
|
-
* `Partial<XRow>` : le premier décrit le contrat d'**entrée**, le second promet la
|
|
744
|
-
* ligne de **table** (`id`, horodatages) que le schéma effacera de toute façon.
|
|
733
|
+
* Sans schéma, `schema.parse(body)` en première ligne de l'action reste le geste
|
|
734
|
+
* d'un cas isolé : une `ZodError` qui remonte devient le même **422**.
|
|
745
735
|
*
|
|
746
736
|
* @example
|
|
747
737
|
* ```ts
|
|
@@ -778,6 +768,75 @@ const Res = paramDecoratorFactory("res");
|
|
|
778
768
|
const UploadedFile = paramDecoratorFactory("file");
|
|
779
769
|
const UploadedFiles = paramDecoratorFactory("files");
|
|
780
770
|
/**
|
|
771
|
+
* La query de l'invocation : celle du chemin INVOQUÉ par le pont WebSocket
|
|
772
|
+
* (`queryOverride`), sinon celle de l'URL. Source UNIQUE de `@Query` et de
|
|
773
|
+
* `input.query` d'une route à schéma.
|
|
774
|
+
*/
|
|
775
|
+
function readQuery(ctx) {
|
|
776
|
+
return readQueryOf(ctx.queryOverride, ctx.request);
|
|
777
|
+
}
|
|
778
|
+
/** {@link readQuery}, depuis ses deux sources. */
|
|
779
|
+
function readQueryOf(queryOverride, request) {
|
|
780
|
+
return queryOverride ?? request?.query;
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Le corps DÉCODÉ de l'invocation — source UNIQUE de `@Body` et de
|
|
784
|
+
* `input.body` d'une route à schéma, pour qu'un corps ne puisse pas prendre
|
|
785
|
+
* deux sens selon la forme de l'action.
|
|
786
|
+
*
|
|
787
|
+
* Pont `api.request` (WS) : le corps d'une mutation voyage dans l'ALS (aucun
|
|
788
|
+
* corps HTTP parsé sur une frame — posé par appel par
|
|
789
|
+
* `RealtimeController.invokeApiRequest`, zéro fuite entre appels) et PRIME sur
|
|
790
|
+
* `request.body` (vide en WS) — même priorité que
|
|
791
|
+
* `AdminApiController.buildRequest`. En HTTP, le magasin ALS ne porte jamais
|
|
792
|
+
* `body`. Aucune forme n'est supposée : un codec binaire y posera des octets.
|
|
793
|
+
*/
|
|
794
|
+
function readBody(ctx) {
|
|
795
|
+
return readInvocationBody(ctx.request);
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Le corps DÉCODÉ d'une invocation, depuis la requête seule — la règle de
|
|
799
|
+
* {@link readBody}, pour qui n'a pas de contexte de paramètres (porte
|
|
800
|
+
* d'idempotence).
|
|
801
|
+
*
|
|
802
|
+
* @param request - la requête du contexte.
|
|
803
|
+
* @returns le corps posé par le pont WebSocket, sinon le corps HTTP lu.
|
|
804
|
+
*/
|
|
805
|
+
function readInvocationBody(request) {
|
|
806
|
+
const alsBody = RequestContext.get()?.body;
|
|
807
|
+
return alsBody !== void 0 ? alsBody : request?.body;
|
|
808
|
+
}
|
|
809
|
+
/** Un champ d'une valeur décodée ; `undefined` si elle n'est pas un objet. */
|
|
810
|
+
function readField(value, key) {
|
|
811
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
812
|
+
return Reflect.get(value, key);
|
|
813
|
+
}
|
|
814
|
+
/** Objet vide partagé : une partie absente n'alloue rien. Gelé, jamais muté. */
|
|
815
|
+
const EMPTY_PART = Object.freeze({});
|
|
816
|
+
/**
|
|
817
|
+
* L'entrée BRUTE d'une action à schéma, lue aux MÊMES sources que les
|
|
818
|
+
* décorateurs de paramètre ({@link readQuery}, {@link readBody}) : pont
|
|
819
|
+
* WebSocket compris. Seule allocation de la voie à schéma : cet objet, propre
|
|
820
|
+
* à l'invocation, que la validation complète en place et que l'action reçoit.
|
|
821
|
+
*
|
|
822
|
+
* Arguments POSITIONNELS, pas un objet de contexte : la voie à schéma n'en
|
|
823
|
+
* alloue aucun de plus.
|
|
824
|
+
*
|
|
825
|
+
* @param paramsMap - variables de route nommées.
|
|
826
|
+
* @param request - la requête du contexte.
|
|
827
|
+
* @param queryOverride - query du chemin invoqué (pont WebSocket), sinon rien.
|
|
828
|
+
* @param bodyStream - `true` : `body` est le flux brut, jamais lu.
|
|
829
|
+
* @returns les quatre parties, brutes.
|
|
830
|
+
*/
|
|
831
|
+
function readRouteInput(paramsMap, request, queryOverride, bodyStream) {
|
|
832
|
+
return {
|
|
833
|
+
params: paramsMap,
|
|
834
|
+
query: readQueryOf(queryOverride, request) ?? EMPTY_PART,
|
|
835
|
+
body: bodyStream ? request?.request : readInvocationBody(request),
|
|
836
|
+
headers: request?.headers ?? EMPTY_PART
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
781
840
|
* Résout la valeur d'un unique paramètre décoré depuis le contexte de requête.
|
|
782
841
|
*
|
|
783
842
|
* @param meta - métadonnée posée par le décorateur (source + clé optionnelle)
|
|
@@ -788,15 +847,13 @@ function resolveParamArg(meta, ctx) {
|
|
|
788
847
|
switch (meta.source) {
|
|
789
848
|
case "param": return meta.key !== void 0 ? ctx.paramsMap[meta.key] : ctx.paramsMap;
|
|
790
849
|
case "query": {
|
|
791
|
-
const
|
|
792
|
-
return meta.key !== void 0 ?
|
|
850
|
+
const q = readQuery(ctx);
|
|
851
|
+
return meta.key !== void 0 ? q?.[meta.key] : q;
|
|
793
852
|
}
|
|
794
853
|
case "body": {
|
|
795
854
|
if (meta.stream) return ctx.request?.request;
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
const qp = ctx.request?.queryPost;
|
|
799
|
-
return meta.key !== void 0 ? qp?.[meta.key] : qp;
|
|
855
|
+
const body = readBody(ctx);
|
|
856
|
+
return meta.key !== void 0 ? readField(body, meta.key) : body;
|
|
800
857
|
}
|
|
801
858
|
case "headers": {
|
|
802
859
|
const h = ctx.request?.headers;
|
|
@@ -806,8 +863,8 @@ function resolveParamArg(meta, ctx) {
|
|
|
806
863
|
case "session": return meta.key !== void 0 ? ctx.session?.get(meta.key) : ctx.session;
|
|
807
864
|
case "req": return ctx.request;
|
|
808
865
|
case "res": return ctx.response;
|
|
809
|
-
case "file": return ctx.request?.
|
|
810
|
-
case "files": return ctx.request?.
|
|
866
|
+
case "file": return ctx.request?.files?.[0];
|
|
867
|
+
case "files": return ctx.request?.files;
|
|
811
868
|
case "user": return RequestContext.isAnonymous() ? void 0 : RequestContext.getUser();
|
|
812
869
|
default: return;
|
|
813
870
|
}
|
|
@@ -866,6 +923,48 @@ function getParamArgsMeta(proto, method) {
|
|
|
866
923
|
return metas !== void 0 && metas.length > 0 ? metas : null;
|
|
867
924
|
}
|
|
868
925
|
/**
|
|
926
|
+
* Le schéma d'entrée d'une méthode (option de route `schema`), ou `null`.
|
|
927
|
+
* Lecture `Reflect` : à faire une fois, hors du chemin de chaque requête.
|
|
928
|
+
*
|
|
929
|
+
* @param proto - le prototype de la classe.
|
|
930
|
+
* @param method - le nom de la méthode.
|
|
931
|
+
* @returns le schéma, ou `null` — une action sans schéma ne paie rien.
|
|
932
|
+
* @throws BootConfigurationError si le schéma rangé est mal formé.
|
|
933
|
+
*/
|
|
934
|
+
function getRouteSchema(proto, method) {
|
|
935
|
+
const schema = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
|
|
936
|
+
if (schema === void 0) return null;
|
|
937
|
+
assertRouteSchemaShape(schema, `${proto.constructor.name}.${method}`);
|
|
938
|
+
return schema;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Refuse, au DÉMARRAGE, une action à schéma mal déclarée — appelé par
|
|
942
|
+
* `Router.setController` pour chaque route du contrôleur, pour qu'une faute
|
|
943
|
+
* ne se révèle pas à la première requête :
|
|
944
|
+
*
|
|
945
|
+
* - schéma mal formé (partie inconnue, valeur qui n'est pas un Standard Schema) ;
|
|
946
|
+
* - schéma ET décorateurs de paramètre (`@Body`…) : une action à schéma reçoit
|
|
947
|
+
* un argument UNIQUE, les deux formes ne se combinent pas ;
|
|
948
|
+
* - `bodyStream: true` ET `schema.body` : un flux ne se valide pas.
|
|
949
|
+
*
|
|
950
|
+
* @param routeDef - la route (nom, contrôleur, méthode).
|
|
951
|
+
* @throws BootConfigurationError qui nomme la route.
|
|
952
|
+
*/
|
|
953
|
+
function assertRouteSchemaDeclaration(routeDef) {
|
|
954
|
+
const ctor = routeDef.controller;
|
|
955
|
+
const method = routeDef.classMethod;
|
|
956
|
+
if (!ctor || !method) return;
|
|
957
|
+
const proto = ctor.prototype;
|
|
958
|
+
const raw = Reflect.getMetadata(ROUTE_SCHEMA_METADATA, proto, method);
|
|
959
|
+
if (raw === void 0) {
|
|
960
|
+
if (Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » exige un « schema » — sans schéma, demander le flux par @Body({ stream: true }).`);
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
assertRouteSchemaShape(raw, routeDef.name);
|
|
964
|
+
if (getParamArgsMeta(proto, method) !== null) throw new BootConfigurationError(`Route "${routeDef.name}" : une action à schéma reçoit un argument unique (input) — retirer les décorateurs de paramètre (@Body, @Query, @Param…) de ${proto.constructor.name}.${method}, ou retirer l'option « schema ».`);
|
|
965
|
+
if (raw.body !== void 0 && Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true) throw new BootConfigurationError(`Route "${routeDef.name}" : « bodyStream: true » et « schema.body » s'excluent — un flux n'est pas lu, il ne peut pas être validé.`);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
869
968
|
* P2.9 — Indique si l'action d'une route attend le **flux brut** du body
|
|
870
969
|
* (un paramètre `@Body({ stream:true })`). Le résultat est **mémoïsé** sur
|
|
871
970
|
* `route.bodyStream` : lecture `Reflect` au 1er appel, O(1) ensuite → 0 coût
|
|
@@ -880,13 +979,15 @@ function routeExpectsBodyStream(routeDef) {
|
|
|
880
979
|
let flag = false;
|
|
881
980
|
const ctor = routeDef.controller;
|
|
882
981
|
const method = routeDef.classMethod;
|
|
883
|
-
if (ctor && method) flag =
|
|
982
|
+
if (ctor && method) flag = Reflect.getMetadata(BODY_STREAM_METADATA, ctor.prototype, method) === true || (getParamArgsMeta(ctor.prototype, method)?.some((m) => m.source === "body" && m.stream === true) ?? false);
|
|
884
983
|
routeDef.bodyStream = flag;
|
|
885
984
|
}
|
|
886
985
|
return routeDef.bodyStream;
|
|
887
986
|
}
|
|
888
987
|
const EMPTY_ACTION_META = {
|
|
889
988
|
paramsMeta: null,
|
|
989
|
+
schema: null,
|
|
990
|
+
bodyStream: false,
|
|
890
991
|
redirectMeta: null,
|
|
891
992
|
httpCode: null,
|
|
892
993
|
headerEntries: null,
|
|
@@ -988,6 +1089,8 @@ function computeActionMeta(ctor, method) {
|
|
|
988
1089
|
const headers = Reflect.getMetadata(HEADERS_METADATA, proto, method);
|
|
989
1090
|
return {
|
|
990
1091
|
paramsMeta: getParamArgsMeta(proto, method),
|
|
1092
|
+
schema: getRouteSchema(proto, method),
|
|
1093
|
+
bodyStream: Reflect.getMetadata(BODY_STREAM_METADATA, proto, method) === true,
|
|
991
1094
|
redirectMeta: redirect ?? null,
|
|
992
1095
|
httpCode: httpCode ?? null,
|
|
993
1096
|
headerEntries: headers ? Object.entries(headers) : null,
|
|
@@ -1073,5 +1176,4 @@ function computeAuthenticationRequirement(ctor, method) {
|
|
|
1073
1176
|
amr: Object.freeze([.../* @__PURE__ */ new Set([...onClass.amr, ...onMethod.amr])])
|
|
1074
1177
|
});
|
|
1075
1178
|
}
|
|
1076
|
-
|
|
1077
|
-
export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
|
|
1179
|
+
export { All, Anonymous, BYPASS_FIREWALL_CLASS_METADATA, BYPASS_FIREWALL_METHOD_METADATA, Body, BypassFirewall, Cookie, Csp, CsrfExempt, CsrfProtect, CurrentUser, DOMAIN_CLASS_METADATA, DOMAIN_METHOD_METADATA, Delete, Domain, Get, HEADERS_METADATA, HTTP_CODE_METADATA, Head, Header, Headers, HttpCode, Idempotent, IsGranted, Options, PARAM_ARGS_METADATA, Param, Patch, Post, Put, Query, REDIRECT_METADATA, Redirect, Req, RequireAuthentication, RequireScope, Res, Scope, Session, USE_SESSION_CLASS_METADATA, USE_SESSION_METHOD_METADATA, UploadedFile, UploadedFiles, UseSession, assertRouteSchemaDeclaration, buildContextParamArgs, buildParamArgs, computeActionMeta, controller, controllers, extractActionScopes, getParamArgsMeta, getRouteSchema, readInvocationBody, readRouteInput, resolveActionMeta, resolveParamArg, resolveSessionIntent, route, routeExpectsBodyStream };
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import __decorate from "../../_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorate.js";
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
3
2
|
import router_default from "./router.js";
|
|
4
3
|
import AdminApiController from "../controller/AdminApiController.js";
|
|
5
|
-
import { ADMIN_DEFAULT_ROLE,
|
|
6
|
-
|
|
4
|
+
import { ADMIN_DEFAULT_ROLE, Service, injectable, resolveAdminRole } from "nodefony";
|
|
5
|
+
// nodefony/service/AdminBroker.ts
|
|
7
6
|
const serviceName = "adminBroker";
|
|
8
7
|
/**
|
|
9
8
|
* Implémentation du data plane admin (Studio) — collecte les {@link IAdminApi}
|
|
@@ -101,7 +100,6 @@ let AdminBroker = class AdminBroker extends Service {
|
|
|
101
100
|
return this.get("router");
|
|
102
101
|
}
|
|
103
102
|
};
|
|
104
|
-
AdminBroker = __decorate([injectable()
|
|
103
|
+
AdminBroker = __decorate([injectable()], AdminBroker);
|
|
105
104
|
var AdminBroker_default = AdminBroker;
|
|
106
|
-
//#endregion
|
|
107
105
|
export { AdminBroker_default as default };
|
|
@@ -2,7 +2,7 @@ import Template from "../src/Template.js";
|
|
|
2
2
|
import { extend, injectable } from "nodefony";
|
|
3
3
|
import { readFile } from "node:fs/promises";
|
|
4
4
|
import { Eta as Eta$1 } from "eta";
|
|
5
|
-
|
|
5
|
+
// nodefony/service/Eta.ts
|
|
6
6
|
/**
|
|
7
7
|
* Options par défaut du moteur Eta.
|
|
8
8
|
*
|
|
@@ -64,5 +64,4 @@ var Eta = class extends Template {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
|
-
//#endregion
|
|
68
67
|
export { Eta as default };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
//#region nodefony/service/IdempotencyStore.ts
|
|
1
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
2
|
+
import { Service, assertPageQuery, injectable } from "nodefony";
|
|
3
|
+
// nodefony/service/IdempotencyStore.ts
|
|
5
4
|
const serviceName = "idempotencyStore";
|
|
6
5
|
/** Réponses mémorisées : 10 min (un rejeu plausible reste dans cette fenêtre). */
|
|
7
6
|
const DEFAULT_TTL_MS = 6e5;
|
|
@@ -129,7 +128,6 @@ let MemoryIdempotencyStore = class MemoryIdempotencyStore extends Service {
|
|
|
129
128
|
}
|
|
130
129
|
}
|
|
131
130
|
};
|
|
132
|
-
MemoryIdempotencyStore = __decorate([injectable()
|
|
131
|
+
MemoryIdempotencyStore = __decorate([injectable()], MemoryIdempotencyStore);
|
|
133
132
|
var IdempotencyStore_default = MemoryIdempotencyStore;
|
|
134
|
-
//#endregion
|
|
135
133
|
export { IdempotencyStore_default as default };
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import Route from "../src/Route.js";
|
|
2
|
-
import { routeExpectsBodyStream } from "../decorators/routerDecorators.js";
|
|
2
|
+
import { assertRouteSchemaDeclaration, routeExpectsBodyStream } from "../decorators/routerDecorators.js";
|
|
3
3
|
import Resolver from "../src/Resolver.js";
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import { Injector, Module, Service, injectable, isPromise, stripTrailingSlashes } from "nodefony";
|
|
4
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.155.0/helpers/esm/decorate.js";
|
|
5
|
+
import { BootConfigurationError, Injector, Module, Service, injectable, isPromise, stripTrailingSlashes } from "nodefony";
|
|
7
6
|
import { HttpError, isDomainAllowed } from "@nodefony/http";
|
|
8
|
-
|
|
7
|
+
// nodefony/service/router.ts
|
|
9
8
|
var _Router;
|
|
10
|
-
let
|
|
9
|
+
let routeMatchLogged = null;
|
|
11
10
|
/**
|
|
12
11
|
* Méthodes que sert une route — lues sur le `methodsSet` COMPILÉ par
|
|
13
12
|
* `Route.compileRequirements`, jamais recalculées depuis la config brute.
|
|
@@ -92,10 +91,21 @@ function indexRoute(route, i, statics, dynamics) {
|
|
|
92
91
|
prefix: literalPrefix(path)
|
|
93
92
|
});
|
|
94
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Marque du Router de `@nodefony/framework` — ce que {@link isFrameworkRouter}
|
|
96
|
+
* lit. `Symbol.for` : la même marque pour deux copies installées du framework,
|
|
97
|
+
* là où `instanceof` échouerait en silence. Un faux routeur de test s'en
|
|
98
|
+
* réclame explicitement.
|
|
99
|
+
*/
|
|
100
|
+
const FRAMEWORK_ROUTER = Symbol.for("nodefony.framework.router");
|
|
95
101
|
let Router = class Router extends Service {
|
|
96
102
|
static {
|
|
97
103
|
_Router = this;
|
|
98
104
|
}
|
|
105
|
+
/** Marque {@link FRAMEWORK_ROUTER} — sur le prototype, aucun champ par instance. */
|
|
106
|
+
get [FRAMEWORK_ROUTER]() {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
99
109
|
static routes = routes;
|
|
100
110
|
routes = _Router.routes;
|
|
101
111
|
singletonControllers = null;
|
|
@@ -167,7 +177,10 @@ let Router = class Router extends Service {
|
|
|
167
177
|
* WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
|
|
168
178
|
* GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
|
|
169
179
|
* `undefined` (GET/HTTP) → match historique sur `context.method`.
|
|
170
|
-
* @returns un `Resolver` (`.resolve === true` si une route a matché)
|
|
180
|
+
* @returns un `Resolver` (`.resolve === true` si une route a matché) — ou,
|
|
181
|
+
* pour un chemin servi en WebSocket SEULEMENT et demandé en HTTP, un
|
|
182
|
+
* `Resolver` dont `exception` porte le `426` (en-têtes `Upgrade` posés) :
|
|
183
|
+
* le pipeline le lève après le pare-feu.
|
|
171
184
|
*/
|
|
172
185
|
resolve(context, cleanPathOverride, methodOverride) {
|
|
173
186
|
const resolver = new Resolver(context);
|
|
@@ -200,8 +213,8 @@ let Router = class Router extends Service {
|
|
|
200
213
|
}
|
|
201
214
|
try {
|
|
202
215
|
if (resolver.match(route, context, cleanPath, fromLiterals)) {
|
|
203
|
-
|
|
204
|
-
if (
|
|
216
|
+
routeMatchLogged ??= this.kernel?.environment !== "production";
|
|
217
|
+
if (routeMatchLogged) this.log(`Match route : ${route.name}`, "DEBUG");
|
|
205
218
|
resolver.exception = void 0;
|
|
206
219
|
routeExpectsBodyStream(route);
|
|
207
220
|
return resolver;
|
|
@@ -221,11 +234,13 @@ let Router = class Router extends Service {
|
|
|
221
234
|
}
|
|
222
235
|
const servesWebsocket = allowed.delete("WEBSOCKET");
|
|
223
236
|
if (allowed.size === 0 && servesWebsocket) {
|
|
224
|
-
context.
|
|
237
|
+
const req = context.request;
|
|
238
|
+
if ((req && "request" in req ? req.request : req)?.httpVersionMajor !== 2) context.response?.setHeaders({
|
|
225
239
|
Upgrade: "websocket",
|
|
226
240
|
Connection: "Upgrade"
|
|
227
241
|
});
|
|
228
|
-
|
|
242
|
+
resolver.exception = new HttpError("Upgrade Required: websocket", 426, context);
|
|
243
|
+
return resolver;
|
|
229
244
|
}
|
|
230
245
|
if (allowed.size > 0) {
|
|
231
246
|
const allowHeader = Array.from(allowed).join(", ");
|
|
@@ -276,6 +291,7 @@ let Router = class Router extends Service {
|
|
|
276
291
|
}
|
|
277
292
|
}
|
|
278
293
|
static createRoute(name, obj) {
|
|
294
|
+
if ("schema" in obj || "bodyStream" in obj) throw new BootConfigurationError(`Route "${name}" : « schema » et « bodyStream » se déclarent par un décorateur de route (@Get, @Post, @route…), pas par createRoute.`);
|
|
279
295
|
const routenew = new Route(name, obj);
|
|
280
296
|
routes.push(routenew);
|
|
281
297
|
invalidateRouteIndex();
|
|
@@ -307,7 +323,10 @@ let Router = class Router extends Service {
|
|
|
307
323
|
});
|
|
308
324
|
const key = `${module.name}:${myconstructor.name}`;
|
|
309
325
|
if (Module.controllers[key]) module.log(/* @__PURE__ */ new Error(`Controller already exist ${key}`), "WARNING");
|
|
310
|
-
for (const r of routes) if (r.controller === myconstructor)
|
|
326
|
+
for (const r of routes) if (r.controller === myconstructor) {
|
|
327
|
+
assertRouteSchemaDeclaration(r);
|
|
328
|
+
r.module = { name: module.name };
|
|
329
|
+
}
|
|
311
330
|
return Module.controllers[key] = myconstructor;
|
|
312
331
|
}
|
|
313
332
|
/**
|
|
@@ -319,7 +338,22 @@ let Router = class Router extends Service {
|
|
|
319
338
|
return routes.filter((r) => r.controller === myconstructor);
|
|
320
339
|
}
|
|
321
340
|
};
|
|
322
|
-
Router = _Router = __decorate([injectable()
|
|
341
|
+
Router = _Router = __decorate([injectable()], Router);
|
|
342
|
+
/**
|
|
343
|
+
* Dit si le service `router` d'un contexte est le Router de
|
|
344
|
+
* `@nodefony/framework` — la garde qui rétrécit le contrat `IRequestRouter` de
|
|
345
|
+
* `@nodefony/http` sans conversion, pour qui lit des membres propres au
|
|
346
|
+
* framework (le Resolver complet que rend `resolve`).
|
|
347
|
+
*
|
|
348
|
+
* Lit la marque {@link FRAMEWORK_ROUTER}, jamais `instanceof` : deux copies
|
|
349
|
+
* installées du framework feraient échouer `instanceof` en silence. Une
|
|
350
|
+
* lecture de propriété.
|
|
351
|
+
*
|
|
352
|
+
* @param router - le routeur lu sur le contexte (`context.router`).
|
|
353
|
+
* @returns `true` si c'est le Router du framework.
|
|
354
|
+
*/
|
|
355
|
+
function isFrameworkRouter(router) {
|
|
356
|
+
return router != null && FRAMEWORK_ROUTER in router;
|
|
357
|
+
}
|
|
323
358
|
var router_default = Router;
|
|
324
|
-
|
|
325
|
-
export { router_default as default };
|
|
359
|
+
export { FRAMEWORK_ROUTER, router_default as default, isFrameworkRouter };
|
|
@@ -3,7 +3,7 @@ import { FileClass, RequestContext, Service } from "nodefony";
|
|
|
3
3
|
import { HttpContext, HttpError, acceptsEventStream } from "@nodefony/http";
|
|
4
4
|
import fs, { createReadStream } from "node:fs";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
-
|
|
6
|
+
// nodefony/src/Controller.ts
|
|
7
7
|
const fsClose = promisify(fs.close);
|
|
8
8
|
/**
|
|
9
9
|
* Un `<script>` EN LIGNE qui ne porte pas d'attribut `nonce`.
|
|
@@ -108,10 +108,9 @@ var Controller = class extends Service {
|
|
|
108
108
|
#request = null;
|
|
109
109
|
#response = null;
|
|
110
110
|
#method = null;
|
|
111
|
-
#queryGet = null;
|
|
112
111
|
#query = null;
|
|
113
|
-
#
|
|
114
|
-
#
|
|
112
|
+
#files = null;
|
|
113
|
+
#body = null;
|
|
115
114
|
module;
|
|
116
115
|
template;
|
|
117
116
|
/**
|
|
@@ -184,21 +183,11 @@ var Controller = class extends Service {
|
|
|
184
183
|
this.#method = method ?? null;
|
|
185
184
|
}
|
|
186
185
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
186
|
+
* Paramètres d'URL de l'appel en cours — jamais le corps, qui reste dans
|
|
187
|
+
* {@link body}. Pour un singleton invoqué par le pont `api.request`,
|
|
188
|
+
* ceux du chemin INVOQUÉ (résolveur d'appel dans l'ALS), pas ceux du
|
|
189
|
+
* handshake de la connexion. Une seule lecture de l'ALS.
|
|
190
190
|
*/
|
|
191
|
-
get queryGet() {
|
|
192
|
-
if (this.#queryGet !== null) return this.#queryGet;
|
|
193
|
-
if (this.#context !== null) return this.#context.request?.queryGet;
|
|
194
|
-
const store = RequestContext.get();
|
|
195
|
-
return (store?.resolver)?.queryOverride ?? ((store?.context)?.request)?.queryGet;
|
|
196
|
-
}
|
|
197
|
-
set queryGet(value) {
|
|
198
|
-
this.#assertPerRequest("queryGet");
|
|
199
|
-
this.#queryGet = value;
|
|
200
|
-
}
|
|
201
|
-
/** Comme {@link queryGet}, pour la query fusionnée (`query`). */
|
|
202
191
|
get query() {
|
|
203
192
|
if (this.#query !== null) return this.#query;
|
|
204
193
|
if (this.#context !== null) return this.#context.request?.query;
|
|
@@ -209,35 +198,30 @@ var Controller = class extends Service {
|
|
|
209
198
|
this.#assertPerRequest("query");
|
|
210
199
|
this.#query = value;
|
|
211
200
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
get queryPost() {
|
|
220
|
-
return this.#queryPost ?? (this.context?.request)?.queryPost;
|
|
201
|
+
/**
|
|
202
|
+
* Les fichiers reçus dans un corps multipart (`UploadedFile`) ; les champs
|
|
203
|
+
* texte du même corps sont dans {@link body}. Pour une action typée,
|
|
204
|
+
* préférer `@UploadedFiles()`.
|
|
205
|
+
*/
|
|
206
|
+
get files() {
|
|
207
|
+
return this.#files ?? (this.context?.request)?.files;
|
|
221
208
|
}
|
|
222
|
-
set
|
|
223
|
-
this.#assertPerRequest("
|
|
224
|
-
this.#
|
|
209
|
+
set files(value) {
|
|
210
|
+
this.#assertPerRequest("files");
|
|
211
|
+
this.#files = value;
|
|
225
212
|
}
|
|
226
213
|
/**
|
|
227
214
|
* Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
|
|
228
|
-
* Fastify, NestJS)
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* corps. Pour une action typée, préférer le décorateur `@Body()`.
|
|
232
|
-
*
|
|
233
|
-
* Getter (aucune allocation) : `queryPost` reste la source unique.
|
|
215
|
+
* Fastify, NestJS). Jamais un paramètre d'URL : ceux-là sont dans
|
|
216
|
+
* {@link query}, et les deux ne fusionnent pas. Pour une action typée,
|
|
217
|
+
* préférer le décorateur `@Body()`.
|
|
234
218
|
*/
|
|
235
219
|
get body() {
|
|
236
|
-
return this.
|
|
220
|
+
return this.#body ?? (this.context?.request)?.body;
|
|
237
221
|
}
|
|
238
222
|
set body(value) {
|
|
239
223
|
this.#assertPerRequest("body");
|
|
240
|
-
this
|
|
224
|
+
this.#body = value;
|
|
241
225
|
}
|
|
242
226
|
/**
|
|
243
227
|
* Session courante, ou `null`. Getter direct sur `context.session` (peuplé au
|
|
@@ -662,5 +646,4 @@ var Controller = class extends Service {
|
|
|
662
646
|
return this.streamFile(File, head, value);
|
|
663
647
|
}
|
|
664
648
|
};
|
|
665
|
-
//#endregion
|
|
666
649
|
export { Controller as default, parseByteRange };
|