@fluojs/validation 1.0.5 → 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.
Files changed (43) hide show
  1. package/README.ko.md +47 -1
  2. package/README.md +48 -1
  3. package/dist/decorators.d.ts +71 -69
  4. package/dist/decorators.d.ts.map +1 -1
  5. package/dist/decorators.js +73 -133
  6. package/dist/internal/decorator-factories.d.ts +12 -0
  7. package/dist/internal/decorator-factories.d.ts.map +1 -0
  8. package/dist/internal/decorator-factories.js +32 -0
  9. package/dist/internal/decorator-metadata.d.ts +7 -0
  10. package/dist/internal/decorator-metadata.d.ts.map +1 -0
  11. package/dist/internal/decorator-metadata.js +38 -0
  12. package/dist/internal/dto-materialization.d.ts +45 -0
  13. package/dist/internal/dto-materialization.d.ts.map +1 -0
  14. package/dist/internal/dto-materialization.js +204 -0
  15. package/dist/internal/dto-metadata-cache.d.ts +17 -0
  16. package/dist/internal/dto-metadata-cache.d.ts.map +1 -0
  17. package/dist/internal/dto-metadata-cache.js +49 -0
  18. package/dist/internal/enum-values.d.ts +2 -0
  19. package/dist/internal/enum-values.d.ts.map +1 -0
  20. package/dist/internal/enum-values.js +16 -0
  21. package/dist/internal/object-utils.d.ts +30 -0
  22. package/dist/internal/object-utils.d.ts.map +1 -0
  23. package/dist/internal/object-utils.js +53 -0
  24. package/dist/internal/rule-handlers.d.ts +19 -0
  25. package/dist/internal/rule-handlers.d.ts.map +1 -0
  26. package/dist/internal/rule-handlers.js +209 -0
  27. package/dist/internal/validation-issues.d.ts +13 -0
  28. package/dist/internal/validation-issues.d.ts.map +1 -0
  29. package/dist/internal/validation-issues.js +44 -0
  30. package/dist/internal/validator-js-adapter.d.ts +5 -0
  31. package/dist/internal/validator-js-adapter.d.ts.map +1 -0
  32. package/dist/internal/validator-js-adapter.js +88 -0
  33. package/dist/mapped-types.d.ts +1 -1
  34. package/dist/mapped-types.d.ts.map +1 -1
  35. package/dist/mapped-types.js +1 -1
  36. package/dist/standard-schema.d.ts +1 -1
  37. package/dist/standard-schema.d.ts.map +1 -1
  38. package/dist/types.d.ts +18 -3
  39. package/dist/types.d.ts.map +1 -1
  40. package/dist/validation.d.ts +2 -2
  41. package/dist/validation.d.ts.map +1 -1
  42. package/dist/validation.js +24 -509
  43. package/package.json +4 -4
@@ -1,399 +1,12 @@
1
- import validator from 'validator';
2
- import { getClassValidationRules, getDtoBindingSchema, getDtoValidationSchema } from '@fluojs/core/internal';
3
1
  import { DtoValidationError } from './errors.js';
4
- function isClassConstructor(dto) {
5
- return typeof dto === 'function' && Function.prototype.toString.call(dto).startsWith('class ');
6
- }
7
- function resolveNestedDto(dto) {
8
- if (isClassConstructor(dto)) {
9
- return dto;
10
- }
11
- return dto();
12
- }
2
+ import { assertValidRootValue, createNestedDtoInstance, enterTraversal, exitTraversal } from './internal/dto-materialization.js';
3
+ import { getCachedDtoMetadata, resolveNestedDto } from './internal/dto-metadata-cache.js';
4
+ import { getIterableValues, isPlainObject } from './internal/object-utils.js';
5
+ import { getRuleHandler } from './internal/rule-handlers.js';
6
+ import { buildIssue, joinFieldPath, normalizeResult, prefixIssues } from './internal/validation-issues.js';
13
7
  function toFieldName(propertyKey) {
14
8
  return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
15
9
  }
