@velajs/vela 1.5.0 → 1.6.0
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 +12 -0
- package/README.md +45 -0
- package/dist/http/execution-context.d.ts +4 -0
- package/dist/http/execution-context.js +17 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/lazy-param.decorator.d.ts +37 -0
- package/dist/http/lazy-param.decorator.js +117 -0
- package/dist/http/route.manager.d.ts +2 -0
- package/dist/http/route.manager.js +58 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/validation/create-zod-dto.js +13 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.0]
|
|
4
|
+
|
|
5
|
+
The exception-filter chain now reaches into attached middlewares for full NestJS parity, and a new `createLazyParamDecorator` helper closes the parameter-decorator-vs-guard ordering hazard at the public-API surface.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`createLazyParamDecorator((data, ctx) => T)`.** Custom parameter decorators whose factory runs the *first time the handler reads a property on the resolved value* — not during argument extraction. Vela's argument resolver runs before guards by design (`extract args → guards → handler`), which means a `createParamDecorator` factory that depends on guard-populated state observes an empty slot. The lazy variant returns a `Proxy` whose traps invoke the factory on demand; the `get` trap short-circuits `prop === 'then'` so `await value` does not consider the proxy a thenable and therefore does not trigger eager resolution. Method results are auto-bound to the resolved real target so detached calls keep `this`; `ownKeys` + `getOwnPropertyDescriptor` are implemented so `JSON.stringify(value)` works after one access. Exported from the root barrel and from `@velajs/vela/internal` via the same surface as `createParamDecorator`. Documented in README under *Custom parameter decorators with deferred resolution*.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Exception-filter chain now catches errors thrown inside vela-attached middlewares.** Previously the `APP_FILTER` / `@UseFilters` chain only handled errors thrown from `@Controller` handler methods; errors from middlewares (global, `MiddlewareConsumer.apply(...).forRoutes(...)`, and per-route attached) bypassed the chain and surfaced as Hono's outer 500. They now flow through the same filter resolution as handler exceptions, with a synthesized `ExecutionContext` whose `getClass()` returns the `VelaMiddlewareHost` marker and whose `getHandler()` returns the `Symbol.for('vela.middleware')` sentinel — `getType()` stays `'http'` for NestJS parity. Only global filters apply at the middleware boundary (per-handler `@UseFilters` requires a controller call frame); for thrown `HttpException`s with no catching filter, the chain renders the exception's own response/status (matching handler-thrown semantics). Non-`HttpException` throws with no catching filter re-throw to preserve the existing default-500 path. Non-throwing middleware paths (returning a `Response`, calling `await next()`, resolving a promise) are byte-identical to before.
|
|
14
|
+
|
|
3
15
|
## Unreleased
|
|
4
16
|
|
|
5
17
|
A sanctioned per-request injectable lands as a framework primitive, the metadata-store unification finally has its regression tests, and dynamic module identity becomes consistent — closing the last open audit item.
|
package/README.md
CHANGED
|
@@ -143,6 +143,51 @@ class MyModule {
|
|
|
143
143
|
|
|
144
144
|
`forRootAsync` callers should pass `key` explicitly when the same module needs multiple async instances — factories aren't structurally hashable.
|
|
145
145
|
|
|
146
|
+
## Custom parameter decorators with deferred resolution
|
|
147
|
+
|
|
148
|
+
Vela's argument resolver runs **before** guards (`extract args → guards → handler`). A custom parameter decorator built with `createParamDecorator` therefore observes any state populated by a guard as still empty — its factory has already fired by the time the guard runs.
|
|
149
|
+
|
|
150
|
+
When a parameter's value depends on guard output (a `REQUEST_CONTEXT`-stored user, a tenant resolved from a JWT, etc.), use `createLazyParamDecorator` instead. The factory does not run during argument extraction; it runs the first time the handler reads a property on the resolved value:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import {
|
|
154
|
+
createLazyParamDecorator,
|
|
155
|
+
Inject,
|
|
156
|
+
Injectable,
|
|
157
|
+
REQUEST_CONTEXT,
|
|
158
|
+
Scope,
|
|
159
|
+
UseGuards,
|
|
160
|
+
} from '@velajs/vela';
|
|
161
|
+
import type { CanActivate, ExecutionContext, RequestContext } from '@velajs/vela';
|
|
162
|
+
|
|
163
|
+
const USER_KEY = Symbol.for('app.user');
|
|
164
|
+
|
|
165
|
+
@Injectable({ scope: Scope.REQUEST })
|
|
166
|
+
class AuthGuard implements CanActivate {
|
|
167
|
+
constructor(@Inject(REQUEST_CONTEXT) private readonly ctx: RequestContext) {}
|
|
168
|
+
canActivate(_e: ExecutionContext): boolean {
|
|
169
|
+
this.ctx.set(USER_KEY, { id: 'u-1', name: 'ada' }); // populated here
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const CurrentUser = createLazyParamDecorator((_data, ctx: ExecutionContext) => {
|
|
175
|
+
const reqCtx = ctx
|
|
176
|
+
.getContext()
|
|
177
|
+
.get('container')
|
|
178
|
+
.resolve<RequestContext>(REQUEST_CONTEXT);
|
|
179
|
+
return reqCtx.get(USER_KEY);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
@UseGuards(AuthGuard)
|
|
183
|
+
@Get('/me')
|
|
184
|
+
me(@CurrentUser() user: { id: string; name: string }) {
|
|
185
|
+
return { id: user.id }; // factory runs here, AFTER AuthGuard
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The proxy short-circuits `then` on its `get` trap so `await value` returns the proxy itself rather than triggering eager resolution. Method results are auto-bound to the resolved real target, so detached method calls keep `this`. `JSON.stringify(value)` works after one access (the proxy implements `ownKeys` + `getOwnPropertyDescriptor`).
|
|
190
|
+
|
|
146
191
|
## Companion packages
|
|
147
192
|
|
|
148
193
|
| Package | Purpose |
|
|
@@ -2,3 +2,7 @@ import type { Context } from 'hono';
|
|
|
2
2
|
import type { Type } from '../container/types';
|
|
3
3
|
import type { ExecutionContext } from '../pipeline/types';
|
|
4
4
|
export declare function buildExecutionContext(c: Context, controller: Type, handlerName: string | symbol): ExecutionContext;
|
|
5
|
+
export declare class VelaMiddlewareHost {
|
|
6
|
+
}
|
|
7
|
+
export declare const VELA_MIDDLEWARE_HANDLER: unique symbol;
|
|
8
|
+
export declare function buildMiddlewareExecutionContext(c: Context): ExecutionContext;
|
|
@@ -13,3 +13,20 @@ export function buildExecutionContext(c, controller, handlerName) {
|
|
|
13
13
|
})
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
+
// Sentinel marker exposed via `ExecutionContext.getClass()` when the host is a
|
|
17
|
+
// vela-attached middleware rather than a controller handler. Filters that
|
|
18
|
+
// branch on the calling class (rare) can detect the middleware origin without
|
|
19
|
+
// introducing a separate `getType()` value — `getType()` stays `'http'` for
|
|
20
|
+
// NestJS parity.
|
|
21
|
+
export class VelaMiddlewareHost {
|
|
22
|
+
}
|
|
23
|
+
// Synthesizes an ExecutionContext for an exception thrown inside a
|
|
24
|
+
// vela-attached middleware. There is no controller class or handler method
|
|
25
|
+
// on the call stack at that point, so `getClass()` returns the
|
|
26
|
+
// `VelaMiddlewareHost` marker and `getHandler()` returns the
|
|
27
|
+
// `vela.middleware` symbol — both stable, comparable values that filter
|
|
28
|
+
// authors can pattern-match against if needed.
|
|
29
|
+
export const VELA_MIDDLEWARE_HANDLER = Symbol.for('vela.middleware');
|
|
30
|
+
export function buildMiddlewareExecutionContext(c) {
|
|
31
|
+
return buildExecutionContext(c, VelaMiddlewareHost, VELA_MIDDLEWARE_HANDLER);
|
|
32
|
+
}
|
package/dist/http/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export type { RouteManagerOptions } from './route.manager';
|
|
|
3
3
|
export { MiddlewareBuilder } from '../module/middleware';
|
|
4
4
|
export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
|
|
5
5
|
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, isController, } from './decorators';
|
|
6
|
+
export { createLazyParamDecorator } from './lazy-param.decorator';
|
|
6
7
|
export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
|
package/dist/http/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { RouteManager } from "./route.manager.js";
|
|
2
2
|
export { MiddlewareBuilder } from "../module/middleware.js";
|
|
3
3
|
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, isController } from "./decorators.js";
|
|
4
|
+
export { createLazyParamDecorator } from "./lazy-param.decorator.js";
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { PipeType } from '../registry/types';
|
|
2
|
+
import type { ExecutionContext } from '../pipeline/types';
|
|
3
|
+
/**
|
|
4
|
+
* Factory for parameter decorators whose value materializes lazily — the
|
|
5
|
+
* factory does not run during argument extraction; it runs the first time
|
|
6
|
+
* the handler reads a property on the resolved value.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: vela's argument resolver runs *before* guards
|
|
9
|
+
* (handler-executor order: extract args → guards → handler). A custom
|
|
10
|
+
* `createParamDecorator` factory that depends on guard-populated state
|
|
11
|
+
* (e.g. a value a guard places into `REQUEST_CONTEXT`) therefore observes
|
|
12
|
+
* an empty slot. `createLazyParamDecorator` defers factory execution to
|
|
13
|
+
* first property access on the proxied result, by which point guards have
|
|
14
|
+
* run and the slot is populated.
|
|
15
|
+
*
|
|
16
|
+
* The proxy explicitly short-circuits `prop === 'then'` so `await value`
|
|
17
|
+
* does not consider the proxy a thenable, which would otherwise trigger
|
|
18
|
+
* eager resolution. This single invariant is what makes the helper safe to
|
|
19
|
+
* pass through `async` boundaries without surprise.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const CurrentUser = createLazyParamDecorator(
|
|
24
|
+
* (_data: unknown, ctx: ExecutionContext) => {
|
|
25
|
+
* const reqCtx = ctx.getContext().get('container').resolve(REQUEST_CONTEXT);
|
|
26
|
+
* return reqCtx.get('user'); // populated by AuthGuard
|
|
27
|
+
* },
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* @UseGuards(AuthGuard)
|
|
31
|
+
* @Get('/me')
|
|
32
|
+
* me(@CurrentUser() user: User) {
|
|
33
|
+
* return { id: user.id }; // factory runs here, after AuthGuard
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare function createLazyParamDecorator<TData = unknown>(factory: (data: TData, ctx: ExecutionContext) => unknown): (data?: TData, ...pipes: PipeType[]) => ParameterDecorator;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
2
|
+
import { buildExecutionContext } from "./execution-context.js";
|
|
3
|
+
const CUSTOM_PARAM_TYPE = 'custom';
|
|
4
|
+
/**
|
|
5
|
+
* Factory for parameter decorators whose value materializes lazily — the
|
|
6
|
+
* factory does not run during argument extraction; it runs the first time
|
|
7
|
+
* the handler reads a property on the resolved value.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists: vela's argument resolver runs *before* guards
|
|
10
|
+
* (handler-executor order: extract args → guards → handler). A custom
|
|
11
|
+
* `createParamDecorator` factory that depends on guard-populated state
|
|
12
|
+
* (e.g. a value a guard places into `REQUEST_CONTEXT`) therefore observes
|
|
13
|
+
* an empty slot. `createLazyParamDecorator` defers factory execution to
|
|
14
|
+
* first property access on the proxied result, by which point guards have
|
|
15
|
+
* run and the slot is populated.
|
|
16
|
+
*
|
|
17
|
+
* The proxy explicitly short-circuits `prop === 'then'` so `await value`
|
|
18
|
+
* does not consider the proxy a thenable, which would otherwise trigger
|
|
19
|
+
* eager resolution. This single invariant is what makes the helper safe to
|
|
20
|
+
* pass through `async` boundaries without surprise.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const CurrentUser = createLazyParamDecorator(
|
|
25
|
+
* (_data: unknown, ctx: ExecutionContext) => {
|
|
26
|
+
* const reqCtx = ctx.getContext().get('container').resolve(REQUEST_CONTEXT);
|
|
27
|
+
* return reqCtx.get('user'); // populated by AuthGuard
|
|
28
|
+
* },
|
|
29
|
+
* );
|
|
30
|
+
*
|
|
31
|
+
* @UseGuards(AuthGuard)
|
|
32
|
+
* @Get('/me')
|
|
33
|
+
* me(@CurrentUser() user: User) {
|
|
34
|
+
* return { id: user.id }; // factory runs here, after AuthGuard
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/ export function createLazyParamDecorator(factory) {
|
|
38
|
+
return (data, ...pipes)=>{
|
|
39
|
+
return (target, propertyKey, parameterIndex)=>{
|
|
40
|
+
if (propertyKey === undefined) {
|
|
41
|
+
throw new Error('Parameter decorators can only be used on method parameters');
|
|
42
|
+
}
|
|
43
|
+
MetadataRegistry.addParameter(target.constructor, propertyKey, {
|
|
44
|
+
index: parameterIndex,
|
|
45
|
+
type: CUSTOM_PARAM_TYPE,
|
|
46
|
+
name: undefined,
|
|
47
|
+
factory: (_unused, ctx)=>{
|
|
48
|
+
const honoCtx = ctx;
|
|
49
|
+
const execCtx = buildExecutionContext(honoCtx, target.constructor, propertyKey);
|
|
50
|
+
return createLazyProxy(()=>factory(data, execCtx));
|
|
51
|
+
},
|
|
52
|
+
...pipes.length > 0 ? {
|
|
53
|
+
pipes
|
|
54
|
+
} : {}
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Single-shot resolution: the factory runs at most once; subsequent reads
|
|
60
|
+
// share the cached real value. The proxy's traps all funnel through
|
|
61
|
+
// `materialize` to keep that invariant in one place.
|
|
62
|
+
function createLazyProxy(produce) {
|
|
63
|
+
let resolved = false;
|
|
64
|
+
let value;
|
|
65
|
+
const materialize = ()=>{
|
|
66
|
+
if (!resolved) {
|
|
67
|
+
value = produce();
|
|
68
|
+
resolved = true;
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
};
|
|
72
|
+
// Target is an empty null-prototype object — proxies still need a
|
|
73
|
+
// backing target for the runtime to forward to, but using a real object
|
|
74
|
+
// would leak its own properties (`hasOwnProperty`, etc.) into trap
|
|
75
|
+
// results. `Object.create(null)` keeps the namespace clean.
|
|
76
|
+
const target = Object.create(null);
|
|
77
|
+
const handler = {
|
|
78
|
+
get (_t, prop, receiver) {
|
|
79
|
+
// Critical: do NOT report the proxy as thenable. If we let the JS
|
|
80
|
+
// runtime read `then` (during `await proxy`), the engine would
|
|
81
|
+
// observe a function-shaped value, treat the proxy as a promise,
|
|
82
|
+
// call `then(resolve, reject)` and trigger eager resolution before
|
|
83
|
+
// the consumer ever touches a real property. Returning `undefined`
|
|
84
|
+
// here makes `await proxy` return the proxy itself, untouched.
|
|
85
|
+
if (prop === 'then') return undefined;
|
|
86
|
+
const real = materialize();
|
|
87
|
+
if (real === null || real === undefined) return undefined;
|
|
88
|
+
const descriptor = Reflect.get(real, prop, receiver);
|
|
89
|
+
// Bind functions back to the real target so `this` works as the
|
|
90
|
+
// consumer expects on instance methods.
|
|
91
|
+
if (typeof descriptor === 'function') {
|
|
92
|
+
return descriptor.bind(real);
|
|
93
|
+
}
|
|
94
|
+
return descriptor;
|
|
95
|
+
},
|
|
96
|
+
has (_t, prop) {
|
|
97
|
+
const real = materialize();
|
|
98
|
+
return real !== null && real !== undefined && Reflect.has(real, prop);
|
|
99
|
+
},
|
|
100
|
+
ownKeys () {
|
|
101
|
+
const real = materialize();
|
|
102
|
+
if (real === null || real === undefined) return [];
|
|
103
|
+
return Reflect.ownKeys(real);
|
|
104
|
+
},
|
|
105
|
+
getOwnPropertyDescriptor (_t, prop) {
|
|
106
|
+
const real = materialize();
|
|
107
|
+
if (real === null || real === undefined) return undefined;
|
|
108
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(real, prop);
|
|
109
|
+
// Proxy invariants require returned descriptors to be configurable
|
|
110
|
+
// when the underlying target lacks the property, which is always
|
|
111
|
+
// true for our null-prototype empty target.
|
|
112
|
+
if (descriptor) descriptor.configurable = true;
|
|
113
|
+
return descriptor;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
return new Proxy(target, handler);
|
|
117
|
+
}
|
|
@@ -37,6 +37,8 @@ export declare class RouteManager {
|
|
|
37
37
|
useGlobalFilterTokens(...filterTokens: Array<Token<ExceptionFilter>>): this;
|
|
38
38
|
private getMiddlewarePriority;
|
|
39
39
|
private getRequestContainer;
|
|
40
|
+
private wrapMiddlewareWithFilters;
|
|
41
|
+
private mapMiddlewareError;
|
|
40
42
|
registerController(controller: Type): this;
|
|
41
43
|
build(): Promise<Hono>;
|
|
42
44
|
private registerRoute;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { HttpMethod } from "../constants.js";
|
|
3
|
+
import { HttpException } from "../errors/http-exception.js";
|
|
3
4
|
import { getMetadata } from "../metadata.js";
|
|
4
5
|
import { joinPaths } from "../registry/paths.js";
|
|
5
6
|
import { ArgumentResolver } from "./argument-resolver.js";
|
|
7
|
+
import { buildMiddlewareExecutionContext } from "./execution-context.js";
|
|
6
8
|
import { HandlerExecutor } from "./handler-executor.js";
|
|
7
9
|
import { instantiate, instantiateMany } from "./instantiate.js";
|
|
8
10
|
import { REQUEST_CONTEXT, createRequestContext } from "./request-context.js";
|
|
11
|
+
import { mapResponse } from "./response-mapper.js";
|
|
9
12
|
import { ComponentManager } from "../pipeline/component.manager.js";
|
|
13
|
+
import { shouldFilterCatch } from "../pipeline/decorators.js";
|
|
10
14
|
import { MetadataRegistry } from "../registry/metadata.registry.js";
|
|
11
15
|
const defaultGetClientIp = (c)=>c.req.raw.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ?? c.req.raw.headers.get('x-real-ip') ?? null;
|
|
12
16
|
export class RouteManager {
|
|
@@ -146,6 +150,54 @@ export class RouteManager {
|
|
|
146
150
|
c.set('container', child);
|
|
147
151
|
return child;
|
|
148
152
|
}
|
|
153
|
+
// Wraps an attached middleware so a thrown error flows through the
|
|
154
|
+
// exception-filter chain instead of bypassing it on the way to Hono's
|
|
155
|
+
// outer error handler. Mirrors the catch tail in HandlerExecutor — global
|
|
156
|
+
// filters are the only scope reachable here, since per-handler filters
|
|
157
|
+
// require a controller call frame that the middleware boundary lacks.
|
|
158
|
+
// Non-throwing middleware paths (returning a Response, calling `next()`,
|
|
159
|
+
// resolving a promise) are byte-identical to before — the wrapper only
|
|
160
|
+
// intercepts thrown / rejected values.
|
|
161
|
+
wrapMiddlewareWithFilters(handler) {
|
|
162
|
+
return async (c, next)=>{
|
|
163
|
+
try {
|
|
164
|
+
return await handler(c, next);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const response = await this.mapMiddlewareError(c, error);
|
|
167
|
+
// Force-replace any handler-set response. A middleware that throws
|
|
168
|
+
// *after* `await next()` has the handler's body already attached
|
|
169
|
+
// to `c.res`; without overwriting it, Hono would emit the
|
|
170
|
+
// pre-throw success response and the filter's render would be
|
|
171
|
+
// dropped.
|
|
172
|
+
c.res = response;
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
async mapMiddlewareError(c, error) {
|
|
178
|
+
const requestContainer = this.getRequestContainer(c);
|
|
179
|
+
const filters = instantiateMany(this.globalFilters, requestContainer);
|
|
180
|
+
const host = buildMiddlewareExecutionContext(c);
|
|
181
|
+
for (const filter of filters){
|
|
182
|
+
if (shouldFilterCatch(filter, error)) {
|
|
183
|
+
try {
|
|
184
|
+
const filtered = await filter.catch(error, host);
|
|
185
|
+
return mapResponse(c, filtered);
|
|
186
|
+
} catch {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (error instanceof HttpException) {
|
|
192
|
+
const response = error.getResponse();
|
|
193
|
+
const status = error.getStatus();
|
|
194
|
+
return c.json(response, status);
|
|
195
|
+
}
|
|
196
|
+
// Non-HttpException with no catching filter: re-throw so Hono's
|
|
197
|
+
// outer error handler produces the same default-500 response it
|
|
198
|
+
// produced before this wrapping was introduced.
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
149
201
|
registerController(controller) {
|
|
150
202
|
const prefix = MetadataRegistry.getControllerPath(controller);
|
|
151
203
|
const options = MetadataRegistry.getControllerOptions(controller);
|
|
@@ -176,11 +228,11 @@ export class RouteManager {
|
|
|
176
228
|
priority: this.getMiddlewarePriority(entry)
|
|
177
229
|
})).sort((a, b)=>a.priority - b.priority || a.index - b.index);
|
|
178
230
|
for (const { entry } of sortedGlobal){
|
|
179
|
-
app.use('*', (c, next)=>{
|
|
231
|
+
app.use('*', this.wrapMiddlewareWithFilters((c, next)=>{
|
|
180
232
|
const requestContainer = this.getRequestContainer(c);
|
|
181
233
|
const resolved = instantiate(entry, requestContainer);
|
|
182
234
|
return resolved.use(c, next);
|
|
183
|
-
});
|
|
235
|
+
}));
|
|
184
236
|
}
|
|
185
237
|
const sortedConsumer = this.consumerMiddlewareDefinitions.map((def, index)=>({
|
|
186
238
|
def,
|
|
@@ -190,7 +242,7 @@ export class RouteManager {
|
|
|
190
242
|
for (const { def } of sortedConsumer){
|
|
191
243
|
const matchRoute = this.compileRouteMatcher(def.routes);
|
|
192
244
|
const matchExclude = this.compileRouteMatcher(def.excludes);
|
|
193
|
-
app.use('*', (c, next)=>{
|
|
245
|
+
app.use('*', this.wrapMiddlewareWithFilters((c, next)=>{
|
|
194
246
|
const path = c.req.path;
|
|
195
247
|
const method = c.req.method;
|
|
196
248
|
if (!matchRoute(path, method) || matchExclude(path, method)) return next();
|
|
@@ -202,7 +254,7 @@ export class RouteManager {
|
|
|
202
254
|
return instance.use(c, ()=>runChain(index + 1)).then(()=>{});
|
|
203
255
|
};
|
|
204
256
|
return runChain(0);
|
|
205
|
-
});
|
|
257
|
+
}));
|
|
206
258
|
}
|
|
207
259
|
// First pass: register all custom routes (must come before CRUD /:id routes).
|
|
208
260
|
for (const { controller, metadata, routes } of this.controllers){
|
|
@@ -215,11 +267,11 @@ export class RouteManager {
|
|
|
215
267
|
const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
|
|
216
268
|
for (const fullPath of versionedPaths){
|
|
217
269
|
for (const middlewareItem of middlewareItems){
|
|
218
|
-
app.use(fullPath, (c, next)=>{
|
|
270
|
+
app.use(fullPath, this.wrapMiddlewareWithFilters((c, next)=>{
|
|
219
271
|
const requestContainer = this.getRequestContainer(c);
|
|
220
272
|
const resolved = instantiate(middlewareItem, requestContainer);
|
|
221
273
|
return resolved.use(c, next);
|
|
222
|
-
});
|
|
274
|
+
}));
|
|
223
275
|
}
|
|
224
276
|
this.registerRoute(app, route.method, fullPath, handler);
|
|
225
277
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type { OpenApiDocument, OpenApiInfo, OpenApiOperation, OpenApiParameter,
|
|
|
9
9
|
export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, forwardRef, ModuleRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, mixin, } from './container/index';
|
|
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
|
-
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';
|
|
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, createLazyParamDecorator, applyDecorators, } from './http/index';
|
|
13
13
|
export { REQUEST_CONTEXT } from './http/request-context';
|
|
14
14
|
export type { RequestContext } from './http/request-context';
|
|
15
15
|
export { Logger, LogLevel } from './services/index';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
|
|
|
11
11
|
// Constants
|
|
12
12
|
export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
|
|
13
13
|
// HTTP Decorators
|
|
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";
|
|
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, createLazyParamDecorator, applyDecorators } from "./http/index.js";
|
|
15
15
|
// Request-scoped context primitive
|
|
16
16
|
export { REQUEST_CONTEXT } from "./http/request-context.js";
|
|
17
17
|
// Services
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
export function createZodDto(schema, options = {}) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
// Computed-property-name idiom: NamedEvaluation reads the property key and
|
|
3
|
+
// stamps `name` on the class at creation, so callers see `MyDto` (or the
|
|
4
|
+
// override) directly in stack traces and OpenAPI output — no post-hoc
|
|
5
|
+
// `Object.defineProperty` patching of the class's `name` slot.
|
|
6
|
+
const className = options.name ?? 'ZodDto';
|
|
7
|
+
const ZodDto = {
|
|
8
|
+
[className]: class {
|
|
9
|
+
static schema = schema;
|
|
10
|
+
constructor(initial){
|
|
11
|
+
if (initial && typeof initial === 'object') {
|
|
12
|
+
Object.assign(this, initial);
|
|
13
|
+
}
|
|
7
14
|
}
|
|
8
15
|
}
|
|
9
|
-
};
|
|
10
|
-
if (options.name) {
|
|
11
|
-
Object.defineProperty(ZodDto, 'name', {
|
|
12
|
-
value: options.name,
|
|
13
|
-
configurable: true
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
+
}[className];
|
|
16
17
|
return ZodDto;
|
|
17
18
|
}
|