@nodefony/framework 10.0.0-beta.2 → 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.152.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/index.js +15 -20
- package/dist/nodefony/config/config.js +1 -2
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controller/AdminApiController.js +45 -11
- package/dist/nodefony/controller/ApiKeyController.js +18 -10
- package/dist/nodefony/controller/BenchController.js +1 -2
- package/dist/nodefony/controller/IssuerMetadataController.js +1 -2
- package/dist/nodefony/controller/LoginPageController.js +117 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -17
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +1 -2
- package/dist/nodefony/controller/SessionAuthController.js +50 -13
- package/dist/nodefony/controller/TokenAuthController.js +14 -8
- package/dist/nodefony/controller/TotpController.js +7 -8
- package/dist/nodefony/controller/WebAuthnController.js +28 -12
- package/dist/nodefony/controller/oauthAuthority.js +1 -2
- package/dist/nodefony/decorators/routerDecorators.js +268 -65
- 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 +97 -34
- package/dist/nodefony/src/Controller.js +71 -43
- package/dist/nodefony/src/FrameworkAdminApi.js +33 -11
- package/dist/nodefony/src/KernelAdminApi.js +82 -18
- package/dist/nodefony/src/PlaygroundAdminApi.js +72 -3
- package/dist/nodefony/src/RedisIdempotencyStore.js +46 -4
- package/dist/nodefony/src/Resolver.js +105 -36
- package/dist/nodefony/src/ResourceController.js +2 -3
- package/dist/nodefony/src/Route.js +54 -8
- package/dist/nodefony/src/SecurityApiController.js +23 -0
- 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 +39 -23
- package/dist/nodefony/src/idempotency.js +14 -22
- package/dist/nodefony/src/idempotencyGc.js +1 -2
- package/dist/nodefony/src/idempotencyStoreRegistry.js +1 -2
- package/dist/nodefony/src/loginPage.js +430 -0
- package/dist/nodefony/src/requireAuthentication.js +35 -0
- 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 +8 -12
- package/dist/types/nodefony/controller/AdminApiController.d.ts +12 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +2 -2
- package/dist/types/nodefony/controller/LoginPageController.d.ts +32 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +67 -8
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +46 -8
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +6 -4
- package/dist/types/nodefony/controller/TotpController.d.ts +2 -2
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +12 -4
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +221 -35
- package/dist/types/nodefony/interfaces/IController.d.ts +2 -3
- package/dist/types/nodefony/interfaces/IRoute.d.ts +1 -2
- package/dist/types/nodefony/service/router.d.ts +40 -1
- package/dist/types/nodefony/src/Controller.d.ts +54 -17
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +12 -0
- package/dist/types/nodefony/src/Resolver.d.ts +44 -1
- package/dist/types/nodefony/src/ResourceController.d.ts +1 -1
- package/dist/types/nodefony/src/Route.d.ts +21 -1
- package/dist/types/nodefony/src/SecurityApiController.d.ts +17 -0
- package/dist/types/nodefony/src/docsReader.d.ts +10 -6
- package/dist/types/nodefony/src/idempotency.d.ts +12 -10
- package/dist/types/nodefony/src/loginPage.d.ts +144 -0
- package/dist/types/nodefony/src/requireAuthentication.d.ts +29 -0
- package/dist/types/nodefony/src/routeSchema.d.ts +112 -0
- package/docs/admin.md +83 -78
- package/docs/controller.md +112 -111
- package/docs/decorateurs.md +212 -163
- package/docs/idempotence.md +137 -135
- package/docs/routing.md +96 -95
- package/docs/templates.md +55 -55
- package/package.json +10 -13
- package/dist/_virtual/_@oxc-project_runtime@0.152.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -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 } 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.
|
|
@@ -52,25 +51,13 @@ function invalidateRouteIndex() {
|
|
|
52
51
|
function buildRouteIndex() {
|
|
53
52
|
const statics = /* @__PURE__ */ new Map();
|
|
54
53
|
const dynamics = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
statics.set(key, list);
|
|
63
|
-
}
|
|
64
|
-
list.push({
|
|
65
|
-
route,
|
|
66
|
-
pos: i,
|
|
67
|
-
prefix: ""
|
|
68
|
-
});
|
|
69
|
-
} else dynamics.push({
|
|
70
|
-
route,
|
|
71
|
-
pos: i,
|
|
72
|
-
prefix: literalPrefix(path)
|
|
73
|
-
});
|
|
54
|
+
const total = routes.length;
|
|
55
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
56
|
+
const fallbackPass = pass === 1;
|
|
57
|
+
for (const [rank, route] of routes.entries()) {
|
|
58
|
+
if (route.fallback !== fallbackPass) continue;
|
|
59
|
+
indexRoute(route, fallbackPass ? total + rank : rank, statics, dynamics);
|
|
60
|
+
}
|
|
74
61
|
}
|
|
75
62
|
return routeIndex = {
|
|
76
63
|
statics,
|
|
@@ -80,10 +67,45 @@ function buildRouteIndex() {
|
|
|
80
67
|
last: routes[routes.length - 1]
|
|
81
68
|
};
|
|
82
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Range une route dans l'index — littérale par son chemin exact, dynamique
|
|
72
|
+
* sinon — à la position `i` qui décide de son rang dans la fusion de `resolve`.
|
|
73
|
+
*/
|
|
74
|
+
function indexRoute(route, i, statics, dynamics) {
|
|
75
|
+
const path = route.path;
|
|
76
|
+
if (path !== void 0 && route.variables.length === 0 && !REG_NON_LITERAL.test(path)) {
|
|
77
|
+
const key = path.toLowerCase();
|
|
78
|
+
let list = statics.get(key);
|
|
79
|
+
if (list === void 0) {
|
|
80
|
+
list = [];
|
|
81
|
+
statics.set(key, list);
|
|
82
|
+
}
|
|
83
|
+
list.push({
|
|
84
|
+
route,
|
|
85
|
+
pos: i,
|
|
86
|
+
prefix: ""
|
|
87
|
+
});
|
|
88
|
+
} else dynamics.push({
|
|
89
|
+
route,
|
|
90
|
+
pos: i,
|
|
91
|
+
prefix: literalPrefix(path)
|
|
92
|
+
});
|
|
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");
|
|
83
101
|
let Router = class Router extends Service {
|
|
84
102
|
static {
|
|
85
103
|
_Router = this;
|
|
86
104
|
}
|
|
105
|
+
/** Marque {@link FRAMEWORK_ROUTER} — sur le prototype, aucun champ par instance. */
|
|
106
|
+
get [FRAMEWORK_ROUTER]() {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
87
109
|
static routes = routes;
|
|
88
110
|
routes = _Router.routes;
|
|
89
111
|
singletonControllers = null;
|
|
@@ -127,6 +149,23 @@ let Router = class Router extends Service {
|
|
|
127
149
|
return pending;
|
|
128
150
|
}
|
|
129
151
|
/**
|
|
152
|
+
* Dit si au moins une route déclare le transport `WEBSOCKET` pour ce chemin
|
|
153
|
+
* — contrôle d'avant `101` du pipeline WebSocket (cf `IRequestRouter`).
|
|
154
|
+
*
|
|
155
|
+
* Parcours de la table entière, sans l'index de `resolve` : il est lu une
|
|
156
|
+
* fois par CONNEXION (pas par message), et une route dont les méthodes
|
|
157
|
+
* changent après coup (`addRequirement`) ne laisse ainsi aucun index périmé.
|
|
158
|
+
* Même normalisation que `resolve` (`stripTrailingSlashes`), même motif.
|
|
159
|
+
*
|
|
160
|
+
* @param pathname - chemin de l'URL d'upgrade
|
|
161
|
+
* @returns `true` si une route WebSocket a un motif qui couvre ce chemin
|
|
162
|
+
*/
|
|
163
|
+
servesWebsocket(pathname) {
|
|
164
|
+
const path = stripTrailingSlashes(pathname);
|
|
165
|
+
for (const route of routes) if (route.methodsSet?.has("WEBSOCKET") === true && route.pattern?.test(path) === true) return true;
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
130
169
|
* Résout une route pour un contexte donné.
|
|
131
170
|
*
|
|
132
171
|
* @param context - le contexte HTTP/WS courant (porte container, méthode, URL…).
|
|
@@ -138,7 +177,10 @@ let Router = class Router extends Service {
|
|
|
138
177
|
* WEBSOCKET (pont WS-RPC `api.request` d'une MUTATION) : lève l'ambiguïté
|
|
139
178
|
* GET-via-WS / POST-via-WS sur un même chemin (`context.method` = "WEBSOCKET").
|
|
140
179
|
* `undefined` (GET/HTTP) → match historique sur `context.method`.
|
|
141
|
-
* @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.
|
|
142
184
|
*/
|
|
143
185
|
resolve(context, cleanPathOverride, methodOverride) {
|
|
144
186
|
const resolver = new Resolver(context);
|
|
@@ -171,8 +213,8 @@ let Router = class Router extends Service {
|
|
|
171
213
|
}
|
|
172
214
|
try {
|
|
173
215
|
if (resolver.match(route, context, cleanPath, fromLiterals)) {
|
|
174
|
-
|
|
175
|
-
if (
|
|
216
|
+
routeMatchLogged ??= this.kernel?.environment !== "production";
|
|
217
|
+
if (routeMatchLogged) this.log(`Match route : ${route.name}`, "DEBUG");
|
|
176
218
|
resolver.exception = void 0;
|
|
177
219
|
routeExpectsBodyStream(route);
|
|
178
220
|
return resolver;
|
|
@@ -192,11 +234,13 @@ let Router = class Router extends Service {
|
|
|
192
234
|
}
|
|
193
235
|
const servesWebsocket = allowed.delete("WEBSOCKET");
|
|
194
236
|
if (allowed.size === 0 && servesWebsocket) {
|
|
195
|
-
context.
|
|
237
|
+
const req = context.request;
|
|
238
|
+
if ((req && "request" in req ? req.request : req)?.httpVersionMajor !== 2) context.response?.setHeaders({
|
|
196
239
|
Upgrade: "websocket",
|
|
197
240
|
Connection: "Upgrade"
|
|
198
241
|
});
|
|
199
|
-
|
|
242
|
+
resolver.exception = new HttpError("Upgrade Required: websocket", 426, context);
|
|
243
|
+
return resolver;
|
|
200
244
|
}
|
|
201
245
|
if (allowed.size > 0) {
|
|
202
246
|
const allowHeader = Array.from(allowed).join(", ");
|
|
@@ -247,6 +291,7 @@ let Router = class Router extends Service {
|
|
|
247
291
|
}
|
|
248
292
|
}
|
|
249
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.`);
|
|
250
295
|
const routenew = new Route(name, obj);
|
|
251
296
|
routes.push(routenew);
|
|
252
297
|
invalidateRouteIndex();
|
|
@@ -278,7 +323,10 @@ let Router = class Router extends Service {
|
|
|
278
323
|
});
|
|
279
324
|
const key = `${module.name}:${myconstructor.name}`;
|
|
280
325
|
if (Module.controllers[key]) module.log(/* @__PURE__ */ new Error(`Controller already exist ${key}`), "WARNING");
|
|
281
|
-
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
|
+
}
|
|
282
330
|
return Module.controllers[key] = myconstructor;
|
|
283
331
|
}
|
|
284
332
|
/**
|
|
@@ -290,7 +338,22 @@ let Router = class Router extends Service {
|
|
|
290
338
|
return routes.filter((r) => r.controller === myconstructor);
|
|
291
339
|
}
|
|
292
340
|
};
|
|
293
|
-
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
|
+
}
|
|
294
358
|
var router_default = Router;
|
|
295
|
-
|
|
296
|
-
export { router_default as default };
|
|
359
|
+
export { FRAMEWORK_ROUTER, router_default as default, isFrameworkRouter };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { isDevelopment, singletonWriteMessage } from "./singletonGuard.js";
|
|
2
2
|
import { FileClass, RequestContext, Service } from "nodefony";
|
|
3
|
-
import { HttpError } from "@nodefony/http";
|
|
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
|
/**
|
|
@@ -167,6 +166,15 @@ var Controller = class extends Service {
|
|
|
167
166
|
this.#assertPerRequest("response");
|
|
168
167
|
this.#response = response;
|
|
169
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* L'utilisateur CONNECTÉ, ou `null` — anonyme, ou route hors de toute zone
|
|
171
|
+
* du pare-feu. C'est l'identité que le pare-feu a établie pour cette
|
|
172
|
+
* requête : aucun appel de plus. Une page publique qui veut savoir qui la
|
|
173
|
+
* visite se place sous une zone `authenticators: ["session", "anonymous"]`.
|
|
174
|
+
*/
|
|
175
|
+
get user() {
|
|
176
|
+
return RequestContext.getAuthenticatedUser();
|
|
177
|
+
}
|
|
170
178
|
get method() {
|
|
171
179
|
return this.#method ?? this.context?.method ?? void 0;
|
|
172
180
|
}
|
|
@@ -175,21 +183,11 @@ var Controller = class extends Service {
|
|
|
175
183
|
this.#method = method ?? null;
|
|
176
184
|
}
|
|
177
185
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
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.
|
|
181
190
|
*/
|
|
182
|
-
get queryGet() {
|
|
183
|
-
if (this.#queryGet !== null) return this.#queryGet;
|
|
184
|
-
if (this.#context !== null) return this.#context.request?.queryGet;
|
|
185
|
-
const store = RequestContext.get();
|
|
186
|
-
return (store?.resolver)?.queryOverride ?? ((store?.context)?.request)?.queryGet;
|
|
187
|
-
}
|
|
188
|
-
set queryGet(value) {
|
|
189
|
-
this.#assertPerRequest("queryGet");
|
|
190
|
-
this.#queryGet = value;
|
|
191
|
-
}
|
|
192
|
-
/** Comme {@link queryGet}, pour la query fusionnée (`query`). */
|
|
193
191
|
get query() {
|
|
194
192
|
if (this.#query !== null) return this.#query;
|
|
195
193
|
if (this.#context !== null) return this.#context.request?.query;
|
|
@@ -200,35 +198,30 @@ var Controller = class extends Service {
|
|
|
200
198
|
this.#assertPerRequest("query");
|
|
201
199
|
this.#query = value;
|
|
202
200
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
get queryPost() {
|
|
211
|
-
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;
|
|
212
208
|
}
|
|
213
|
-
set
|
|
214
|
-
this.#assertPerRequest("
|
|
215
|
-
this.#
|
|
209
|
+
set files(value) {
|
|
210
|
+
this.#assertPerRequest("files");
|
|
211
|
+
this.#files = value;
|
|
216
212
|
}
|
|
217
213
|
/**
|
|
218
214
|
* Le CORPS de la requête, parsé — nom universel de l'écosystème (Express,
|
|
219
|
-
* Fastify, NestJS)
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
* corps. Pour une action typée, préférer le décorateur `@Body()`.
|
|
223
|
-
*
|
|
224
|
-
* 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()`.
|
|
225
218
|
*/
|
|
226
219
|
get body() {
|
|
227
|
-
return this.
|
|
220
|
+
return this.#body ?? (this.context?.request)?.body;
|
|
228
221
|
}
|
|
229
222
|
set body(value) {
|
|
230
223
|
this.#assertPerRequest("body");
|
|
231
|
-
this
|
|
224
|
+
this.#body = value;
|
|
232
225
|
}
|
|
233
226
|
/**
|
|
234
227
|
* Session courante, ou `null`. Getter direct sur `context.session` (peuplé au
|
|
@@ -345,14 +338,13 @@ var Controller = class extends Service {
|
|
|
345
338
|
const fe = this.get("frontend");
|
|
346
339
|
if (!fe?.renderTags) return param;
|
|
347
340
|
const nonce = this.context?.cspNonce;
|
|
348
|
-
const host = this.context?.domain;
|
|
349
341
|
const renderTags = fe.renderTags;
|
|
350
342
|
const renderDocument = fe.renderDocument;
|
|
351
343
|
return {
|
|
352
|
-
frontendTags: (entry) => renderTags(entry, nonce
|
|
344
|
+
frontendTags: (entry) => renderTags(entry, nonce),
|
|
353
345
|
frontendDocument: (entry) => {
|
|
354
346
|
if (!renderDocument) throw new Error("frontend service does not expose renderDocument");
|
|
355
|
-
return renderDocument(entry, nonce
|
|
347
|
+
return renderDocument(entry, nonce);
|
|
356
348
|
},
|
|
357
349
|
asset: (p) => fe.assetUrl ? fe.assetUrl(p) : p,
|
|
358
350
|
...param
|
|
@@ -370,6 +362,43 @@ var Controller = class extends Service {
|
|
|
370
362
|
this.setContextJson();
|
|
371
363
|
return this.renderResponse(data, "utf8", status, headers);
|
|
372
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Le client demande-t-il un flux d'événements (`Accept: text/event-stream`) ?
|
|
367
|
+
*
|
|
368
|
+
* La route ne change pas de méthode pour autant : un même `POST` répond en
|
|
369
|
+
* JSON ou en flux selon cette réponse. Toujours `false` sur WebSocket.
|
|
370
|
+
*
|
|
371
|
+
* @returns `true` quand `text/event-stream` est accepté avec un poids non nul.
|
|
372
|
+
*/
|
|
373
|
+
acceptsSse() {
|
|
374
|
+
if (!(this.context instanceof HttpContext)) return false;
|
|
375
|
+
return acceptsEventStream(this.context.request.headers.accept);
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Répond par un flux d'événements serveur, en HTTP/1.1 comme en HTTP/2.
|
|
379
|
+
*
|
|
380
|
+
* L'action garde la main : elle `send()` à son rythme et `close()` quand elle
|
|
381
|
+
* a fini ; le départ du client ferme le flux (`sse.closed`, `sse.onClose`).
|
|
382
|
+
* Rendre le flux depuis l'action est sans effet — la réponse est déjà prise.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* @Get("/progress")
|
|
387
|
+
* async progress() {
|
|
388
|
+
* const sse = await this.renderSse();
|
|
389
|
+
* for (const step of steps) await sse.send(step, { event: "step" });
|
|
390
|
+
* return sse.close();
|
|
391
|
+
* }
|
|
392
|
+
* ```
|
|
393
|
+
*
|
|
394
|
+
* @param options - battement de cœur, délai `retry:` initial.
|
|
395
|
+
* @returns le flux, ou sa promesse quand une session est à sauver d'abord.
|
|
396
|
+
* @throws HttpError 500 hors HTTP (WebSocket) ou quand la réponse est partie.
|
|
397
|
+
*/
|
|
398
|
+
renderSse(options) {
|
|
399
|
+
if (!(this.context instanceof HttpContext)) throw new HttpError("SSE : un flux d'événements exige une requête HTTP", 500, this.context);
|
|
400
|
+
return this.context.openSse(options);
|
|
401
|
+
}
|
|
373
402
|
setRoute(route) {
|
|
374
403
|
this.#assertPerRequest("route");
|
|
375
404
|
this.#route = route;
|
|
@@ -617,5 +646,4 @@ var Controller = class extends Service {
|
|
|
617
646
|
return this.streamFile(File, head, value);
|
|
618
647
|
}
|
|
619
648
|
};
|
|
620
|
-
//#endregion
|
|
621
649
|
export { Controller as default, parseByteRange };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import router_default from "../service/router.js";
|
|
2
2
|
import { createPlaygroundEndpoints } from "./PlaygroundAdminApi.js";
|
|
3
|
-
import { parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
-
|
|
3
|
+
import { PageQueryError, parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
+
// nodefony/src/FrameworkAdminApi.ts
|
|
5
5
|
/**
|
|
6
6
|
* Capacités de page d'un endpoint, ÉVALUÉES sous filet.
|
|
7
7
|
*
|
|
@@ -29,6 +29,34 @@ function readPageCapabilities(caps) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
+
* Lit le paramètre `filters` de `routes/page` : un tableau JSON de
|
|
33
|
+
* `{ key, op, value }`. Absent → aucun filtre.
|
|
34
|
+
*
|
|
35
|
+
* @throws PageQueryError (400) si le JSON est cassé ou d'une autre forme — un
|
|
36
|
+
* filtre ignoré rendrait TOUTES les routes, lues comme son résultat.
|
|
37
|
+
*/
|
|
38
|
+
function parseRouteFilters(raw) {
|
|
39
|
+
if (!raw) return [];
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
throw new PageQueryError(`Invalid "filters": not JSON.`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(parsed)) throw new PageQueryError(`Invalid "filters": expected a JSON array.`);
|
|
47
|
+
const entries = parsed;
|
|
48
|
+
const filters = [];
|
|
49
|
+
for (const item of entries) {
|
|
50
|
+
if (typeof item !== "object" || item === null || !("key" in item) || !("op" in item) || typeof item.key !== "string" || typeof item.op !== "string") throw new PageQueryError(`Invalid "filters": each entry needs a string "key" and "op".`);
|
|
51
|
+
filters.push({
|
|
52
|
+
key: item.key,
|
|
53
|
+
op: item.op,
|
|
54
|
+
value: "value" in item ? item.value : void 0
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return filters;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
32
60
|
* Producteur `IAdminApi` du module **framework** — exposé sous
|
|
33
61
|
* `/nodefony/framework/api/*`.
|
|
34
62
|
*
|
|
@@ -68,7 +96,8 @@ function createFrameworkAdminApi(broker, opts) {
|
|
|
68
96
|
scope: route.controller?.scope ?? null,
|
|
69
97
|
module: route.module?.name ?? null,
|
|
70
98
|
host: route.host ?? null,
|
|
71
|
-
bypassFirewall: route.bypassFirewall
|
|
99
|
+
bypassFirewall: route.bypassFirewall,
|
|
100
|
+
fallback: route.fallback
|
|
72
101
|
});
|
|
73
102
|
const descriptor = {
|
|
74
103
|
label: "Routes",
|
|
@@ -204,13 +233,7 @@ function createFrameworkAdminApi(broker, opts) {
|
|
|
204
233
|
});
|
|
205
234
|
parseFilters(request.query, {}, { accepts: ["filters"] });
|
|
206
235
|
const search = query.q?.toLowerCase() ?? "";
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const raw = one(request.query.filters);
|
|
210
|
-
if (raw) filters = JSON.parse(raw);
|
|
211
|
-
} catch {
|
|
212
|
-
filters = [];
|
|
213
|
-
}
|
|
236
|
+
const filters = parseRouteFilters(one(request.query.filters));
|
|
214
237
|
let rows = router_default.routes.map(serializeRoute);
|
|
215
238
|
if (search) rows = rows.filter((r) => matchesSearch(r, search));
|
|
216
239
|
for (const f of filters) rows = rows.filter((r) => matchOp(cell(r, f.key), f.op, f.value));
|
|
@@ -296,5 +319,4 @@ function createFrameworkAdminApi(broker, opts) {
|
|
|
296
319
|
adminEndpoints: () => endpoints
|
|
297
320
|
};
|
|
298
321
|
}
|
|
299
|
-
//#endregion
|
|
300
322
|
export { createFrameworkAdminApi };
|