From e4e78e9b3b67b1e834ad6d8ee6f33726db88ac3b Mon Sep 17 00:00:00 2001 From: laggu91 Date: Sat, 16 May 2026 18:57:37 +0900 Subject: [PATCH 1/2] Clarify binding precedence for request authors Document the order users should expect when source, request, transform, virtual, and missing-value decorators interact. Constraint: Keep README guidance aligned across English and Korean root readmes. Rejected: Leaving precedence as implicit behavior | users need clear rules when decorators are combined. Confidence: high Scope-risk: narrow Directive: Keep future binding-order docs using request terminology, not DTO terminology. Tested: pnpm --filter express-cargo test -- --runTestsByPath tests/binding/sourceBinding.test.ts tests/binding/requestBinding.test.ts tests/binding/virtualBinding.test.ts Not-tested: Full workspace build and docs site build were not run for this docs-only change. Co-authored-by: OmX --- README.ko.md | 43 +++++++++++++++++++++++++++++++++++++++++++ README.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/README.ko.md b/README.ko.md index 316ca11..8b4b2e2 100644 --- a/README.ko.md +++ b/README.ko.md @@ -151,6 +151,49 @@ app.listen(3000) | `@Request(transformer)` | Express Request 객체에서 값 추출 | `@Request(req => req.ip) clientIp!: string` | | `@Virtual(transformer)` | 다른 필드들을 기반으로 값 계산 | `@Virtual(obj => obj.firstName + ' ' + obj.lastName) fullName!: string` | +### 바인딩 순서와 우선순위 + +`bindingCargo()`는 각 request object를 정해진 순서대로 채웁니다. 필드에 transformer가 있거나 계산 필드가 다른 필드에 의존할 때 이 순서가 중요합니다. + +1. `@Request()` 필드가 먼저 실행됩니다. raw Express `Request` 객체를 받고, source 조회와 기본 타입 캐스팅을 건너뜁니다. +2. Source 데코레이터가 다음에 실행됩니다: `@Body()`, `@Query()`, `@Params()`, `@Uri()`, `@Header()`, `@Session()`. + 선택된 request source에서 값을 읽고, 선언된 property 타입으로 캐스팅한 뒤, `@Transform()`이 있으면 실행하고, 그 다음 검증합니다. +3. `@Virtual()` 필드는 마지막에 실행됩니다. request field와 source field가 바인딩된 뒤의 request object를 받습니다. + +```ts +class OrderRequest { + @Body() + price!: number + + @Body() + quantity!: number + + @Virtual((order: OrderRequest) => order.price * order.quantity) + total!: number +} +``` + +이 예시에서는 `price`와 `quantity`가 먼저 바인딩되고, 그 다음 `total`이 계산됩니다. + +같은 property에 데코레이터를 함께 사용할 때는 아래 규칙을 따릅니다: + +| 조합 | 결과 | +|-------------------------------------|--------------------------------------------------------------------| +| `@Request()` + source decorator | `@Request()`가 우선합니다. 해당 property의 source decorator는 바인딩에 사용되지 않습니다. | +| `@Transform()` + source decorator | Source 값이 먼저 타입 캐스팅되고, 그 다음 `@Transform()`과 validator가 실행됩니다. | +| `@Virtual()` + source 또는 `@Request()` | 피하는 것이 좋습니다. `@Virtual()`이 마지막에 실행되어 최종 값을 덮어쓸 수 있습니다. | +| 여러 source decorator | 피하는 것이 좋습니다. Property 하나에는 source decorator 하나만 사용하세요. fallback chain이 아닙니다. | + +`@Virtual()`을 source decorator나 `@Request()`와 같은 property에 섞으면, virtual 값이 필드를 덮어쓰기 전에 validation이 먼저 실행될 수 있습니다. + +값이 없을 때는 transform과 validation보다 먼저 아래 처리가 적용됩니다: + +| 데코레이터 | 값이 없을 때 동작 | +|--------------------|---------------------------------------------------------------| +| `@Default(value)` | 기본값을 사용하고, 해당 필드의 transform과 validation을 건너뜁니다. | +| `@Optional()` | Property를 `null`로 설정하고, 해당 필드의 transform과 validation을 건너뜁니다. | +| Default/Optional 없음 | required field 에러를 추가하고, 해당 필드의 이후 validator를 건너뜁니다. | + ### 유틸리티 데코레이터 | 데코레이터 | 설명 | 예시 | diff --git a/README.md b/README.md index 9db3d64..7701d41 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,49 @@ Full guide and API reference: | `@Request(transformer)` | Extract value from Express Request object | `@Request(req => req.ip) clientIp!: string` | | `@Virtual(transformer)` | Compute value from other fields | `@Virtual(obj => obj.firstName + ' ' + obj.lastName) fullName!: string` | +### Binding Order and Priority + +`bindingCargo()` fills each request object in a fixed order. This matters when a field uses a transformer or when a computed field depends on other fields. + +1. `@Request()` fields run first. They receive the raw Express `Request` object and bypass source lookup and built-in type casting. +2. Source decorators run next: `@Body()`, `@Query()`, `@Params()`, `@Uri()`, `@Header()`, and `@Session()`. + The value is read from the selected request source, type-cast to the declared property type, passed through `@Transform()` when present, and then validated. +3. `@Virtual()` fields run last. They receive the request object after request and source fields have been bound. + +```ts +class OrderRequest { + @Body() + price!: number + + @Body() + quantity!: number + + @Virtual((order: OrderRequest) => order.price * order.quantity) + total!: number +} +``` + +In this example, `price` and `quantity` are bound before `total` is computed. + +When decorators are combined on the same property, use these rules: + +| Combination | Result | +|-------------------------------------|-------------------------------------------------------------------------------------------------| +| `@Request()` + source decorator | `@Request()` wins. The source decorator is ignored for binding that property. | +| `@Transform()` + source decorator | Source value is type-cast first, then `@Transform()` runs, then validators run. | +| `@Virtual()` + source or `@Request()` | Avoid this. `@Virtual()` runs last and may overwrite the final value. | +| Multiple source decorators | Avoid this. Use one source decorator per property; source decorators are not a fallback chain. | + +If `@Virtual()` is mixed with a source decorator or `@Request()` on the same property, validation can run before the virtual value overwrites the field. + +Missing values are handled before transform and validation: + +| Decorator | Missing value behavior | +|--------------------|------------------------------------------------------------------------------| +| `@Default(value)` | Uses the default value and skips transform and validation for that field. | +| `@Optional()` | Sets the property to `null` and skips transform and validation for that field. | +| No default/optional | Adds a required-field error and skips later validators for that field. | + ### Utility Decorators | Decorator | Description | Example | From 942bcc74378e825b4533892aed52dad7dcd7a0ce Mon Sep 17 00:00:00 2001 From: laggu91 Date: Sat, 23 May 2026 16:53:40 +0900 Subject: [PATCH 2/2] Clarify missing-value precedence for request authors Separate binding-order guidance into value-present and value-missing paths so readers can see where Default, Optional, and required errors apply. Constraint: Address PR review feedback while keeping duplicate-decorator rules out of this README section. Rejected: Documenting duplicate decorator combinations here | that guidance will be written separately. Confidence: high Scope-risk: narrow Directive: Keep binding-order docs focused on request terminology and runtime order, not DTO terminology. Tested: Not run; README-only wording change. Not-tested: Full workspace build, package tests, and docs site build were not run for this docs-only change. Co-authored-by: OmX --- README.ko.md | 27 ++++++++++----------------- README.md | 27 ++++++++++----------------- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/README.ko.md b/README.ko.md index 8b4b2e2..aec9e30 100644 --- a/README.ko.md +++ b/README.ko.md @@ -155,10 +155,12 @@ app.listen(3000) `bindingCargo()`는 각 request object를 정해진 순서대로 채웁니다. 필드에 transformer가 있거나 계산 필드가 다른 필드에 의존할 때 이 순서가 중요합니다. -1. `@Request()` 필드가 먼저 실행됩니다. raw Express `Request` 객체를 받고, source 조회와 기본 타입 캐스팅을 건너뜁니다. +#### 값이 있을 때 + +1. `@Request()` 데코레이터가 먼저 실행됩니다. raw Express `Request` 객체를 받고, source 조회와 기본 타입 캐스팅을 건너뜁니다. 2. Source 데코레이터가 다음에 실행됩니다: `@Body()`, `@Query()`, `@Params()`, `@Uri()`, `@Header()`, `@Session()`. 선택된 request source에서 값을 읽고, 선언된 property 타입으로 캐스팅한 뒤, `@Transform()`이 있으면 실행하고, 그 다음 검증합니다. -3. `@Virtual()` 필드는 마지막에 실행됩니다. request field와 source field가 바인딩된 뒤의 request object를 받습니다. +3. `@Virtual()` 데코레이터는 마지막에 실행됩니다. request field와 source field가 바인딩된 뒤의 request object를 받습니다. ```ts class OrderRequest { @@ -175,24 +177,15 @@ class OrderRequest { 이 예시에서는 `price`와 `quantity`가 먼저 바인딩되고, 그 다음 `total`이 계산됩니다. -같은 property에 데코레이터를 함께 사용할 때는 아래 규칙을 따릅니다: - -| 조합 | 결과 | -|-------------------------------------|--------------------------------------------------------------------| -| `@Request()` + source decorator | `@Request()`가 우선합니다. 해당 property의 source decorator는 바인딩에 사용되지 않습니다. | -| `@Transform()` + source decorator | Source 값이 먼저 타입 캐스팅되고, 그 다음 `@Transform()`과 validator가 실행됩니다. | -| `@Virtual()` + source 또는 `@Request()` | 피하는 것이 좋습니다. `@Virtual()`이 마지막에 실행되어 최종 값을 덮어쓸 수 있습니다. | -| 여러 source decorator | 피하는 것이 좋습니다. Property 하나에는 source decorator 하나만 사용하세요. fallback chain이 아닙니다. | +#### 값이 없을 때 -`@Virtual()`을 source decorator나 `@Request()`와 같은 property에 섞으면, virtual 값이 필드를 덮어쓰기 전에 validation이 먼저 실행될 수 있습니다. +값이 `undefined` 또는 `null`이면 express-cargo는 아래 순서로 처리합니다: -값이 없을 때는 transform과 validation보다 먼저 아래 처리가 적용됩니다: +1. `@Default(value)`가 있으면 기본값을 사용합니다. +2. `@Optional()`이 있으면 property를 `null`로 설정합니다. +3. 둘 다 없으면 required field 에러를 추가합니다. -| 데코레이터 | 값이 없을 때 동작 | -|--------------------|---------------------------------------------------------------| -| `@Default(value)` | 기본값을 사용하고, 해당 필드의 transform과 validation을 건너뜁니다. | -| `@Optional()` | Property를 `null`로 설정하고, 해당 필드의 transform과 validation을 건너뜁니다. | -| Default/Optional 없음 | required field 에러를 추가하고, 해당 필드의 이후 validator를 건너뜁니다. | +위 값 없음 처리 경로에 들어가면 해당 필드의 남은 transform과 validation 단계는 건너뜁니다. ### 유틸리티 데코레이터 diff --git a/README.md b/README.md index 7701d41..833be90 100644 --- a/README.md +++ b/README.md @@ -154,10 +154,12 @@ Full guide and API reference: `bindingCargo()` fills each request object in a fixed order. This matters when a field uses a transformer or when a computed field depends on other fields. -1. `@Request()` fields run first. They receive the raw Express `Request` object and bypass source lookup and built-in type casting. +#### When the Value Exists + +1. The `@Request()` decorator runs first. Its transformer receives the raw Express `Request` object and bypasses source lookup and built-in type casting. 2. Source decorators run next: `@Body()`, `@Query()`, `@Params()`, `@Uri()`, `@Header()`, and `@Session()`. The value is read from the selected request source, type-cast to the declared property type, passed through `@Transform()` when present, and then validated. -3. `@Virtual()` fields run last. They receive the request object after request and source fields have been bound. +3. The `@Virtual()` decorator runs last. Its transformer receives the request object after request and source fields have been bound. ```ts class OrderRequest { @@ -174,24 +176,15 @@ class OrderRequest { In this example, `price` and `quantity` are bound before `total` is computed. -When decorators are combined on the same property, use these rules: - -| Combination | Result | -|-------------------------------------|-------------------------------------------------------------------------------------------------| -| `@Request()` + source decorator | `@Request()` wins. The source decorator is ignored for binding that property. | -| `@Transform()` + source decorator | Source value is type-cast first, then `@Transform()` runs, then validators run. | -| `@Virtual()` + source or `@Request()` | Avoid this. `@Virtual()` runs last and may overwrite the final value. | -| Multiple source decorators | Avoid this. Use one source decorator per property; source decorators are not a fallback chain. | +#### When the Value Is Missing -If `@Virtual()` is mixed with a source decorator or `@Request()` on the same property, validation can run before the virtual value overwrites the field. +When a value is `undefined` or `null`, express-cargo handles it in this order: -Missing values are handled before transform and validation: +1. `@Default(value)` uses the default value. +2. `@Optional()` sets the property to `null`. +3. If neither is present, a required-field error is added. -| Decorator | Missing value behavior | -|--------------------|------------------------------------------------------------------------------| -| `@Default(value)` | Uses the default value and skips transform and validation for that field. | -| `@Optional()` | Sets the property to `null` and skips transform and validation for that field. | -| No default/optional | Adds a required-field error and skips later validators for that field. | +After a missing value is handled by one of these paths, the remaining transform and validation steps for that field are skipped. ### Utility Decorators