@velajs/vela 1.4.0 → 1.4.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/CHANGELOG.md +3 -0
- package/README.md +13 -0
- package/dist/http/argument-resolver.d.ts +10 -0
- package/dist/http/argument-resolver.js +89 -0
- package/dist/http/handler-executor.d.ts +20 -0
- package/dist/http/handler-executor.js +108 -0
- package/dist/http/instantiate.d.ts +4 -0
- package/dist/http/instantiate.js +18 -0
- package/dist/http/response-mapper.d.ts +8 -0
- package/dist/http/response-mapper.js +42 -0
- package/dist/http/route.manager.d.ts +1 -9
- package/dist/http/route.manager.js +20 -240
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -24,6 +24,9 @@ A sanctioned per-request injectable lands as a framework primitive, and the meta
|
|
|
24
24
|
### Internal cleanup
|
|
25
25
|
|
|
26
26
|
- **Metadata stacking + funnel coverage** (audit #8 follow-up). New `metadata-stacking.test.ts` asserts `appendCustomHandlerMeta` is order-deterministic, `Reflect.defineMetadata` round-trips through `MetadataRegistry`'s typed slots, `MetadataRegistry.reset()` clears `classMeta`/`handlerMeta`, and class+handler `@SetMetadata` on the same key remain independent.
|
|
27
|
+
- **`NestModule.configure()` regression coverage** (audit #4 follow-up). New `configure-resolution.test.ts` asserts configure-time DI works (modules can constructor-inject providers from their own scope), synchronous errors thrown from `configure()` propagate, and unresolvable constructor deps fail loudly rather than silently skipping middleware setup.
|
|
28
|
+
- **Edge-safe contract documented** (audit #7 follow-up). README now states the contract explicitly: the main export is edge-safe and audited in CI by `src/__tests__/edge-runtime-audit.test.ts`; `@velajs/vela/schedule-node` is the one opt-in Node/Bun carve-out.
|
|
29
|
+
- **`RouteManager` split into focused units** (audit #9 follow-up). New files `argument-resolver.ts`, `handler-executor.ts`, `response-mapper.ts`, and `instantiate.ts` carry parameter extraction, the per-request orchestration closure, response/redirect mapping, and the container-aware factory. `RouteManager` is now focused on Hono route registration and path composition. Internal-only refactor — no public API change, no behavior change.
|
|
27
30
|
- **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
|
|
28
31
|
- **`Container.setRequestInstance(token, value)`** — public method to pre-seed the per-request cache. Used by `RouteManager` to populate `REQUEST_CONTEXT` before any handler resolution runs.
|
|
29
32
|
|
package/README.md
CHANGED
|
@@ -73,6 +73,19 @@ Vela runs on any runtime that supports the Web Standards API:
|
|
|
73
73
|
|
|
74
74
|
No Node.js-specific APIs (`node:fs`, `Buffer`, `process`) are used.
|
|
75
75
|
|
|
76
|
+
### Edge-safe contract
|
|
77
|
+
|
|
78
|
+
The **main export** (`@velajs/vela`) is edge-safe by contract — no `node:*` imports, no `Buffer`, no `process`, no `setInterval`, no `Bun.serve`. This is enforced in CI by [`src/__tests__/edge-runtime-audit.test.ts`](src/__tests__/edge-runtime-audit.test.ts), which fails the build if any file under `src/` references a forbidden API.
|
|
79
|
+
|
|
80
|
+
One subpath, **`@velajs/vela/schedule-node`**, is an opt-in Node/Bun adapter for `setInterval`-based job execution. It uses runtime-specific APIs by design and is **excluded from the edge-runtime audit**. Edge runtimes (Cloudflare Workers, Deno Deploy, Vercel Edge) should not import it — use platform cron triggers instead (e.g., `@velajs/cloudflare` ≥ 0.2.0 dispatches `@Cron` jobs via the Workers `scheduled()` handler).
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// Node / Bun only — opt-in
|
|
84
|
+
import { ScheduleNodeModule } from '@velajs/vela/schedule-node';
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The other subpaths (`@velajs/vela/internal`, `@velajs/vela/streaming`) follow the main export's edge-safe contract.
|
|
88
|
+
|
|
76
89
|
## Dynamic modules
|
|
77
90
|
|
|
78
91
|
Configurable modules use `forRoot` (sync) and `forRootAsync` (DI-resolved):
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Container } from '../container/container';
|
|
3
|
+
import type { PipeTransform } from '../pipeline/types';
|
|
4
|
+
import type { ParamMetadata } from './types';
|
|
5
|
+
export declare class ArgumentResolver {
|
|
6
|
+
private readonly ipExtractor;
|
|
7
|
+
constructor(ipExtractor: (c: Context) => string | null);
|
|
8
|
+
extract(c: Context, paramMetadata: ParamMetadata[], pipes: PipeTransform[], requestContainer: Container, paramTypes?: unknown[]): Promise<unknown[]>;
|
|
9
|
+
private extractParam;
|
|
10
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { getCookie } from "hono/cookie";
|
|
2
|
+
import { ParamType } from "../constants.js";
|
|
3
|
+
import { instantiate } from "./instantiate.js";
|
|
4
|
+
const PARAM_EXTRACTORS = new Map([
|
|
5
|
+
[
|
|
6
|
+
ParamType.PARAM,
|
|
7
|
+
(c, p)=>p.name ? c.req.param(p.name) : c.req.param()
|
|
8
|
+
],
|
|
9
|
+
[
|
|
10
|
+
ParamType.QUERY,
|
|
11
|
+
(c, p)=>p.name ? c.req.query(p.name) : c.req.query()
|
|
12
|
+
],
|
|
13
|
+
[
|
|
14
|
+
ParamType.BODY,
|
|
15
|
+
async (c, p)=>{
|
|
16
|
+
let body;
|
|
17
|
+
try {
|
|
18
|
+
body = await c.req.json();
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
ParamType.HEADERS,
|
|
27
|
+
(c, p)=>p.name ? c.req.header(p.name) : c.req.header()
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
ParamType.REQUEST,
|
|
31
|
+
(c)=>c
|
|
32
|
+
],
|
|
33
|
+
[
|
|
34
|
+
ParamType.RESPONSE,
|
|
35
|
+
(c)=>c
|
|
36
|
+
],
|
|
37
|
+
[
|
|
38
|
+
ParamType.COOKIE,
|
|
39
|
+
(c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
ParamType.RAW_BODY,
|
|
43
|
+
async (c)=>new Uint8Array(await c.req.arrayBuffer())
|
|
44
|
+
]
|
|
45
|
+
]);
|
|
46
|
+
// Pulls handler arguments from the request, applies shared pipes (global +
|
|
47
|
+
// controller + method) and then per-param pipes. Empty paramMetadata returns
|
|
48
|
+
// `[c]` to match the old direct-Hono-handler shape — a back-compat behavior
|
|
49
|
+
// the framework's tests rely on.
|
|
50
|
+
export class ArgumentResolver {
|
|
51
|
+
ipExtractor;
|
|
52
|
+
constructor(ipExtractor){
|
|
53
|
+
this.ipExtractor = ipExtractor;
|
|
54
|
+
}
|
|
55
|
+
async extract(c, paramMetadata, pipes, requestContainer, paramTypes) {
|
|
56
|
+
if (paramMetadata.length === 0) {
|
|
57
|
+
return [
|
|
58
|
+
c
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
const maxIndex = paramMetadata.at(-1).index;
|
|
62
|
+
const args = new Array(maxIndex + 1).fill(undefined);
|
|
63
|
+
for (const param of paramMetadata){
|
|
64
|
+
let value = await this.extractParam(c, param);
|
|
65
|
+
const metadata = {
|
|
66
|
+
type: param.type,
|
|
67
|
+
data: param.name,
|
|
68
|
+
metatype: paramTypes?.[param.index]
|
|
69
|
+
};
|
|
70
|
+
for (const pipe of pipes){
|
|
71
|
+
value = await pipe.transform(value, metadata);
|
|
72
|
+
}
|
|
73
|
+
if (param.pipes && param.pipes.length > 0) {
|
|
74
|
+
for (const paramPipe of param.pipes){
|
|
75
|
+
const pipeInstance = instantiate(paramPipe, requestContainer);
|
|
76
|
+
value = await pipeInstance.transform(value, metadata);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
args[param.index] = value;
|
|
80
|
+
}
|
|
81
|
+
return args;
|
|
82
|
+
}
|
|
83
|
+
extractParam(c, param) {
|
|
84
|
+
if (param.type === ParamType.IP) return this.ipExtractor(c);
|
|
85
|
+
const extractor = PARAM_EXTRACTORS.get(param.type);
|
|
86
|
+
if (extractor) return extractor(c, param);
|
|
87
|
+
return param.factory ? param.factory(param.name, c) : undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import type { Container } from '../container/container';
|
|
3
|
+
import type { Token, Type } from '../container/types';
|
|
4
|
+
import type { CanActivate, ExceptionFilter, NestInterceptor, PipeTransform } from '../pipeline/types';
|
|
5
|
+
import type { FilterType, GuardType, InterceptorType, ParameterMetadata, PipeType } from '../registry/types';
|
|
6
|
+
import type { ArgumentResolver } from './argument-resolver';
|
|
7
|
+
import type { RouteMetadata } from './types';
|
|
8
|
+
export interface HandlerGlobals {
|
|
9
|
+
guards: Array<GuardType | Token<CanActivate>>;
|
|
10
|
+
pipes: Array<PipeType | Token<PipeTransform>>;
|
|
11
|
+
interceptors: Array<InterceptorType | Token<NestInterceptor>>;
|
|
12
|
+
filters: Array<FilterType | Token<ExceptionFilter>>;
|
|
13
|
+
}
|
|
14
|
+
export declare class HandlerExecutor {
|
|
15
|
+
private readonly argumentResolver;
|
|
16
|
+
private readonly getGlobals;
|
|
17
|
+
private readonly getRequestContainer;
|
|
18
|
+
constructor(argumentResolver: ArgumentResolver, getGlobals: () => HandlerGlobals, getRequestContainer: (c: Context) => Container);
|
|
19
|
+
create(route: RouteMetadata, controller: Type, allParamMetadata: Map<string | symbol, ParameterMetadata[]>): (c: Context) => Promise<Response>;
|
|
20
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { ForbiddenException, HttpException } from "../errors/http-exception.js";
|
|
2
|
+
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
3
|
+
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
4
|
+
import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
|
|
5
|
+
import { buildExecutionContext } from "./execution-context.js";
|
|
6
|
+
import { instantiateMany } from "./instantiate.js";
|
|
7
|
+
import { applyResponseHeaders, mapRedirect, mapResponse } from "./response-mapper.js";
|
|
8
|
+
// Builds the per-request closure that runs guards, pipes, interceptors, the
|
|
9
|
+
// handler itself, and exception filters. Pure orchestration — Hono route
|
|
10
|
+
// registration stays in RouteManager. Globals are read via a callback so
|
|
11
|
+
// post-create additions (`useGlobalGuards`, etc.) propagate to subsequent
|
|
12
|
+
// requests without rebuilding routes.
|
|
13
|
+
export class HandlerExecutor {
|
|
14
|
+
argumentResolver;
|
|
15
|
+
getGlobals;
|
|
16
|
+
getRequestContainer;
|
|
17
|
+
constructor(argumentResolver, getGlobals, getRequestContainer){
|
|
18
|
+
this.argumentResolver = argumentResolver;
|
|
19
|
+
this.getGlobals = getGlobals;
|
|
20
|
+
this.getRequestContainer = getRequestContainer;
|
|
21
|
+
}
|
|
22
|
+
create(route, controller, allParamMetadata) {
|
|
23
|
+
const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
|
|
24
|
+
// Read param types once at build time for metatype population.
|
|
25
|
+
// Routed via Reflect so the polyfill funnels both src-level and dist-level
|
|
26
|
+
// consumers to the same registry (matters in tests that import from dist).
|
|
27
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
|
|
28
|
+
const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
|
|
29
|
+
const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
|
|
30
|
+
const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
|
|
31
|
+
// Filters: reverse order (handler → controller → global) — closest to handler runs first
|
|
32
|
+
const methodFilters = [
|
|
33
|
+
...ComponentManager.getComponents('filter', controller, route.handlerName)
|
|
34
|
+
].reverse();
|
|
35
|
+
const httpCode = getHttpCode(controller, route.handlerName);
|
|
36
|
+
const responseHeaders = getResponseHeaders(controller, route.handlerName);
|
|
37
|
+
const redirect = getRedirect(controller, route.handlerName);
|
|
38
|
+
return async (c)=>{
|
|
39
|
+
// Combine global + method at request time so post-create registrations propagate.
|
|
40
|
+
const requestContainer = this.getRequestContainer(c);
|
|
41
|
+
const globals = this.getGlobals();
|
|
42
|
+
const guards = [
|
|
43
|
+
...instantiateMany(globals.guards, requestContainer),
|
|
44
|
+
...instantiateMany(methodGuards, requestContainer)
|
|
45
|
+
];
|
|
46
|
+
const pipes = [
|
|
47
|
+
...instantiateMany(globals.pipes, requestContainer),
|
|
48
|
+
...instantiateMany(methodPipes, requestContainer)
|
|
49
|
+
];
|
|
50
|
+
const interceptors = [
|
|
51
|
+
...instantiateMany(globals.interceptors, requestContainer),
|
|
52
|
+
...instantiateMany(methodInterceptors, requestContainer)
|
|
53
|
+
];
|
|
54
|
+
const filters = [
|
|
55
|
+
...instantiateMany(methodFilters, requestContainer),
|
|
56
|
+
...instantiateMany(globals.filters, requestContainer)
|
|
57
|
+
];
|
|
58
|
+
const executionContext = buildExecutionContext(c, controller, route.handlerName);
|
|
59
|
+
try {
|
|
60
|
+
const instance = requestContainer.resolve(controller);
|
|
61
|
+
// 1. Extract args + run pipes (vela order: args before guards).
|
|
62
|
+
const args = await this.argumentResolver.extract(c, paramMetadata, pipes, requestContainer, paramTypes);
|
|
63
|
+
// 2. Guards (fail-fast).
|
|
64
|
+
for (const guard of guards){
|
|
65
|
+
const canActivate = await guard.canActivate(executionContext);
|
|
66
|
+
if (!canActivate) {
|
|
67
|
+
throw new ForbiddenException();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// 3. Get handler method.
|
|
71
|
+
const method = Reflect.get(instance, route.handlerName);
|
|
72
|
+
if (typeof method !== 'function') {
|
|
73
|
+
throw new Error(`Method ${String(route.handlerName)} not found on controller`);
|
|
74
|
+
}
|
|
75
|
+
// 4. Build core handler.
|
|
76
|
+
const coreHandler = async ()=>Reflect.apply(method, instance, args);
|
|
77
|
+
// 5. Run interceptor chain.
|
|
78
|
+
const result = await ComponentManager.runInterceptorChain(interceptors, executionContext, coreHandler);
|
|
79
|
+
if (redirect) {
|
|
80
|
+
return mapRedirect(c, result, redirect);
|
|
81
|
+
}
|
|
82
|
+
const response = mapResponse(c, result, httpCode);
|
|
83
|
+
applyResponseHeaders(response, responseHeaders);
|
|
84
|
+
return response;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
for (const filter of filters){
|
|
87
|
+
if (shouldFilterCatch(filter, error)) {
|
|
88
|
+
try {
|
|
89
|
+
const filtered = await filter.catch(error, executionContext);
|
|
90
|
+
return mapResponse(c, filtered);
|
|
91
|
+
} catch {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof HttpException) {
|
|
97
|
+
const response = error.getResponse();
|
|
98
|
+
const status = error.getStatus();
|
|
99
|
+
return c.json(response, status);
|
|
100
|
+
}
|
|
101
|
+
return c.json({
|
|
102
|
+
statusCode: 500,
|
|
103
|
+
message: 'Internal Server Error'
|
|
104
|
+
}, 500);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Container } from '../container/container';
|
|
2
|
+
import type { Token, Type } from '../container/types';
|
|
3
|
+
export declare function instantiate<T>(classOrInstance: Type<T> | Token<T> | T, container: Container): T;
|
|
4
|
+
export declare function instantiateMany<T>(items: Array<Type<T> | Token<T> | T>, container: Container): T[];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Resolve a class/token through the container if registered, otherwise treat
|
|
2
|
+
// the input as a plain instance. Used by RouteManager and HandlerExecutor to
|
|
3
|
+
// materialize middleware, guards, pipes, interceptors, and filters per request.
|
|
4
|
+
export function instantiate(classOrInstance, container) {
|
|
5
|
+
if (typeof classOrInstance === 'function') {
|
|
6
|
+
if (container.has(classOrInstance)) {
|
|
7
|
+
return container.resolve(classOrInstance);
|
|
8
|
+
}
|
|
9
|
+
return new classOrInstance();
|
|
10
|
+
}
|
|
11
|
+
if (container.has(classOrInstance)) {
|
|
12
|
+
return container.resolve(classOrInstance);
|
|
13
|
+
}
|
|
14
|
+
return classOrInstance;
|
|
15
|
+
}
|
|
16
|
+
export function instantiateMany(items, container) {
|
|
17
|
+
return items.map((item)=>instantiate(item, container));
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
export interface ResponseRedirect {
|
|
3
|
+
url: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function mapResponse(c: Context, result: unknown, statusCode?: number): Response;
|
|
7
|
+
export declare function mapRedirect(c: Context, result: unknown, redirect: ResponseRedirect): Response;
|
|
8
|
+
export declare function applyResponseHeaders(response: Response, headers: Iterable<readonly [string, string]>): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// A handler returning `{ url, statusCode? }` may override the @Redirect-decorator
|
|
2
|
+
// destination — kept identical to the previous inline behavior.
|
|
3
|
+
function parseRedirectResult(result) {
|
|
4
|
+
if (typeof result !== 'object' || result === null) return undefined;
|
|
5
|
+
if (!('url' in result)) return undefined;
|
|
6
|
+
const obj = result;
|
|
7
|
+
if (typeof obj.url !== 'string') return undefined;
|
|
8
|
+
return {
|
|
9
|
+
url: obj.url,
|
|
10
|
+
...typeof obj.statusCode === 'number' ? {
|
|
11
|
+
statusCode: obj.statusCode
|
|
12
|
+
} : {}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
// Maps a controller return value to a Hono Response. `null`/`undefined` → 204
|
|
16
|
+
// (empty body); strings → text; anything else → JSON. A pre-built Response
|
|
17
|
+
// passes through unchanged.
|
|
18
|
+
export function mapResponse(c, result, statusCode) {
|
|
19
|
+
if (result instanceof Response) {
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
if (result === null || result === undefined) {
|
|
23
|
+
return c.body(null, statusCode ?? 204);
|
|
24
|
+
}
|
|
25
|
+
if (typeof result === 'string') {
|
|
26
|
+
return c.text(result, statusCode ?? 200);
|
|
27
|
+
}
|
|
28
|
+
return c.json(result, statusCode ?? 200);
|
|
29
|
+
}
|
|
30
|
+
// Honors @Redirect on the handler. If the handler returned `{ url, statusCode? }`,
|
|
31
|
+
// those override the decorator's defaults; otherwise the decorator wins.
|
|
32
|
+
export function mapRedirect(c, result, redirect) {
|
|
33
|
+
const overrides = parseRedirectResult(result);
|
|
34
|
+
const finalUrl = overrides?.url ?? redirect.url;
|
|
35
|
+
const finalStatus = overrides?.statusCode ?? redirect.statusCode;
|
|
36
|
+
return c.redirect(finalUrl, finalStatus);
|
|
37
|
+
}
|
|
38
|
+
export function applyResponseHeaders(response, headers) {
|
|
39
|
+
for (const [name, value] of headers){
|
|
40
|
+
response.headers.set(name, value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -13,7 +13,6 @@ export interface RouteManagerOptions {
|
|
|
13
13
|
export declare class RouteManager {
|
|
14
14
|
private container;
|
|
15
15
|
private static readonly METHOD_REGISTRAR;
|
|
16
|
-
private static readonly PARAM_EXTRACTORS;
|
|
17
16
|
private controllers;
|
|
18
17
|
private globalMiddleware;
|
|
19
18
|
private globalPipes;
|
|
@@ -22,7 +21,7 @@ export declare class RouteManager {
|
|
|
22
21
|
private globalFilters;
|
|
23
22
|
private globalPrefix;
|
|
24
23
|
private consumerMiddlewareDefinitions;
|
|
25
|
-
private readonly
|
|
24
|
+
private readonly handlerExecutor;
|
|
26
25
|
constructor(container: Container, options?: RouteManagerOptions);
|
|
27
26
|
registerConsumerMiddleware(definitions: MiddlewareRouteDefinition[]): this;
|
|
28
27
|
setGlobalPrefix(prefix: string): this;
|
|
@@ -36,17 +35,10 @@ export declare class RouteManager {
|
|
|
36
35
|
useGlobalInterceptorTokens(...interceptorTokens: Array<Token<NestInterceptor>>): this;
|
|
37
36
|
useGlobalFilters(...filters: FilterType[]): this;
|
|
38
37
|
useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
|
|
39
|
-
private instantiate;
|
|
40
|
-
private instantiateMany;
|
|
41
38
|
private getMiddlewarePriority;
|
|
42
39
|
private getRequestContainer;
|
|
43
40
|
registerController(controller: Type): this;
|
|
44
41
|
build(): Promise<Hono>;
|
|
45
|
-
private createExecutionContext;
|
|
46
|
-
private createHandler;
|
|
47
|
-
private extractArguments;
|
|
48
|
-
private extractParam;
|
|
49
|
-
private createResponse;
|
|
50
42
|
private registerRoute;
|
|
51
43
|
private buildVersionedPaths;
|
|
52
44
|
private compilePathMatcher;
|
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
-
import {
|
|
3
|
-
import { HttpMethod, ParamType } from "../constants.js";
|
|
2
|
+
import { HttpMethod } from "../constants.js";
|
|
4
3
|
import { getMetadata } from "../metadata.js";
|
|
5
4
|
import { joinPaths } from "../registry/paths.js";
|
|
6
|
-
import {
|
|
5
|
+
import { ArgumentResolver } from "./argument-resolver.js";
|
|
6
|
+
import { HandlerExecutor } from "./handler-executor.js";
|
|
7
|
+
import { instantiate, instantiateMany } from "./instantiate.js";
|
|
7
8
|
import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
|
|
8
|
-
import { ForbiddenException, HttpException } from "../errors/http-exception.js";
|
|
9
9
|
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
10
|
-
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
11
10
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
12
|
-
import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
|
|
13
11
|
const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
|
|
14
|
-
function parseRedirectResult(result) {
|
|
15
|
-
if (typeof result !== 'object' || result === null) return undefined;
|
|
16
|
-
if (!('url' in result)) return undefined;
|
|
17
|
-
const obj = result;
|
|
18
|
-
if (typeof obj.url !== 'string') return undefined;
|
|
19
|
-
return {
|
|
20
|
-
url: obj.url,
|
|
21
|
-
...typeof obj.statusCode === 'number' ? {
|
|
22
|
-
statusCode: obj.statusCode
|
|
23
|
-
} : {}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
12
|
export class RouteManager {
|
|
27
13
|
container;
|
|
28
14
|
static METHOD_REGISTRAR = new Map([
|
|
@@ -59,48 +45,6 @@ export class RouteManager {
|
|
|
59
45
|
(app, p, h)=>app.all(p, h)
|
|
60
46
|
]
|
|
61
47
|
]);
|
|
62
|
-
static PARAM_EXTRACTORS = new Map([
|
|
63
|
-
[
|
|
64
|
-
ParamType.PARAM,
|
|
65
|
-
(c, p)=>p.name ? c.req.param(p.name) : c.req.param()
|
|
66
|
-
],
|
|
67
|
-
[
|
|
68
|
-
ParamType.QUERY,
|
|
69
|
-
(c, p)=>p.name ? c.req.query(p.name) : c.req.query()
|
|
70
|
-
],
|
|
71
|
-
[
|
|
72
|
-
ParamType.BODY,
|
|
73
|
-
async (c, p)=>{
|
|
74
|
-
let body;
|
|
75
|
-
try {
|
|
76
|
-
body = await c.req.json();
|
|
77
|
-
} catch {
|
|
78
|
-
return undefined;
|
|
79
|
-
}
|
|
80
|
-
return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
|
|
81
|
-
}
|
|
82
|
-
],
|
|
83
|
-
[
|
|
84
|
-
ParamType.HEADERS,
|
|
85
|
-
(c, p)=>p.name ? c.req.header(p.name) : c.req.header()
|
|
86
|
-
],
|
|
87
|
-
[
|
|
88
|
-
ParamType.REQUEST,
|
|
89
|
-
(c)=>c
|
|
90
|
-
],
|
|
91
|
-
[
|
|
92
|
-
ParamType.RESPONSE,
|
|
93
|
-
(c)=>c
|
|
94
|
-
],
|
|
95
|
-
[
|
|
96
|
-
ParamType.COOKIE,
|
|
97
|
-
(c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
|
|
98
|
-
],
|
|
99
|
-
[
|
|
100
|
-
ParamType.RAW_BODY,
|
|
101
|
-
async (c)=>new Uint8Array(await c.req.arrayBuffer())
|
|
102
|
-
]
|
|
103
|
-
]);
|
|
104
48
|
controllers = [];
|
|
105
49
|
globalMiddleware = [];
|
|
106
50
|
globalPipes = [];
|
|
@@ -109,10 +53,16 @@ export class RouteManager {
|
|
|
109
53
|
globalFilters = [];
|
|
110
54
|
globalPrefix = '';
|
|
111
55
|
consumerMiddlewareDefinitions = [];
|
|
112
|
-
|
|
56
|
+
handlerExecutor;
|
|
113
57
|
constructor(container, options = {}){
|
|
114
58
|
this.container = container;
|
|
115
|
-
|
|
59
|
+
const argumentResolver = new ArgumentResolver(options.getClientIp ?? defaultGetClientIp);
|
|
60
|
+
this.handlerExecutor = new HandlerExecutor(argumentResolver, ()=>({
|
|
61
|
+
guards: this.globalGuards,
|
|
62
|
+
pipes: this.globalPipes,
|
|
63
|
+
interceptors: this.globalInterceptors,
|
|
64
|
+
filters: this.globalFilters
|
|
65
|
+
}), (c)=>this.getRequestContainer(c));
|
|
116
66
|
}
|
|
117
67
|
registerConsumerMiddleware(definitions) {
|
|
118
68
|
this.consumerMiddlewareDefinitions.push(...definitions);
|
|
@@ -162,21 +112,6 @@ export class RouteManager {
|
|
|
162
112
|
this.globalFilters.push(...filterTokens);
|
|
163
113
|
return this;
|
|
164
114
|
}
|
|
165
|
-
instantiate(classOrInstance, container) {
|
|
166
|
-
if (typeof classOrInstance === 'function') {
|
|
167
|
-
if (container.has(classOrInstance)) {
|
|
168
|
-
return container.resolve(classOrInstance);
|
|
169
|
-
}
|
|
170
|
-
return new classOrInstance();
|
|
171
|
-
}
|
|
172
|
-
if (container.has(classOrInstance)) {
|
|
173
|
-
return container.resolve(classOrInstance);
|
|
174
|
-
}
|
|
175
|
-
return classOrInstance;
|
|
176
|
-
}
|
|
177
|
-
instantiateMany(items, container) {
|
|
178
|
-
return items.map((item)=>this.instantiate(item, container));
|
|
179
|
-
}
|
|
180
115
|
getMiddlewarePriority(entry) {
|
|
181
116
|
if (entry == null) return 0;
|
|
182
117
|
if (typeof entry === 'function') {
|
|
@@ -189,7 +124,7 @@ export class RouteManager {
|
|
|
189
124
|
if (typeof inst.constructor?.priority === 'number') return inst.constructor.priority;
|
|
190
125
|
}
|
|
191
126
|
try {
|
|
192
|
-
const resolved =
|
|
127
|
+
const resolved = instantiate(entry, this.container);
|
|
193
128
|
if (resolved && typeof resolved === 'object') {
|
|
194
129
|
const p = resolved.priority;
|
|
195
130
|
if (typeof p === 'number') return p;
|
|
@@ -237,21 +172,18 @@ export class RouteManager {
|
|
|
237
172
|
index,
|
|
238
173
|
priority: this.getMiddlewarePriority(entry)
|
|
239
174
|
})).sort((a, b)=>a.priority - b.priority || a.index - b.index);
|
|
240
|
-
// Register global middleware on the Hono app
|
|
241
175
|
for (const { entry } of sortedGlobal){
|
|
242
176
|
app.use('*', (c, next)=>{
|
|
243
177
|
const requestContainer = this.getRequestContainer(c);
|
|
244
|
-
const resolved =
|
|
178
|
+
const resolved = instantiate(entry, requestContainer);
|
|
245
179
|
return resolved.use(c, next);
|
|
246
180
|
});
|
|
247
181
|
}
|
|
248
|
-
// Sort consumer middleware with the same stable-by-index rule.
|
|
249
182
|
const sortedConsumer = this.consumerMiddlewareDefinitions.map((def, index)=>({
|
|
250
183
|
def,
|
|
251
184
|
index,
|
|
252
185
|
priority: def.priority ?? 0
|
|
253
186
|
})).sort((a, b)=>a.priority - b.priority || a.index - b.index);
|
|
254
|
-
// Register MiddlewareConsumer-configured middleware
|
|
255
187
|
for (const { def } of sortedConsumer){
|
|
256
188
|
const matchRoute = this.compileRouteMatcher(def.routes);
|
|
257
189
|
const matchExclude = this.compileRouteMatcher(def.excludes);
|
|
@@ -262,30 +194,27 @@ export class RouteManager {
|
|
|
262
194
|
const requestContainer = this.getRequestContainer(c);
|
|
263
195
|
const runChain = (index)=>{
|
|
264
196
|
if (index >= def.middleware.length) return next();
|
|
265
|
-
const instance =
|
|
197
|
+
const instance = instantiate(def.middleware[index], requestContainer);
|
|
266
198
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
267
199
|
return instance.use(c, ()=>runChain(index + 1)).then(()=>{});
|
|
268
200
|
};
|
|
269
201
|
return runChain(0);
|
|
270
202
|
});
|
|
271
203
|
}
|
|
272
|
-
// First pass: register all custom routes (must come before CRUD /:id routes)
|
|
204
|
+
// First pass: register all custom routes (must come before CRUD /:id routes).
|
|
273
205
|
for (const { controller, metadata, routes } of this.controllers){
|
|
274
206
|
if (routes.length > 0) {
|
|
275
207
|
const allParamMetadata = MetadataRegistry.getParameters(controller);
|
|
276
208
|
for (const route of routes){
|
|
277
|
-
// Determine effective version: route-level overrides controller-level
|
|
278
209
|
const effectiveVersion = route.version ?? metadata.version;
|
|
279
|
-
// Build versioned paths
|
|
280
210
|
const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
|
|
281
|
-
// Register per-route middleware as Hono middleware before the handler
|
|
282
211
|
const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
|
|
283
|
-
const handler = this.
|
|
212
|
+
const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
|
|
284
213
|
for (const fullPath of versionedPaths){
|
|
285
214
|
for (const middlewareItem of middlewareItems){
|
|
286
215
|
app.use(fullPath, (c, next)=>{
|
|
287
216
|
const requestContainer = this.getRequestContainer(c);
|
|
288
|
-
const resolved =
|
|
217
|
+
const resolved = instantiate(middlewareItem, requestContainer);
|
|
289
218
|
return resolved.use(c, next);
|
|
290
219
|
});
|
|
291
220
|
}
|
|
@@ -294,7 +223,7 @@ export class RouteManager {
|
|
|
294
223
|
}
|
|
295
224
|
}
|
|
296
225
|
}
|
|
297
|
-
// Second pass: mount CRUD sub-apps (/:id routes registered last)
|
|
226
|
+
// Second pass: mount CRUD sub-apps (/:id routes registered last).
|
|
298
227
|
for (const { controller, metadata } of this.controllers){
|
|
299
228
|
const crudConfig = getMetadata('vela:crud', controller);
|
|
300
229
|
if (crudConfig) {
|
|
@@ -303,7 +232,7 @@ export class RouteManager {
|
|
|
303
232
|
const { buildCrudRoutes } = await import(pkg);
|
|
304
233
|
await buildCrudRoutes(app, controller, metadata.prefix, crudConfig, {
|
|
305
234
|
globalPrefix: this.globalPrefix,
|
|
306
|
-
globalGuards:
|
|
235
|
+
globalGuards: instantiateMany(this.globalGuards, this.container),
|
|
307
236
|
joinPaths
|
|
308
237
|
});
|
|
309
238
|
} catch (err) {
|
|
@@ -315,155 +244,6 @@ export class RouteManager {
|
|
|
315
244
|
}
|
|
316
245
|
return app;
|
|
317
246
|
}
|
|
318
|
-
createExecutionContext(c, controller, route) {
|
|
319
|
-
return buildExecutionContext(c, controller, route.handlerName);
|
|
320
|
-
}
|
|
321
|
-
createHandler(route, controller, allParamMetadata) {
|
|
322
|
-
const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
|
|
323
|
-
// Read param types once at build time for metatype population.
|
|
324
|
-
// Routed via Reflect so the polyfill funnels both src-level and dist-level
|
|
325
|
-
// consumers to the same registry (matters in tests that import from dist).
|
|
326
|
-
const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
|
|
327
|
-
// Collect controller + method level components at build time
|
|
328
|
-
const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
|
|
329
|
-
const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
|
|
330
|
-
const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
|
|
331
|
-
// Filters: reverse order (handler → controller → global) — closest to handler runs first
|
|
332
|
-
const methodFilters = [
|
|
333
|
-
...ComponentManager.getComponents('filter', controller, route.handlerName)
|
|
334
|
-
].reverse();
|
|
335
|
-
// Read response decorators at build time
|
|
336
|
-
const httpCode = getHttpCode(controller, route.handlerName);
|
|
337
|
-
const responseHeaders = getResponseHeaders(controller, route.handlerName);
|
|
338
|
-
const redirect = getRedirect(controller, route.handlerName);
|
|
339
|
-
return async (c)=>{
|
|
340
|
-
// Combine global + method at request time (allows post-create registration)
|
|
341
|
-
const requestContainer = this.getRequestContainer(c);
|
|
342
|
-
const guards = [
|
|
343
|
-
...this.instantiateMany(this.globalGuards, requestContainer),
|
|
344
|
-
...this.instantiateMany(methodGuards, requestContainer)
|
|
345
|
-
];
|
|
346
|
-
const pipes = [
|
|
347
|
-
...this.instantiateMany(this.globalPipes, requestContainer),
|
|
348
|
-
...this.instantiateMany(methodPipes, requestContainer)
|
|
349
|
-
];
|
|
350
|
-
const interceptors = [
|
|
351
|
-
...this.instantiateMany(this.globalInterceptors, requestContainer),
|
|
352
|
-
...this.instantiateMany(methodInterceptors, requestContainer)
|
|
353
|
-
];
|
|
354
|
-
const filters = [
|
|
355
|
-
...this.instantiateMany(methodFilters, requestContainer),
|
|
356
|
-
...this.instantiateMany(this.globalFilters, requestContainer)
|
|
357
|
-
];
|
|
358
|
-
const executionContext = this.createExecutionContext(c, controller, route);
|
|
359
|
-
try {
|
|
360
|
-
const instance = requestContainer.resolve(controller);
|
|
361
|
-
// 1. Extract args + run pipes
|
|
362
|
-
const args = await this.extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes);
|
|
363
|
-
// 2. Guards (fail-fast)
|
|
364
|
-
for (const guard of guards){
|
|
365
|
-
const canActivate = await guard.canActivate(executionContext);
|
|
366
|
-
if (!canActivate) {
|
|
367
|
-
throw new ForbiddenException();
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
// 3. Get handler method
|
|
371
|
-
const method = Reflect.get(instance, route.handlerName);
|
|
372
|
-
if (typeof method !== 'function') {
|
|
373
|
-
throw new Error(`Method ${String(route.handlerName)} not found on controller`);
|
|
374
|
-
}
|
|
375
|
-
// 4. Build core handler
|
|
376
|
-
const coreHandler = async ()=>Reflect.apply(method, instance, args);
|
|
377
|
-
// 5. Run interceptor chain
|
|
378
|
-
const result = await ComponentManager.runInterceptorChain(interceptors, executionContext, coreHandler);
|
|
379
|
-
// Handle @Redirect
|
|
380
|
-
if (redirect) {
|
|
381
|
-
const overrides = parseRedirectResult(result);
|
|
382
|
-
const finalUrl = overrides?.url ?? redirect.url;
|
|
383
|
-
const finalStatus = overrides?.statusCode ?? redirect.statusCode;
|
|
384
|
-
return c.redirect(finalUrl, finalStatus);
|
|
385
|
-
}
|
|
386
|
-
// Build response with @HttpCode and @Header support
|
|
387
|
-
const response = this.createResponse(c, result, httpCode);
|
|
388
|
-
// Apply @Header decorators
|
|
389
|
-
for (const [headerName, headerValue] of responseHeaders){
|
|
390
|
-
response.headers.set(headerName, headerValue);
|
|
391
|
-
}
|
|
392
|
-
return response;
|
|
393
|
-
} catch (error) {
|
|
394
|
-
// Run exception filters
|
|
395
|
-
for (const filter of filters){
|
|
396
|
-
if (shouldFilterCatch(filter, error)) {
|
|
397
|
-
try {
|
|
398
|
-
const result = await filter.catch(error, executionContext);
|
|
399
|
-
return this.createResponse(c, result);
|
|
400
|
-
} catch {
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
// Default HttpException handling
|
|
406
|
-
if (error instanceof HttpException) {
|
|
407
|
-
const response = error.getResponse();
|
|
408
|
-
const status = error.getStatus();
|
|
409
|
-
return c.json(response, status);
|
|
410
|
-
}
|
|
411
|
-
// Default 500
|
|
412
|
-
return c.json({
|
|
413
|
-
statusCode: 500,
|
|
414
|
-
message: 'Internal Server Error'
|
|
415
|
-
}, 500);
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
}
|
|
419
|
-
async extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes) {
|
|
420
|
-
if (paramMetadata.length === 0) {
|
|
421
|
-
return [
|
|
422
|
-
c
|
|
423
|
-
];
|
|
424
|
-
}
|
|
425
|
-
const maxIndex = paramMetadata.at(-1).index;
|
|
426
|
-
const args = new Array(maxIndex + 1).fill(undefined);
|
|
427
|
-
for (const param of paramMetadata){
|
|
428
|
-
let value = await this.extractParam(c, param);
|
|
429
|
-
const metadata = {
|
|
430
|
-
type: param.type,
|
|
431
|
-
data: param.name,
|
|
432
|
-
metatype: paramTypes?.[param.index]
|
|
433
|
-
};
|
|
434
|
-
// Run shared pipes (global + controller + method)
|
|
435
|
-
for (const pipe of pipes){
|
|
436
|
-
value = await pipe.transform(value, metadata);
|
|
437
|
-
}
|
|
438
|
-
// Run param-level pipes
|
|
439
|
-
if (param.pipes && param.pipes.length > 0) {
|
|
440
|
-
for (const paramPipe of param.pipes){
|
|
441
|
-
const pipeInstance = this.instantiate(paramPipe, requestContainer);
|
|
442
|
-
value = await pipeInstance.transform(value, metadata);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
args[param.index] = value;
|
|
446
|
-
}
|
|
447
|
-
return args;
|
|
448
|
-
}
|
|
449
|
-
extractParam(c, param) {
|
|
450
|
-
if (param.type === ParamType.IP) return this.ipExtractor(c);
|
|
451
|
-
const extractor = RouteManager.PARAM_EXTRACTORS.get(param.type);
|
|
452
|
-
if (extractor) return extractor(c, param);
|
|
453
|
-
return param.factory ? param.factory(param.name, c) : undefined;
|
|
454
|
-
}
|
|
455
|
-
createResponse(c, result, statusCode) {
|
|
456
|
-
if (result instanceof Response) {
|
|
457
|
-
return result;
|
|
458
|
-
}
|
|
459
|
-
if (result === null || result === undefined) {
|
|
460
|
-
return c.body(null, statusCode ?? 204);
|
|
461
|
-
}
|
|
462
|
-
if (typeof result === 'string') {
|
|
463
|
-
return c.text(result, statusCode ?? 200);
|
|
464
|
-
}
|
|
465
|
-
return c.json(result, statusCode ?? 200);
|
|
466
|
-
}
|
|
467
247
|
registerRoute(app, method, path, handler) {
|
|
468
248
|
const normalizedPath = path || '/';
|
|
469
249
|
const registrar = RouteManager.METHOD_REGISTRAR.get(method);
|