16
- function normalizeIssue(issue, field, source) {
17
- return {
18
- code: issue.code,
19
- field: issue.field ?? field,
20
- message: issue.message,
21
- source: issue.source ?? source
22
- };
23
- }
24
- function normalizeResult(result, field, source, fallback) {
25
- if (result === undefined || result === true) {
26
- return [];
27
- }
28
- if (result === false) {
29
- return [{
30
- code: fallback.code,
31
- field,
32
- message: fallback.message,
33
- source
34
- }];
35
- }
36
- if (Array.isArray(result)) {
37
- return result.map(issue => normalizeIssue(issue, field, source));
38
- }
39
- return [normalizeIssue(result, field, source)];
40
- }
41
- function getIterableValues(value) {
42
- if (Array.isArray(value)) return value;
43
- if (value instanceof Set) return Array.from(value.values());
44
- if (value instanceof Map) return Array.from(value.values());
45
- return undefined;
46
- }
47
- function isPlainObject(value) {
48
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
49
- return false;
50
- }
51
- const prototype = Object.getPrototypeOf(value);
52
- return prototype === Object.prototype || prototype === null;
53
- }
54
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
55
- function assignSafeOwnEnumerableProperties(target, source) {
56
- for (const key of Reflect.ownKeys(source)) {
57
- if (typeof key === 'string' && DANGEROUS_KEYS.has(key)) {
58
- continue;
59
- }
60
- if (!Object.prototype.propertyIsEnumerable.call(source, key)) {
61
- continue;
62
- }
63
- target[key] = source[key];
64
- }
65
- }
66
- function isEmptyValue(value) {
67
- return value === '' || value === null || value === undefined;
68
- }
69
- function joinFieldPath(parent, child) {
70
- if (!child) return parent;
71
- return child.startsWith('[') ? `${parent}${child}` : `${parent}.${child}`;
72
- }
73
- function prefixIssues(issues, fieldPrefix, source) {
74
- return issues.map(issue => ({
75
- ...issue,
76
- field: joinFieldPath(fieldPrefix, issue.field),
77
- source: issue.source ?? source
78
- }));
79
- }
80
- function enterTraversal(value, context) {
81
- if (!context || typeof value !== 'object' || value === null) {
82
- return true;
83
- }
84
- if (context.active.has(value)) {
85
- return false;
86
- }
87
- context.active.add(value);
88
- return true;
89
- }
90
- function exitTraversal(value, context) {
91
- if (!context || typeof value !== 'object' || value === null) {
92
- return;
93
- }
94
- context.active.delete(value);
95
- }
96
- const dtoMetadataCache = new WeakMap();
97
- function collectNestedDtoTransforms(dtoValidationSchema) {
98
- const nestedEntries = [];
99
- for (const entry of dtoValidationSchema) {
100
- const nestedRule = entry.rules.find(rule => rule.kind === 'nested');
101
- if (!nestedRule) {
102
- continue;
103
- }
104
- nestedEntries.push({
105
- each: nestedRule.each === true,
106
- propertyKey: entry.propertyKey,
107
- target: resolveNestedDto(nestedRule.dto)
108
- });
109
- }
110
- return nestedEntries;
111
- }
112
- function getCachedDtoMetadata(target) {
113
- const cached = dtoMetadataCache.get(target);
114
- if (cached) {
115
- return cached;
116
- }
117
- const bindingMap = getDtoBindingMap(target);
118
- const dtoValidationSchema = getDtoValidationSchema(target);
119
- const classValidationRules = getClassValidationRules(target);
120
- const mergedPropertyKeys = new Set([...bindingMap.keys(), ...dtoValidationSchema.map(entry => entry.propertyKey)]);
121
- const nestedDtoTransforms = collectNestedDtoTransforms(dtoValidationSchema);
122
- const next = {
123
- bindingMap,
124
- classValidationRules,
125
- dtoValidationSchema,
126
- mergedPropertyKeys,
127
- nestedDtoTransforms
128
- };
129
- dtoMetadataCache.set(target, next);
130
- return next;
131
- }
132
- function getRuleHandler(rule) {
133
- return RULE_HANDLERS[rule.kind];
134
- }
135
- const RULE_HANDLERS = {
136
- validateIf: {
137
- defaultCode: 'VALIDATE_IF',
138
- describe: field => `${field} is conditionally invalid.`,
139
- validate: () => true
140
- },
141
- defined: {
142
- defaultCode: 'REQUIRED',
143
- describe: field => `${field} is required.`,
144
- validate: (_rule, value) => value !== undefined && value !== null
145
- },
146
- optional: {
147
- defaultCode: 'OPTIONAL',
148
- describe: field => `${field} is optional.`,
149
- validate: () => true
150
- },
151
- equals: {
152
- defaultCode: 'EQUALS',
153
- describe: (field, rule) => `${field} must equal ${String(rule.value)}.`,
154
- validate: (rule, value) => value === rule.value
155
- },
156
- notEquals: {
157
- defaultCode: 'NOT_EQUALS',
158
- describe: (field, rule) => `${field} must not equal ${String(rule.value)}.`,
159
- validate: (rule, value) => value !== rule.value
160
- },
161
- empty: {
162
- defaultCode: 'EMPTY',
163
- describe: field => `${field} must be empty.`,
164
- validate: (_rule, value) => isEmptyValue(value)
165
- },
166
- notEmpty: {
167
- defaultCode: 'NOT_EMPTY',
168
- describe: field => `${field} should not be empty.`,
169
- validate: (_rule, value) => !isEmptyValue(value)
170
- },
171
- in: {
172
- defaultCode: 'IN',
173
- describe: field => `${field} must be one of the allowed values.`,
174
- validate: (rule, value) => rule.values.includes(value)
175
- },
176
- notIn: {
177
- defaultCode: 'NOT_IN',
178
- describe: field => `${field} contains a forbidden value.`,
179
- validate: (rule, value) => !rule.values.includes(value)
180
- },
181
- string: {
182
- defaultCode: 'INVALID_STRING',
183
- describe: field => `${field} must be a string.`,
184
- validate: (_rule, value) => typeof value === 'string'
185
- },
186
- number: {
187
- defaultCode: 'INVALID_NUMBER',
188
- describe: field => `${field} must be a number.`,
189
- validate: (rule, value) => typeof value === 'number' && (rule.allowNaN || !Number.isNaN(value))
190
- },
191
- boolean: {
192
- defaultCode: 'INVALID_BOOLEAN',
193
- describe: field => `${field} must be a boolean.`,
194
- validate: (_rule, value) => typeof value === 'boolean'
195
- },
196
- date: {
197
- defaultCode: 'INVALID_DATE',
198
- describe: field => `${field} must be a Date instance.`,
199
- validate: (_rule, value) => value instanceof Date && !Number.isNaN(value.getTime())
200
- },
201
- array: {
202
- defaultCode: 'INVALID_ARRAY',
203
- describe: field => `${field} must be an array.`,
204
- validate: (_rule, value) => Array.isArray(value)
205
- },
206
- object: {
207
- defaultCode: 'INVALID_OBJECT',
208
- describe: field => `${field} must be an object.`,
209
- validate: (_rule, value) => isPlainObject(value)
210
- },
211
- enum: {
212
- defaultCode: 'INVALID_ENUM',
213
- describe: field => `${field} must be a supported enum value.`,
214
- validate: (rule, value) => rule.values.includes(value)
215
- },
216
- int: {
217
- defaultCode: 'INVALID_INT',
218
- describe: field => `${field} must be an integer.`,
219
- validate: (_rule, value) => typeof value === 'number' && Number.isInteger(value)
220
- },
221
- divisibleBy: {
222
- defaultCode: 'DIVISIBLE_BY',
223
- describe: (field, rule) => `${field} must be divisible by ${String(rule.value)}.`,
224
- validate: (rule, value) => typeof value === 'number' && !Number.isNaN(value) && value % rule.value === 0
225
- },
226
- positive: {
227
- defaultCode: 'POSITIVE',
228
- describe: field => `${field} must be positive.`,
229
- validate: (_rule, value) => typeof value === 'number' && value > 0
230
- },
231
- negative: {
232
- defaultCode: 'NEGATIVE',
233
- describe: field => `${field} must be negative.`,
234
- validate: (_rule, value) => typeof value === 'number' && value < 0
235
- },
236
- min: {
237
- defaultCode: 'MIN',
238
- describe: (field, rule) => `${field} must be greater than or equal to ${String(rule.value)}.`,
239
- validate: (rule, value) => typeof value === 'number' && !Number.isNaN(value) && value >= rule.value
240
- },
241
- max: {
242
- defaultCode: 'MAX',
243
- describe: (field, rule) => `${field} must be less than or equal to ${String(rule.value)}.`,
244
- validate: (rule, value) => typeof value === 'number' && !Number.isNaN(value) && value <= rule.value
245
- },
246
- minDate: {
247
- defaultCode: 'MIN_DATE',
248
- describe: (field, rule) => `${field} must be on or after ${rule.value.toISOString()}.`,
249
- validate: (rule, value) => value instanceof Date && !Number.isNaN(value.getTime()) && value.getTime() >= rule.value.getTime()
250
- },
251
- maxDate: {
252
- defaultCode: 'MAX_DATE',
253
- describe: (field, rule) => `${field} must be on or before ${rule.value.toISOString()}.`,
254
- validate: (rule, value) => value instanceof Date && !Number.isNaN(value.getTime()) && value.getTime() <= rule.value.getTime()
255
- },
256
- contains: {
257
- defaultCode: 'CONTAINS',
258
- describe: (field, rule) => `${field} must contain ${rule.value}.`,
259
- validate: (rule, value) => typeof value === 'string' && value.includes(rule.value)
260
- },
261
- notContains: {
262
- defaultCode: 'NOT_CONTAINS',
263
- describe: (field, rule) => `${field} must not contain ${rule.value}.`,
264
- validate: (rule, value) => typeof value === 'string' && !value.includes(rule.value)
265
- },
266
- length: {
267
- defaultCode: 'LENGTH',
268
- describe: field => `${field} must have a valid length.`,
269
- validate: (rule, value) => typeof value === 'string' && value.length >= rule.min && (rule.max === undefined || value.length <= rule.max)
270
- },
271
- minLength: {
272
- defaultCode: 'MIN_LENGTH',
273
- describe: (field, rule) => `${field} must have length at least ${String(rule.value)}.`,
274
- validate: (rule, value) => typeof value === 'string' && value.length >= rule.value
275
- },
276
- maxLength: {
277
- defaultCode: 'MAX_LENGTH',
278
- describe: (field, rule) => `${field} must have length at most ${String(rule.value)}.`,
279
- validate: (rule, value) => typeof value === 'string' && value.length <= rule.value
280
- },
281
- nested: {
282
- defaultCode: 'INVALID_NESTED',
283
- describe: field => `${field} contains invalid nested data.`,
284
- validate: () => true
285
- },
286
- validatorjs: {
287
- defaultCode: 'INVALID_FIELD',
288
- describe: field => `${field} is invalid.`,
289
- validate: (rule, value) => runValidatorJs(rule, value)
290
- },
291
- arrayContains: {
292
- defaultCode: 'ARRAY_CONTAINS',
293
- describe: field => `${field} must contain the required values.`,
294
- validate: (rule, value) => Array.isArray(value) && rule.values.every(expected => value.includes(expected))
295
- },
296
- arrayNotContains: {
297
- defaultCode: 'ARRAY_NOT_CONTAINS',
298
- describe: field => `${field} contains forbidden values.`,
299
- validate: (rule, value) => Array.isArray(value) && rule.values.every(expected => !value.includes(expected))
300
- },
301
- arrayNotEmpty: {
302
- defaultCode: 'ARRAY_NOT_EMPTY',
303
- describe: field => `${field} must not be an empty array.`,
304
- validate: (_rule, value) => Array.isArray(value) && value.length > 0
305
- },
306
- arrayMinSize: {
307
- defaultCode: 'ARRAY_MIN_SIZE',
308
- describe: (field, rule) => `${field} must contain at least ${String(rule.value)} items.`,
309
- validate: (rule, value) => Array.isArray(value) && value.length >= rule.value
310
- },
311
- arrayMaxSize: {
312
- defaultCode: 'ARRAY_MAX_SIZE',
313
- describe: (field, rule) => `${field} must contain at most ${String(rule.value)} items.`,
314
- validate: (rule, value) => Array.isArray(value) && value.length <= rule.value
315
- },
316
- arrayUnique: {
317
- defaultCode: 'ARRAY_UNIQUE',
318
- describe: field => `${field} must contain unique values.`,
319
- validate: (rule, value) => {
320
- if (!Array.isArray(value)) return false;
321
- const seen = new Set();
322
- for (const entry of value) {
323
- const key = rule.selector ? rule.selector(entry) : entry;
324
- if (seen.has(key)) return false;
325
- seen.add(key);
326
- }
327
- return true;
328
- }
329
- },
330
- custom: {
331
- defaultCode: 'INVALID_FIELD',
332
- describe: field => `${field} is invalid.`,
333
- validate: () => true
334
- }
335
- };
336
- function createNestedDtoInstance(target, rawValue, context) {
337
- if (rawValue instanceof target) {
338
- return rawValue;
339
- }
340
- if (!isPlainObject(rawValue)) {
341
- return rawValue;
342
- }
343
- const instance = new target();
344
- if (!enterTraversal(rawValue, context)) {
345
- return rawValue;
346
- }
347
- try {
348
- assignSafeOwnEnumerableProperties(instance, rawValue);
349
- const metadata = getCachedDtoMetadata(target);
350
- applyBindingValues(instance, rawValue, metadata.mergedPropertyKeys, metadata.bindingMap);
351
- for (const nestedEntry of metadata.nestedDtoTransforms) {
352
- const currentValue = instance[nestedEntry.propertyKey];
353
- if (currentValue === undefined || currentValue === null) {
354
- continue;
355
- }
356
- instance[nestedEntry.propertyKey] = nestedEntry.each ? transformNestedEachValue(currentValue, nestedEntry.target, context) : transformNestedValue(currentValue, nestedEntry.target, context);
357
- }
358
- return instance;
359
- } finally {
360
- exitTraversal(rawValue, context);
361
- }
362
- }
363
- function materializeNestedDtoValue(target, rawValue, context) {
364
- if (rawValue instanceof target) {
365
- return rawValue;
366
- }
367
- if (!isPlainObject(rawValue)) {
368
- return rawValue;
369
- }
370
- return createNestedDtoInstance(target, rawValue, context);
371
- }
372
- function getDtoBindingMap(target) {
373
- return new Map(getDtoBindingSchema(target).map(entry => [entry.propertyKey, entry.metadata]));
374
- }
375
- function applyBindingValues(instance, rawValue, keys, bindingMap) {
376
- for (const propertyKey of keys) {
377
- const sourceKey = bindingMap.get(propertyKey)?.key;
378
- if (!sourceKey) continue;
379
- instance[propertyKey] = rawValue[sourceKey];
380
- }
381
- }
382
- function transformNestedValue(value, target, context) {
383
- return value === undefined || value === null ? value : materializeNestedDtoValue(target, value, context);
384
- }
385
- function transformNestedEachValue(value, target, context) {
386
- if (Array.isArray(value)) {
387
- return value.map(item => transformNestedValue(item, target, context));
388
- }
389
- if (value instanceof Set) {
390
- return new Set(Array.from(value.values(), item => transformNestedValue(item, target, context)));
391
- }
392
- if (value instanceof Map) {
393
- return new Map(Array.from(value.entries(), ([key, item]) => [key, transformNestedValue(item, target, context)]));
394
- }
395
- return transformNestedValue(value, target, context);
396
- }
397
10
  function describeValidator(rule, field) {
398
11
  const handler = getRuleHandler(rule);
399
12
  return {
@@ -401,118 +14,11 @@ function describeValidator(rule, field) {
401
14
  message: rule.message ?? handler.describe(field, rule)
402
15
  };
403
16
  }
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
- }
414
- switch (rule.validator) {
415
- case 'alpha':
416
- return validator.isAlpha(value);
417
- case 'alphanumeric':
418
- return validator.isAlphanumeric(value);
419
- case 'ascii':
420
- return validator.isAscii(value);
421
- case 'base64':
422
- return validator.isBase64(value);
423
- case 'booleanString':
424
- return validator.isBoolean(value);
425
- case 'currency':
426
- return validator.isCurrency(value, rule.args?.[0]);
427
- case 'dataURI':
428
- return validator.isDataURI(value);
429
- case 'dateString':
430
- return validator.isISO8601(value);
431
- case 'decimal':
432
- return validator.isDecimal(value);
433
- case 'email':
434
- return validator.isEmail(value, rule.args?.[0]);
435
- case 'fqdn':
436
- return validator.isFQDN(value, rule.args?.[0]);
437
- case 'hexColor':
438
- return validator.isHexColor(value);
439
- case 'hexadecimal':
440
- return validator.isHexadecimal(value);
441
- case 'ip':
442
- return validator.isIP(value, rule.args?.[0]);
443
- case 'isbn':
444
- return validator.isISBN(value, rule.args?.[0]);
445
- case 'issn':
446
- return validator.isISSN(value);
447
- case 'json':
448
- return validator.isJSON(value);
449
- case 'jwt':
450
- return validator.isJWT(value);
451
- case 'locale':
452
- return validator.isLocale(value);
453
- case 'lowercase':
454
- return validator.isLowercase(value);
455
- case 'magnetURI':
456
- return validator.isMagnetURI(value);
457
- case 'matches':
458
- return validator.matches(value, rule.args?.[0], rule.args?.[1]);
459
- case 'mimeType':
460
- return validator.isMimeType(value);
461
- case 'mobilePhone':
462
- return validator.isMobilePhone(value, rule.args?.[0]);
463
- case 'mongoId':
464
- return validator.isMongoId(value);
465
- case 'numberString':
466
- return validator.isNumeric(value);
467
- case 'port':
468
- return validator.isPort(value);
469
- case 'postalCode':
470
- return validator.isPostalCode(value, rule.args?.[0] ?? 'any');
471
- case 'rgbColor':
472
- return validator.isRgbColor(value, rule.args?.[0]);
473
- case 'rfc3339':
474
- return validator.isRFC3339(value);
475
- case 'semVer':
476
- return validator.isSemVer(value);
477
- case 'uppercase':
478
- return validator.isUppercase(value);
479
- case 'url':
480
- return validator.isURL(value, rule.args?.[0]);
481
- case 'uuid':
482
- return validator.isUUID(value, rule.args?.[0]);
483
- case 'iso8601':
484
- return validator.isISO8601(value);
485
- case 'latLong':
486
- return validator.isLatLong(value);
487
- default:
488
- return false;
489
- }
490
- }
491
- function buildIssue(fallback, field, source) {
492
- return {
493
- code: fallback.code,
494
- field,
495
- message: fallback.message,
496
- source
497
- };
498
- }
499
- function buildInvalidRootIssue() {
500
- return {
501
- code: 'INVALID_DTO',
502
- message: 'DTO root value must be a plain object.'
503
- };
504
- }
505
- function assertValidRootValue(value, target) {
506
- if (value instanceof target || isPlainObject(value)) {
507
- return;
508
- }
509
- throw new DtoValidationError('Validation failed.', [buildInvalidRootIssue()]);
510
- }
511
17
  function getRuleValues(value) {
512
18
  return getIterableValues(value) ?? [value];
513
19
  }
