@nodefony/framework 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +50 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +211 -0
- package/dist/nodefony/config/config.js +61 -0
- package/dist/nodefony/config/defineModuleConfig.js +36 -0
- package/dist/nodefony/controller/AdminApiController.js +163 -0
- package/dist/nodefony/controller/ApiKeyController.js +151 -0
- package/dist/nodefony/controller/BenchController.js +132 -0
- package/dist/nodefony/controller/IssuerMetadataController.js +148 -0
- package/dist/nodefony/controller/OAuth2Controller.js +133 -0
- package/dist/nodefony/controller/ProtectedResourceMetadataController.js +221 -0
- package/dist/nodefony/controller/SessionAuthController.js +141 -0
- package/dist/nodefony/controller/TokenAuthController.js +113 -0
- package/dist/nodefony/controller/TotpController.js +129 -0
- package/dist/nodefony/controller/WebAuthnController.js +242 -0
- package/dist/nodefony/controller/oauthAuthority.js +74 -0
- package/dist/nodefony/decorators/routerDecorators.js +967 -0
- package/dist/nodefony/interfaces/IAdminBroker.js +1 -0
- package/dist/nodefony/interfaces/IController.js +1 -0
- package/dist/nodefony/interfaces/IIdempotencyStore.js +1 -0
- package/dist/nodefony/interfaces/IResolver.js +1 -0
- package/dist/nodefony/interfaces/IRoute.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/AdminBroker.js +106 -0
- package/dist/nodefony/service/Eta.js +68 -0
- package/dist/nodefony/service/IdempotencyStore.js +136 -0
- package/dist/nodefony/service/router.js +243 -0
- package/dist/nodefony/src/Controller.js +515 -0
- package/dist/nodefony/src/FrameworkAdminApi.js +268 -0
- package/dist/nodefony/src/KernelAdminApi.js +1243 -0
- package/dist/nodefony/src/PlaygroundAdminApi.js +97 -0
- package/dist/nodefony/src/RedisIdempotencyStore.js +254 -0
- package/dist/nodefony/src/Resolver.js +416 -0
- package/dist/nodefony/src/ResourceController.js +148 -0
- package/dist/nodefony/src/Route.js +476 -0
- package/dist/nodefony/src/SyslogAdminApi.js +466 -0
- package/dist/nodefony/src/Template.js +15 -0
- package/dist/nodefony/src/configMutation.js +186 -0
- package/dist/nodefony/src/docsReader.js +929 -0
- package/dist/nodefony/src/idempotency.js +137 -0
- package/dist/nodefony/src/idempotencyGc.js +32 -0
- package/dist/nodefony/src/idempotencyStoreRegistry.js +36 -0
- package/dist/nodefony/src/scopeCatalog.js +40 -0
- package/dist/nodefony/src/syslogFilters.js +51 -0
- package/dist/types/index.d.ts +96 -0
- package/dist/types/nodefony/config/config.d.ts +41 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
- package/dist/types/nodefony/controller/AdminApiController.d.ts +70 -0
- package/dist/types/nodefony/controller/ApiKeyController.d.ts +49 -0
- package/dist/types/nodefony/controller/BenchController.d.ts +45 -0
- package/dist/types/nodefony/controller/IssuerMetadataController.d.ts +86 -0
- package/dist/types/nodefony/controller/OAuth2Controller.d.ts +81 -0
- package/dist/types/nodefony/controller/ProtectedResourceMetadataController.d.ts +147 -0
- package/dist/types/nodefony/controller/SessionAuthController.d.ts +74 -0
- package/dist/types/nodefony/controller/TokenAuthController.d.ts +56 -0
- package/dist/types/nodefony/controller/TotpController.d.ts +42 -0
- package/dist/types/nodefony/controller/WebAuthnController.d.ts +123 -0
- package/dist/types/nodefony/controller/oauthAuthority.d.ts +54 -0
- package/dist/types/nodefony/decorators/routerDecorators.d.ts +632 -0
- package/dist/types/nodefony/interfaces/IAdminBroker.d.ts +79 -0
- package/dist/types/nodefony/interfaces/IController.d.ts +38 -0
- package/dist/types/nodefony/interfaces/IIdempotencyStore.d.ts +1 -0
- package/dist/types/nodefony/interfaces/IResolver.d.ts +25 -0
- package/dist/types/nodefony/interfaces/IRoute.d.ts +31 -0
- package/dist/types/nodefony/interfaces/index.d.ts +5 -0
- package/dist/types/nodefony/service/AdminBroker.d.ts +37 -0
- package/dist/types/nodefony/service/Eta.d.ts +25 -0
- package/dist/types/nodefony/service/IdempotencyStore.d.ts +45 -0
- package/dist/types/nodefony/service/router.d.ts +53 -0
- package/dist/types/nodefony/src/Controller.d.ts +193 -0
- package/dist/types/nodefony/src/FrameworkAdminApi.d.ts +35 -0
- package/dist/types/nodefony/src/KernelAdminApi.d.ts +71 -0
- package/dist/types/nodefony/src/PlaygroundAdminApi.d.ts +98 -0
- package/dist/types/nodefony/src/RedisIdempotencyStore.d.ts +104 -0
- package/dist/types/nodefony/src/Resolver.d.ts +165 -0
- package/dist/types/nodefony/src/ResourceController.d.ts +171 -0
- package/dist/types/nodefony/src/Route.d.ts +192 -0
- package/dist/types/nodefony/src/SyslogAdminApi.d.ts +38 -0
- package/dist/types/nodefony/src/Template.d.ts +8 -0
- package/dist/types/nodefony/src/configMutation.d.ts +109 -0
- package/dist/types/nodefony/src/docsReader.d.ts +369 -0
- package/dist/types/nodefony/src/idempotency.d.ts +96 -0
- package/dist/types/nodefony/src/idempotencyGc.d.ts +30 -0
- package/dist/types/nodefony/src/idempotencyStoreRegistry.d.ts +59 -0
- package/dist/types/nodefony/src/scopeCatalog.d.ts +26 -0
- package/dist/types/nodefony/src/syslogFilters.d.ts +52 -0
- package/docs/admin.md +451 -0
- package/docs/controller.md +645 -0
- package/docs/decorateurs.md +845 -0
- package/docs/idempotence.md +741 -0
- package/docs/index.md +151 -0
- package/docs/routing.md +648 -0
- package/docs/templates.md +380 -0
- package/package.json +83 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import router_default from "../service/router.js";
|
|
2
|
+
import { createPlaygroundEndpoints } from "./PlaygroundAdminApi.js";
|
|
3
|
+
import { parseFilters, parsePageQuery } from "nodefony";
|
|
4
|
+
//#region nodefony/src/FrameworkAdminApi.ts
|
|
5
|
+
/**
|
|
6
|
+
* Capacités de page d'un endpoint, ÉVALUÉES sous filet.
|
|
7
|
+
*
|
|
8
|
+
* `sortable` et `search` sont des fonctions parce que la réponse dépend du
|
|
9
|
+
* store effectivement branché au démarrage — donc elles peuvent lever. Ce
|
|
10
|
+
* catalogue est lu au chargement de la console : une exception ici privait
|
|
11
|
+
* l'application entière de sa navigation à cause d'un seul module en panne. Ne
|
|
12
|
+
* rien publier est le repli sûr — le plan REFUSE en 400 ce qui n'est pas
|
|
13
|
+
* déclaré, une capacité tue ne promet donc rien de faux.
|
|
14
|
+
*
|
|
15
|
+
* @param caps - la déclaration du producteur, ou `undefined`.
|
|
16
|
+
* @returns le fragment `{ page }` à fusionner, ou rien.
|
|
17
|
+
*/
|
|
18
|
+
function readPageCapabilities(caps) {
|
|
19
|
+
if (!caps) return {};
|
|
20
|
+
try {
|
|
21
|
+
return { page: {
|
|
22
|
+
sortable: caps.sortable?.() ?? [],
|
|
23
|
+
filters: caps.filters ?? {},
|
|
24
|
+
search: caps.search?.() ?? false,
|
|
25
|
+
facets: caps.facets ?? {}
|
|
26
|
+
} };
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Producteur `IAdminApi` du module **framework** — exposé sous
|
|
33
|
+
* `/nodefony/framework/api/*`.
|
|
34
|
+
*
|
|
35
|
+
* 3ᵉ producteur du data plane admin (kernel, http, puis framework). Introspecte
|
|
36
|
+
* le **Router** : c'est l'équivalent web de `nodefony router:dump` et la source
|
|
37
|
+
* de la future vue « Routes » de Studio (P10.8).
|
|
38
|
+
*
|
|
39
|
+
* Le framework héberge le broker → il s'enregistre directement (pas besoin de
|
|
40
|
+
* passer par `IAdminRegistry` du container comme un module externe).
|
|
41
|
+
*
|
|
42
|
+
* Endpoints :
|
|
43
|
+
* - `GET /nodefony/framework/api/routes` → toutes les routes enregistrées
|
|
44
|
+
* - `GET /nodefony/framework/api/info` → résumé (nb routes, méthodes, modules)
|
|
45
|
+
* - `GET /nodefony/framework/api/admin` → **catalogue** du data plane admin
|
|
46
|
+
* (tous les producteurs + descriptors + endpoints) — pièce « discovery »
|
|
47
|
+
* de P10.2 que Studio lit pour générer sa navigation admin.
|
|
48
|
+
*
|
|
49
|
+
* @param broker - le broker admin (pour le catalogue). Optionnel : sans lui,
|
|
50
|
+
* `admin` renvoie une liste vide.
|
|
51
|
+
* @param opts - composition (`playground: true` → endpoints Playground, dev-only).
|
|
52
|
+
* @returns le contrat admin de framework, prêt à `broker.register()`.
|
|
53
|
+
*/
|
|
54
|
+
function createFrameworkAdminApi(broker, opts) {
|
|
55
|
+
/** Normalise `requirements.methods` (string | string[]) → tableau majuscule. */
|
|
56
|
+
const methodsOf = (route) => {
|
|
57
|
+
const m = route.requirements?.methods ?? route.method;
|
|
58
|
+
if (Array.isArray(m)) return m.map((x) => String(x).toUpperCase());
|
|
59
|
+
if (typeof m === "string") return m.split(",").map((s) => s.trim().toUpperCase()).filter(Boolean);
|
|
60
|
+
return ["ANY"];
|
|
61
|
+
};
|
|
62
|
+
const serializeRoute = (route) => ({
|
|
63
|
+
name: route.name,
|
|
64
|
+
path: route.path ?? null,
|
|
65
|
+
methods: methodsOf(route),
|
|
66
|
+
controller: route.controller?.name ?? null,
|
|
67
|
+
action: route.classMethod ?? null,
|
|
68
|
+
module: route.module?.name ?? null,
|
|
69
|
+
host: route.host ?? null,
|
|
70
|
+
bypassFirewall: route.bypassFirewall
|
|
71
|
+
});
|
|
72
|
+
const descriptor = {
|
|
73
|
+
label: "Routes",
|
|
74
|
+
icon: "route",
|
|
75
|
+
order: 2
|
|
76
|
+
};
|
|
77
|
+
/** Premier param d'une query (string|string[]). */
|
|
78
|
+
const one = (v) => Array.isArray(v) ? v[0] : v;
|
|
79
|
+
/**
|
|
80
|
+
* Colonnes sur lesquelles `routes/page` sait trier — l'allowlist que
|
|
81
|
+
* `parsePageQuery` fait respecter. Elle a exactement les clés que {@link cell}
|
|
82
|
+
* sait rendre : un `order` sur autre chose est refusé (400) au lieu de trier
|
|
83
|
+
* sur une chaîne vide, ce qui rendait l'ordre arbitraire sans le dire.
|
|
84
|
+
*/
|
|
85
|
+
const SORTABLE_COLUMNS = [
|
|
86
|
+
"methods",
|
|
87
|
+
"path",
|
|
88
|
+
"name",
|
|
89
|
+
"controller",
|
|
90
|
+
"module",
|
|
91
|
+
"firewall"
|
|
92
|
+
];
|
|
93
|
+
/**
|
|
94
|
+
* `routes/page` sait TOUJOURS chercher : sa collection est le dump du Router,
|
|
95
|
+
* en mémoire, donc `q` est un simple balayage — ce qui n'est vrai d'aucune
|
|
96
|
+
* ressource persistée, d'où la déclaration explicite plutôt qu'un défaut.
|
|
97
|
+
*
|
|
98
|
+
* Constante partagée entre la publication (`page.search`) et le handler
|
|
99
|
+
* (`parsePageQuery(..., { searchable })`) : les écrire séparément recréerait
|
|
100
|
+
* la divergence que la publication vient supprimer — une console qui affiche
|
|
101
|
+
* une barre de recherche que le serveur refuse en 400.
|
|
102
|
+
*/
|
|
103
|
+
const SEARCHABLE = true;
|
|
104
|
+
/** Valeur d'une colonne pour le tri/filtre serveur (clés = colonnes du front). */
|
|
105
|
+
const cell = (r, key) => {
|
|
106
|
+
switch (key) {
|
|
107
|
+
case "methods": return r.methods.join(",");
|
|
108
|
+
case "path": return r.path ?? "";
|
|
109
|
+
case "name": return r.name ?? "";
|
|
110
|
+
case "controller": return [r.controller, r.action].filter(Boolean).join(".");
|
|
111
|
+
case "module": return r.module ?? "";
|
|
112
|
+
case "firewall": return r.bypassFirewall ? "bypass" : "protected";
|
|
113
|
+
default: return "";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Applique un opérateur de filtre (miroir serveur du DataGrid).
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ **Ce langage d'opérateurs appartient à CET endpoint, pas au framework.**
|
|
120
|
+
* Le contrat de filtre (`parseFilters`, cœur) est `nom=valeur` sans opérateur ;
|
|
121
|
+
* `routes/page` peut se permettre davantage parce que sa collection est en
|
|
122
|
+
* mémoire (le dump du Router) et qu'aucun store n'a à l'indexer. Un data plane
|
|
123
|
+
* adossé à une ressource persistée déclare un `IFilterSpec` et laisse le cœur
|
|
124
|
+
* valider — il ne recopie pas ce `matchOp`.
|
|
125
|
+
*/
|
|
126
|
+
const matchOp = (raw, op, value) => {
|
|
127
|
+
const s = String(raw ?? "");
|
|
128
|
+
const v = String(value ?? "");
|
|
129
|
+
if (op !== "isEmpty" && op !== "notEmpty" && v === "") return true;
|
|
130
|
+
switch (op) {
|
|
131
|
+
case "contains": return s.toLowerCase().includes(v.toLowerCase());
|
|
132
|
+
case "equals": return s === v;
|
|
133
|
+
case "in": {
|
|
134
|
+
const split = (list) => list.split(",").map((t) => t.trim()).filter((t) => t !== "");
|
|
135
|
+
const wanted = split(v);
|
|
136
|
+
if (wanted.length === 0) return true;
|
|
137
|
+
return split(s).some((c) => wanted.includes(c));
|
|
138
|
+
}
|
|
139
|
+
case "startsWith": return s.toLowerCase().startsWith(v.toLowerCase());
|
|
140
|
+
case "endsWith": return s.toLowerCase().endsWith(v.toLowerCase());
|
|
141
|
+
case "isEmpty": return s === "";
|
|
142
|
+
case "notEmpty": return s !== "";
|
|
143
|
+
default: return true;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const endpoints = [
|
|
147
|
+
{
|
|
148
|
+
path: "routes",
|
|
149
|
+
summary: "All registered routes (Router dump) — name, path, methods, controller",
|
|
150
|
+
handler: () => router_default.routes.map(serializeRoute)
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
path: "routes/page",
|
|
154
|
+
summary: "Routes paginées côté SERVEUR — contrat IPageQuery : ?limit&offset&order=champ:ASC&q, plus filters(JSON) — langage d'opérateurs PROPRE à cet endpoint (collection en mémoire), sérialisé par la vue Routes seule. Rend un IPage.",
|
|
155
|
+
page: {
|
|
156
|
+
sortable: () => SORTABLE_COLUMNS,
|
|
157
|
+
search: () => SEARCHABLE
|
|
158
|
+
},
|
|
159
|
+
handler: (request) => {
|
|
160
|
+
const query = parsePageQuery(request.query, {
|
|
161
|
+
defaultLimit: 25,
|
|
162
|
+
sortable: SORTABLE_COLUMNS,
|
|
163
|
+
searchable: SEARCHABLE
|
|
164
|
+
});
|
|
165
|
+
parseFilters(request.query, {}, { accepts: ["filters"] });
|
|
166
|
+
const search = query.q?.toLowerCase() ?? "";
|
|
167
|
+
let filters = [];
|
|
168
|
+
try {
|
|
169
|
+
const raw = one(request.query.filters);
|
|
170
|
+
if (raw) filters = JSON.parse(raw);
|
|
171
|
+
} catch {
|
|
172
|
+
filters = [];
|
|
173
|
+
}
|
|
174
|
+
let rows = router_default.routes.map(serializeRoute);
|
|
175
|
+
if (search) rows = rows.filter((r) => [
|
|
176
|
+
r.methods.join(","),
|
|
177
|
+
r.path,
|
|
178
|
+
r.name,
|
|
179
|
+
r.controller,
|
|
180
|
+
r.action,
|
|
181
|
+
r.module,
|
|
182
|
+
r.bypassFirewall ? "bypass" : "protected"
|
|
183
|
+
].filter(Boolean).join(" ").toLowerCase().includes(search));
|
|
184
|
+
for (const f of filters) rows = rows.filter((r) => matchOp(cell(r, f.key), f.op, f.value));
|
|
185
|
+
const total = rows.length;
|
|
186
|
+
if (query.order?.length) {
|
|
187
|
+
const order = query.order;
|
|
188
|
+
rows = [...rows].sort((a, b) => {
|
|
189
|
+
for (const [key, dir] of order) {
|
|
190
|
+
const cmp = cell(a, key).localeCompare(cell(b, key));
|
|
191
|
+
if (cmp !== 0) return dir === "DESC" ? -cmp : cmp;
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const offset = query.offset ?? 0;
|
|
197
|
+
return {
|
|
198
|
+
items: rows.slice(offset, offset + query.limit),
|
|
199
|
+
total,
|
|
200
|
+
limit: query.limit,
|
|
201
|
+
offset,
|
|
202
|
+
hasNext: offset + query.limit < total
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
path: "info",
|
|
208
|
+
summary: "Routing summary — total routes, methods, owning modules",
|
|
209
|
+
handler: () => {
|
|
210
|
+
const routes = router_default.routes;
|
|
211
|
+
const methods = /* @__PURE__ */ new Set();
|
|
212
|
+
const modules = /* @__PURE__ */ new Set();
|
|
213
|
+
for (const r of routes) {
|
|
214
|
+
methodsOf(r).forEach((m) => methods.add(m));
|
|
215
|
+
if (r.module?.name) modules.add(r.module.name);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
routesTotal: routes.length,
|
|
219
|
+
methods: [...methods].sort(),
|
|
220
|
+
modules: [...modules].sort()
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
path: "admin",
|
|
226
|
+
summary: "Admin data plane catalog — producers, descriptors, endpoints",
|
|
227
|
+
handler: () => {
|
|
228
|
+
if (!broker) return { producers: [] };
|
|
229
|
+
const descByNs = new Map(broker.list().map((p) => [p.adminNamespace, p.adminDescriptor()]));
|
|
230
|
+
const byNs = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const r of broker.routes()) {
|
|
232
|
+
let arr = byNs.get(r.namespace);
|
|
233
|
+
if (!arr) {
|
|
234
|
+
arr = [];
|
|
235
|
+
byNs.set(r.namespace, arr);
|
|
236
|
+
}
|
|
237
|
+
const caps = r.endpoint.page;
|
|
238
|
+
arr.push({
|
|
239
|
+
method: r.method,
|
|
240
|
+
path: r.path,
|
|
241
|
+
role: r.role,
|
|
242
|
+
summary: r.endpoint.summary ?? null,
|
|
243
|
+
...readPageCapabilities(caps)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return { producers: [...byNs.keys()].map((ns) => {
|
|
247
|
+
const d = descByNs.get(ns);
|
|
248
|
+
return {
|
|
249
|
+
namespace: ns,
|
|
250
|
+
label: d?.label ?? ns,
|
|
251
|
+
icon: d?.icon ?? null,
|
|
252
|
+
order: d?.order ?? 99,
|
|
253
|
+
role: d?.role ?? null,
|
|
254
|
+
endpoints: byNs.get(ns)
|
|
255
|
+
};
|
|
256
|
+
}).sort((a, b) => a.order - b.order) };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
if (opts?.playground) endpoints.push(...createPlaygroundEndpoints());
|
|
261
|
+
return {
|
|
262
|
+
adminNamespace: "framework",
|
|
263
|
+
adminDescriptor: () => descriptor,
|
|
264
|
+
adminEndpoints: () => endpoints
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
export { createFrameworkAdminApi };
|