@fluojs/validation 1.0.0-beta.1

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