514
20
  function shouldSkipRuleForMissingValue(rule, value) {
515
- return (value === undefined || value === null) && rule.kind !== 'defined' && rule.kind !== 'notEmpty' && rule.kind !== 'empty';
21
+ return (value === undefined || value === null) && rule.kind !== 'defined' && rule.kind !== 'empty';
516
22
  }
517
23
  async function evaluateCustomRule(rule, value, dto, propertyKey, fieldPath, source, fallback) {
518
24
  if (!rule.each) {
@@ -541,12 +47,14 @@ function runRulePredicate(rule, value) {
541
47
  return getRuleHandler(rule).validate(rule, value);
542
48
  }
543
49
  async function validateNestedRule(rule, value, fieldPath, inheritedSource, context) {
544
- const values = rule.each ? getIterableValues(value) ?? [value] : [value];
50
+ const collectionValues = getIterableValues(value);
51
+ const values = collectionValues ?? [value];
52
+ const useIndexedPath = rule.each === true || collectionValues !== undefined;
545
53
  const issues = [];
546
54
  const resolvedDto = resolveNestedDto(rule.dto);
547
55
  for (const [index, entry] of values.entries()) {
548
56
  if (entry === undefined || entry === null) continue;
549
- const nestedPath = rule.each ? `${fieldPath}[${String(index)}]` : fieldPath;
57
+ const nestedPath = useIndexedPath ? `${fieldPath}[${String(index)}]` : fieldPath;
550
58
  const trackedEntry = typeof entry === 'object' && entry !== null ? entry : undefined;
551
59
  if (!(entry instanceof resolvedDto) && !isPlainObject(entry)) {
552
60
  issues.push(buildIssue(describeValidator(rule, nestedPath), nestedPath, inheritedSource));
@@ -556,7 +64,7 @@ async function validateNestedRule(rule, value, fieldPath, inheritedSource, conte
556
64
  issues.push(buildIssue(describeValidator(rule, nestedPath), nestedPath, inheritedSource));
557
65
  continue;
558
66
  }
559
- const nestedDto = createNestedDtoInstance(resolvedDto, entry, context);
67
+ const nestedDto = createNestedDtoInstance(resolvedDto, entry, context, nestedPath);
560
68
  const shouldTrackEntry = trackedEntry && !(entry instanceof resolvedDto) ? enterTraversal(trackedEntry, context) : false;
561
69
  try {
562
70
  issues.push(...(await collectValidationIssuesInternal(resolvedDto, nestedDto, {
@@ -594,10 +102,10 @@ async function evaluateRule(rule, value, dto, propertyKey, fieldPath, source, co
594
102
  return [];
595
103
  }
596
104
  async function applyPropertyRules(rules, value, dto, propertyKey, fieldPath, source, context) {
597
- const conditionallySkip = await shouldConditionallySkip(rules, dto, value);
598
105
  if (rules.some(rule => rule.kind === 'optional') && (value === undefined || value === null)) {
599
106
  return [];
600
107
  }
108
+ const conditionallySkip = await shouldConditionallySkip(rules, dto, value);
601
109
  const issues = [];
602
110
  for (const rule of rules) {
603
111
  if (rule.kind === 'validateIf' || rule.kind === 'optional') continue;
@@ -659,12 +167,19 @@ export class DefaultValidator {
659
167
  if (issues.length === 0) return;
660
168
  throw new DtoValidationError('Validation failed.', issues);
661
169
  }
662
- async materialize(value, target) {
170
+ async materialize(value, target, options = {}) {
663
171
  assertValidRootValue(value, target);
664
- const instance = createNestedDtoInstance(target, value, {
665
- active: new WeakSet()
666
- });
667
- const issues = await collectValidationIssues(target, instance);
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);
668
183
  if (issues.length > 0) {
669
184
  throw new DtoValidationError('Validation failed.', issues);
670
185
  }
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "decorators",
10
10
  "schema"
11
11
  ],
12
- "version": "1.0.5",
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": ">=20.0.0"
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": "^1.0.3"
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": "^3.2.4",
51
+ "vitest": "^4.1.11",
52
52
  "zod": "^4.1.11"
53
53
  },
54
54
  "scripts": {