@fluojs/validation 1.0.6 → 2.0.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/README.ko.md +25 -5
- package/README.md +26 -5
- package/dist/decorators.d.ts +4 -2
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +6 -3
- package/dist/internal/dto-materialization.d.ts +45 -0
- package/dist/internal/dto-materialization.d.ts.map +1 -0
- package/dist/internal/dto-materialization.js +204 -0
- package/dist/internal/dto-metadata-cache.d.ts.map +1 -1
- package/dist/internal/dto-metadata-cache.js +8 -7
- package/dist/internal/enum-values.d.ts +2 -0
- package/dist/internal/enum-values.d.ts.map +1 -0
- package/dist/internal/enum-values.js +16 -0
- package/dist/internal/object-utils.d.ts +26 -0
- package/dist/internal/object-utils.d.ts.map +1 -1
- package/dist/internal/object-utils.js +32 -4
- package/dist/internal/validation-issues.d.ts +13 -0
- package/dist/internal/validation-issues.d.ts.map +1 -0
- package/dist/internal/validation-issues.js +44 -0
- package/dist/types.d.ts +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +2 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +18 -139
- package/package.json +4 -4
package/README.ko.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
4
|
|
|
5
|
+
Node.js 지원 범위는 `>=24.0.0 <27`입니다. 업그레이드 절차는 [Node.js 지원 및 마이그레이션](../../docs/reference/node-support.ko.md)을 참조하세요.
|
|
6
|
+
|
|
5
7
|
fluo를 위한 입력값 검증 데코레이터, Mapped DTO 헬퍼 및 검증 엔진입니다.
|
|
6
8
|
|
|
7
9
|
## 목차
|
|
@@ -74,11 +76,13 @@ class rule이 실행되기 전에 deterministic `DtoValidationError`로 거부
|
|
|
74
76
|
DTO 바인딩 메타데이터를 적용한 뒤 `@ValidateNested(...)` 필드를 재귀적으로
|
|
75
77
|
실체화합니다. 어떤 요청 소스를 선택하고 스칼라 값을 변환할지는 transport 또는
|
|
76
78
|
binder가 검증 전에 담당한다는 request-pipeline 계약을 유지합니다.
|
|
77
|
-
Plain 입력 객체를 materialize할 때 안전한 추가 own enumerable 속성은 유지됩니다. `__proto__`, `constructor`,
|
|
79
|
+
Plain 입력 객체를 materialize할 때 안전한 추가 own enumerable 속성은 기본적으로 유지됩니다. `__proto__`, `constructor`,
|
|
78
80
|
`prototype` 같은 위험한 key와 inherited 또는 non-enumerable 속성만 제외합니다.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
세 번째 `materialize()` 인자로 `{ undeclaredProperties: 'reject' }`를 전달하면
|
|
82
|
+
초기화된 DTO field, validation 또는 binding metadata field, binding alias로 선언되지
|
|
83
|
+
않은 안전한 enumerable 속성을 거부합니다. 이 정책은 plain nested DTO 값에도
|
|
84
|
+
재귀적으로 적용되며 입력을 조용히 제거하지 않고 `UNDECLARED_PROPERTY` issue를
|
|
85
|
+
보고합니다. 기존 DTO instance는 undeclared-property 경계로 검사하지 않습니다.
|
|
82
86
|
선언된 중첩 DTO의 인스턴스인 기존 중첩 값은 그대로 보존하고, plain 중첩 값만
|
|
83
87
|
해당 필드 또는 collection entry 단위로 실체화합니다.
|
|
84
88
|
`materialize()`에 넘기는 루트 값은 plain 객체이거나 대상 DTO 인스턴스여야 합니다.
|
|
@@ -143,6 +147,11 @@ validation 및 binding metadata를 보존합니다. `PickType`, `OmitType`,
|
|
|
143
147
|
`PartialType`은 생략되었거나 optional이 된 필드에 의존할 수 있는 base
|
|
144
148
|
class-level validator를 derived DTO로 전달하지 않습니다.
|
|
145
149
|
|
|
150
|
+
`IntersectionType(...)` source가 같은 property에 서로 다른
|
|
151
|
+
`@ValidateNested(...)` target을 선언하면 모든 nested rule이 보존되고 검증됩니다.
|
|
152
|
+
공유 property의 plain value는 초기 실체화 중 plain 상태로 유지되므로 각 nested
|
|
153
|
+
target이 해당 value를 독립적으로 실체화하고 검증할 수 있습니다.
|
|
154
|
+
|
|
146
155
|
### Standard Schema 지원
|
|
147
156
|
|
|
148
157
|
Standard Schema adapter는 유효하지 않은 입력을 명시적인 issue로 보고해야 합니다. issue가 없는 검증 결과는 성공으로 처리합니다.
|
|
@@ -161,6 +170,14 @@ class RestrictedUserDto {
|
|
|
161
170
|
|
|
162
171
|
`ValidateClass(...)`는 custom class-level validator도 받을 수 있습니다. `Validate(...)`는 built-in decorator만으로 부족할 때 custom field-level validator를 붙이고, `ValidateIf(...)`는 predicate가 false를 반환하면 dependent validator를 short-circuit합니다.
|
|
163
172
|
|
|
173
|
+
### Custom field 검증
|
|
174
|
+
|
|
175
|
+
`Validate(callback)`는 callback을 `(value, context)`로 호출합니다. `context.dto`는
|
|
176
|
+
포함하는 DTO이고, `context.propertyKey`는 decorator가 적용된 field key입니다.
|
|
177
|
+
|
|
178
|
+
`@IsObject()`는 null-prototype record를 포함한 plain object만 허용합니다. Class
|
|
179
|
+
instance, `Date`, `Map`, `Set` 값은 거부합니다.
|
|
180
|
+
|
|
164
181
|
### 네트워크 검증기
|
|
165
182
|
|
|
166
183
|
`@IsIP()`는 기본적으로 IPv4와 IPv6 문자열을 모두 검증합니다. 한 IP 버전으로
|
|
@@ -179,9 +196,12 @@ Array, `Set`, `Map` member는 자동으로 검증되고 실체화되므로 neste
|
|
|
179
196
|
`materialize()`는 의도적으로 엄격합니다. Transport가 `'42'`를 넘기고 DTO가 `number`를 기대한다면, transport나 binding layer가 먼저 변환해야 합니다.
|
|
180
197
|
`@IsLatitude()`, `@IsLongitude()`를 포함한 numeric validator는 numeric string을 이미 변환된 number처럼 취급하지 않고 DTO의 numeric 값을 검증합니다.
|
|
181
198
|
|
|
199
|
+
`@IsEnum(...)`은 선언된 enum 값만 허용합니다. 숫자형 TypeScript enum에서 생성된
|
|
200
|
+
reverse-map 멤버 이름은 값이 아니므로 거부됩니다.
|
|
201
|
+
|
|
182
202
|
## 공개 API
|
|
183
203
|
|
|
184
|
-
- **검증 엔진**: `DefaultValidator`, `DtoValidationError`, `ValidationIssue`, `Validator`
|
|
204
|
+
- **검증 엔진**: `DefaultValidator`, `DtoValidationError`, `MaterializeOptions`, `ValidationIssue`, `Validator`
|
|
185
205
|
- **핵심 데코레이터**: `IsString`, `IsNumber`, `IsBoolean`, `IsDate`, `IsArray`, `IsObject`, `IsEnum`, `IsInt`, `IsDefined`, `IsOptional`, `ValidateNested`, `ValidateIf`, `Validate`, `ValidateClass`
|
|
186
206
|
- **존재 및 비교 데코레이터**: `IsEmpty`, `IsNotEmpty`, `Equals`, `NotEquals`, `IsIn`, `IsNotIn`
|
|
187
207
|
- **문자열 및 네트워크 데코레이터**: `IsEmail`, `IsUrl`, `IsUUID`, `IsIP`, `IsAlpha`, `IsAlphanumeric`, `IsAscii`, `IsBase64`, `IsBooleanString`, `IsDataURI`, `IsDateString`, `IsDecimal`, `IsFQDN`, `IsHexColor`, `IsHexadecimal`, `IsJSON`, `IsJWT`, `IsLocale`, `IsLowercase`, `IsMagnetURI`, `IsMimeType`, `IsMongoId`, `IsNumberString`, `IsPort`, `IsRFC3339`, `IsSemVer`, `IsUppercase`, `IsISO8601`, `Matches`, `Length`, `MinLength`, `MaxLength`, `Contains`, `NotContains`
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
4
|
|
|
5
|
+
Node.js support is `>=24.0.0 <27`. See [Node.js support and migration](../../docs/reference/node-support.md) before upgrading.
|
|
6
|
+
|
|
5
7
|
Input-side validation decorators, mapped DTO helpers, and the validation engine for fluo.
|
|
6
8
|
|
|
7
9
|
## Table of Contents
|
|
@@ -77,11 +79,14 @@ coercion.
|
|
|
77
79
|
applies DTO binding metadata, and recursively hydrates `@ValidateNested(...)`
|
|
78
80
|
fields. It preserves the request-pipeline contract that transports or binders own
|
|
79
81
|
source selection and scalar conversion before validation runs.
|
|
80
|
-
When materializing a plain input object, safe extra own enumerable properties are retained; only dangerous keys such as
|
|
82
|
+
When materializing a plain input object, safe extra own enumerable properties are retained by default; only dangerous keys such as
|
|
81
83
|
`__proto__`, `constructor`, and `prototype`, plus inherited or non-enumerable
|
|
82
|
-
properties, are excluded.
|
|
83
|
-
`
|
|
84
|
-
|
|
84
|
+
properties, are excluded. Pass `{ undeclaredProperties: 'reject' }` as the third
|
|
85
|
+
`materialize()` argument to reject safe enumerable properties that are not
|
|
86
|
+
initialized DTO fields, validation or binding metadata fields, or binding aliases.
|
|
87
|
+
The policy applies recursively to plain nested DTO values and reports
|
|
88
|
+
`UNDECLARED_PROPERTY` issues instead of silently stripping input. Existing DTO
|
|
89
|
+
instances are not an undeclared-property boundary.
|
|
85
90
|
Existing nested values that are already instances of the declared nested DTO are
|
|
86
91
|
preserved; plain nested values are hydrated only for the affected nested field or
|
|
87
92
|
collection entry.
|
|
@@ -147,6 +152,11 @@ the documented subclassing pattern shown above. `PickType`, `OmitType`, and
|
|
|
147
152
|
`PartialType` do not carry base class-level validators onto derived DTOs because
|
|
148
153
|
those validators can depend on fields that were omitted or made optional.
|
|
149
154
|
|
|
155
|
+
When `IntersectionType(...)` sources declare different `@ValidateNested(...)`
|
|
156
|
+
targets for the same property, every nested rule is preserved and validated.
|
|
157
|
+
Plain values for that shared property remain plain during the initial hydration
|
|
158
|
+
so each nested target can materialize and validate the value independently.
|
|
159
|
+
|
|
150
160
|
### Standard Schema support
|
|
151
161
|
|
|
152
162
|
Standard Schema adapters are expected to report invalid input through explicit issues. Validation results without issues are treated as successful.
|
|
@@ -165,6 +175,14 @@ class RestrictedUserDto {
|
|
|
165
175
|
|
|
166
176
|
`ValidateClass(...)` also accepts custom class-level validators. `Validate(...)` attaches custom field-level validators when built-in decorators are not enough, and `ValidateIf(...)` short-circuits dependent validators when its predicate returns false.
|
|
167
177
|
|
|
178
|
+
### Custom field validation
|
|
179
|
+
|
|
180
|
+
`Validate(callback)` invokes `callback(value, context)`. `context.dto` is the
|
|
181
|
+
containing DTO, and `context.propertyKey` is the decorated field key.
|
|
182
|
+
|
|
183
|
+
`@IsObject()` accepts only plain objects, including null-prototype records. It
|
|
184
|
+
rejects class instances, `Date`, `Map`, and `Set` values.
|
|
185
|
+
|
|
168
186
|
### Network validators
|
|
169
187
|
|
|
170
188
|
`@IsIP()` validates IPv4 and IPv6 strings by default. Pass `@IsIP('4')` or
|
|
@@ -183,9 +201,12 @@ Pass either a DTO class or a lazy constructor factory such as `() => ChildDto` o
|
|
|
183
201
|
`materialize()` is intentionally strict. If a transport gives you `'42'` and your DTO expects `number`, the transport or binding layer must convert it first.
|
|
184
202
|
Numeric validators, including `@IsLatitude()` and `@IsLongitude()`, validate numeric DTO values without treating numeric strings as already-converted numbers.
|
|
185
203
|
|
|
204
|
+
`@IsEnum(...)` accepts declared enum values. For numeric TypeScript enums, generated
|
|
205
|
+
reverse-map member names are not values and are rejected.
|
|
206
|
+
|
|
186
207
|
## Public API
|
|
187
208
|
|
|
188
|
-
- **Validator engine**: `DefaultValidator`, `DtoValidationError`, `ValidationIssue`, `Validator`
|
|
209
|
+
- **Validator engine**: `DefaultValidator`, `DtoValidationError`, `MaterializeOptions`, `ValidationIssue`, `Validator`
|
|
189
210
|
- **Core decorators**: `IsString`, `IsNumber`, `IsBoolean`, `IsDate`, `IsArray`, `IsObject`, `IsEnum`, `IsInt`, `IsDefined`, `IsOptional`, `ValidateNested`, `ValidateIf`, `Validate`, `ValidateClass`
|
|
190
211
|
- **Presence and comparison decorators**: `IsEmpty`, `IsNotEmpty`, `Equals`, `NotEquals`, `IsIn`, `IsNotIn`
|
|
191
212
|
- **String and network decorators**: `IsEmail`, `IsUrl`, `IsUUID`, `IsIP`, `IsAlpha`, `IsAlphanumeric`, `IsAscii`, `IsBase64`, `IsBooleanString`, `IsDataURI`, `IsDateString`, `IsDecimal`, `IsFQDN`, `IsHexColor`, `IsHexadecimal`, `IsJSON`, `IsJWT`, `IsLocale`, `IsLowercase`, `IsMagnetURI`, `IsMimeType`, `IsMongoId`, `IsNumberString`, `IsPort`, `IsRFC3339`, `IsSemVer`, `IsUppercase`, `IsISO8601`, `Matches`, `Length`, `MinLength`, `MaxLength`, `Contains`, `NotContains`
|
package/dist/decorators.d.ts
CHANGED
|
@@ -109,7 +109,8 @@ export declare const IsDate: (options?: ValidationDecoratorOptions) => FieldDeco
|
|
|
109
109
|
*/
|
|
110
110
|
export declare const IsArray: (options?: ValidationDecoratorOptions) => FieldDecoratorFn;
|
|
111
111
|
/**
|
|
112
|
-
* Validates that the decorated field is
|
|
112
|
+
* Validates that the decorated field is a plain object value, including a null-prototype record.
|
|
113
|
+
* Class instances, `Date`, `Map`, and `Set` values are rejected.
|
|
113
114
|
*
|
|
114
115
|
* @param options Optional validation behavior (`message`, `code`, `each`).
|
|
115
116
|
* @returns A field decorator that registers an object validation rule.
|
|
@@ -562,7 +563,8 @@ export declare function ArrayUnique(selectorOrOptions?: ((value: unknown) => unk
|
|
|
562
563
|
/**
|
|
563
564
|
* Registers a custom field-level validation function.
|
|
564
565
|
*
|
|
565
|
-
* @param validate Custom validator callback invoked with `(
|
|
566
|
+
* @param validate Custom validator callback invoked with `(value, context)`, where `context.dto`
|
|
567
|
+
* is the containing DTO and `context.propertyKey` is the decorated field key.
|
|
566
568
|
* @param options Optional custom-validator metadata (`message`, `code`, `source`, `each`).
|
|
567
569
|
* @returns A field decorator that registers a custom validation rule.
|
|
568
570
|
*/
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EACV,oBAAoB,EACpB,oBAAoB,EACpB,gCAAgC,EAEhC,0BAA0B,EAC3B,MAAM,+BAA+B,CAAC;AAGvC,OAAO,EAAqC,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"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"}
|
package/dist/decorators.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createArrayValidationDecorator, createFlagValidationDecorator, createValidationDecorator, createValidationOptionsWithConfigDecorator, createValidatorJsDecorator } from './internal/decorator-factories.js';
|
|
2
2
|
import { appendStandardClassValidationRule } from './internal/decorator-metadata.js';
|
|
3
|
+
import { normalizeEnumValues } from './internal/enum-values.js';
|
|
3
4
|
import { createClassValidatorFromStandardSchema, isStandardSchemaLike } from './standard-schema.js';
|
|
4
5
|
function resolveClassValidator(validate) {
|
|
5
6
|
if (!isStandardSchemaLike(validate)) {
|
|
@@ -169,7 +170,8 @@ export const IsArray = createFlagValidationDecorator(options => ({
|
|
|
169
170
|
...options
|
|
170
171
|
}));
|
|
171
172
|
/**
|
|
172
|
-
* Validates that the decorated field is
|
|
173
|
+
* Validates that the decorated field is a plain object value, including a null-prototype record.
|
|
174
|
+
* Class instances, `Date`, `Map`, and `Set` values are rejected.
|
|
173
175
|
*
|
|
174
176
|
* @param options Optional validation behavior (`message`, `code`, `each`).
|
|
175
177
|
* @returns A field decorator that registers an object validation rule.
|
|
@@ -217,7 +219,7 @@ export const IsNegative = createFlagValidationDecorator(options => ({
|
|
|
217
219
|
* @returns A field decorator that registers an enum-membership rule.
|
|
218
220
|
*/
|
|
219
221
|
export function IsEnum(values, options) {
|
|
220
|
-
const normalized =
|
|
222
|
+
const normalized = normalizeEnumValues(values);
|
|
221
223
|
return createValidationDecorator(() => ({
|
|
222
224
|
kind: 'enum',
|
|
223
225
|
values: normalized,
|
|
@@ -770,7 +772,8 @@ export function ArrayUnique(selectorOrOptions, options) {
|
|
|
770
772
|
/**
|
|
771
773
|
* Registers a custom field-level validation function.
|
|
772
774
|
*
|
|
773
|
-
* @param validate Custom validator callback invoked with `(
|
|
775
|
+
* @param validate Custom validator callback invoked with `(value, context)`, where `context.dto`
|
|
776
|
+
* is the containing DTO and `context.propertyKey` is the decorated field key.
|
|
774
777
|
* @param options Optional custom-validator metadata (`message`, `code`, `source`, `each`).
|
|
775
778
|
* @returns A field decorator that registers a custom validation rule.
|
|
776
779
|
*/
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Constructor } from '@fluojs/core';
|
|
2
|
+
import type { MaterializeOptions } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Carries invocation-local DTO traversal state across materialization and validation.
|
|
5
|
+
*/
|
|
6
|
+
export interface NestedTraversalContext {
|
|
7
|
+
readonly active: WeakSet<object>;
|
|
8
|
+
readonly declaredNestedKeys?: WeakMap<object, ReadonlySet<PropertyKey>>;
|
|
9
|
+
readonly hydrateExistingInstances?: boolean;
|
|
10
|
+
readonly undeclaredProperties?: MaterializeOptions['undeclaredProperties'];
|
|
11
|
+
readonly materialized?: WeakMap<object, WeakMap<Constructor, object>>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Enters a traversal value when it is not already active.
|
|
15
|
+
*
|
|
16
|
+
* @param value Candidate traversal value.
|
|
17
|
+
* @param context Invocation-local traversal state.
|
|
18
|
+
* @returns Whether traversal may continue for the value.
|
|
19
|
+
*/
|
|
20
|
+
export declare function enterTraversal(value: unknown, context?: NestedTraversalContext): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Releases a traversal value after materialization or validation completes.
|
|
23
|
+
*
|
|
24
|
+
* @param value Traversal value to release.
|
|
25
|
+
* @param context Invocation-local traversal state.
|
|
26
|
+
*/
|
|
27
|
+
export declare function exitTraversal(value: unknown, context?: NestedTraversalContext): void;
|
|
28
|
+
/**
|
|
29
|
+
* Creates or reuses a nested DTO instance for a raw value.
|
|
30
|
+
*
|
|
31
|
+
* @param target Nested DTO constructor.
|
|
32
|
+
* @param rawValue Raw nested value.
|
|
33
|
+
* @param context Invocation-local traversal state.
|
|
34
|
+
* @param fieldPrefix Parent path used for undeclared-property issues.
|
|
35
|
+
* @returns The materialized DTO value or the original unsupported value.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createNestedDtoInstance<T>(target: Constructor<T>, rawValue: unknown, context?: NestedTraversalContext, fieldPrefix?: string): T;
|
|
38
|
+
/**
|
|
39
|
+
* Asserts that a root value can be validated as the requested DTO.
|
|
40
|
+
*
|
|
41
|
+
* @param value Root value to validate.
|
|
42
|
+
* @param target Requested DTO constructor.
|
|
43
|
+
*/
|
|
44
|
+
export declare function assertValidRootValue(value: unknown, target: Constructor): void;
|
|
45
|
+
//# sourceMappingURL=dto-materialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dto-materialization.d.ts","sourceRoot":"","sources":["../../src/internal/dto-materialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,aAAa,CAAC;AAUvE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IACjC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC;IACxE,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,CAAC,oBAAoB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;IAC3E,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;CACvE;AAwED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAWxF;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI,CAUpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EACtB,QAAQ,EAAE,OAAO,EACjB,OAAO,CAAC,EAAE,sBAAsB,EAChC,WAAW,CAAC,EAAE,MAAM,GACnB,CAAC,CA4FH;AAyDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAM9E"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { DtoValidationError } from '../errors.js';
|
|
2
|
+
import { getCachedDtoMetadata } from './dto-metadata-cache.js';
|
|
3
|
+
import { assignSafeOwnEnumerableProperties, getIterableValues, isPlainObject, isSafeOwnEnumerableProperty } from './object-utils.js';
|
|
4
|
+
import { joinFieldPath } from './validation-issues.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Carries invocation-local DTO traversal state across materialization and validation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const materializationCycleTarget = class {};
|
|
11
|
+
function getMaterializedInstance(rawValue, target, context) {
|
|
12
|
+
return context?.materialized?.get(rawValue)?.get(target);
|
|
13
|
+
}
|
|
14
|
+
function rememberMaterializedInstance(rawValue, target, instance, context) {
|
|
15
|
+
if (!context?.materialized) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let instancesByTarget = context.materialized.get(rawValue);
|
|
19
|
+
if (!instancesByTarget) {
|
|
20
|
+
instancesByTarget = new WeakMap();
|
|
21
|
+
context.materialized.set(rawValue, instancesByTarget);
|
|
22
|
+
}
|
|
23
|
+
instancesByTarget.set(target, instance);
|
|
24
|
+
}
|
|
25
|
+
function canMaterialize(value, target) {
|
|
26
|
+
return value instanceof target || isPlainObject(value);
|
|
27
|
+
}
|
|
28
|
+
function collectDeclaredKeys(instance, target) {
|
|
29
|
+
const metadata = getCachedDtoMetadata(target);
|
|
30
|
+
return new Set([...Reflect.ownKeys(instance), ...metadata.mergedPropertyKeys, ...Array.from(metadata.bindingMap.values()).flatMap(binding => binding.key === undefined ? [] : [binding.key])]);
|
|
31
|
+
}
|
|
32
|
+
function rememberConflictingNestedKeys(value, targets, context) {
|
|
33
|
+
if (!context?.declaredNestedKeys) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const declaredKeys = new Set();
|
|
37
|
+
for (const target of targets) {
|
|
38
|
+
for (const key of collectDeclaredKeys(new target(), target)) {
|
|
39
|
+
declaredKeys.add(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const entry of getIterableValues(value) ?? [value]) {
|
|
43
|
+
if (isPlainObject(entry)) {
|
|
44
|
+
context.declaredNestedKeys.set(entry, declaredKeys);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Enters a traversal value when it is not already active.
|
|
51
|
+
*
|
|
52
|
+
* @param value Candidate traversal value.
|
|
53
|
+
* @param context Invocation-local traversal state.
|
|
54
|
+
* @returns Whether traversal may continue for the value.
|
|
55
|
+
*/
|
|
56
|
+
export function enterTraversal(value, context) {
|
|
57
|
+
if (!context || typeof value !== 'object' || value === null) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (context.active.has(value)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
context.active.add(value);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Releases a traversal value after materialization or validation completes.
|
|
69
|
+
*
|
|
70
|
+
* @param value Traversal value to release.
|
|
71
|
+
* @param context Invocation-local traversal state.
|
|
72
|
+
*/
|
|
73
|
+
export function exitTraversal(value, context) {
|
|
74
|
+
if (!context || typeof value !== 'object' || value === null) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (context.materialized?.get(value)?.has(materializationCycleTarget)) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
context.active.delete(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates or reuses a nested DTO instance for a raw value.
|
|
85
|
+
*
|
|
86
|
+
* @param target Nested DTO constructor.
|
|
87
|
+
* @param rawValue Raw nested value.
|
|
88
|
+
* @param context Invocation-local traversal state.
|
|
89
|
+
* @param fieldPrefix Parent path used for undeclared-property issues.
|
|
90
|
+
* @returns The materialized DTO value or the original unsupported value.
|
|
91
|
+
*/
|
|
92
|
+
export function createNestedDtoInstance(target, rawValue, context, fieldPrefix) {
|
|
93
|
+
if (rawValue instanceof target && context?.hydrateExistingInstances !== true) {
|
|
94
|
+
return rawValue;
|
|
95
|
+
}
|
|
96
|
+
if (!canMaterialize(rawValue, target)) {
|
|
97
|
+
return rawValue;
|
|
98
|
+
}
|
|
99
|
+
const rawObject = rawValue;
|
|
100
|
+
const rememberedInstance = getMaterializedInstance(rawObject, target, context);
|
|
101
|
+
if (rememberedInstance) {
|
|
102
|
+
return rememberedInstance;
|
|
103
|
+
}
|
|
104
|
+
if (context?.active.has(rawObject)) {
|
|
105
|
+
rememberMaterializedInstance(rawObject, materializationCycleTarget, rawObject, context);
|
|
106
|
+
return rawValue;
|
|
107
|
+
}
|
|
108
|
+
const instance = rawValue instanceof target ? rawValue : new target();
|
|
109
|
+
rememberMaterializedInstance(rawObject, target, instance, context);
|
|
110
|
+
if (!enterTraversal(rawValue, context)) {
|
|
111
|
+
return rawValue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const metadata = getCachedDtoMetadata(target);
|
|
115
|
+
if (isPlainObject(rawValue)) {
|
|
116
|
+
if (context?.undeclaredProperties === 'reject') {
|
|
117
|
+
const declaredKeys = collectDeclaredKeys(instance, target);
|
|
118
|
+
const declaredNestedKeys = context.declaredNestedKeys?.get(rawValue);
|
|
119
|
+
if (declaredNestedKeys) {
|
|
120
|
+
for (const key of declaredNestedKeys) {
|
|
121
|
+
declaredKeys.add(key);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const issues = Reflect.ownKeys(rawValue).filter(key => isSafeOwnEnumerableProperty(rawValue, key) && !declaredKeys.has(key)).map(key => {
|
|
125
|
+
const field = fieldPrefix ? joinFieldPath(fieldPrefix, String(key)) : String(key);
|
|
126
|
+
return {
|
|
127
|
+
code: 'UNDECLARED_PROPERTY',
|
|
128
|
+
field,
|
|
129
|
+
message: `${field} is not declared by the DTO.`
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
if (issues.length > 0) {
|
|
133
|
+
throw new DtoValidationError('Validation failed.', issues);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
assignSafeOwnEnumerableProperties(instance, rawValue);
|
|
137
|
+
for (const propertyKey of metadata.mergedPropertyKeys) {
|
|
138
|
+
const sourceKey = metadata.bindingMap.get(propertyKey)?.key;
|
|
139
|
+
if (!sourceKey) continue;
|
|
140
|
+
instance[propertyKey] = rawValue[sourceKey];
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const nestedEntry of metadata.nestedDtoTransforms) {
|
|
144
|
+
const nestedTargets = metadata.nestedDtoTransforms.filter(candidate => candidate.propertyKey === nestedEntry.propertyKey).map(candidate => candidate.target);
|
|
145
|
+
const hasConflictingTarget = nestedTargets.some(candidate => candidate !== nestedEntry.target);
|
|
146
|
+
if (hasConflictingTarget) {
|
|
147
|
+
rememberConflictingNestedKeys(instance[nestedEntry.propertyKey], nestedTargets, context);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const currentValue = instance[nestedEntry.propertyKey];
|
|
151
|
+
if (currentValue === undefined || currentValue === null) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const fieldPath = fieldPrefix ? joinFieldPath(fieldPrefix, String(nestedEntry.propertyKey)) : String(nestedEntry.propertyKey);
|
|
155
|
+
instance[nestedEntry.propertyKey] = transformNestedCollectionValue(currentValue, nestedEntry.target, context, fieldPath);
|
|
156
|
+
}
|
|
157
|
+
return instance;
|
|
158
|
+
} finally {
|
|
159
|
+
exitTraversal(rawValue, context);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function transformNestedValue(value, target, context, fieldPrefix) {
|
|
163
|
+
if (value === undefined || value === null) {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
if (value instanceof target && context?.hydrateExistingInstances !== true) {
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
if (!(value instanceof target) && !isPlainObject(value)) {
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
return createNestedDtoInstance(target, value, context, fieldPrefix);
|
|
173
|
+
}
|
|
174
|
+
function transformNestedCollectionValue(value, target, context, fieldPrefix) {
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
return value.map((item, index) => transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`));
|
|
177
|
+
}
|
|
178
|
+
if (value instanceof Set) {
|
|
179
|
+
return new Set(Array.from(value.values(), (item, index) => transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`)));
|
|
180
|
+
}
|
|
181
|
+
if (value instanceof Map) {
|
|
182
|
+
return new Map(Array.from(value.entries(), ([key, item], index) => [key, transformNestedValue(item, target, context, `${fieldPrefix}[${String(index)}]`)]));
|
|
183
|
+
}
|
|
184
|
+
return transformNestedValue(value, target, context, fieldPrefix);
|
|
185
|
+
}
|
|
186
|
+
function buildInvalidRootIssue() {
|
|
187
|
+
return {
|
|
188
|
+
code: 'INVALID_DTO',
|
|
189
|
+
message: 'DTO root value must be a plain object.'
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Asserts that a root value can be validated as the requested DTO.
|
|
195
|
+
*
|
|
196
|
+
* @param value Root value to validate.
|
|
197
|
+
* @param target Requested DTO constructor.
|
|
198
|
+
*/
|
|
199
|
+
export function assertValidRootValue(value, target) {
|
|
200
|
+
if (value instanceof target || isPlainObject(value)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
|
|
204
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dto-metadata-cache.d.ts","sourceRoot":"","sources":["../../src/internal/dto-metadata-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EACL,KAAK,uBAAuB,
|
|
1
|
+
{"version":3,"file":"dto-metadata-cache.d.ts","sourceRoot":"","sources":["../../src/internal/dto-metadata-cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,EACL,KAAK,uBAAuB,EAC5B,uBAAuB,EAEvB,sBAAsB,EACvB,MAAM,+BAA+B,CAAC;AAMvC,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,CAAC,GAAG,WAAW,CAMpF;AAED,KAAK,mBAAmB,GAAG,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAErE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,GAAG,CAAC,mBAAmB,EAAE,uBAAuB,CAAC,CAAC;IAC9D,oBAAoB,EAAE,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;IACjE,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,kBAAkB,EAAE,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC7C,mBAAmB,EAAE,SAAS;QAC5B,WAAW,EAAE,mBAAmB,CAAC;QACjC,MAAM,EAAE,WAAW,CAAC;KACrB,EAAE,CAAC;CACL;AA6BD,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,WAAW,GAAG,iBAAiB,CAyB3E"}
|
|
@@ -15,14 +15,15 @@ function getDtoBindingMap(target) {
|
|
|
15
15
|
function collectNestedDtoTransforms(dtoValidationSchema) {
|
|
16
16
|
const nestedEntries = [];
|
|
17
17
|
for (const entry of dtoValidationSchema) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
for (const rule of entry.rules) {
|
|
19
|
+
if (rule.kind !== 'nested') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
nestedEntries.push({
|
|
23
|
+
propertyKey: entry.propertyKey,
|
|
24
|
+
target: resolveNestedDto(rule.dto)
|
|
25
|
+
});
|
|
21
26
|
}
|
|
22
|
-
nestedEntries.push({
|
|
23
|
-
propertyKey: entry.propertyKey,
|
|
24
|
-
target: resolveNestedDto(nestedRule.dto)
|
|
25
|
-
});
|
|
26
27
|
}
|
|
27
28
|
return nestedEntries;
|
|
28
29
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enum-values.d.ts","sourceRoot":"","sources":["../../src/internal/enum-values.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,OAAO,EAAE,GAAG,SAAS,OAAO,EAAE,CAuB5G"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function normalizeEnumValues(values) {
|
|
2
|
+
if (Array.isArray(values)) {
|
|
3
|
+
return values;
|
|
4
|
+
}
|
|
5
|
+
const entries = Object.entries(values);
|
|
6
|
+
const ownMembers = new Map(entries);
|
|
7
|
+
const normalized = new Set();
|
|
8
|
+
for (const [key, value] of entries) {
|
|
9
|
+
const numericValue = Number(key);
|
|
10
|
+
const hasForwardMember = typeof value === 'string' && ownMembers.has(value);
|
|
11
|
+
const forwardValue = typeof value === 'string' ? ownMembers.get(value) : undefined;
|
|
12
|
+
const isReverseEntry = typeof value === 'string' && key === String(numericValue) && (typeof forwardValue === 'number' && key === String(forwardValue) || value === '__proto__' && !hasForwardMember && Number.isFinite(numericValue));
|
|
13
|
+
normalized.add(isReverseEntry ? numericValue : value);
|
|
14
|
+
}
|
|
15
|
+
return [...normalized];
|
|
16
|
+
}
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns collection members for supported nested collection values.
|
|
3
|
+
*
|
|
4
|
+
* @param value Candidate collection value.
|
|
5
|
+
* @returns Collection members, or `undefined` for unsupported values.
|
|
6
|
+
*/
|
|
1
7
|
export declare function getIterableValues(value: unknown): unknown[] | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Identifies plain records accepted at DTO boundaries.
|
|
10
|
+
*
|
|
11
|
+
* @param value Candidate record value.
|
|
12
|
+
* @returns Whether the value has an object or null prototype.
|
|
13
|
+
*/
|
|
2
14
|
export declare function isPlainObject(value: unknown): value is Record<PropertyKey, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* Identifies an enumerable own property that is safe to copy onto a DTO.
|
|
17
|
+
*
|
|
18
|
+
* @param source Source object.
|
|
19
|
+
* @param key Candidate property key.
|
|
20
|
+
* @returns Whether the property is own, enumerable, and not prototype-sensitive.
|
|
21
|
+
*/
|
|
22
|
+
export declare function isSafeOwnEnumerableProperty(source: object, key: PropertyKey): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Copies safe enumerable own properties between records.
|
|
25
|
+
*
|
|
26
|
+
* @param target Destination record.
|
|
27
|
+
* @param source Source record.
|
|
28
|
+
*/
|
|
3
29
|
export declare function assignSafeOwnEnumerableProperties(target: Record<PropertyKey, unknown>, source: Record<PropertyKey, unknown>): void;
|
|
4
30
|
//# sourceMappingURL=object-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object-utils.d.ts","sourceRoot":"","sources":["../../src/internal/object-utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,SAAS,CAKvE;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAOnF;AAID,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,GACnC,IAAI,
|
|
1
|
+
{"version":3,"file":"object-utils.d.ts","sourceRoot":"","sources":["../../src/internal/object-utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,EAAE,GAAG,SAAS,CAKvE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,CAOnF;AAID;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,CAGrF;AAED;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAC/C,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,OAAO,CAAC,GACnC,IAAI,CAQN"}
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns collection members for supported nested collection values.
|
|
3
|
+
*
|
|
4
|
+
* @param value Candidate collection value.
|
|
5
|
+
* @returns Collection members, or `undefined` for unsupported values.
|
|
6
|
+
*/
|
|
1
7
|
export function getIterableValues(value) {
|
|
2
8
|
if (Array.isArray(value)) return value;
|
|
3
9
|
if (value instanceof Set) return Array.from(value.values());
|
|
4
10
|
if (value instanceof Map) return Array.from(value.values());
|
|
5
11
|
return undefined;
|
|
6
12
|
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Identifies plain records accepted at DTO boundaries.
|
|
16
|
+
*
|
|
17
|
+
* @param value Candidate record value.
|
|
18
|
+
* @returns Whether the value has an object or null prototype.
|
|
19
|
+
*/
|
|
7
20
|
export function isPlainObject(value) {
|
|
8
21
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
9
22
|
return false;
|
|
@@ -12,12 +25,27 @@ export function isPlainObject(value) {
|
|
|
12
25
|
return prototype === Object.prototype || prototype === null;
|
|
13
26
|
}
|
|
14
27
|
const dangerousKeys = new Set(['__proto__', 'constructor', 'prototype']);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Identifies an enumerable own property that is safe to copy onto a DTO.
|
|
31
|
+
*
|
|
32
|
+
* @param source Source object.
|
|
33
|
+
* @param key Candidate property key.
|
|
34
|
+
* @returns Whether the property is own, enumerable, and not prototype-sensitive.
|
|
35
|
+
*/
|
|
36
|
+
export function isSafeOwnEnumerableProperty(source, key) {
|
|
37
|
+
return !(typeof key === 'string' && dangerousKeys.has(key)) && Object.prototype.propertyIsEnumerable.call(source, key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Copies safe enumerable own properties between records.
|
|
42
|
+
*
|
|
43
|
+
* @param target Destination record.
|
|
44
|
+
* @param source Source record.
|
|
45
|
+
*/
|
|
15
46
|
export function assignSafeOwnEnumerableProperties(target, source) {
|
|
16
47
|
for (const key of Reflect.ownKeys(source)) {
|
|
17
|
-
if (
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
if (!Object.prototype.propertyIsEnumerable.call(source, key)) {
|
|
48
|
+
if (!isSafeOwnEnumerableProperty(source, key)) {
|
|
21
49
|
continue;
|
|
22
50
|
}
|
|
23
51
|
target[key] = source[key];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ValidationRuleResult } from '@fluojs/core/request-pipeline';
|
|
2
|
+
import type { ValidationIssue } from '../types.js';
|
|
3
|
+
export declare function normalizeResult(result: ValidationRuleResult, field: string | undefined, source: ValidationIssue['source'], fallback: {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
}): ValidationIssue[];
|
|
7
|
+
export declare function joinFieldPath(parent: string, child?: string): string;
|
|
8
|
+
export declare function prefixIssues(issues: readonly ValidationIssue[], fieldPrefix: string, source: ValidationIssue['source']): ValidationIssue[];
|
|
9
|
+
export declare function buildIssue(fallback: {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}, field: string, source: ValidationIssue['source']): ValidationIssue;
|
|
13
|
+
//# sourceMappingURL=validation-issues.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation-issues.d.ts","sourceRoot":"","sources":["../../src/internal/validation-issues.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAEnG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAenD,wBAAgB,eAAe,CAC7B,MAAM,EAAE,oBAAoB,EAC5B,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,EACjC,QAAQ,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC5D,eAAe,EAAE,CAcnB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAChC,eAAe,EAAE,CAEnB;AAED,wBAAgB,UAAU,CACxB,QAAQ,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC7D,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,eAAe,CAAC,QAAQ,CAAC,GAChC,eAAe,CAOjB"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function normalizeIssue(issue, field, source) {
|
|
2
|
+
return {
|
|
3
|
+
code: issue.code,
|
|
4
|
+
field: issue.field ?? field,
|
|
5
|
+
message: issue.message,
|
|
6
|
+
source: issue.source ?? source
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export function normalizeResult(result, field, source, fallback) {
|
|
10
|
+
if (result === undefined || result === true) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
if (result === false) {
|
|
14
|
+
return [{
|
|
15
|
+
code: fallback.code,
|
|
16
|
+
field,
|
|
17
|
+
message: fallback.message,
|
|
18
|
+
source
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
if (Array.isArray(result)) {
|
|
22
|
+
return result.map(issue => normalizeIssue(issue, field, source));
|
|
23
|
+
}
|
|
24
|
+
return [normalizeIssue(result, field, source)];
|
|
25
|
+
}
|
|
26
|
+
export function joinFieldPath(parent, child) {
|
|
27
|
+
if (!child) return parent;
|
|
28
|
+
return child.startsWith('[') ? `${parent}${child}` : `${parent}.${child}`;
|
|
29
|
+
}
|
|
30
|
+
export function prefixIssues(issues, fieldPrefix, source) {
|
|
31
|
+
return issues.map(issue => ({
|
|
32
|
+
...issue,
|
|
33
|
+
field: joinFieldPath(fieldPrefix, issue.field),
|
|
34
|
+
source: issue.source ?? source
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
export function buildIssue(fallback, field, source) {
|
|
38
|
+
return {
|
|
39
|
+
code: fallback.code,
|
|
40
|
+
field,
|
|
41
|
+
message: fallback.message,
|
|
42
|
+
source
|
|
43
|
+
};
|
|
44
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface ValidationIssue {
|
|
|
12
12
|
/** Optional metadata source that produced this rule. */
|
|
13
13
|
source?: MetadataSource;
|
|
14
14
|
}
|
|
15
|
+
/** Controls DTO materialization behavior. */
|
|
16
|
+
export interface MaterializeOptions {
|
|
17
|
+
/** Policy for safe own enumerable input properties not declared by the DTO. */
|
|
18
|
+
readonly undeclaredProperties?: 'preserve' | 'reject';
|
|
19
|
+
}
|
|
15
20
|
/**
|
|
16
21
|
* Validation engine contract used by HTTP binding and app-level validation flows.
|
|
17
22
|
*/
|
|
@@ -22,7 +27,13 @@ export interface Validator {
|
|
|
22
27
|
* without replacing the caller's properties.
|
|
23
28
|
*/
|
|
24
29
|
validate(value: unknown, target: Constructor): MaybePromise<void>;
|
|
25
|
-
/**
|
|
26
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Materializes and validates a value into a typed DTO instance.
|
|
32
|
+
*
|
|
33
|
+
* @param value Root value to materialize.
|
|
34
|
+
* @param target Requested DTO constructor.
|
|
35
|
+
* @param options Optional materialization policy.
|
|
36
|
+
*/
|
|
37
|
+
materialize<T>(value: unknown, target: Constructor<T>, options?: MaterializeOptions): MaybePromise<T>;
|
|
27
38
|
}
|
|
28
39
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClE
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9E;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,oBAAoB,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;CACvD;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;;OAMG;IACH,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;CACvG"}
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { Constructor } from '@fluojs/core';
|
|
2
|
-
import type { Validator } from './types.js';
|
|
2
|
+
import type { MaterializeOptions, Validator } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Represents the default validator.
|
|
5
5
|
*/
|
|
6
6
|
export declare class DefaultValidator implements Validator {
|
|
7
7
|
validate(value: unknown, target: Constructor): Promise<void>;
|
|
8
|
-
materialize<T>(value: unknown, target: Constructor<T
|
|
8
|
+
materialize<T>(value: unknown, target: Constructor<T>, options?: MaterializeOptions): Promise<T>;
|
|
9
9
|
}
|
|
10
10
|
//# sourceMappingURL=validation.d.ts.map
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,cAAc,CAAC;AAetB,OAAO,KAAK,EAAE,kBAAkB,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAC;AA+OjF;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAC1C,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,CAAC,CAAC;CAqB3G"}
|
package/dist/validation.js
CHANGED
|
@@ -1,120 +1,12 @@
|
|
|
1
1
|
import { DtoValidationError } from './errors.js';
|
|
2
|
+
import { assertValidRootValue, createNestedDtoInstance, enterTraversal, exitTraversal } from './internal/dto-materialization.js';
|
|
2
3
|
import { getCachedDtoMetadata, resolveNestedDto } from './internal/dto-metadata-cache.js';
|
|
3
|
-
import {
|
|
4
|
+
import { getIterableValues, isPlainObject } from './internal/object-utils.js';
|
|
4
5
|
import { getRuleHandler } from './internal/rule-handlers.js';
|
|
6
|
+
import { buildIssue, joinFieldPath, normalizeResult, prefixIssues } from './internal/validation-issues.js';
|
|
5
7
|
function toFieldName(propertyKey) {
|
|
6
8
|
return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
|
|
7
9
|
}
|
|
8
|
-
function normalizeIssue(issue, field, source) {
|
|
9
|
-
return {
|
|
10
|
-
code: issue.code,
|
|
11
|
-
field: issue.field ?? field,
|
|
12
|
-
message: issue.message,
|
|
13
|
-
source: issue.source ?? source
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
function normalizeResult(result, field, source, fallback) {
|
|
17
|
-
if (result === undefined || result === true) {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
if (result === false) {
|
|
21
|
-
return [{
|
|
22
|
-
code: fallback.code,
|
|
23
|
-
field,
|
|
24
|
-
message: fallback.message,
|
|
25
|
-
source
|
|
26
|
-
}];
|
|
27
|
-
}
|
|
28
|
-
if (Array.isArray(result)) {
|
|
29
|
-
return result.map(issue => normalizeIssue(issue, field, source));
|
|
30
|
-
}
|
|
31
|
-
return [normalizeIssue(result, field, source)];
|
|
32
|
-
}
|
|
33
|
-
function joinFieldPath(parent, child) {
|
|
34
|
-
if (!child) return parent;
|
|
35
|
-
return child.startsWith('[') ? `${parent}${child}` : `${parent}.${child}`;
|
|
36
|
-
}
|
|
37
|
-
function prefixIssues(issues, fieldPrefix, source) {
|
|
38
|
-
return issues.map(issue => ({
|
|
39
|
-
...issue,
|
|
40
|
-
field: joinFieldPath(fieldPrefix, issue.field),
|
|
41
|
-
source: issue.source ?? source
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
44
|
-
function enterTraversal(value, context) {
|
|
45
|
-
if (!context || typeof value !== 'object' || value === null) {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
if (context.active.has(value)) {
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
context.active.add(value);
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
function exitTraversal(value, context) {
|
|
55
|
-
if (!context || typeof value !== 'object' || value === null) {
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
context.active.delete(value);
|
|
59
|
-
}
|
|
60
|
-
function createNestedDtoInstance(target, rawValue, context) {
|
|
61
|
-
if (rawValue instanceof target) {
|
|
62
|
-
return rawValue;
|
|
63
|
-
}
|
|
64
|
-
if (!isPlainObject(rawValue)) {
|
|
65
|
-
return rawValue;
|
|
66
|
-
}
|
|
67
|
-
const instance = new target();
|
|
68
|
-
if (!enterTraversal(rawValue, context)) {
|
|
69
|
-
return rawValue;
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
assignSafeOwnEnumerableProperties(instance, rawValue);
|
|
73
|
-
const metadata = getCachedDtoMetadata(target);
|
|
74
|
-
applyBindingValues(instance, rawValue, metadata.mergedPropertyKeys, metadata.bindingMap);
|
|
75
|
-
for (const nestedEntry of metadata.nestedDtoTransforms) {
|
|
76
|
-
const currentValue = instance[nestedEntry.propertyKey];
|
|
77
|
-
if (currentValue === undefined || currentValue === null) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
instance[nestedEntry.propertyKey] = transformNestedCollectionValue(currentValue, nestedEntry.target, context);
|
|
81
|
-
}
|
|
82
|
-
return instance;
|
|
83
|
-
} finally {
|
|
84
|
-
exitTraversal(rawValue, context);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
function materializeNestedDtoValue(target, rawValue, context) {
|
|
88
|
-
if (rawValue instanceof target) {
|
|
89
|
-
return rawValue;
|
|
90
|
-
}
|
|
91
|
-
if (!isPlainObject(rawValue)) {
|
|
92
|
-
return rawValue;
|
|
93
|
-
}
|
|
94
|
-
return createNestedDtoInstance(target, rawValue, context);
|
|
95
|
-
}
|
|
96
|
-
function applyBindingValues(instance, rawValue, keys, bindingMap) {
|
|
97
|
-
for (const propertyKey of keys) {
|
|
98
|
-
const sourceKey = bindingMap.get(propertyKey)?.key;
|
|
99
|
-
if (!sourceKey) continue;
|
|
100
|
-
instance[propertyKey] = rawValue[sourceKey];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
function transformNestedValue(value, target, context) {
|
|
104
|
-
return value === undefined || value === null ? value : materializeNestedDtoValue(target, value, context);
|
|
105
|
-
}
|
|
106
|
-
function transformNestedCollectionValue(value, target, context) {
|
|
107
|
-
if (Array.isArray(value)) {
|
|
108
|
-
return value.map(item => transformNestedValue(item, target, context));
|
|
109
|
-
}
|
|
110
|
-
if (value instanceof Set) {
|
|
111
|
-
return new Set(Array.from(value.values(), item => transformNestedValue(item, target, context)));
|
|
112
|
-
}
|
|
113
|
-
if (value instanceof Map) {
|
|
114
|
-
return new Map(Array.from(value.entries(), ([key, item]) => [key, transformNestedValue(item, target, context)]));
|
|
115
|
-
}
|
|
116
|
-
return transformNestedValue(value, target, context);
|
|
117
|
-
}
|
|
118
10
|
function describeValidator(rule, field) {
|
|
119
11
|
const handler = getRuleHandler(rule);
|
|
120
12
|
return {
|
|
@@ -122,31 +14,11 @@ function describeValidator(rule, field) {
|
|
|
122
14
|
message: rule.message ?? handler.describe(field, rule)
|
|
123
15
|
};
|
|
124
16
|
}
|
|
125
|
-
function buildIssue(fallback, field, source) {
|
|
126
|
-
return {
|
|
127
|
-
code: fallback.code,
|
|
128
|
-
field,
|
|
129
|
-
message: fallback.message,
|
|
130
|
-
source
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
function buildInvalidRootIssue() {
|
|
134
|
-
return {
|
|
135
|
-
code: 'INVALID_DTO',
|
|
136
|
-
message: 'DTO root value must be a plain object.'
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
function assertValidRootValue(value, target) {
|
|
140
|
-
if (value instanceof target || isPlainObject(value)) {
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
|
|
144
|
-
}
|
|
145
17
|
function getRuleValues(value) {
|
|
146
18
|
return getIterableValues(value) ?? [value];
|
|
147
19
|
}
|
|
148
20
|
function shouldSkipRuleForMissingValue(rule, value) {
|
|
149
|
-
return (value === undefined || value === null) && rule.kind !== 'defined' && rule.kind !== '
|
|
21
|
+
return (value === undefined || value === null) && rule.kind !== 'defined' && rule.kind !== 'empty';
|
|
150
22
|
}
|
|
151
23
|
async function evaluateCustomRule(rule, value, dto, propertyKey, fieldPath, source, fallback) {
|
|
152
24
|
if (!rule.each) {
|
|
@@ -192,7 +64,7 @@ async function validateNestedRule(rule, value, fieldPath, inheritedSource, conte
|
|
|
192
64
|
issues.push(buildIssue(describeValidator(rule, nestedPath), nestedPath, inheritedSource));
|
|
193
65
|
continue;
|
|
194
66
|
}
|
|
195
|
-
const nestedDto = createNestedDtoInstance(resolvedDto, entry, context);
|
|
67
|
+
const nestedDto = createNestedDtoInstance(resolvedDto, entry, context, nestedPath);
|
|
196
68
|
const shouldTrackEntry = trackedEntry && !(entry instanceof resolvedDto) ? enterTraversal(trackedEntry, context) : false;
|
|
197
69
|
try {
|
|
198
70
|
issues.push(...(await collectValidationIssuesInternal(resolvedDto, nestedDto, {
|
|
@@ -230,10 +102,10 @@ async function evaluateRule(rule, value, dto, propertyKey, fieldPath, source, co
|
|
|
230
102
|
return [];
|
|
231
103
|
}
|
|
232
104
|
async function applyPropertyRules(rules, value, dto, propertyKey, fieldPath, source, context) {
|
|
233
|
-
const conditionallySkip = await shouldConditionallySkip(rules, dto, value);
|
|
234
105
|
if (rules.some(rule => rule.kind === 'optional') && (value === undefined || value === null)) {
|
|
235
106
|
return [];
|
|
236
107
|
}
|
|
108
|
+
const conditionallySkip = await shouldConditionallySkip(rules, dto, value);
|
|
237
109
|
const issues = [];
|
|
238
110
|
for (const rule of rules) {
|
|
239
111
|
if (rule.kind === 'validateIf' || rule.kind === 'optional') continue;
|
|
@@ -295,12 +167,19 @@ export class DefaultValidator {
|
|
|
295
167
|
if (issues.length === 0) return;
|
|
296
168
|
throw new DtoValidationError('Validation failed.', issues);
|
|
297
169
|
}
|
|
298
|
-
async materialize(value, target) {
|
|
170
|
+
async materialize(value, target, options = {}) {
|
|
299
171
|
assertValidRootValue(value, target);
|
|
300
|
-
const
|
|
301
|
-
active: new WeakSet()
|
|
302
|
-
|
|
303
|
-
|
|
172
|
+
const traversal = {
|
|
173
|
+
active: new WeakSet(),
|
|
174
|
+
hydrateExistingInstances: true,
|
|
175
|
+
undeclaredProperties: options.undeclaredProperties ?? 'preserve',
|
|
176
|
+
materialized: new WeakMap(),
|
|
177
|
+
...(options.undeclaredProperties === 'reject' ? {
|
|
178
|
+
declaredNestedKeys: new WeakMap()
|
|
179
|
+
} : {})
|
|
180
|
+
};
|
|
181
|
+
const instance = createNestedDtoInstance(target, value, traversal);
|
|
182
|
+
const issues = await collectValidationIssuesInternal(target, instance, {}, traversal);
|
|
304
183
|
if (issues.length > 0) {
|
|
305
184
|
throw new DtoValidationError('Validation failed.', issues);
|
|
306
185
|
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"decorators",
|
|
10
10
|
"schema"
|
|
11
11
|
],
|
|
12
|
-
"version": "
|
|
12
|
+
"version": "2.0.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/validation"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=24.0.0 <27"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@standard-schema/spec": "^1.1.0",
|
|
44
44
|
"validator": "^13.15.26",
|
|
45
|
-
"@fluojs/core": "^
|
|
45
|
+
"@fluojs/core": "^2.0.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@types/validator": "^13.15.10",
|
|
49
49
|
"arktype": "^2.2.0",
|
|
50
50
|
"valibot": "^1.0.0",
|
|
51
|
-
"vitest": "^
|
|
51
|
+
"vitest": "^4.1.11",
|
|
52
52
|
"zod": "^4.1.11"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|