@fluojs/http 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +142 -0
- package/README.md +144 -0
- package/dist/adapter.d.ts +58 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +42 -0
- package/dist/adapters/binding.d.ts +11 -0
- package/dist/adapters/binding.d.ts.map +1 -0
- package/dist/adapters/binding.js +185 -0
- package/dist/adapters/dto-validation-adapter.d.ts +10 -0
- package/dist/adapters/dto-validation-adapter.d.ts.map +1 -0
- package/dist/adapters/dto-validation-adapter.js +46 -0
- package/dist/client-identity.d.ts +21 -0
- package/dist/client-identity.d.ts.map +1 -0
- package/dist/client-identity.js +108 -0
- package/dist/context/request-context.d.ts +53 -0
- package/dist/context/request-context.d.ts.map +1 -0
- package/dist/context/request-context.js +89 -0
- package/dist/context/sse.d.ts +21 -0
- package/dist/context/sse.d.ts.map +1 -0
- package/dist/context/sse.js +106 -0
- package/dist/decorators.d.ts +188 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +378 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts +9 -0
- package/dist/dispatch/dispatch-content-negotiation.d.ts.map +1 -0
- package/dist/dispatch/dispatch-content-negotiation.js +164 -0
- package/dist/dispatch/dispatch-error-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-error-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-error-policy.js +24 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts +3 -0
- package/dist/dispatch/dispatch-handler-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-handler-policy.js +21 -0
- package/dist/dispatch/dispatch-response-policy.d.ts +7 -0
- package/dist/dispatch/dispatch-response-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-response-policy.js +45 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts +4 -0
- package/dist/dispatch/dispatch-routing-policy.d.ts.map +1 -0
- package/dist/dispatch/dispatch-routing-policy.js +14 -0
- package/dist/dispatch/dispatcher.d.ts +36 -0
- package/dist/dispatch/dispatcher.d.ts.map +1 -0
- package/dist/dispatch/dispatcher.js +196 -0
- package/dist/errors.d.ts +23 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +41 -0
- package/dist/exceptions.d.ts +174 -0
- package/dist/exceptions.d.ts.map +1 -0
- package/dist/exceptions.js +222 -0
- package/dist/guards.d.ts +3 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +19 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/input-error-detail.d.ts +10 -0
- package/dist/input-error-detail.d.ts.map +1 -0
- package/dist/input-error-detail.js +8 -0
- package/dist/interceptors.d.ts +3 -0
- package/dist/interceptors.d.ts.map +1 -0
- package/dist/interceptors.js +22 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +2 -0
- package/dist/mapping.d.ts +7 -0
- package/dist/mapping.d.ts.map +1 -0
- package/dist/mapping.js +244 -0
- package/dist/middleware/correlation.d.ts +3 -0
- package/dist/middleware/correlation.d.ts.map +1 -0
- package/dist/middleware/correlation.js +19 -0
- package/dist/middleware/cors.d.ts +11 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +57 -0
- package/dist/middleware/middleware.d.ts +8 -0
- package/dist/middleware/middleware.d.ts.map +1 -0
- package/dist/middleware/middleware.js +64 -0
- package/dist/middleware/rate-limit.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +106 -0
- package/dist/middleware/security-headers.d.ts +12 -0
- package/dist/middleware/security-headers.d.ts.map +1 -0
- package/dist/middleware/security-headers.js +47 -0
- package/dist/route-path.d.ts +15 -0
- package/dist/route-path.d.ts.map +1 -0
- package/dist/route-path.js +69 -0
- package/dist/types.d.ts +274 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +114 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fluo contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @fluojs/http
|
|
2
|
+
|
|
3
|
+
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
|
|
5
|
+
라우트 메타데이터를 DTO 바인딩, 검증, 가드, 인터셉터, 응답 작성으로 이어지는 요청 파이프라인으로 바꾸는 HTTP 실행 레이어입니다.
|
|
6
|
+
|
|
7
|
+
## 목차
|
|
8
|
+
|
|
9
|
+
- [설치](#설치)
|
|
10
|
+
- [사용 시점](#사용-시점)
|
|
11
|
+
- [빠른 시작](#빠른-시작)
|
|
12
|
+
- [주요 패턴](#주요-패턴)
|
|
13
|
+
- [공개 API](#공개-api)
|
|
14
|
+
- [관련 패키지](#관련-패키지)
|
|
15
|
+
- [예제 소스](#예제-소스)
|
|
16
|
+
|
|
17
|
+
## 설치
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/http
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 사용 시점
|
|
24
|
+
|
|
25
|
+
- `@Controller`, `@Get`, `@Post` 같은 데코레이터로 REST 스타일 엔드포인트를 선언할 때
|
|
26
|
+
- `@FromBody`, `@FromPath`, `@FromQuery`로 요청 데이터를 DTO에 바인딩할 때
|
|
27
|
+
- 가드, 인터셉터, 미들웨어를 예측 가능한 요청 라이프사이클에 얹고 싶을 때
|
|
28
|
+
- 현재 요청을 `RequestContext`로 깊은 호출 스택에서 접근하고 싶을 때
|
|
29
|
+
|
|
30
|
+
## 빠른 시작
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { Controller, FromBody, FromPath, Get, Post, RequestDto } from '@fluojs/http';
|
|
34
|
+
import { IsString, MinLength } from '@fluojs/validation';
|
|
35
|
+
|
|
36
|
+
class CreateUserDto {
|
|
37
|
+
@FromBody()
|
|
38
|
+
@IsString()
|
|
39
|
+
@MinLength(3)
|
|
40
|
+
name!: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@Controller('/users')
|
|
44
|
+
export class UserController {
|
|
45
|
+
@Post('/')
|
|
46
|
+
@RequestDto(CreateUserDto)
|
|
47
|
+
create(input: CreateUserDto) {
|
|
48
|
+
return { id: '1', name: input.name };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Get('/:id')
|
|
52
|
+
getById(@FromPath('id') id: string) {
|
|
53
|
+
return { id, name: 'John Doe' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 라우트 경로 계약
|
|
59
|
+
|
|
60
|
+
`@Controller()`, `@Get()`, `@Post()` 같은 HTTP 라우트 데코레이터는 다음만 허용합니다.
|
|
61
|
+
|
|
62
|
+
- `/users`, `/healthz` 같은 literal 세그먼트
|
|
63
|
+
- `/:id`, `/users/:userId/posts/:postId` 같은 full-segment path param
|
|
64
|
+
|
|
65
|
+
트레일링 슬래시와 중복 슬래시는 라우트 매핑 단계에서 정규화되므로 `//users///:id/`는 `/users/:id`로 해석됩니다.
|
|
66
|
+
|
|
67
|
+
라우트 데코레이터는 `*`, `?`, `/(.*)`, `user-:id`, `:id.json` 같은 wildcard, regex 유사 문법, mixed segment를 지원하지 않습니다. 와일드카드 매칭은 계속 `forRoutes('/users/*')` 같은 미들웨어 설정에서만 지원됩니다.
|
|
68
|
+
|
|
69
|
+
## 주요 패턴
|
|
70
|
+
|
|
71
|
+
### 가드와 인터셉터
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { Controller, Get, UseGuards, UseInterceptors } from '@fluojs/http';
|
|
75
|
+
|
|
76
|
+
@Controller('/admin')
|
|
77
|
+
@UseGuards(AdminGuard)
|
|
78
|
+
@UseInterceptors(LoggingInterceptor)
|
|
79
|
+
class AdminController {
|
|
80
|
+
@Get('/')
|
|
81
|
+
dashboard() {
|
|
82
|
+
return { data: 'secret' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 비동기 요청 컨텍스트
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { getCurrentRequestContext } from '@fluojs/http';
|
|
91
|
+
|
|
92
|
+
function someDeepHelper() {
|
|
93
|
+
const ctx = getCurrentRequestContext();
|
|
94
|
+
console.log(ctx?.requestId);
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 프록시 뒤의 속도 제한
|
|
99
|
+
|
|
100
|
+
`createRateLimitMiddleware(...)`는 기본적으로 raw socket `remoteAddress`만으로 클라이언트 식별자를 해석합니다. `Forwarded`, `X-Forwarded-For`, `X-Real-IP`를 신뢰하려면 해당 헤더를 신뢰 가능한 프록시가 덮어쓰는 환경에서만 `trustProxyHeaders: true`를 명시적으로 켜세요. 어댑터가 신뢰 가능한 프록시 체인도 raw socket 식별자도 제공하지 않는다면 공유 fallback 버킷에 의존하지 말고 명시적인 `keyResolver`를 설정하세요.
|
|
101
|
+
|
|
102
|
+
### 서버 전송 이벤트
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { Get, SseResponse, type RequestContext } from '@fluojs/http';
|
|
106
|
+
|
|
107
|
+
@Get('/events')
|
|
108
|
+
stream(_input: undefined, ctx: RequestContext) {
|
|
109
|
+
const sse = new SseResponse(ctx);
|
|
110
|
+
sse.send({ message: 'hello' });
|
|
111
|
+
return sse;
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 공개 API
|
|
116
|
+
|
|
117
|
+
- **라우팅 데코레이터**: `Controller`, `Get`, `Post`, `Put`, `Patch`, `Delete`, `All`
|
|
118
|
+
- **바인딩 데코레이터**: `FromBody`, `FromQuery`, `FromPath`, `FromHeader`, `FromCookie`, `RequestDto`
|
|
119
|
+
- **실행 데코레이터**: `UseGuards`, `UseInterceptors`, `HttpCode`, `Version`, `Header`, `Redirect`
|
|
120
|
+
- **핵심 런타임 타입**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
121
|
+
- **예외**: `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `InternalServerErrorException`, `PayloadTooLargeException`
|
|
122
|
+
- **헬퍼**: `createHandlerMapping`, `createDispatcher`, `createCorsMiddleware`, `createRateLimitMiddleware`, `getCurrentRequestContext`
|
|
123
|
+
|
|
124
|
+
## 내부 서브경로 (`@fluojs/http/internal`)
|
|
125
|
+
|
|
126
|
+
`./internal` 서브경로는 플랫폼 어댑터와 핵심 런타임에서 사용하는 저수준 유틸리티만 내보냅니다. 이들은 변경될 수 있으며 일반적인 애플리케이션 코드에서 사용해서는 안 됩니다.
|
|
127
|
+
|
|
128
|
+
- `DefaultBinder`: 런타임 부트스트랩 경로에서 사용하는 기본 DTO/요청 바인더.
|
|
129
|
+
- `resolveClientIdentity(request)`: 속도 제한과 런타임 통합에서 사용하는 보수적 클라이언트 식별 해석기.
|
|
130
|
+
|
|
131
|
+
## 관련 패키지
|
|
132
|
+
|
|
133
|
+
- `@fluojs/core`: 컨트롤러, 라우트, DTO 메타데이터를 저장합니다.
|
|
134
|
+
- `@fluojs/validation`: HTTP 바인딩 이후 DTO를 검증합니다.
|
|
135
|
+
- `@fluojs/runtime`: 부트스트랩 중 디스패처를 조립합니다.
|
|
136
|
+
- `@fluojs/passport`: 같은 가드 체인 안에서 인증을 연결합니다.
|
|
137
|
+
|
|
138
|
+
## 예제 소스
|
|
139
|
+
|
|
140
|
+
- `examples/realworld-api/src/users/create-user.dto.ts`
|
|
141
|
+
- `examples/auth-jwt-passport/src/auth/auth.controller.ts`
|
|
142
|
+
- `packages/http/src/dispatch/dispatcher.test.ts`
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# @fluojs/http
|
|
2
|
+
|
|
3
|
+
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
|
|
5
|
+
The HTTP execution layer that turns route metadata into a request pipeline with binding, validation, guards, interceptors, and response writing.
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [When to Use](#when-to-use)
|
|
11
|
+
- [Quick Start](#quick-start)
|
|
12
|
+
- [Common Patterns](#common-patterns)
|
|
13
|
+
- [Public API](#public-api)
|
|
14
|
+
- [Related Packages](#related-packages)
|
|
15
|
+
- [Example Sources](#example-sources)
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @fluojs/http
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## When to Use
|
|
24
|
+
|
|
25
|
+
Use this package when you need to:
|
|
26
|
+
|
|
27
|
+
- define REST-style controllers with decorators such as `@Controller`, `@Get`, and `@Post`
|
|
28
|
+
- bind request data into DTOs with `@FromBody`, `@FromPath`, `@FromQuery`, and related decorators
|
|
29
|
+
- run guards, interceptors, and middleware in a predictable request lifecycle
|
|
30
|
+
- access the active request through `RequestContext` without passing it through every function
|
|
31
|
+
|
|
32
|
+
## Quick Start
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { Controller, FromBody, FromPath, Get, Post, RequestDto } from '@fluojs/http';
|
|
36
|
+
import { IsString, MinLength } from '@fluojs/validation';
|
|
37
|
+
|
|
38
|
+
class CreateUserDto {
|
|
39
|
+
@FromBody()
|
|
40
|
+
@IsString()
|
|
41
|
+
@MinLength(3)
|
|
42
|
+
name!: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Controller('/users')
|
|
46
|
+
export class UserController {
|
|
47
|
+
@Post('/')
|
|
48
|
+
@RequestDto(CreateUserDto)
|
|
49
|
+
create(input: CreateUserDto) {
|
|
50
|
+
return { id: '1', name: input.name };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@Get('/:id')
|
|
54
|
+
getById(@FromPath('id') id: string) {
|
|
55
|
+
return { id, name: 'John Doe' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Route path contract
|
|
61
|
+
|
|
62
|
+
HTTP route decorators such as `@Controller()`, `@Get()`, and `@Post()` accept only:
|
|
63
|
+
|
|
64
|
+
- literal path segments like `/users` or `/healthz`
|
|
65
|
+
- full-segment path params like `/:id` or `/users/:userId/posts/:postId`
|
|
66
|
+
|
|
67
|
+
Trailing slashes and duplicate slashes are normalized during route mapping, so `//users///:id/` resolves to `/users/:id`.
|
|
68
|
+
|
|
69
|
+
Route decorators do **not** support wildcard, regex-like, or mixed-segment syntax such as `*`, `?`, `/(.*)`, `user-:id`, or `:id.json`. Wildcard matching remains middleware-only via `forRoutes('/users/*')`.
|
|
70
|
+
|
|
71
|
+
## Common Patterns
|
|
72
|
+
|
|
73
|
+
### Guards and interceptors
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { Controller, Get, UseGuards, UseInterceptors } from '@fluojs/http';
|
|
77
|
+
|
|
78
|
+
@Controller('/admin')
|
|
79
|
+
@UseGuards(AdminGuard)
|
|
80
|
+
@UseInterceptors(LoggingInterceptor)
|
|
81
|
+
class AdminController {
|
|
82
|
+
@Get('/')
|
|
83
|
+
dashboard() {
|
|
84
|
+
return { data: 'secret' };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Async request context
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { getCurrentRequestContext } from '@fluojs/http';
|
|
93
|
+
|
|
94
|
+
function someDeepHelper() {
|
|
95
|
+
const ctx = getCurrentRequestContext();
|
|
96
|
+
console.log(ctx?.requestId);
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Rate limiting behind proxies
|
|
101
|
+
|
|
102
|
+
`createRateLimitMiddleware(...)` resolves client identity from the raw socket `remoteAddress` by default. To trust `Forwarded`, `X-Forwarded-For`, or `X-Real-IP`, opt in with `trustProxyHeaders: true` only when your adapter sits behind a trusted proxy that overwrites those headers. If your adapter exposes neither a trusted proxy chain nor a raw socket identity, provide an explicit `keyResolver`.
|
|
103
|
+
|
|
104
|
+
### Server-sent events
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { Get, SseResponse, type RequestContext } from '@fluojs/http';
|
|
108
|
+
|
|
109
|
+
@Get('/events')
|
|
110
|
+
stream(_input: undefined, ctx: RequestContext) {
|
|
111
|
+
const sse = new SseResponse(ctx);
|
|
112
|
+
sse.send({ message: 'hello' });
|
|
113
|
+
return sse;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Public API
|
|
118
|
+
|
|
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`
|
|
122
|
+
- **Core runtime types**: `RequestContext`, `FrameworkRequest`, `FrameworkResponse`, `SseResponse`
|
|
123
|
+
- **Exceptions**: `BadRequestException`, `UnauthorizedException`, `ForbiddenException`, `NotFoundException`, `InternalServerErrorException`, `PayloadTooLargeException`
|
|
124
|
+
- **Helpers**: `createHandlerMapping`, `createDispatcher`, `createCorsMiddleware`, `createRateLimitMiddleware`, `getCurrentRequestContext`
|
|
125
|
+
|
|
126
|
+
## Internal Subpath (`@fluojs/http/internal`)
|
|
127
|
+
|
|
128
|
+
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
|
+
|
|
130
|
+
- `DefaultBinder`: Default DTO/request binder used by the runtime bootstrap path.
|
|
131
|
+
- `resolveClientIdentity(request)`: Conservative client identity resolver used by rate limiting and other runtime integrations.
|
|
132
|
+
|
|
133
|
+
## Related Packages
|
|
134
|
+
|
|
135
|
+
- `@fluojs/core`: stores controller, route, and DTO metadata
|
|
136
|
+
- `@fluojs/validation`: validates DTOs after HTTP binding
|
|
137
|
+
- `@fluojs/runtime`: assembles the dispatcher during application bootstrap
|
|
138
|
+
- `@fluojs/passport`: plugs auth guards into the same HTTP guard chain
|
|
139
|
+
|
|
140
|
+
## Example Sources
|
|
141
|
+
|
|
142
|
+
- `examples/realworld-api/src/users/create-user.dto.ts`
|
|
143
|
+
- `examples/auth-jwt-passport/src/auth/auth.controller.ts`
|
|
144
|
+
- `packages/http/src/dispatch/dispatcher.test.ts`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { MaybePromise } from '@fluojs/core';
|
|
2
|
+
import type { Dispatcher } from './types.js';
|
|
3
|
+
export interface ServerBackedHttpAdapterRealtimeCapability {
|
|
4
|
+
kind: 'server-backed';
|
|
5
|
+
server: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface UnsupportedHttpAdapterRealtimeCapability {
|
|
8
|
+
kind: 'unsupported';
|
|
9
|
+
mode: 'no-op';
|
|
10
|
+
reason: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FetchStyleHttpAdapterRealtimeCapability {
|
|
13
|
+
contract: 'raw-websocket-expansion';
|
|
14
|
+
kind: 'fetch-style';
|
|
15
|
+
mode: 'request-upgrade';
|
|
16
|
+
reason: string;
|
|
17
|
+
support: 'contract-only' | 'supported';
|
|
18
|
+
version: 1;
|
|
19
|
+
}
|
|
20
|
+
export type HttpAdapterRealtimeCapability = ServerBackedHttpAdapterRealtimeCapability | FetchStyleHttpAdapterRealtimeCapability | UnsupportedHttpAdapterRealtimeCapability;
|
|
21
|
+
export declare function createServerBackedHttpAdapterRealtimeCapability(server: unknown): ServerBackedHttpAdapterRealtimeCapability;
|
|
22
|
+
export declare function createUnsupportedHttpAdapterRealtimeCapability(reason: string): UnsupportedHttpAdapterRealtimeCapability;
|
|
23
|
+
export declare function createFetchStyleHttpAdapterRealtimeCapability(reason: string, options?: {
|
|
24
|
+
support?: FetchStyleHttpAdapterRealtimeCapability['support'];
|
|
25
|
+
}): FetchStyleHttpAdapterRealtimeCapability;
|
|
26
|
+
/**
|
|
27
|
+
* Minimal HTTP adapter contract that binds the application lifecycle to a transport implementation.
|
|
28
|
+
*/
|
|
29
|
+
export interface HttpApplicationAdapter {
|
|
30
|
+
/**
|
|
31
|
+
* Returns the underlying transport server object when the adapter exposes one.
|
|
32
|
+
*
|
|
33
|
+
* @returns The transport-native server instance, or `undefined` when the adapter does not expose it.
|
|
34
|
+
*/
|
|
35
|
+
getServer?(): unknown;
|
|
36
|
+
getRealtimeCapability?(): HttpAdapterRealtimeCapability;
|
|
37
|
+
/**
|
|
38
|
+
* Starts the adapter and binds request dispatching to the framework dispatcher.
|
|
39
|
+
*
|
|
40
|
+
* @param dispatcher Dispatcher created by `@fluojs/http` that executes the request pipeline.
|
|
41
|
+
* @returns A promise that resolves when the adapter is ready to accept requests.
|
|
42
|
+
*/
|
|
43
|
+
listen(dispatcher: Dispatcher): MaybePromise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Stops the adapter and releases transport resources.
|
|
46
|
+
*
|
|
47
|
+
* @param signal Optional shutdown reason propagated by runtime lifecycle hooks.
|
|
48
|
+
* @returns A promise that resolves after transport shutdown is complete.
|
|
49
|
+
*/
|
|
50
|
+
close(signal?: string): MaybePromise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a no-op adapter that preserves lifecycle behavior without binding a real HTTP server.
|
|
54
|
+
*
|
|
55
|
+
* @returns A lifecycle-compatible adapter whose `listen()` and `close()` methods resolve immediately.
|
|
56
|
+
*/
|
|
57
|
+
export declare function createNoopHttpApplicationAdapter(): HttpApplicationAdapter;
|
|
58
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function createServerBackedHttpAdapterRealtimeCapability(server) {
|
|
2
|
+
return {
|
|
3
|
+
kind: 'server-backed',
|
|
4
|
+
server
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function createUnsupportedHttpAdapterRealtimeCapability(reason) {
|
|
8
|
+
return {
|
|
9
|
+
kind: 'unsupported',
|
|
10
|
+
mode: 'no-op',
|
|
11
|
+
reason
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function createFetchStyleHttpAdapterRealtimeCapability(reason, options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
contract: 'raw-websocket-expansion',
|
|
17
|
+
kind: 'fetch-style',
|
|
18
|
+
mode: 'request-upgrade',
|
|
19
|
+
reason,
|
|
20
|
+
support: options.support ?? 'contract-only',
|
|
21
|
+
version: 1
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Minimal HTTP adapter contract that binds the application lifecycle to a transport implementation.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a no-op adapter that preserves lifecycle behavior without binding a real HTTP server.
|
|
31
|
+
*
|
|
32
|
+
* @returns A lifecycle-compatible adapter whose `listen()` and `close()` methods resolve immediately.
|
|
33
|
+
*/
|
|
34
|
+
export function createNoopHttpApplicationAdapter() {
|
|
35
|
+
return {
|
|
36
|
+
async close() {},
|
|
37
|
+
getRealtimeCapability() {
|
|
38
|
+
return createUnsupportedHttpAdapterRealtimeCapability('No-op HTTP adapter does not expose a server-backed realtime capability.');
|
|
39
|
+
},
|
|
40
|
+
async listen() {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Constructor } from '@fluojs/core';
|
|
2
|
+
import type { ArgumentResolverContext, Binder, Converter, ConverterLike, ConverterTarget } from '../types.js';
|
|
3
|
+
export declare class DefaultConverter implements Converter {
|
|
4
|
+
convert(value: unknown, _target: ConverterTarget): unknown;
|
|
5
|
+
}
|
|
6
|
+
export declare class DefaultBinder implements Binder {
|
|
7
|
+
private readonly converters;
|
|
8
|
+
constructor(converters?: readonly ConverterLike[]);
|
|
9
|
+
bind(dto: Constructor, context: ArgumentResolverContext): Promise<unknown>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../src/adapters/binding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,WAAW,EAA6D,MAAM,cAAc,CAAC;AAK3H,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAC;AA4FhI,qBAAa,gBAAiB,YAAW,SAAS;IAChD,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,eAAe,GAAG,OAAO;CAG3D;AAyDD,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;CA0EjF"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { InvariantError } from '@fluojs/core';
|
|
2
|
+
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
3
|
+
import { BadRequestException } from '../exceptions.js';
|
|
4
|
+
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
5
|
+
const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
if (typeof value !== 'object' || value === null) {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const prototype = Object.getPrototypeOf(value);
|
|
11
|
+
return prototype === Object.prototype || prototype === null;
|
|
12
|
+
}
|
|
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
|
+
function validateBodyKeys(request, bodyKeys) {
|
|
52
|
+
if (request.body === undefined || request.body === null) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!isPlainObject(request.body)) {
|
|
56
|
+
throw new BadRequestException('Request body must be a plain object.', {
|
|
57
|
+
details: [toInputErrorDetail({
|
|
58
|
+
code: 'INVALID_BODY',
|
|
59
|
+
message: 'Request body must be a plain object.',
|
|
60
|
+
source: 'body'
|
|
61
|
+
})]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const details = [];
|
|
65
|
+
for (const key of Object.keys(request.body)) {
|
|
66
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
67
|
+
details.push(toInputErrorDetail({
|
|
68
|
+
code: 'DANGEROUS_KEY',
|
|
69
|
+
field: key,
|
|
70
|
+
message: `Dangerous body key ${key} is not allowed.`,
|
|
71
|
+
source: 'body'
|
|
72
|
+
}));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!bodyKeys.has(key)) {
|
|
76
|
+
details.push(toInputErrorDetail({
|
|
77
|
+
code: 'UNKNOWN_FIELD',
|
|
78
|
+
field: key,
|
|
79
|
+
message: `Unknown body field ${key}.`,
|
|
80
|
+
source: 'body'
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (details.length > 0) {
|
|
85
|
+
throw new BadRequestException('Request body contains unsupported fields.', {
|
|
86
|
+
details
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class DefaultConverter {
|
|
91
|
+
convert(value, _target) {
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function isConverter(value) {
|
|
96
|
+
return typeof value === 'object' && value !== null && 'convert' in value && typeof value.convert === 'function';
|
|
97
|
+
}
|
|
98
|
+
function isConverterToken(value) {
|
|
99
|
+
return typeof value === 'function' || typeof value === 'string' || typeof value === 'symbol';
|
|
100
|
+
}
|
|
101
|
+
async function resolveConverter(value, context, cache) {
|
|
102
|
+
if (!value) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
if (cache.has(value)) {
|
|
106
|
+
return cache.get(value);
|
|
107
|
+
}
|
|
108
|
+
if (isConverter(value)) {
|
|
109
|
+
cache.set(value, value);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
if (!isConverterToken(value)) {
|
|
113
|
+
throw new InvariantError('Converter metadata must be a converter instance or DI token.');
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const resolved = await context.requestContext.container.resolve(value);
|
|
117
|
+
if (!isConverter(resolved)) {
|
|
118
|
+
throw new InvariantError('Resolved converter token does not implement convert().');
|
|
119
|
+
}
|
|
120
|
+
cache.set(value, resolved);
|
|
121
|
+
return resolved;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (typeof value === 'function') {
|
|
124
|
+
const instantiated = new value();
|
|
125
|
+
if (!isConverter(instantiated)) {
|
|
126
|
+
throw new InvariantError('Converter class must implement convert(value, target).');
|
|
127
|
+
}
|
|
128
|
+
cache.set(value, instantiated);
|
|
129
|
+
return instantiated;
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export class DefaultBinder {
|
|
135
|
+
constructor(converters = []) {
|
|
136
|
+
this.converters = converters;
|
|
137
|
+
}
|
|
138
|
+
async bind(dto, context) {
|
|
139
|
+
const schema = getDtoBindingSchema(dto);
|
|
140
|
+
const value = new dto();
|
|
141
|
+
const converterCache = new Map();
|
|
142
|
+
const bodyKeys = new Set(schema.filter(entry => entry.metadata.source === 'body').map(entry => resolveSourceKey(entry.propertyKey, entry.metadata.key)));
|
|
143
|
+
const globalConverters = (await Promise.all(this.converters.map(converter => resolveConverter(converter, context, converterCache)))).filter(converter => Boolean(converter));
|
|
144
|
+
validateBodyKeys(context.requestContext.request, bodyKeys);
|
|
145
|
+
const details = [];
|
|
146
|
+
for (const entry of schema) {
|
|
147
|
+
const rawValue = readSourceValue(context.requestContext.request, entry.metadata.source, entry.propertyKey, entry.metadata.key);
|
|
148
|
+
if (rawValue === undefined) {
|
|
149
|
+
if (entry.metadata.optional) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
details.push(toInputErrorDetail({
|
|
153
|
+
code: 'MISSING_FIELD',
|
|
154
|
+
field: toFieldName(entry.propertyKey),
|
|
155
|
+
message: `Missing required ${entry.metadata.source} field ${resolveSourceKey(entry.propertyKey, entry.metadata.key)}.`,
|
|
156
|
+
source: entry.metadata.source
|
|
157
|
+
}));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const target = {
|
|
161
|
+
dto,
|
|
162
|
+
handler: context.handler,
|
|
163
|
+
key: resolveSourceKey(entry.propertyKey, entry.metadata.key),
|
|
164
|
+
propertyKey: entry.propertyKey,
|
|
165
|
+
requestContext: context.requestContext,
|
|
166
|
+
source: entry.metadata.source
|
|
167
|
+
};
|
|
168
|
+
let convertedValue = rawValue;
|
|
169
|
+
for (const converter of globalConverters) {
|
|
170
|
+
convertedValue = await converter.convert(convertedValue, target);
|
|
171
|
+
}
|
|
172
|
+
const fieldConverter = await resolveConverter(entry.metadata.converter, context, converterCache);
|
|
173
|
+
if (fieldConverter) {
|
|
174
|
+
convertedValue = await fieldConverter.convert(convertedValue, target);
|
|
175
|
+
}
|
|
176
|
+
value[entry.propertyKey] = convertedValue;
|
|
177
|
+
}
|
|
178
|
+
if (details.length > 0) {
|
|
179
|
+
throw new BadRequestException('Request binding failed.', {
|
|
180
|
+
details
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Constructor } from '@fluojs/core';
|
|
2
|
+
import type { Validator } from '../types.js';
|
|
3
|
+
export declare class HttpDtoValidationAdapter implements Validator {
|
|
4
|
+
private readonly validator;
|
|
5
|
+
private throwBadRequestForValidationError;
|
|
6
|
+
private filterUnboundRequestDtoFields;
|
|
7
|
+
validate(value: unknown, target: Constructor): Promise<void>;
|
|
8
|
+
materialize<T>(value: unknown, target: Constructor<T>): Promise<T>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=dto-validation-adapter.d.ts.map
|
|
@@ -0,0 +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;AAMhD,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,aAAa,CAAC;AAE9D,qBAAa,wBAAyB,YAAW,SAAS;IACxD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8B;IAExD,OAAO,CAAC,iCAAiC;IAMzC,OAAO,CAAC,6BAA6B;IAiB/B,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAa5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAWzE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { getDtoBindingSchema } from '@fluojs/core/internal';
|
|
2
|
+
import { DefaultValidator as BaseDefaultValidator, DtoValidationError } from '@fluojs/validation';
|
|
3
|
+
import { BadRequestException } from '../exceptions.js';
|
|
4
|
+
import { toInputErrorDetail } from '../input-error-detail.js';
|
|
5
|
+
export class HttpDtoValidationAdapter {
|
|
6
|
+
validator = new BaseDefaultValidator();
|
|
7
|
+
throwBadRequestForValidationError(error) {
|
|
8
|
+
throw new BadRequestException(error.message, {
|
|
9
|
+
details: error.issues.map(issue => toInputErrorDetail(issue))
|
|
10
|
+
});
|
|
11
|
+
}
|
|
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
|
+
async validate(value, target) {
|
|
26
|
+
try {
|
|
27
|
+
const filteredValue = this.filterUnboundRequestDtoFields(value, target);
|
|
28
|
+
await this.validator.validate(filteredValue, target);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error instanceof DtoValidationError) {
|
|
31
|
+
this.throwBadRequestForValidationError(error);
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async materialize(value, target) {
|
|
37
|
+
try {
|
|
38
|
+
return await this.validator.materialize(value, target);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error instanceof DtoValidationError) {
|
|
41
|
+
this.throwBadRequestForValidationError(error);
|
|
42
|
+
}
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|