@velajs/vela 1.3.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 +30 -0
- package/README.md +13 -0
- package/dist/container/container.d.ts +1 -0
- package/dist/container/container.js +8 -0
- package/dist/factory/bootstrap.js +14 -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/request-context.d.ts +13 -0
- package/dist/http/request-context.js +22 -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 +22 -240
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/registry/metadata.registry.js +0 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
A sanctioned per-request injectable lands as a framework primitive, and the metadata-store unification finally has its regression tests.
|
|
6
|
+
|
|
7
|
+
### New
|
|
8
|
+
|
|
9
|
+
- **`REQUEST_CONTEXT` injectable.** A request-scoped primitive carrying a stable `id` (mirrored from inbound `x-request-id` if present, else `crypto.randomUUID()`), `receivedAt`, the raw `Request`, the Hono `Context`, and a typed `set/get/has` bag for cross-cutting metadata. Seeded by `RouteManager` into each per-request child container; resolves through `@Inject(REQUEST_CONTEXT)` from any request-scoped service. No `AsyncLocalStorage` — edge-runtime contract intact (verified live under workerd via `pnpm test:workers`).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Inject, Injectable, Scope, REQUEST_CONTEXT } from '@velajs/vela';
|
|
13
|
+
import type { RequestContext } from '@velajs/vela';
|
|
14
|
+
|
|
15
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
16
|
+
class TenantResolver {
|
|
17
|
+
constructor(@Inject(REQUEST_CONTEXT) private readonly ctx: RequestContext) {}
|
|
18
|
+
resolve() {
|
|
19
|
+
return this.ctx.hono.req.header('x-tenant') ?? 'default';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Internal cleanup
|
|
25
|
+
|
|
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.
|
|
30
|
+
- **Stale `WeakMap` comment removed** from `MetadataRegistry.reset()` — the WeakMap fallback was retired in 1.1.0; the comment was documentation drift.
|
|
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.
|
|
32
|
+
|
|
33
|
+
## 1.3.0
|
|
34
|
+
|
|
5
35
|
Module boundaries are enforced. NestJS-shape: a service cannot resolve dependencies from a module it didn't import. Bootstrap is consolidated into a single primitive, discovery failures are diagnostically routed, and a generic plugin composer is included.
|
|
6
36
|
|
|
7
37
|
### New
|
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):
|
|
@@ -15,6 +15,7 @@ export declare class Container {
|
|
|
15
15
|
private recordOrigin;
|
|
16
16
|
registerScope(scope: ModuleScope): void;
|
|
17
17
|
markGlobalToken(token: Token): void;
|
|
18
|
+
setRequestInstance(token: Token, value: unknown): void;
|
|
18
19
|
getDiagnostics(): Diagnostics;
|
|
19
20
|
resolve<T>(token: Token<T>, requestingModuleId?: string): T;
|
|
20
21
|
resolveAll<T>(token: Token<T>, requestingModuleId?: string): T[];
|
|
@@ -86,6 +86,14 @@ export class Container {
|
|
|
86
86
|
markGlobalToken(token) {
|
|
87
87
|
this.globals.add(token);
|
|
88
88
|
}
|
|
89
|
+
// Pre-seed the per-request cache. Only meaningful on a child container
|
|
90
|
+
// produced by createChild() — the root's requestInstances map is unused.
|
|
91
|
+
// Used by RouteManager to populate framework-provided request-scope
|
|
92
|
+
// values (REQUEST_CONTEXT) before any handler resolution runs, so the
|
|
93
|
+
// provider's factory never fires on the request path.
|
|
94
|
+
setRequestInstance(token, value) {
|
|
95
|
+
this.requestInstances.set(token, value);
|
|
96
|
+
}
|
|
89
97
|
getDiagnostics() {
|
|
90
98
|
return this.diagnostics;
|
|
91
99
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Scope } from "../constants.js";
|
|
1
2
|
import { Container } from "../container/container.js";
|
|
2
3
|
import { ModuleRef } from "../container/module-ref.js";
|
|
4
|
+
import { REQUEST_CONTEXT } from "../http/request-context.js";
|
|
3
5
|
import { RouteManager } from "../http/route.manager.js";
|
|
4
6
|
import { ModuleLoader } from "../module/module-loader.js";
|
|
5
7
|
import { bindAppProviders } from "../pipeline/app-providers.js";
|
|
@@ -39,6 +41,18 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
|
|
|
39
41
|
]){
|
|
40
42
|
container.markGlobalToken(t);
|
|
41
43
|
}
|
|
44
|
+
// REQUEST_CONTEXT is seeded into each request-scoped child by RouteManager
|
|
45
|
+
// before any handler resolves it (see route.manager.ts:getRequestContainer).
|
|
46
|
+
// The factory throws so misuse outside the request path surfaces immediately
|
|
47
|
+
// instead of materializing a phantom context.
|
|
48
|
+
container.register({
|
|
49
|
+
provide: REQUEST_CONTEXT,
|
|
50
|
+
scope: Scope.REQUEST,
|
|
51
|
+
useFactory: ()=>{
|
|
52
|
+
throw new Error('REQUEST_CONTEXT can only be resolved inside a request — ' + 'it is seeded by RouteManager when the request enters the pipeline.');
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
container.markGlobalToken(REQUEST_CONTEXT);
|
|
42
56
|
const routeManager = new RouteManager(container, options);
|
|
43
57
|
ComponentManager.init(container);
|
|
44
58
|
const loader = new ModuleLoader(container, routeManager);
|
|
@@ -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,13 @@
|
|
|
1
|
+
import type { Context } from 'hono';
|
|
2
|
+
import { InjectionToken } from '../container/types';
|
|
3
|
+
export interface RequestContext {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly receivedAt: Date;
|
|
6
|
+
readonly request: Request;
|
|
7
|
+
readonly hono: Context;
|
|
8
|
+
set<T>(key: string | symbol, value: T): void;
|
|
9
|
+
get<T>(key: string | symbol): T | undefined;
|
|
10
|
+
has(key: string | symbol): boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare const REQUEST_CONTEXT: InjectionToken<RequestContext>;
|
|
13
|
+
export declare function createRequestContext(c: Context): RequestContext;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { InjectionToken } from "../container/types.js";
|
|
2
|
+
export const REQUEST_CONTEXT = new InjectionToken('vela.RequestContext');
|
|
3
|
+
export function createRequestContext(c) {
|
|
4
|
+
const bag = new Map();
|
|
5
|
+
const inboundId = c.req.raw.headers.get('x-request-id');
|
|
6
|
+
const id = inboundId && inboundId.length > 0 ? inboundId : crypto.randomUUID();
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
receivedAt: new Date(),
|
|
10
|
+
request: c.req.raw,
|
|
11
|
+
hono: c,
|
|
12
|
+
set (key, value) {
|
|
13
|
+
bag.set(key, value);
|
|
14
|
+
},
|
|
15
|
+
get (key) {
|
|
16
|
+
return bag.get(key);
|
|
17
|
+
},
|
|
18
|
+
has (key) {
|
|
19
|
+
return bag.has(key);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -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,27 +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 {
|
|
7
|
-
import {
|
|
5
|
+
import { ArgumentResolver } from "./argument-resolver.js";
|
|
6
|
+
import { HandlerExecutor } from "./handler-executor.js";
|
|
7
|
+
import { instantiate, instantiateMany } from "./instantiate.js";
|
|
8
|
+
import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
|
|
8
9
|
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
9
|
-
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
10
10
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
11
|
-
import { getHttpCode, getRedirect, getResponseHeaders } from "./decorators.js";
|
|
12
11
|
const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
|
|
13
|
-
function parseRedirectResult(result) {
|
|
14
|
-
if (typeof result !== 'object' || result === null) return undefined;
|
|
15
|
-
if (!('url' in result)) return undefined;
|
|
16
|
-
const obj = result;
|
|
17
|
-
if (typeof obj.url !== 'string') return undefined;
|
|
18
|
-
return {
|
|
19
|
-
url: obj.url,
|
|
20
|
-
...typeof obj.statusCode === 'number' ? {
|
|
21
|
-
statusCode: obj.statusCode
|
|
22
|
-
} : {}
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
12
|
export class RouteManager {
|
|
26
13
|
container;
|
|
27
14
|
static METHOD_REGISTRAR = new Map([
|
|
@@ -58,48 +45,6 @@ export class RouteManager {
|
|
|
58
45
|
(app, p, h)=>app.all(p, h)
|
|
59
46
|
]
|
|
60
47
|
]);
|
|
61
|
-
static PARAM_EXTRACTORS = new Map([
|
|
62
|
-
[
|
|
63
|
-
ParamType.PARAM,
|
|
64
|
-
(c, p)=>p.name ? c.req.param(p.name) : c.req.param()
|
|
65
|
-
],
|
|
66
|
-
[
|
|
67
|
-
ParamType.QUERY,
|
|
68
|
-
(c, p)=>p.name ? c.req.query(p.name) : c.req.query()
|
|
69
|
-
],
|
|
70
|
-
[
|
|
71
|
-
ParamType.BODY,
|
|
72
|
-
async (c, p)=>{
|
|
73
|
-
let body;
|
|
74
|
-
try {
|
|
75
|
-
body = await c.req.json();
|
|
76
|
-
} catch {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
return p.name && body !== null && typeof body === 'object' ? body[p.name] : body;
|
|
80
|
-
}
|
|
81
|
-
],
|
|
82
|
-
[
|
|
83
|
-
ParamType.HEADERS,
|
|
84
|
-
(c, p)=>p.name ? c.req.header(p.name) : c.req.header()
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
ParamType.REQUEST,
|
|
88
|
-
(c)=>c
|
|
89
|
-
],
|
|
90
|
-
[
|
|
91
|
-
ParamType.RESPONSE,
|
|
92
|
-
(c)=>c
|
|
93
|
-
],
|
|
94
|
-
[
|
|
95
|
-
ParamType.COOKIE,
|
|
96
|
-
(c, p)=>p.name ? getCookie(c, p.name) : getCookie(c)
|
|
97
|
-
],
|
|
98
|
-
[
|
|
99
|
-
ParamType.RAW_BODY,
|
|
100
|
-
async (c)=>new Uint8Array(await c.req.arrayBuffer())
|
|
101
|
-
]
|
|
102
|
-
]);
|
|
103
48
|
controllers = [];
|
|
104
49
|
globalMiddleware = [];
|
|
105
50
|
globalPipes = [];
|
|
@@ -108,10 +53,16 @@ export class RouteManager {
|
|
|
108
53
|
globalFilters = [];
|
|
109
54
|
globalPrefix = '';
|
|
110
55
|
consumerMiddlewareDefinitions = [];
|
|
111
|
-
|
|
56
|
+
handlerExecutor;
|
|
112
57
|
constructor(container, options = {}){
|
|
113
58
|
this.container = container;
|
|
114
|
-
|
|
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));
|
|
115
66
|
}
|
|
116
67
|
registerConsumerMiddleware(definitions) {
|
|
117
68
|
this.consumerMiddlewareDefinitions.push(...definitions);
|
|
@@ -161,21 +112,6 @@ export class RouteManager {
|
|
|
161
112
|
this.globalFilters.push(...filterTokens);
|
|
162
113
|
return this;
|
|
163
114
|
}
|
|
164
|
-
instantiate(classOrInstance, container) {
|
|
165
|
-
if (typeof classOrInstance === 'function') {
|
|
166
|
-
if (container.has(classOrInstance)) {
|
|
167
|
-
return container.resolve(classOrInstance);
|
|
168
|
-
}
|
|
169
|
-
return new classOrInstance();
|
|
170
|
-
}
|
|
171
|
-
if (container.has(classOrInstance)) {
|
|
172
|
-
return container.resolve(classOrInstance);
|
|
173
|
-
}
|
|
174
|
-
return classOrInstance;
|
|
175
|
-
}
|
|
176
|
-
instantiateMany(items, container) {
|
|
177
|
-
return items.map((item)=>this.instantiate(item, container));
|
|
178
|
-
}
|
|
179
115
|
getMiddlewarePriority(entry) {
|
|
180
116
|
if (entry == null) return 0;
|
|
181
117
|
if (typeof entry === 'function') {
|
|
@@ -188,7 +124,7 @@ export class RouteManager {
|
|
|
188
124
|
if (typeof inst.constructor?.priority === 'number') return inst.constructor.priority;
|
|
189
125
|
}
|
|
190
126
|
try {
|
|
191
|
-
const resolved =
|
|
127
|
+
const resolved = instantiate(entry, this.container);
|
|
192
128
|
if (resolved && typeof resolved === 'object') {
|
|
193
129
|
const p = resolved.priority;
|
|
194
130
|
if (typeof p === 'number') return p;
|
|
@@ -206,6 +142,7 @@ export class RouteManager {
|
|
|
206
142
|
return existing;
|
|
207
143
|
}
|
|
208
144
|
const child = this.container.createChild();
|
|
145
|
+
child.setRequestInstance(REQUEST_CONTEXT, createRequestContext(c));
|
|
209
146
|
c.set('container', child);
|
|
210
147
|
return child;
|
|
211
148
|
}
|
|
@@ -235,21 +172,18 @@ export class RouteManager {
|
|
|
235
172
|
index,
|
|
236
173
|
priority: this.getMiddlewarePriority(entry)
|
|
237
174
|
})).sort((a, b)=>a.priority - b.priority || a.index - b.index);
|
|
238
|
-
// Register global middleware on the Hono app
|
|
239
175
|
for (const { entry } of sortedGlobal){
|
|
240
176
|
app.use('*', (c, next)=>{
|
|
241
177
|
const requestContainer = this.getRequestContainer(c);
|
|
242
|
-
const resolved =
|
|
178
|
+
const resolved = instantiate(entry, requestContainer);
|
|
243
179
|
return resolved.use(c, next);
|
|
244
180
|
});
|
|
245
181
|
}
|
|
246
|
-
// Sort consumer middleware with the same stable-by-index rule.
|
|
247
182
|
const sortedConsumer = this.consumerMiddlewareDefinitions.map((def, index)=>({
|
|
248
183
|
def,
|
|
249
184
|
index,
|
|
250
185
|
priority: def.priority ?? 0
|
|
251
186
|
})).sort((a, b)=>a.priority - b.priority || a.index - b.index);
|
|
252
|
-
// Register MiddlewareConsumer-configured middleware
|
|
253
187
|
for (const { def } of sortedConsumer){
|
|
254
188
|
const matchRoute = this.compileRouteMatcher(def.routes);
|
|
255
189
|
const matchExclude = this.compileRouteMatcher(def.excludes);
|
|
@@ -260,30 +194,27 @@ export class RouteManager {
|
|
|
260
194
|
const requestContainer = this.getRequestContainer(c);
|
|
261
195
|
const runChain = (index)=>{
|
|
262
196
|
if (index >= def.middleware.length) return next();
|
|
263
|
-
const instance =
|
|
197
|
+
const instance = instantiate(def.middleware[index], requestContainer);
|
|
264
198
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
265
199
|
return instance.use(c, ()=>runChain(index + 1)).then(()=>{});
|
|
266
200
|
};
|
|
267
201
|
return runChain(0);
|
|
268
202
|
});
|
|
269
203
|
}
|
|
270
|
-
// 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).
|
|
271
205
|
for (const { controller, metadata, routes } of this.controllers){
|
|
272
206
|
if (routes.length > 0) {
|
|
273
207
|
const allParamMetadata = MetadataRegistry.getParameters(controller);
|
|
274
208
|
for (const route of routes){
|
|
275
|
-
// Determine effective version: route-level overrides controller-level
|
|
276
209
|
const effectiveVersion = route.version ?? metadata.version;
|
|
277
|
-
// Build versioned paths
|
|
278
210
|
const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
|
|
279
|
-
// Register per-route middleware as Hono middleware before the handler
|
|
280
211
|
const middlewareItems = ComponentManager.getComponents('middleware', controller, route.handlerName);
|
|
281
|
-
const handler = this.
|
|
212
|
+
const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
|
|
282
213
|
for (const fullPath of versionedPaths){
|
|
283
214
|
for (const middlewareItem of middlewareItems){
|
|
284
215
|
app.use(fullPath, (c, next)=>{
|
|
285
216
|
const requestContainer = this.getRequestContainer(c);
|
|
286
|
-
const resolved =
|
|
217
|
+
const resolved = instantiate(middlewareItem, requestContainer);
|
|
287
218
|
return resolved.use(c, next);
|
|
288
219
|
});
|
|
289
220
|
}
|
|
@@ -292,7 +223,7 @@ export class RouteManager {
|
|
|
292
223
|
}
|
|
293
224
|
}
|
|
294
225
|
}
|
|
295
|
-
// Second pass: mount CRUD sub-apps (/:id routes registered last)
|
|
226
|
+
// Second pass: mount CRUD sub-apps (/:id routes registered last).
|
|
296
227
|
for (const { controller, metadata } of this.controllers){
|
|
297
228
|
const crudConfig = getMetadata('vela:crud', controller);
|
|
298
229
|
if (crudConfig) {
|
|
@@ -301,7 +232,7 @@ export class RouteManager {
|
|
|
301
232
|
const { buildCrudRoutes } = await import(pkg);
|
|
302
233
|
await buildCrudRoutes(app, controller, metadata.prefix, crudConfig, {
|
|
303
234
|
globalPrefix: this.globalPrefix,
|
|
304
|
-
globalGuards:
|
|
235
|
+
globalGuards: instantiateMany(this.globalGuards, this.container),
|
|
305
236
|
joinPaths
|
|
306
237
|
});
|
|
307
238
|
} catch (err) {
|
|
@@ -313,155 +244,6 @@ export class RouteManager {
|
|
|
313
244
|
}
|
|
314
245
|
return app;
|
|
315
246
|
}
|
|
316
|
-
createExecutionContext(c, controller, route) {
|
|
317
|
-
return buildExecutionContext(c, controller, route.handlerName);
|
|
318
|
-
}
|
|
319
|
-
createHandler(route, controller, allParamMetadata) {
|
|
320
|
-
const paramMetadata = (allParamMetadata.get(route.handlerName) || []).sort((a, b)=>a.index - b.index);
|
|
321
|
-
// Read param types once at build time for metatype population.
|
|
322
|
-
// Routed via Reflect so the polyfill funnels both src-level and dist-level
|
|
323
|
-
// consumers to the same registry (matters in tests that import from dist).
|
|
324
|
-
const paramTypes = Reflect.getMetadata('design:paramtypes', controller.prototype, route.handlerName);
|
|
325
|
-
// Collect controller + method level components at build time
|
|
326
|
-
const methodGuards = ComponentManager.getComponents('guard', controller, route.handlerName);
|
|
327
|
-
const methodPipes = ComponentManager.getComponents('pipe', controller, route.handlerName);
|
|
328
|
-
const methodInterceptors = ComponentManager.getComponents('interceptor', controller, route.handlerName);
|
|
329
|
-
// Filters: reverse order (handler → controller → global) — closest to handler runs first
|
|
330
|
-
const methodFilters = [
|
|
331
|
-
...ComponentManager.getComponents('filter', controller, route.handlerName)
|
|
332
|
-
].reverse();
|
|
333
|
-
// Read response decorators at build time
|
|
334
|
-
const httpCode = getHttpCode(controller, route.handlerName);
|
|
335
|
-
const responseHeaders = getResponseHeaders(controller, route.handlerName);
|
|
336
|
-
const redirect = getRedirect(controller, route.handlerName);
|
|
337
|
-
return async (c)=>{
|
|
338
|
-
// Combine global + method at request time (allows post-create registration)
|
|
339
|
-
const requestContainer = this.getRequestContainer(c);
|
|
340
|
-
const guards = [
|
|
341
|
-
...this.instantiateMany(this.globalGuards, requestContainer),
|
|
342
|
-
...this.instantiateMany(methodGuards, requestContainer)
|
|
343
|
-
];
|
|
344
|
-
const pipes = [
|
|
345
|
-
...this.instantiateMany(this.globalPipes, requestContainer),
|
|
346
|
-
...this.instantiateMany(methodPipes, requestContainer)
|
|
347
|
-
];
|
|
348
|
-
const interceptors = [
|
|
349
|
-
...this.instantiateMany(this.globalInterceptors, requestContainer),
|
|
350
|
-
...this.instantiateMany(methodInterceptors, requestContainer)
|
|
351
|
-
];
|
|
352
|
-
const filters = [
|
|
353
|
-
...this.instantiateMany(methodFilters, requestContainer),
|
|
354
|
-
...this.instantiateMany(this.globalFilters, requestContainer)
|
|
355
|
-
];
|
|
356
|
-
const executionContext = this.createExecutionContext(c, controller, route);
|
|
357
|
-
try {
|
|
358
|
-
const instance = requestContainer.resolve(controller);
|
|
359
|
-
// 1. Extract args + run pipes
|
|
360
|
-
const args = await this.extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes);
|
|
361
|
-
// 2. Guards (fail-fast)
|
|
362
|
-
for (const guard of guards){
|
|
363
|
-
const canActivate = await guard.canActivate(executionContext);
|
|
364
|
-
if (!canActivate) {
|
|
365
|
-
throw new ForbiddenException();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// 3. Get handler method
|
|
369
|
-
const method = Reflect.get(instance, route.handlerName);
|
|
370
|
-
if (typeof method !== 'function') {
|
|
371
|
-
throw new Error(`Method ${String(route.handlerName)} not found on controller`);
|
|
372
|
-
}
|
|
373
|
-
// 4. Build core handler
|
|
374
|
-
const coreHandler = async ()=>Reflect.apply(method, instance, args);
|
|
375
|
-
// 5. Run interceptor chain
|
|
376
|
-
const result = await ComponentManager.runInterceptorChain(interceptors, executionContext, coreHandler);
|
|
377
|
-
// Handle @Redirect
|
|
378
|
-
if (redirect) {
|
|
379
|
-
const overrides = parseRedirectResult(result);
|
|
380
|
-
const finalUrl = overrides?.url ?? redirect.url;
|
|
381
|
-
const finalStatus = overrides?.statusCode ?? redirect.statusCode;
|
|
382
|
-
return c.redirect(finalUrl, finalStatus);
|
|
383
|
-
}
|
|
384
|
-
// Build response with @HttpCode and @Header support
|
|
385
|
-
const response = this.createResponse(c, result, httpCode);
|
|
386
|
-
// Apply @Header decorators
|
|
387
|
-
for (const [headerName, headerValue] of responseHeaders){
|
|
388
|
-
response.headers.set(headerName, headerValue);
|
|
389
|
-
}
|
|
390
|
-
return response;
|
|
391
|
-
} catch (error) {
|
|
392
|
-
// Run exception filters
|
|
393
|
-
for (const filter of filters){
|
|
394
|
-
if (shouldFilterCatch(filter, error)) {
|
|
395
|
-
try {
|
|
396
|
-
const result = await filter.catch(error, executionContext);
|
|
397
|
-
return this.createResponse(c, result);
|
|
398
|
-
} catch {
|
|
399
|
-
break;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// Default HttpException handling
|
|
404
|
-
if (error instanceof HttpException) {
|
|
405
|
-
const response = error.getResponse();
|
|
406
|
-
const status = error.getStatus();
|
|
407
|
-
return c.json(response, status);
|
|
408
|
-
}
|
|
409
|
-
// Default 500
|
|
410
|
-
return c.json({
|
|
411
|
-
statusCode: 500,
|
|
412
|
-
message: 'Internal Server Error'
|
|
413
|
-
}, 500);
|
|
414
|
-
}
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
async extractArguments(c, paramMetadata, pipes, requestContainer, paramTypes) {
|
|
418
|
-
if (paramMetadata.length === 0) {
|
|
419
|
-
return [
|
|
420
|
-
c
|
|
421
|
-
];
|
|
422
|
-
}
|
|
423
|
-
const maxIndex = paramMetadata.at(-1).index;
|
|
424
|
-
const args = new Array(maxIndex + 1).fill(undefined);
|
|
425
|
-
for (const param of paramMetadata){
|
|
426
|
-
let value = await this.extractParam(c, param);
|
|
427
|
-
const metadata = {
|
|
428
|
-
type: param.type,
|
|
429
|
-
data: param.name,
|
|
430
|
-
metatype: paramTypes?.[param.index]
|
|
431
|
-
};
|
|
432
|
-
// Run shared pipes (global + controller + method)
|
|
433
|
-
for (const pipe of pipes){
|
|
434
|
-
value = await pipe.transform(value, metadata);
|
|
435
|
-
}
|
|
436
|
-
// Run param-level pipes
|
|
437
|
-
if (param.pipes && param.pipes.length > 0) {
|
|
438
|
-
for (const paramPipe of param.pipes){
|
|
439
|
-
const pipeInstance = this.instantiate(paramPipe, requestContainer);
|
|
440
|
-
value = await pipeInstance.transform(value, metadata);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
args[param.index] = value;
|
|
444
|
-
}
|
|
445
|
-
return args;
|
|
446
|
-
}
|
|
447
|
-
extractParam(c, param) {
|
|
448
|
-
if (param.type === ParamType.IP) return this.ipExtractor(c);
|
|
449
|
-
const extractor = RouteManager.PARAM_EXTRACTORS.get(param.type);
|
|
450
|
-
if (extractor) return extractor(c, param);
|
|
451
|
-
return param.factory ? param.factory(param.name, c) : undefined;
|
|
452
|
-
}
|
|
453
|
-
createResponse(c, result, statusCode) {
|
|
454
|
-
if (result instanceof Response) {
|
|
455
|
-
return result;
|
|
456
|
-
}
|
|
457
|
-
if (result === null || result === undefined) {
|
|
458
|
-
return c.body(null, statusCode ?? 204);
|
|
459
|
-
}
|
|
460
|
-
if (typeof result === 'string') {
|
|
461
|
-
return c.text(result, statusCode ?? 200);
|
|
462
|
-
}
|
|
463
|
-
return c.json(result, statusCode ?? 200);
|
|
464
|
-
}
|
|
465
247
|
registerRoute(app, method, path, handler) {
|
|
466
248
|
const normalizedPath = path || '/';
|
|
467
249
|
const registrar = RouteManager.METHOD_REGISTRAR.get(method);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
|
|
|
10
10
|
export type { Type, Token, InjectableOptions, ProviderOptions, ModuleScope, ContainerOptions, Diagnostics, } from './container/index';
|
|
11
11
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
|
|
12
12
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, } from './http/index';
|
|
13
|
+
export { REQUEST_CONTEXT } from './http/request-context';
|
|
14
|
+
export type { RequestContext } from './http/request-context';
|
|
13
15
|
export { Logger, LogLevel } from './services/index';
|
|
14
16
|
export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
|
|
15
17
|
export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,8 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
|
|
|
12
12
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
|
|
13
13
|
// HTTP Decorators
|
|
14
14
|
export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators } from "./http/index.js";
|
|
15
|
+
// Request-scoped context primitive
|
|
16
|
+
export { REQUEST_CONTEXT } from "./http/request-context.js";
|
|
15
17
|
// Services
|
|
16
18
|
export { Logger, LogLevel } from "./services/index.js";
|
|
17
19
|
// Config
|