@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.
- package/README.ko.md +47 -1
- package/README.md +48 -1
- package/dist/decorators.d.ts +71 -69
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +73 -133
- package/dist/internal/decorator-factories.d.ts +12 -0
- package/dist/internal/decorator-factories.d.ts.map +1 -0
- package/dist/internal/decorator-factories.js +32 -0
- package/dist/internal/decorator-metadata.d.ts +7 -0
- package/dist/internal/decorator-metadata.d.ts.map +1 -0
- package/dist/internal/decorator-metadata.js +38 -0
- 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 +17 -0
- package/dist/internal/dto-metadata-cache.d.ts.map +1 -0
- package/dist/internal/dto-metadata-cache.js +49 -0
- 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 +30 -0
- package/dist/internal/object-utils.d.ts.map +1 -0
- package/dist/internal/object-utils.js +53 -0
- package/dist/internal/rule-handlers.d.ts +19 -0
- package/dist/internal/rule-handlers.d.ts.map +1 -0
- package/dist/internal/rule-handlers.js +209 -0
- 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/internal/validator-js-adapter.d.ts +5 -0
- package/dist/internal/validator-js-adapter.d.ts.map +1 -0
- package/dist/internal/validator-js-adapter.js +88 -0
- package/dist/mapped-types.d.ts +1 -1
- package/dist/mapped-types.d.ts.map +1 -1
- package/dist/mapped-types.js +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/types.d.ts +18 -3
- 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 +24 -509
- package/package.json +4 -4
package/dist/validation.js
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
|
|
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 !== '
|
|
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
|
|
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 =
|
|
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
|
|
665
|
-
active: new WeakSet()
|
|
666
|
-
|
|
667
|
-
|
|
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": "
|
|
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": {
|