@fluojs/validation 1.0.4 → 1.0.6

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.
@@ -1,15 +1,7 @@
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 { getCachedDtoMetadata, resolveNestedDto } from './internal/dto-metadata-cache.js';
3
+ import { assignSafeOwnEnumerableProperties, getIterableValues, isPlainObject } from './internal/object-utils.js';
4
+ import { getRuleHandler } from './internal/rule-handlers.js';
13
5
  function toFieldName(propertyKey) {
14
6
  return typeof propertyKey === 'string' ? propertyKey : String(propertyKey);
15
7
  }
@@ -38,34 +30,6 @@ function normalizeResult(result, field, source, fallback) {
38
30
  }
39
31
  return [normalizeIssue(result, field, source)];
40
32
  }
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
33
  function joinFieldPath(parent, child) {
70
34
  if (!child) return parent;
71
35
  return child.startsWith('[') ? `${parent}${child}` : `${parent}.${child}`;
@@ -93,246 +57,6 @@ function exitTraversal(value, context) {
93
57
  }
94
58
  context.active.delete(value);
95
59
  }
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
60
  function createNestedDtoInstance(target, rawValue, context) {
337
61
  if (rawValue instanceof target) {
338
62
  return rawValue;
@@ -353,7 +77,7 @@ function createNestedDtoInstance(target, rawValue, context) {
353
77
  if (currentValue === undefined || currentValue === null) {
354
78
  continue;
355
79
  }
356
- instance[nestedEntry.propertyKey] = nestedEntry.each ? transformNestedEachValue(currentValue, nestedEntry.target, context) : transformNestedValue(currentValue, nestedEntry.target, context);
80
+ instance[nestedEntry.propertyKey] = transformNestedCollectionValue(currentValue, nestedEntry.target, context);
357
81
  }
358
82
  return instance;
359
83
  } finally {
@@ -369,9 +93,6 @@ function materializeNestedDtoValue(target, rawValue, context) {
369
93
  }
370
94
  return createNestedDtoInstance(target, rawValue, context);
371
95
  }
372
- function getDtoBindingMap(target) {
373
- return new Map(getDtoBindingSchema(target).map(entry => [entry.propertyKey, entry.metadata]));
374
- }
375
96
  function applyBindingValues(instance, rawValue, keys, bindingMap) {
376
97
  for (const propertyKey of keys) {
377
98
  const sourceKey = bindingMap.get(propertyKey)?.key;
@@ -382,7 +103,7 @@ function applyBindingValues(instance, rawValue, keys, bindingMap) {
382
103
  function transformNestedValue(value, target, context) {
383
104
  return value === undefined || value === null ? value : materializeNestedDtoValue(target, value, context);
384
105
  }
385
- function transformNestedEachValue(value, target, context) {
106
+ function transformNestedCollectionValue(value, target, context) {
386
107
  if (Array.isArray(value)) {
387
108
  return value.map(item => transformNestedValue(item, target, context));
388
109
  }
@@ -401,93 +122,6 @@ function describeValidator(rule, field) {
401
122
  message: rule.message ?? handler.describe(field, rule)
402
123
  };
403
124
  }
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
125
  function buildIssue(fallback, field, source) {
492
126
  return {
493
127
  code: fallback.code,
@@ -541,12 +175,14 @@ function runRulePredicate(rule, value) {
541
175
  return getRuleHandler(rule).validate(rule, value);
542
176
  }
543
177
  async function validateNestedRule(rule, value, fieldPath, inheritedSource, context) {
544
- const values = rule.each ? getIterableValues(value) ?? [value] : [value];
178
+ const collectionValues = getIterableValues(value);
179
+ const values = collectionValues ?? [value];
180
+ const useIndexedPath = rule.each === true || collectionValues !== undefined;
545
181
  const issues = [];
546
182
  const resolvedDto = resolveNestedDto(rule.dto);
547
183
  for (const [index, entry] of values.entries()) {
548
184
  if (entry === undefined || entry === null) continue;
549
- const nestedPath = rule.each ? `${fieldPath}[${String(index)}]` : fieldPath;
185
+ const nestedPath = useIndexedPath ? `${fieldPath}[${String(index)}]` : fieldPath;
550
186
  const trackedEntry = typeof entry === 'object' && entry !== null ? entry : undefined;
551
187
  if (!(entry instanceof resolvedDto) && !isPlainObject(entry)) {
552
188
  issues.push(buildIssue(describeValidator(rule, nestedPath), nestedPath, inheritedSource));
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "decorators",
10
10
  "schema"
11
11
  ],
12
- "version": "1.0.4",
12
+ "version": "1.0.6",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "dependencies": {
43
43
  "@standard-schema/spec": "^1.1.0",
44
44
  "validator": "^13.15.26",
45
- "@fluojs/core": "^1.0.3"
45
+ "@fluojs/core": "^1.1.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/validator": "^13.15.10",