@fluojs/validation 1.0.0-beta.3 → 1.0.0-beta.4

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 CHANGED
@@ -64,10 +64,17 @@ try {
64
64
  - **`materialize<T>(value, target)`**: **입력 처리**에 가장 적합합니다. plain 객체를 받아 대상 클래스의 인스턴스를 생성하고, 값을 복사하며, 중첩된 DTO를 재귀적으로 처리한 후 모든 검증 규칙을 실행합니다.
65
65
  - **`validate(instance, target)`**: **기존 루트 객체 확인**에 적합합니다. 이미 생성된 루트 값에 대해 검증 규칙을 실행하며, plain 객체인 `@ValidateNested(...)` 값은 중첩 DTO 규칙을 실행하기 위해 임시로 실체화할 수 있습니다. 이 임시 실체화는 호출자가 넘긴 속성 값을 대체하지 않습니다.
66
66
 
67
+ `validate()`는 문자열, 배열, `null`, `undefined` 같은 잘못된 루트 값을 field 또는
68
+ class rule이 실행되기 전에 deterministic `DtoValidationError`로 거부합니다. 이미
69
+ 생성된 대상 DTO 인스턴스와 plain 루트 객체는 허용하므로 request-pipeline binder가
70
+ 준비한 DTO payload를 scalar coercion 없이 검증할 수 있습니다.
71
+
67
72
  `materialize()`는 plain 입력 객체의 안전한 own enumerable 속성을 복사하고,
68
73
  DTO 바인딩 메타데이터를 적용한 뒤 `@ValidateNested(...)` 필드를 재귀적으로
69
74
  실체화합니다. 어떤 요청 소스를 선택하고 스칼라 값을 변환할지는 transport 또는
70
75
  binder가 검증 전에 담당한다는 request-pipeline 계약을 유지합니다.
76
+ 선언된 중첩 DTO의 인스턴스인 기존 중첩 값은 그대로 보존하고, plain 중첩 값만
77
+ 해당 필드 또는 collection entry 단위로 실체화합니다.
71
78
  `materialize()`에 넘기는 루트 값은 plain 객체이거나 대상 DTO 인스턴스여야 합니다.
72
79
  문자열, 배열, `null` 같은 잘못된 루트 값은 대상 DTO 생성자나 필드 initializer가
73
80
  실행되기 전에 거부됩니다.
@@ -124,10 +131,12 @@ class RestrictedUserDto {
124
131
  ### 중첩 검증
125
132
 
126
133
  `@ValidateNested(...)`는 객체 필드, 배열, `Set`, `Map`을 지원합니다. 중첩 DTO path는 validation issue에서 dot/index 표기법을 사용하며, cycle은 안전하게 감지되고 shared reference는 허용됩니다.
134
+ 중첩 타입을 지연 해석해야 할 때는 DTO 클래스 자체나 `() => ChildDto`, `function resolveChildDto() { return ChildDto; }` 같은 lazy constructor factory를 전달할 수 있습니다.
127
135
 
128
136
  ### 암묵적 scalar coercion 없음
129
137
 
130
138
  `materialize()`는 의도적으로 엄격합니다. Transport가 `'42'`를 넘기고 DTO가 `number`를 기대한다면, transport나 binding layer가 먼저 변환해야 합니다.
139
+ `@IsLatitude()`, `@IsLongitude()`를 포함한 numeric validator는 numeric string을 이미 변환된 number처럼 취급하지 않고 DTO의 numeric 값을 검증합니다.
131
140
 
132
141
  ## 공개 API
133
142
 
package/README.md CHANGED
@@ -66,10 +66,19 @@ try {
66
66
  temporarily materialize plain nested `@ValidateNested(...)` values to run their
67
67
  nested DTO rules without replacing the caller's properties
68
68
 
69
+ `validate()` rejects malformed roots such as strings, arrays, `null`, and
70
+ `undefined` with a deterministic `DtoValidationError` before field or class rules
71
+ run. It accepts already-created target DTO instances and plain root objects so
72
+ request-pipeline binders can validate their prepared DTO payloads without scalar
73
+ coercion.
74
+
69
75
  `materialize()` copies safe own enumerable properties from plain input objects,
70
76
  applies DTO binding metadata, and recursively hydrates `@ValidateNested(...)`
71
77
  fields. It preserves the request-pipeline contract that transports or binders own
72
78
  source selection and scalar conversion before validation runs.
79
+ Existing nested values that are already instances of the declared nested DTO are
80
+ preserved; plain nested values are hydrated only for the affected nested field or
81
+ collection entry.
73
82
  The root value passed to `materialize()` must already be a plain object or an
74
83
  instance of the target DTO; malformed roots such as strings, arrays, and `null`
75
84
  are rejected before the target DTO constructor or field initializers run.
@@ -126,10 +135,12 @@ class RestrictedUserDto {
126
135
  ### Nested validation
127
136
 
128
137
  `@ValidateNested(...)` supports object fields, arrays, `Set`, and `Map`. Nested DTO paths use dot/index notation in validation issues, cycles are detected safely, and shared references are allowed.
138
+ Pass either a DTO class or a lazy constructor factory such as `() => ChildDto` or `function resolveChildDto() { return ChildDto; }` when nested types need deferred resolution.
129
139
 
130
140
  ### No implicit scalar coercion
131
141
 
132
142
  `materialize()` is intentionally strict. If a transport gives you `'42'` and your DTO expects `number`, the transport or binding layer must convert it first.
143
+ Numeric validators, including `@IsLatitude()` and `@IsLongitude()`, validate numeric DTO values without treating numeric strings as already-converted numbers.
133
144
 
134
145
  ## Public API
135
146
 
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,cAAc,CAAC;AAatB,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAC;AAgxB7D;;GAEG;AACH,qBAAa,gBAAiB,YAAW,SAAS;IAC1C,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAM5D,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CAczE"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,cAAc,CAAC;AAatB,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,YAAY,CAAC;AAsyB7D;;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,GAAG,OAAO,CAAC,CAAC,CAAC;CAYzE"}
@@ -1,8 +1,11 @@
1
1
  import validator from 'validator';
2
2
  import { getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/internal';
3
3
  import { DtoValidationError } from './errors.js';
4
+ function isClassConstructor(dto) {
5
+ return typeof dto === 'function' && Function.prototype.toString.call(dto).startsWith('class ');
6
+ }
4
7
  function resolveNestedDto(dto) {
5
- if (typeof dto === 'function' && 'prototype' in dto && dto.prototype) {
8
+ if (isClassConstructor(dto)) {
6
9
  return dto;
7
10
  }
8
11
  return dto();
@@ -283,7 +286,7 @@ const RULE_HANDLERS = {
283
286
  validatorjs: {
284
287
  defaultCode: 'INVALID_FIELD',
285
288
  describe: field => `${field} is invalid.`,
286
- validate: (rule, value) => typeof value === 'string' && runValidatorJs(rule, value)
289
+ validate: (rule, value) => runValidatorJs(rule, value)
287
290
  },
288
291
  arrayContains: {
289
292
  defaultCode: 'ARRAY_CONTAINS',
@@ -399,6 +402,15 @@ function describeValidator(rule, field) {
399
402
  };
400
403
  }
401
404
  function runValidatorJs(rule, value) {
405
+ if (rule.validator === 'latitude') {
406
+ return typeof value === 'number' && Number.isFinite(value) && value >= -90 && value <= 90;
407
+ }
408
+ if (rule.validator === 'longitude') {
409
+ return typeof value === 'number' && Number.isFinite(value) && value >= -180 && value <= 180;
410
+ }
411
+ if (typeof value !== 'string') {
412
+ return false;
413
+ }
402
414
  switch (rule.validator) {
403
415
  case 'alpha':
404
416
  return validator.isAlpha(value);
@@ -470,16 +482,6 @@ function runValidatorJs(rule, value) {
470
482
  return validator.isUUID(value, rule.args?.[0]);
471
483
  case 'iso8601':
472
484
  return validator.isISO8601(value);
473
- case 'latitude':
474
- {
475
- const number = Number(value);
476
- return !Number.isNaN(number) && number >= -90 && number <= 90;
477
- }
478
- case 'longitude':
479
- {
480
- const number = Number(value);
481
- return !Number.isNaN(number) && number >= -180 && number <= 180;
482
- }
483
485
  case 'latLong':
484
486
  return validator.isLatLong(value);
485
487
  default:
@@ -500,6 +502,12 @@ function buildInvalidRootIssue() {
500
502
  message: 'DTO root value must be a plain object.'
501
503
  };
502
504
  }
505
+ function assertValidRootValue(value, target) {
506
+ if (value instanceof target || isPlainObject(value)) {
507
+ return;
508
+ }
509
+ throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
510
+ }
503
511
  function getRuleValues(value) {
504
512
  return getIterableValues(value) ?? [value];
505
513
  }
@@ -646,14 +654,13 @@ async function collectValidationIssuesInternal(target, value, context, traversal
646
654
  */
647
655
  export class DefaultValidator {
648
656
  async validate(value, target) {
657
+ assertValidRootValue(value, target);
649
658
  const issues = await collectValidationIssues(target, value);
650
659
  if (issues.length === 0) return;
651
660
  throw new DtoValidationError('Validation failed.', issues);
652
661
  }
653
662
  async materialize(value, target) {
654
- if (!(value instanceof target) && !isPlainObject(value)) {
655
- throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
656
- }
663
+ assertValidRootValue(value, target);
657
664
  const instance = createNestedDtoInstance(target, value, {
658
665
  active: new WeakSet()
659
666
  });
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "decorators",
10
10
  "schema"
11
11
  ],
12
- "version": "1.0.0-beta.3",
12
+ "version": "1.0.0-beta.4",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@standard-schema/spec": "^1.1.0",
44
44
  "validator": "^13.15.26",
45
- "@fluojs/core": "^1.0.0-beta.4"
45
+ "@fluojs/core": "^1.0.0-beta.5"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/validator": "^13.15.10",