@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.
- package/LICENSE +21 -0
- package/README.ko.md +138 -0
- package/README.md +118 -0
- package/dist/decorators.d.ts +165 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +443 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/mapped-types.d.ts +99 -0
- package/dist/mapped-types.d.ts.map +1 -0
- package/dist/mapped-types.js +207 -0
- package/dist/standard-schema.d.ts +6 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +75 -0
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/validation.d.ts +7 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +653 -0
- package/package.json +61 -0
|
@@ -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
|
+
}
|