@fluojs/http 1.0.0-beta.1 → 1.0.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +36 -5
- package/README.md +36 -5
- package/dist/adapter.d.ts +31 -0
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +37 -0
- package/dist/adapters/binding.d.ts +6 -0
- package/dist/adapters/binding.d.ts.map +1 -1
- package/dist/adapters/binding.js +54 -55
- package/dist/adapters/dto-binding-plan.d.ts +22 -0
- package/dist/adapters/dto-binding-plan.d.ts.map +1 -0
- package/dist/adapters/dto-binding-plan.js +86 -0
- package/dist/adapters/dto-validation-adapter.d.ts +3 -1
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -1
- package/dist/adapters/dto-validation-adapter.js +10 -16
- package/dist/context/sse.d.ts +38 -0
- package/dist/context/sse.d.ts.map +1 -1
- package/dist/context/sse.js +50 -2
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +262 -53
- package/dist/dispatch/dispatch-content-negotiation.d.ts +17 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -1
- package/dist/dispatch/dispatch-content-negotiation.js +21 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +8 -0
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-error-policy.js +9 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts +11 -1
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-handler-policy.js +17 -5
- package/dist/dispatch/dispatch-response-policy.d.ts +11 -1
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-response-policy.js +44 -2
- package/dist/dispatch/dispatch-routing-policy.d.ts +13 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -1
- package/dist/dispatch/dispatch-routing-policy.js +49 -4
- package/dist/dispatch/dispatcher.d.ts +24 -7
- package/dist/dispatch/dispatcher.d.ts.map +1 -1
- package/dist/dispatch/dispatcher.js +460 -48
- package/dist/dispatch/fast-path/debug-visibility.d.ts +18 -0
- package/dist/dispatch/fast-path/debug-visibility.d.ts.map +1 -0
- package/dist/dispatch/fast-path/debug-visibility.js +39 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts +22 -0
- package/dist/dispatch/fast-path/eligibility-checker.d.ts.map +1 -0
- package/dist/dispatch/fast-path/eligibility-checker.js +107 -0
- package/dist/dispatch/fast-path/eligibility.d.ts +61 -0
- package/dist/dispatch/fast-path/eligibility.d.ts.map +1 -0
- package/dist/dispatch/fast-path/eligibility.js +23 -0
- package/dist/dispatch/fast-path/fast-path-executor.d.ts +21 -0
- package/dist/dispatch/fast-path/fast-path-executor.d.ts.map +1 -0
- package/dist/dispatch/fast-path/fast-path-executor.js +80 -0
- package/dist/dispatch/fast-path/index.d.ts +6 -0
- package/dist/dispatch/fast-path/index.d.ts.map +1 -0
- package/dist/dispatch/fast-path/index.js +4 -0
- package/dist/dispatch/native-route-handoff.d.ts +53 -0
- package/dist/dispatch/native-route-handoff.d.ts.map +1 -0
- package/dist/dispatch/native-route-handoff.js +97 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/guards.d.ts +7 -0
- package/dist/guards.d.ts.map +1 -1
- package/dist/guards.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/input-error-detail.d.ts +9 -0
- package/dist/input-error-detail.d.ts.map +1 -1
- package/dist/input-error-detail.js +10 -0
- package/dist/interceptors.d.ts +8 -0
- package/dist/interceptors.d.ts.map +1 -1
- package/dist/interceptors.js +14 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +2 -1
- package/dist/mapping.d.ts +7 -0
- package/dist/mapping.d.ts.map +1 -1
- package/dist/mapping.js +93 -11
- package/dist/middleware/correlation.d.ts +5 -0
- package/dist/middleware/correlation.d.ts.map +1 -1
- package/dist/middleware/correlation.js +6 -0
- package/dist/middleware/cors.d.ts +9 -0
- package/dist/middleware/cors.d.ts.map +1 -1
- package/dist/middleware/cors.js +11 -0
- package/dist/middleware/middleware.d.ts +34 -0
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +47 -0
- package/dist/middleware/security-headers.d.ts +9 -0
- package/dist/middleware/security-headers.d.ts.map +1 -1
- package/dist/middleware/security-headers.js +11 -0
- package/dist/route-path.d.ts +41 -0
- package/dist/route-path.d.ts.map +1 -1
- package/dist/route-path.js +50 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
package/README.ko.md
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- [사용 시점](#사용-시점)
|
|
11
11
|
- [빠른 시작](#빠른-시작)
|
|
12
12
|
- [주요 패턴](#주요-패턴)
|
|
13
|
+
- [요청 정리와 런타임 이식성](#요청-정리와-런타임-이식성)
|
|
13
14
|
- [공개 API](#공개-api)
|
|
14
15
|
- [관련 패키지](#관련-패키지)
|
|
15
16
|
- [예제 소스](#예제-소스)
|
|
@@ -112,20 +113,50 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
112
113
|
}
|
|
113
114
|
```
|
|
114
115
|
|
|
116
|
+
### Versioning
|
|
117
|
+
|
|
118
|
+
`createHandlerMapping(...)`은 `VersioningType`과 `versioning` option을 통해 URI, header, media-type, custom versioning strategy를 지원합니다. Route registration은 exact/static match를 fallback보다 앞에 두고, 동등하게 정규화된 route는 registration order를 보존합니다.
|
|
119
|
+
|
|
120
|
+
### Request context helper
|
|
121
|
+
|
|
122
|
+
Framework integration이 명시적인 request context boundary나 typed per-request storage가 필요할 때 `runWithRequestContext(...)`, `assertRequestContext()`, `createRequestContext(...)`, `createContextKey(...)`, `getContextValue(...)`, `setContextValue(...)`를 사용합니다.
|
|
123
|
+
|
|
124
|
+
### Fast-path observability
|
|
125
|
+
|
|
126
|
+
Dispatcher는 adapter와 diagnostics를 위해 `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, `getDispatcherFastPathStats(...)`로 fast-path observability를 노출합니다.
|
|
127
|
+
|
|
128
|
+
### Bun decorator bundling compatibility
|
|
129
|
+
|
|
130
|
+
Fluo의 HTTP 데코레이터는 TC39 표준 데코레이터이며, runtime 또는 compiler가 표준 decorator context를 제공하면 계속 `context.metadata`를 통해 metadata를 기록합니다. Bun이 legacy TypeScript decorator transform으로 애플리케이션을 번들링하는 경우에도 controller, route, DTO binding, guard/interceptor, header, redirect, versioning, status, request DTO, `@Produces(...)` metadata를 Fluo 내부 metadata store에 기록하여 생성된 Bun bundle의 route mapping 동작을 보존합니다.
|
|
131
|
+
|
|
132
|
+
이 호환 경로는 Bun bundle output을 위한 실행 fallback입니다. 애플리케이션 소스는 계속 Fluo 표준 데코레이터를 사용해야 하며, `emitDecoratorMetadata`를 켜거나 `reflect-metadata`에 의존해서는 안 됩니다.
|
|
133
|
+
|
|
134
|
+
## 요청 정리와 런타임 이식성
|
|
135
|
+
|
|
136
|
+
디스패처는 활성 dispatch 동안에만 `AsyncLocalStorage`로 `RequestContext`를 바인딩합니다. 요청이 controller graph, middleware, guard, interceptor, observer, DTO converter, custom binder 또는 수동 `getCurrentRequestContext()` / `assertRequestContext()` container 접근을 통해 request-scoped DI를 사용할 수 있으면, 디스패처는 요청 observer가 끝난 뒤 `finally` 경로에서 isolated request-scoped DI 컨테이너를 생성하고 dispose합니다. Singleton-only route는 `RequestContext.container`가 접근되기 전까지 이 컨테이너 lifecycle을 건너뛰어 baseline 경로의 불필요한 per-request allocation을 피하면서도, graph가 모호하거나 request-scoped이면 request-scoped provider isolation을 유지합니다. 따라서 공개 `RequestContext.container` 읽기는 request-scoped provider resolve에 항상 안전합니다. singleton-only fast path는 내부 dispatcher 최적화일 뿐, 공개 context가 root container를 노출한다는 약속이 아닙니다.
|
|
137
|
+
|
|
138
|
+
어댑터는 플랫폼이 제공한다면 `FrameworkRequest.signal`에 `AbortSignal`을 전달해야 합니다. SSE에서는 가능하면 `FrameworkResponse.stream.onClose(...)`도 노출해야 합니다. `SseResponse`는 request abort와 raw stream close를 모두 구독하고, 멱등하게 닫히며, 어느 쪽이 먼저 종료되더라도 등록한 listener를 제거합니다.
|
|
139
|
+
|
|
115
140
|
## 공개 API
|
|
116
141
|
|
|
117
|
-
- **라우팅 데코레이터**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`
|
|
118
|
-
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`
|
|
119
|
-
- **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`
|
|
142
|
+
- **라우팅 데코레이터**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
143
|
+
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
|
|
144
|
+
- **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
|
|
120
145
|
- **핵심 런타임 타입**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
121
|
-
-
|
|
122
|
-
-
|
|
146
|
+
- **Adapter API**: `HttpApplicationAdapter`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
|
|
147
|
+
- **예외와 오류**: `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `HandlerNotFoundError`, `RequestAbortedError`
|
|
148
|
+
- **헬퍼**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createSecurityHeadersMiddleware`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`
|
|
149
|
+
- **Option type**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `SecurityHeadersOptions`, `SseSendOptions`
|
|
123
150
|
|
|
124
151
|
## 내부 서브경로 (`@fluojs/http/internal`)
|
|
125
152
|
|
|
126
153
|
`./internal` 서브경로는 플랫폼 어댑터와 핵심 런타임에서 사용하는 저수준 유틸리티만 내보냅니다. 이들은 변경될 수 있으며 일반적인 애플리케이션 코드에서 사용해서는 안 됩니다.
|
|
127
154
|
|
|
128
155
|
- `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
|
|
156
|
+
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: public dispatcher API를 넓히지 않고 의미 보존이 가능한 native route match를 재사용하기 위한 내부 adapter/runtime 헬퍼.
|
|
157
|
+
- `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: native route handoff를 읽거나 소비하기 위한 내부 helper.
|
|
158
|
+
- Native route handoff는 framework request에 붙는 시점의 method와 path를 함께 스냅샷합니다. app middleware가 handler matching 전에 둘 중 하나를 rewrite하면 dispatcher는 stale handoff를 무시하고 일반 route matching으로 fallback합니다.
|
|
159
|
+
- `isRoutePathNormalizationSensitive(path)`: duplicate slash와 trailing slash 요청을 generic dispatcher 경로에 남기기 위한 내부 guard.
|
|
129
160
|
- `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
|
|
130
161
|
|
|
131
162
|
## 관련 패키지
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ The HTTP execution layer that turns route metadata into a request pipeline with
|
|
|
10
10
|
- [When to Use](#when-to-use)
|
|
11
11
|
- [Quick Start](#quick-start)
|
|
12
12
|
- [Common Patterns](#common-patterns)
|
|
13
|
+
- [Request Cleanup and Portability](#request-cleanup-and-portability)
|
|
13
14
|
- [Public API](#public-api)
|
|
14
15
|
- [Related Packages](#related-packages)
|
|
15
16
|
- [Example Sources](#example-sources)
|
|
@@ -114,20 +115,50 @@ stream(_input: undefined, ctx: RequestContext) {
|
|
|
114
115
|
}
|
|
115
116
|
```
|
|
116
117
|
|
|
118
|
+
### Versioning
|
|
119
|
+
|
|
120
|
+
`createHandlerMapping(...)` supports URI, header, media-type, and custom versioning strategies through `VersioningType` and the `versioning` option. Route registration keeps exact/static matches ahead of fallbacks while preserving registration order for equivalent normalized routes.
|
|
121
|
+
|
|
122
|
+
### Request context helpers
|
|
123
|
+
|
|
124
|
+
Use `runWithRequestContext(...)`, `assertRequestContext()`, `createRequestContext(...)`, `createContextKey(...)`, `getContextValue(...)`, and `setContextValue(...)` when framework integrations need explicit request context boundaries or typed per-request storage.
|
|
125
|
+
|
|
126
|
+
### Fast-path observability
|
|
127
|
+
|
|
128
|
+
The dispatcher exposes fast-path observability for adapters and diagnostics through `FAST_PATH_ELIGIBILITY_SYMBOL`, `FAST_PATH_STATS_SYMBOL`, `formatFastPathStats(...)`, and `getDispatcherFastPathStats(...)`.
|
|
129
|
+
|
|
130
|
+
### Bun decorator bundling compatibility
|
|
131
|
+
|
|
132
|
+
Fluo's HTTP decorators are standard TC39 decorators and continue to record metadata through `context.metadata` when the runtime or compiler provides the standard decorator context. When Bun bundles an application through its legacy TypeScript decorator transform, the same controller, route, DTO binding, guard/interceptor, header, redirect, versioning, status, request DTO, and `@Produces(...)` metadata is recorded through Fluo's internal metadata stores so generated Bun bundles preserve route mapping behavior.
|
|
133
|
+
|
|
134
|
+
This compatibility path is an execution fallback for Bun bundle output; application source should still use Fluo's standard decorators and should not enable `emitDecoratorMetadata` or rely on `reflect-metadata`.
|
|
135
|
+
|
|
136
|
+
## Request Cleanup and Portability
|
|
137
|
+
|
|
138
|
+
The dispatcher binds `RequestContext` with `AsyncLocalStorage` for the active dispatch only. When a request may use request-scoped DI through its controller graph, middleware, guards, interceptors, observers, DTO converters, a custom binder, or manual `getCurrentRequestContext()` / `assertRequestContext()` container access, the dispatcher creates and disposes an isolated request-scoped DI container from its `finally` path after request observers finish. Singleton-only routes skip that container lifecycle until `RequestContext.container` is accessed, so the baseline path avoids unnecessary per-request allocation while preserving request-scoped provider isolation whenever the graph is ambiguous or request-scoped. Public `RequestContext.container` reads are therefore always safe for resolving request-scoped providers; the singleton-only fast path is an internal dispatcher optimization, not a promise that the public context exposes the root container.
|
|
139
|
+
|
|
140
|
+
Adapters should pass an `AbortSignal` on `FrameworkRequest.signal` when the platform exposes one. For SSE, adapters should also expose `FrameworkResponse.stream.onClose(...)` when possible; `SseResponse` listens to both request abort and raw stream close, closes idempotently, and removes registered listeners when either side terminates first.
|
|
141
|
+
|
|
117
142
|
## Public API
|
|
118
143
|
|
|
119
|
-
- **Routing decorators**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`
|
|
120
|
-
- **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`
|
|
121
|
-
- **Execution decorators**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`
|
|
144
|
+
- **Routing decorators**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`, `Options`, `Head`
|
|
145
|
+
- **Binding decorators**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`, `Optional`, `Convert`
|
|
146
|
+
- **Execution decorators**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`, `Produces`
|
|
122
147
|
- **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
123
|
-
- **
|
|
124
|
-
- **
|
|
148
|
+
- **Adapter API**: `HttpApplicationAdapter`, `createNoopHttpApplicationAdapter`, `createServerBackedHttpAdapterRealtimeCapability`, `createUnsupportedHttpAdapterRealtimeCapability`, `createFetchStyleHttpAdapterRealtimeCapability`
|
|
149
|
+
- **Exceptions and errors**: `HttpException`, `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `ConflictException`, `NotAcceptableException`, `TooManyRequestsException`, `InternalServerErrorException`, `PayloadTooLargeException`, `createErrorResponse`, `RouteConflictError`, `InvalidRoutePathError`, `HandlerNotFoundError`, `RequestAbortedError`
|
|
150
|
+
- **Helpers**: `createHandlerMapping`, `createDispatcher`, `forRoutes`, `normalizeRoutePattern`, `matchRoutePattern`, `isMiddlewareRouteConfig`, `createCorrelationMiddleware`, `createCorsMiddleware`, `createRateLimitMiddleware`, `createSecurityHeadersMiddleware`, `runWithRequestContext`, `getCurrentRequestContext`, `assertRequestContext`, `createRequestContext`, `createContextKey`, `getContextValue`, `setContextValue`, `encodeSseComment`, `encodeSseMessage`
|
|
151
|
+
- **Option types**: `CorsOptions`, `RateLimitOptions`, `RateLimitStore`, `SecurityHeadersOptions`, `SseSendOptions`
|
|
125
152
|
|
|
126
153
|
## Internal Subpath (`@fluojs/http/internal`)
|
|
127
154
|
|
|
128
155
|
The `./internal` subpath exports only the low-level utilities used by platform adapters and the core runtime. These are subject to change and should not be used in typical application code.
|
|
129
156
|
|
|
130
157
|
- `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
|
|
158
|
+
- `bindRawRequestNativeRouteHandoff(...)` / `attachFrameworkRequestNativeRouteHandoff(...)`: Internal adapter/runtime helpers for reusing semantically safe native route matches without widening the public dispatcher API.
|
|
159
|
+
- `consumeRawRequestNativeRouteHandoff(...)` / `readFrameworkRequestNativeRouteHandoff(...)`: Internal helpers for reading or consuming native route handoffs.
|
|
160
|
+
- Native route handoffs snapshot the framework request method and path when attached; if app middleware rewrites either value before handler matching, the dispatcher ignores the stale handoff and falls back to normal route matching.
|
|
161
|
+
- `isRoutePathNormalizationSensitive(path)`: Internal guard for keeping duplicate-slash and trailing-slash requests on the generic dispatcher path.
|
|
131
162
|
- `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
|
|
132
163
|
|
|
133
164
|
## Related Packages
|
package/dist/adapter.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
import type { MaybePromise } from '@fluojs/core';
|
|
2
2
|
import type { Dispatcher } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Describes the server backed http adapter realtime capability contract.
|
|
5
|
+
*/
|
|
3
6
|
export interface ServerBackedHttpAdapterRealtimeCapability {
|
|
4
7
|
kind: 'server-backed';
|
|
5
8
|
server: unknown;
|
|
6
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Describes the unsupported http adapter realtime capability contract.
|
|
12
|
+
*/
|
|
7
13
|
export interface UnsupportedHttpAdapterRealtimeCapability {
|
|
8
14
|
kind: 'unsupported';
|
|
9
15
|
mode: 'no-op';
|
|
10
16
|
reason: string;
|
|
11
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Describes the fetch style http adapter realtime capability contract.
|
|
20
|
+
*/
|
|
12
21
|
export interface FetchStyleHttpAdapterRealtimeCapability {
|
|
13
22
|
contract: 'raw-websocket-expansion';
|
|
14
23
|
kind: 'fetch-style';
|
|
@@ -17,9 +26,31 @@ export interface FetchStyleHttpAdapterRealtimeCapability {
|
|
|
17
26
|
support: 'contract-only' | 'supported';
|
|
18
27
|
version: 1;
|
|
19
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Defines the http adapter realtime capability type.
|
|
31
|
+
*/
|
|
20
32
|
export type HttpAdapterRealtimeCapability = ServerBackedHttpAdapterRealtimeCapability | FetchStyleHttpAdapterRealtimeCapability | UnsupportedHttpAdapterRealtimeCapability;
|
|
33
|
+
/**
|
|
34
|
+
* Create server backed http adapter realtime capability.
|
|
35
|
+
*
|
|
36
|
+
* @param server The server.
|
|
37
|
+
* @returns The create server backed http adapter realtime capability result.
|
|
38
|
+
*/
|
|
21
39
|
export declare function createServerBackedHttpAdapterRealtimeCapability(server: unknown): ServerBackedHttpAdapterRealtimeCapability;
|
|
40
|
+
/**
|
|
41
|
+
* Create unsupported http adapter realtime capability.
|
|
42
|
+
*
|
|
43
|
+
* @param reason The reason.
|
|
44
|
+
* @returns The create unsupported http adapter realtime capability result.
|
|
45
|
+
*/
|
|
22
46
|
export declare function createUnsupportedHttpAdapterRealtimeCapability(reason: string): UnsupportedHttpAdapterRealtimeCapability;
|
|
47
|
+
/**
|
|
48
|
+
* Create fetch style http adapter realtime capability.
|
|
49
|
+
*
|
|
50
|
+
* @param reason The reason.
|
|
51
|
+
* @param options The options.
|
|
52
|
+
* @returns The create fetch style http adapter realtime capability result.
|
|
53
|
+
*/
|
|
23
54
|
export declare function createFetchStyleHttpAdapterRealtimeCapability(reason: string, options?: {
|
|
24
55
|
support?: FetchStyleHttpAdapterRealtimeCapability['support'];
|
|
25
56
|
}): FetchStyleHttpAdapterRealtimeCapability;
|
package/dist/adapter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,WAAW,yCAAyC;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,wCAAwC;IACvD,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uCAAuC;IACtD,QAAQ,EAAE,yBAAyB,CAAC;IACpC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,eAAe,GAAG,WAAW,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC;CACZ;AAED,MAAM,MAAM,6BAA6B,GACrC,yCAAyC,GACzC,uCAAuC,GACvC,wCAAwC,CAAC;AAE7C,wBAAgB,+CAA+C,CAC7D,MAAM,EAAE,OAAO,GACd,yCAAyC,CAK3C;AAED,wBAAgB,8CAA8C,CAC5D,MAAM,EAAE,MAAM,GACb,wCAAwC,CAM1C;AAED,wBAAgB,6CAA6C,CAC3D,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,uCAAuC,CAAC,SAAS,CAAC,CAAC;CACzD,GACL,uCAAuC,CASzC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC;IAEtB,qBAAqB,CAAC,IAAI,6BAA6B,CAAC;IAExD;;;;;OAKG;IACH,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEnD;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,IAAI,sBAAsB,CAUzE"}
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;GAEG;AACH,MAAM,WAAW,yCAAyC;IACxD,IAAI,EAAE,eAAe,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,wCAAwC;IACvD,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,uCAAuC;IACtD,QAAQ,EAAE,yBAAyB,CAAC;IACpC,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,eAAe,GAAG,WAAW,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,MAAM,6BAA6B,GACrC,yCAAyC,GACzC,uCAAuC,GACvC,wCAAwC,CAAC;AAE7C;;;;;GAKG;AACH,wBAAgB,+CAA+C,CAC7D,MAAM,EAAE,OAAO,GACd,yCAAyC,CAK3C;AAED;;;;;GAKG;AACH,wBAAgB,8CAA8C,CAC5D,MAAM,EAAE,MAAM,GACb,wCAAwC,CAM1C;AAED;;;;;;GAMG;AACH,wBAAgB,6CAA6C,CAC3D,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IACP,OAAO,CAAC,EAAE,uCAAuC,CAAC,SAAS,CAAC,CAAC;CACzD,GACL,uCAAuC,CASzC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,SAAS,CAAC,IAAI,OAAO,CAAC;IAEtB,qBAAqB,CAAC,IAAI,6BAA6B,CAAC;IAExD;;;;;OAKG;IACH,MAAM,CAAC,UAAU,EAAE,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAEnD;;;;;OAKG;IACH,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,gCAAgC,IAAI,sBAAsB,CAUzE"}
|
package/dist/adapter.js
CHANGED
|
@@ -1,9 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes the server backed http adapter realtime capability contract.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Describes the unsupported http adapter realtime capability contract.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Describes the fetch style http adapter realtime capability contract.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defines the http adapter realtime capability type.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create server backed http adapter realtime capability.
|
|
19
|
+
*
|
|
20
|
+
* @param server The server.
|
|
21
|
+
* @returns The create server backed http adapter realtime capability result.
|
|
22
|
+
*/
|
|
1
23
|
export function createServerBackedHttpAdapterRealtimeCapability(server) {
|
|
2
24
|
return {
|
|
3
25
|
kind: 'server-backed',
|
|
4
26
|
server
|
|
5
27
|
};
|
|
6
28
|
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create unsupported http adapter realtime capability.
|
|
32
|
+
*
|
|
33
|
+
* @param reason The reason.
|
|
34
|
+
* @returns The create unsupported http adapter realtime capability result.
|
|
35
|
+
*/
|
|
7
36
|
export function createUnsupportedHttpAdapterRealtimeCapability(reason) {
|
|
8
37
|
return {
|
|
9
38
|
kind: 'unsupported',
|
|
@@ -11,6 +40,14 @@ export function createUnsupportedHttpAdapterRealtimeCapability(reason) {
|
|
|
11
40
|
reason
|
|
12
41
|
};
|
|
13
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create fetch style http adapter realtime capability.
|
|
46
|
+
*
|
|
47
|
+
* @param reason The reason.
|
|
48
|
+
* @param options The options.
|
|
49
|
+
* @returns The create fetch style http adapter realtime capability result.
|
|
50
|
+
*/
|
|
14
51
|
export function createFetchStyleHttpAdapterRealtimeCapability(reason, options = {}) {
|
|
15
52
|
return {
|
|
16
53
|
contract: 'raw-websocket-expansion',
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import { type Constructor } from '@fluojs/core';
|
|
2
2
|
import type { ArgumentResolverContext, Binder, Converter, ConverterLike, ConverterTarget } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents the default converter.
|
|
5
|
+
*/
|
|
3
6
|
export declare class DefaultConverter implements Converter {
|
|
4
7
|
convert(value: unknown, _target: ConverterTarget): unknown;
|
|
5
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Represents the default binder.
|
|
11
|
+
*/
|
|
6
12
|
export declare class DefaultBinder implements Binder {
|
|
7
13
|
private readonly converters;
|
|
8
14
|
constructor(converters?: readonly ConverterLike[]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAAc,MAAM,cAAc,CAAC;AAI5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAC;AAkDhI;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;CAG3D;AAyDD;;GAEG;AACH,qBAAa,aAAc,YAAW,MAAM;IAC9B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAAV,UAAU,GAAE,SAAS,aAAa,EAAO;IAEhE,IAAI,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,uBAAuB,GAAG,OAAO,CAAC,OAAO,CAAC;CAuGjF"}
|
package/dist/adapters/binding.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { InvariantError } from '@fluojs/core';
|
|
2
|
-
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
3
2
|
import { BadRequestException } from '../exceptions.js';
|
|
4
3
|
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
4
|
+
import { getCompiledDtoBindingPlan } from './dto-binding-plan.js';
|
|
5
5
|
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
|
+
const NO_CONVERTERS = [];
|
|
6
7
|
function isPlainObject(value) {
|
|
7
8
|
if (typeof value !== 'object' || value === null) {
|
|
8
9
|
return false;
|
|
@@ -10,44 +11,6 @@ function isPlainObject(value) {
|
|
|
10
11
|
const prototype = Object.getPrototypeOf(value);
|
|
11
12
|
return prototype === Object.prototype || prototype === null;
|
|
12
13
|
}
|
|
13
|
-
function toFieldName(propertyKey) {
|
|
14
|
-
return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
15
|
-
}
|
|
16
|
-
function resolveSourceKey(propertyKey, key) {
|
|
17
|
-
return key ?? toFieldName(propertyKey);
|
|
18
|
-
}
|
|
19
|
-
function readHeader(request, key) {
|
|
20
|
-
return request.headers[key.toLowerCase()] ?? request.headers[key];
|
|
21
|
-
}
|
|
22
|
-
function readSourceValue(request, source, propertyKey, key) {
|
|
23
|
-
const resolvedKey = resolveSourceKey(propertyKey, key);
|
|
24
|
-
switch (source) {
|
|
25
|
-
case 'path':
|
|
26
|
-
return request.params[resolvedKey];
|
|
27
|
-
case 'query':
|
|
28
|
-
return request.query[resolvedKey];
|
|
29
|
-
case 'header':
|
|
30
|
-
return readHeader(request, resolvedKey);
|
|
31
|
-
case 'cookie':
|
|
32
|
-
return request.cookies[resolvedKey];
|
|
33
|
-
case 'body':
|
|
34
|
-
{
|
|
35
|
-
if (!isPlainObject(request.body)) {
|
|
36
|
-
if (request.body !== undefined && request.body !== null) {
|
|
37
|
-
throw new BadRequestException('Request body must be a plain object.', {
|
|
38
|
-
details: [toInputErrorDetail({
|
|
39
|
-
code: 'INVALID_BODY',
|
|
40
|
-
message: 'Request body must be a plain object.',
|
|
41
|
-
source: 'body'
|
|
42
|
-
})]
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
return request.body[resolvedKey];
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
14
|
function validateBodyKeys(request, bodyKeys) {
|
|
52
15
|
if (request.body === undefined || request.body === null) {
|
|
53
16
|
return;
|
|
@@ -87,6 +50,10 @@ function validateBodyKeys(request, bodyKeys) {
|
|
|
87
50
|
});
|
|
88
51
|
}
|
|
89
52
|
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Represents the default converter.
|
|
56
|
+
*/
|
|
90
57
|
export class DefaultConverter {
|
|
91
58
|
convert(value, _target) {
|
|
92
59
|
return value;
|
|
@@ -131,47 +98,79 @@ async function resolveConverter(value, context, cache) {
|
|
|
131
98
|
throw error;
|
|
132
99
|
}
|
|
133
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Represents the default binder.
|
|
104
|
+
*/
|
|
134
105
|
export class DefaultBinder {
|
|
135
106
|
constructor(converters = []) {
|
|
136
107
|
this.converters = converters;
|
|
137
108
|
}
|
|
138
109
|
async bind(dto, context) {
|
|
139
|
-
const
|
|
110
|
+
const plan = getCompiledDtoBindingPlan(dto);
|
|
111
|
+
const request = context.requestContext.request;
|
|
140
112
|
const value = new dto();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
validateBodyKeys(context.requestContext.request, bodyKeys);
|
|
113
|
+
if (request.body !== undefined && request.body !== null) {
|
|
114
|
+
validateBodyKeys(request, plan.bodyKeys);
|
|
115
|
+
}
|
|
145
116
|
const details = [];
|
|
146
|
-
|
|
147
|
-
|
|
117
|
+
if (this.converters.length === 0 && !plan.hasFieldConverters) {
|
|
118
|
+
for (const entry of plan.entries) {
|
|
119
|
+
const rawValue = entry.read(request);
|
|
120
|
+
if (rawValue === undefined) {
|
|
121
|
+
if (entry.optional) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
details.push(toInputErrorDetail({
|
|
125
|
+
code: 'MISSING_FIELD',
|
|
126
|
+
field: entry.fieldName,
|
|
127
|
+
message: `Missing required ${entry.source} field ${entry.sourceKey}.`,
|
|
128
|
+
source: entry.source
|
|
129
|
+
}));
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
value[entry.propertyKey] = rawValue;
|
|
133
|
+
}
|
|
134
|
+
if (details.length > 0) {
|
|
135
|
+
throw new BadRequestException('Request binding failed.', {
|
|
136
|
+
details
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
const converterCache = new Map();
|
|
142
|
+
const globalConverters = this.converters.length === 0 ? NO_CONVERTERS : (await Promise.all(this.converters.map(converter => resolveConverter(converter, context, converterCache)))).filter(converter => Boolean(converter));
|
|
143
|
+
for (const entry of plan.entries) {
|
|
144
|
+
const rawValue = entry.read(request);
|
|
148
145
|
if (rawValue === undefined) {
|
|
149
|
-
if (entry.
|
|
146
|
+
if (entry.optional) {
|
|
150
147
|
continue;
|
|
151
148
|
}
|
|
152
149
|
details.push(toInputErrorDetail({
|
|
153
150
|
code: 'MISSING_FIELD',
|
|
154
|
-
field:
|
|
155
|
-
message: `Missing required ${entry.
|
|
156
|
-
source: entry.
|
|
151
|
+
field: entry.fieldName,
|
|
152
|
+
message: `Missing required ${entry.source} field ${entry.sourceKey}.`,
|
|
153
|
+
source: entry.source
|
|
157
154
|
}));
|
|
158
155
|
continue;
|
|
159
156
|
}
|
|
160
157
|
const target = {
|
|
161
158
|
dto,
|
|
162
159
|
handler: context.handler,
|
|
163
|
-
key:
|
|
160
|
+
key: entry.sourceKey,
|
|
164
161
|
propertyKey: entry.propertyKey,
|
|
165
162
|
requestContext: context.requestContext,
|
|
166
|
-
source: entry.
|
|
163
|
+
source: entry.source
|
|
167
164
|
};
|
|
168
165
|
let convertedValue = rawValue;
|
|
169
166
|
for (const converter of globalConverters) {
|
|
170
167
|
convertedValue = await converter.convert(convertedValue, target);
|
|
171
168
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
169
|
+
if (entry.converter !== undefined) {
|
|
170
|
+
const fieldConverter = await resolveConverter(entry.converter, context, converterCache);
|
|
171
|
+
if (fieldConverter) {
|
|
172
|
+
convertedValue = await fieldConverter.convert(convertedValue, target);
|
|
173
|
+
}
|
|
175
174
|
}
|
|
176
175
|
value[entry.propertyKey] = convertedValue;
|
|
177
176
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Constructor, type MetadataPropertyKey, type MetadataSource } from '@fluojs/core';
|
|
2
|
+
import { type DtoFieldBindingMetadata } from '@fluojs/core/internal';
|
|
3
|
+
import type { FrameworkRequest } from '../types.js';
|
|
4
|
+
export interface CompiledDtoBindingPlanEntry {
|
|
5
|
+
readonly converter?: DtoFieldBindingMetadata['converter'];
|
|
6
|
+
readonly fieldName: string;
|
|
7
|
+
readonly optional: boolean;
|
|
8
|
+
readonly propertyKey: MetadataPropertyKey;
|
|
9
|
+
readonly read: (request: FrameworkRequest) => unknown;
|
|
10
|
+
readonly source: MetadataSource;
|
|
11
|
+
readonly sourceKey: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CompiledDtoBindingPlan {
|
|
14
|
+
readonly bodyKeys: ReadonlySet<string>;
|
|
15
|
+
readonly entries: readonly CompiledDtoBindingPlanEntry[];
|
|
16
|
+
readonly hasFieldConverters: boolean;
|
|
17
|
+
readonly needsValidation: boolean;
|
|
18
|
+
readonly propertyKeys: readonly MetadataPropertyKey[];
|
|
19
|
+
readonly toValidationValue: (value: unknown) => unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare function getCompiledDtoBindingPlan(dto: Constructor): CompiledDtoBindingPlan;
|
|
22
|
+
//# sourceMappingURL=dto-binding-plan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto-binding-plan.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-binding-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,mBAAmB,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAC/F,OAAO,EAKL,KAAK,uBAAuB,EAE7B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAcpD,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,CAAC,EAAE,uBAAuB,CAAC,WAAW,CAAC,CAAC;IAC1D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;IAC1C,QAAQ,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,2BAA2B,EAAE,CAAC;IACzD,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACtD,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CACzD;AAkDD,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,WAAW,GAAG,sBAAsB,CA4ClF"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/internal';
|
|
2
|
+
function toFieldName(propertyKey) {
|
|
3
|
+
return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
4
|
+
}
|
|
5
|
+
function resolveSourceKey(propertyKey, key) {
|
|
6
|
+
return key ?? toFieldName(propertyKey);
|
|
7
|
+
}
|
|
8
|
+
function readHeader(request, key) {
|
|
9
|
+
return request.headers[key.toLowerCase()] ?? request.headers[key];
|
|
10
|
+
}
|
|
11
|
+
const dtoBindingPlanCache = new WeakMap();
|
|
12
|
+
function createSourceReader(source, sourceKey) {
|
|
13
|
+
switch (source) {
|
|
14
|
+
case 'path':
|
|
15
|
+
return request => request.params[sourceKey];
|
|
16
|
+
case 'query':
|
|
17
|
+
return request => request.query[sourceKey];
|
|
18
|
+
case 'header':
|
|
19
|
+
return request => readHeader(request, sourceKey);
|
|
20
|
+
case 'cookie':
|
|
21
|
+
return request => request.cookies[sourceKey];
|
|
22
|
+
case 'body':
|
|
23
|
+
return request => request.body?.[sourceKey];
|
|
24
|
+
default:
|
|
25
|
+
return () => undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function identityValidationValue(value) {
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function createValidationValueFilter(propertyKeys) {
|
|
32
|
+
return value => {
|
|
33
|
+
if (typeof value !== 'object' || value === null) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
const source = value;
|
|
37
|
+
const filtered = Object.create(Object.getPrototypeOf(value));
|
|
38
|
+
for (const propertyKey of propertyKeys) {
|
|
39
|
+
if (Object.hasOwn(source, propertyKey)) {
|
|
40
|
+
filtered[propertyKey] = source[propertyKey];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filtered;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function isDtoAwareValidationRule(rule) {
|
|
47
|
+
return rule.kind === 'custom' || rule.kind === 'validateIf';
|
|
48
|
+
}
|
|
49
|
+
export function getCompiledDtoBindingPlan(dto) {
|
|
50
|
+
const cached = dtoBindingPlanCache.get(dto);
|
|
51
|
+
if (cached) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
const entries = getDtoBindingSchema(dto).map(entry => {
|
|
55
|
+
const sourceKey = resolveSourceKey(entry.propertyKey, entry.metadata.key);
|
|
56
|
+
return {
|
|
57
|
+
...(entry.metadata.converter === undefined ? {} : {
|
|
58
|
+
converter: entry.metadata.converter
|
|
59
|
+
}),
|
|
60
|
+
fieldName: toFieldName(entry.propertyKey),
|
|
61
|
+
optional: entry.metadata.optional === true,
|
|
62
|
+
propertyKey: entry.propertyKey,
|
|
63
|
+
read: createSourceReader(entry.metadata.source, sourceKey),
|
|
64
|
+
source: entry.metadata.source,
|
|
65
|
+
sourceKey
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const propertyKeys = entries.map(entry => entry.propertyKey);
|
|
69
|
+
const boundPropertyKeys = new Set(propertyKeys);
|
|
70
|
+
const validationSchema = getDtoValidationSchema(dto);
|
|
71
|
+
const validationPropertyKeys = validationSchema.map(entry => entry.propertyKey);
|
|
72
|
+
const hasClassValidationRules = getClassValidationRules(dto).length > 0;
|
|
73
|
+
const hasDtoAwareValidationRules = validationSchema.some(entry => entry.rules.some(rule => isDtoAwareValidationRule(rule)));
|
|
74
|
+
const needsValidation = hasClassValidationRules || validationPropertyKeys.length > 0;
|
|
75
|
+
const requiresValidationFilter = hasClassValidationRules || hasDtoAwareValidationRules || validationPropertyKeys.some(propertyKey => !boundPropertyKeys.has(propertyKey));
|
|
76
|
+
const next = {
|
|
77
|
+
bodyKeys: new Set(entries.filter(entry => entry.source === 'body').map(entry => entry.sourceKey)),
|
|
78
|
+
entries,
|
|
79
|
+
hasFieldConverters: entries.some(entry => entry.converter !== undefined),
|
|
80
|
+
needsValidation,
|
|
81
|
+
propertyKeys,
|
|
82
|
+
toValidationValue: requiresValidationFilter ? createValidationValueFilter(propertyKeys) : identityValidationValue
|
|
83
|
+
};
|
|
84
|
+
dtoBindingPlanCache.set(dto, next);
|
|
85
|
+
return next;
|
|
86
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { type Constructor } from '@fluojs/core';
|
|
2
2
|
import type { Validator } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Represents the http dto validation adapter.
|
|
5
|
+
*/
|
|
3
6
|
export declare class HttpDtoValidationAdapter implements Validator {
|
|
4
7
|
private readonly validator;
|
|
5
8
|
private throwBadRequestForValidationError;
|
|
6
|
-
private filterUnboundRequestDtoFields;
|
|
7
9
|
validate(value: unknown, target: Constructor): Promise<void>;
|
|
8
10
|
materialize<T>(value: unknown, target: Constructor<T>): Promise<T>;
|
|
9
11
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dto-validation-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-validation-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"dto-validation-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/dto-validation-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAKhD,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,aAAa,CAAC;AAG9D;;GAEG;AACH,qBAAa,wBAAyB,YAAW,SAAS;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IAExD,OAAO,CAAC,iCAAiC;IAMnC,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAWzE"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
2
1
|
import { DefaultValidator as BaseDefaultValidator, DtoValidationError } from '@fluojs/validation';
|
|
3
2
|
import { BadRequestException } from '../exceptions.js';
|
|
4
3
|
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
4
|
+
import { getCompiledDtoBindingPlan } from './dto-binding-plan.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents the http dto validation adapter.
|
|
8
|
+
*/
|
|
5
9
|
export class HttpDtoValidationAdapter {
|
|
6
10
|
validator = new BaseDefaultValidator();
|
|
7
11
|
throwBadRequestForValidationError(error) {
|
|
@@ -9,23 +13,13 @@ export class HttpDtoValidationAdapter {
|
|
|
9
13
|
details: error.issues.map(issue => toInputErrorDetail(issue))
|
|
10
14
|
});
|
|
11
15
|
}
|
|
12
|
-
filterUnboundRequestDtoFields(value, target) {
|
|
13
|
-
if (typeof value !== 'object' || value === null) {
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
|
-
const source = value;
|
|
17
|
-
const filtered = Object.create(Object.getPrototypeOf(value));
|
|
18
|
-
for (const binding of getDtoBindingSchema(target)) {
|
|
19
|
-
if (Object.hasOwn(source, binding.propertyKey)) {
|
|
20
|
-
filtered[binding.propertyKey] = source[binding.propertyKey];
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return filtered;
|
|
24
|
-
}
|
|
25
16
|
async validate(value, target) {
|
|
26
17
|
try {
|
|
27
|
-
const
|
|
28
|
-
|
|
18
|
+
const plan = getCompiledDtoBindingPlan(target);
|
|
19
|
+
if (!plan.needsValidation) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await this.validator.validate(plan.toValidationValue(value), target);
|
|
29
23
|
} catch (error) {
|
|
30
24
|
if (error instanceof DtoValidationError) {
|
|
31
25
|
this.throwBadRequestForValidationError(error);
|