koatty_validation 1.6.0 → 1.6.3
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/.turbo/turbo-build.log +120 -0
- package/.turbo/turbo-test.log +8 -0
- package/CHANGELOG.md +31 -26
- package/README.md +106 -3
- package/dist/README.md +106 -3
- package/dist/index.d.ts +94 -60
- package/dist/index.js +337 -284
- package/dist/index.mjs +338 -286
- package/dist/package.json +10 -14
- package/examples/validated-async-sync-example.ts +213 -0
- package/package.json +21 -26
package/dist/index.mjs
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @Author: richen
|
|
3
|
-
* @Date:
|
|
3
|
+
* @Date: 2026-01-28 12:40:10
|
|
4
4
|
* @License: BSD (3-Clause)
|
|
5
5
|
* @Copyright (c) - <richenlin(at)gmail.com>
|
|
6
6
|
* @HomePage: https://koatty.org/
|
|
7
7
|
*/
|
|
8
8
|
import * as helper from 'koatty_lib';
|
|
9
9
|
import 'reflect-metadata';
|
|
10
|
-
import { getOriginMetadata } from 'koatty_container';
|
|
10
|
+
import { getOriginMetadata, IOCContainer } from 'koatty_container';
|
|
11
11
|
import { LRUCache } from 'lru-cache';
|
|
12
12
|
import { validate, isNotIn, isIn, contains, notEquals, equals, isHash, isURL, isPhoneNumber, isIP, isEmail, registerDecorator } from 'class-validator';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* koatty_validation
|
|
15
|
+
* koatty_validation type definitions
|
|
16
16
|
* @author richen
|
|
17
17
|
* @copyright Copyright (c) - <richenlin(at)gmail.com>
|
|
18
18
|
* @license MIT
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Parameter type key constant
|
|
22
22
|
*/
|
|
23
23
|
const PARAM_TYPE_KEY = 'PARAM_TYPE_KEY';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Performance cache module - Provides multi-level caching and performance monitoring
|
|
27
27
|
* @author richen
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* Metadata cache
|
|
31
31
|
*/
|
|
32
32
|
class MetadataCache {
|
|
33
33
|
constructor() {
|
|
@@ -40,7 +40,7 @@ class MetadataCache {
|
|
|
40
40
|
return MetadataCache.instance;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
43
|
+
* Get metadata cache for a class
|
|
44
44
|
*/
|
|
45
45
|
getClassCache(target) {
|
|
46
46
|
if (!this.cache.has(target)) {
|
|
@@ -49,28 +49,28 @@ class MetadataCache {
|
|
|
49
49
|
return this.cache.get(target);
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* Cache metadata
|
|
53
53
|
*/
|
|
54
54
|
setMetadata(target, key, value) {
|
|
55
55
|
const classCache = this.getClassCache(target);
|
|
56
56
|
classCache.set(key, value);
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Get cached metadata
|
|
60
60
|
*/
|
|
61
61
|
getMetadata(target, key) {
|
|
62
62
|
const classCache = this.getClassCache(target);
|
|
63
63
|
return classCache.get(key);
|
|
64
64
|
}
|
|
65
65
|
/**
|
|
66
|
-
*
|
|
66
|
+
* Check if metadata is cached
|
|
67
67
|
*/
|
|
68
68
|
hasMetadata(target, key) {
|
|
69
69
|
const classCache = this.getClassCache(target);
|
|
70
70
|
return classCache.has(key);
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Clear cache for a specific class
|
|
74
74
|
*/
|
|
75
75
|
clearClassCache(target) {
|
|
76
76
|
if (this.cache.has(target)) {
|
|
@@ -79,7 +79,7 @@ class MetadataCache {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
*
|
|
82
|
+
* Validation result cache
|
|
83
83
|
*/
|
|
84
84
|
class ValidationCache {
|
|
85
85
|
constructor(options) {
|
|
@@ -87,7 +87,7 @@ class ValidationCache {
|
|
|
87
87
|
this.misses = 0;
|
|
88
88
|
this.cache = new LRUCache({
|
|
89
89
|
max: (options === null || options === void 0 ? void 0 : options.max) || 5000,
|
|
90
|
-
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 10, // 10
|
|
90
|
+
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 10, // 10 minutes
|
|
91
91
|
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
92
92
|
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
93
93
|
});
|
|
@@ -205,7 +205,7 @@ class RegexCache {
|
|
|
205
205
|
constructor(options) {
|
|
206
206
|
this.cache = new LRUCache({
|
|
207
207
|
max: (options === null || options === void 0 ? void 0 : options.max) || 200,
|
|
208
|
-
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 30, // 30
|
|
208
|
+
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 30, // 30 minutes
|
|
209
209
|
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
210
210
|
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
211
211
|
});
|
|
@@ -389,7 +389,7 @@ function cached(validator, ttl) {
|
|
|
389
389
|
try {
|
|
390
390
|
const result = originalMethod.apply(this, args);
|
|
391
391
|
validationCache.set(validator, value, result, ...additionalArgs);
|
|
392
|
-
//
|
|
392
|
+
// If TTL is specified, set expiration time
|
|
393
393
|
if (ttl && ttl > 0) {
|
|
394
394
|
validationCache.setTTL(validator, value, ttl, ...additionalArgs);
|
|
395
395
|
}
|
|
@@ -799,6 +799,209 @@ function plateNumber(value) {
|
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Improved error handling mechanism
|
|
804
|
+
* @author richen
|
|
805
|
+
*/
|
|
806
|
+
/**
|
|
807
|
+
* Error message internationalization
|
|
808
|
+
*/
|
|
809
|
+
const ERROR_MESSAGES = {
|
|
810
|
+
zh: {
|
|
811
|
+
// Chinese localization validation
|
|
812
|
+
IsCnName: '必须是有效的中文姓名',
|
|
813
|
+
IsIdNumber: '必须是有效的身份证号码',
|
|
814
|
+
IsZipCode: '必须是有效的邮政编码',
|
|
815
|
+
IsMobile: '必须是有效的手机号码',
|
|
816
|
+
IsPlateNumber: '必须是有效的车牌号码',
|
|
817
|
+
// Basic validation
|
|
818
|
+
IsNotEmpty: '不能为空',
|
|
819
|
+
IsDate: '必须是有效的日期',
|
|
820
|
+
IsEmail: '必须是有效的邮箱地址',
|
|
821
|
+
IsIP: '必须是有效的IP地址',
|
|
822
|
+
IsPhoneNumber: '必须是有效的电话号码',
|
|
823
|
+
IsUrl: '必须是有效的URL地址',
|
|
824
|
+
IsHash: '必须是有效的哈希值',
|
|
825
|
+
// Comparison validation
|
|
826
|
+
Equals: '必须等于 {comparison}',
|
|
827
|
+
NotEquals: '不能等于 {comparison}',
|
|
828
|
+
Contains: '必须包含 {seed}',
|
|
829
|
+
IsIn: '必须是以下值之一: {possibleValues}',
|
|
830
|
+
IsNotIn: '不能是以下值之一: {possibleValues}',
|
|
831
|
+
Gt: '必须大于 {min}',
|
|
832
|
+
Gte: '必须大于或等于 {min}',
|
|
833
|
+
Lt: '必须小于 {max}',
|
|
834
|
+
Lte: '必须小于或等于 {max}',
|
|
835
|
+
// Common errors
|
|
836
|
+
invalidParameter: '参数 {field} 无效',
|
|
837
|
+
validationFailed: '验证失败',
|
|
838
|
+
},
|
|
839
|
+
en: {
|
|
840
|
+
// Chinese localization validators
|
|
841
|
+
IsCnName: 'must be a valid Chinese name',
|
|
842
|
+
IsIdNumber: 'must be a valid ID number',
|
|
843
|
+
IsZipCode: 'must be a valid zip code',
|
|
844
|
+
IsMobile: 'must be a valid mobile number',
|
|
845
|
+
IsPlateNumber: 'must be a valid plate number',
|
|
846
|
+
// Basic validators
|
|
847
|
+
IsNotEmpty: 'should not be empty',
|
|
848
|
+
IsDate: 'must be a valid date',
|
|
849
|
+
IsEmail: 'must be a valid email',
|
|
850
|
+
IsIP: 'must be a valid IP address',
|
|
851
|
+
IsPhoneNumber: 'must be a valid phone number',
|
|
852
|
+
IsUrl: 'must be a valid URL',
|
|
853
|
+
IsHash: 'must be a valid hash',
|
|
854
|
+
// Comparison validators
|
|
855
|
+
Equals: 'must equal to {comparison}',
|
|
856
|
+
NotEquals: 'should not equal to {comparison}',
|
|
857
|
+
Contains: 'must contain {seed}',
|
|
858
|
+
IsIn: 'must be one of the following values: {possibleValues}',
|
|
859
|
+
IsNotIn: 'should not be one of the following values: {possibleValues}',
|
|
860
|
+
Gt: 'must be greater than {min}',
|
|
861
|
+
Gte: 'must be greater than or equal to {min}',
|
|
862
|
+
Lt: 'must be less than {max}',
|
|
863
|
+
Lte: 'must be less than or equal to {max}',
|
|
864
|
+
// Common errors
|
|
865
|
+
invalidParameter: 'invalid parameter {field}',
|
|
866
|
+
validationFailed: 'validation failed',
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
/**
|
|
870
|
+
* Enhanced validation error class
|
|
871
|
+
*/
|
|
872
|
+
class KoattyValidationError extends Error {
|
|
873
|
+
constructor(errors, message) {
|
|
874
|
+
const errorMessage = message || 'Validation failed';
|
|
875
|
+
super(errorMessage);
|
|
876
|
+
this.name = 'KoattyValidationError';
|
|
877
|
+
this.errors = errors;
|
|
878
|
+
this.statusCode = 400;
|
|
879
|
+
this.timestamp = new Date();
|
|
880
|
+
// Ensure correct prototype chain
|
|
881
|
+
Object.setPrototypeOf(this, KoattyValidationError.prototype);
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get the first error message
|
|
885
|
+
*/
|
|
886
|
+
getFirstError() {
|
|
887
|
+
return this.errors[0];
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Get errors for a specific field
|
|
891
|
+
*/
|
|
892
|
+
getFieldErrors(field) {
|
|
893
|
+
return this.errors.filter(error => error.field === field);
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Convert to JSON format
|
|
897
|
+
*/
|
|
898
|
+
toJSON() {
|
|
899
|
+
return {
|
|
900
|
+
name: this.name,
|
|
901
|
+
message: this.message,
|
|
902
|
+
statusCode: this.statusCode,
|
|
903
|
+
timestamp: this.timestamp,
|
|
904
|
+
errors: this.errors
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Error message formatter
|
|
910
|
+
*/
|
|
911
|
+
class ErrorMessageFormatter {
|
|
912
|
+
constructor(language = 'zh') {
|
|
913
|
+
this.language = 'zh';
|
|
914
|
+
this.language = language;
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Set language
|
|
918
|
+
*/
|
|
919
|
+
setLanguage(language) {
|
|
920
|
+
this.language = language;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Format error message
|
|
924
|
+
*/
|
|
925
|
+
formatMessage(constraint, field, value, context) {
|
|
926
|
+
const messages = ERROR_MESSAGES[this.language];
|
|
927
|
+
let template = messages[constraint] || messages.invalidParameter;
|
|
928
|
+
// Replace placeholders
|
|
929
|
+
template = template.replace('{field}', field);
|
|
930
|
+
// Prioritize values from context, then use passed value
|
|
931
|
+
if (context) {
|
|
932
|
+
Object.entries(context).forEach(([key, val]) => {
|
|
933
|
+
template = template.replace(`{${key}}`, this.formatValue(val));
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
// If there's still a {value} placeholder and value was passed, replace it
|
|
937
|
+
if (value !== undefined && template.includes('{value}')) {
|
|
938
|
+
template = template.replace('{value}', this.formatValue(value));
|
|
939
|
+
}
|
|
940
|
+
return template;
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Format value for message display
|
|
944
|
+
* @private
|
|
945
|
+
*/
|
|
946
|
+
formatValue(value) {
|
|
947
|
+
if (value === null)
|
|
948
|
+
return 'null';
|
|
949
|
+
if (value === undefined)
|
|
950
|
+
return 'undefined';
|
|
951
|
+
if (typeof value === 'number')
|
|
952
|
+
return String(value);
|
|
953
|
+
if (typeof value === 'string')
|
|
954
|
+
return `"${value}"`;
|
|
955
|
+
if (Array.isArray(value))
|
|
956
|
+
return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
|
|
957
|
+
if (typeof value === 'object') {
|
|
958
|
+
try {
|
|
959
|
+
return JSON.stringify(value);
|
|
960
|
+
}
|
|
961
|
+
catch {
|
|
962
|
+
// Handle circular references
|
|
963
|
+
return '[Circular Reference]';
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
return String(value);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Global error message formatter instance
|
|
971
|
+
*/
|
|
972
|
+
const errorFormatter = new ErrorMessageFormatter();
|
|
973
|
+
/**
|
|
974
|
+
* Set global language
|
|
975
|
+
*/
|
|
976
|
+
function setValidationLanguage(language) {
|
|
977
|
+
errorFormatter.setLanguage(language);
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Create validation error
|
|
981
|
+
*/
|
|
982
|
+
function createValidationError(field, value, constraint, customMessage, context) {
|
|
983
|
+
const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
|
|
984
|
+
return {
|
|
985
|
+
field,
|
|
986
|
+
value,
|
|
987
|
+
constraint,
|
|
988
|
+
message,
|
|
989
|
+
context
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Create validation errors in batch
|
|
994
|
+
*/
|
|
995
|
+
function createValidationErrors(errors, separator = '; ') {
|
|
996
|
+
const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
|
|
997
|
+
// Generate combined message from all errors
|
|
998
|
+
const combinedMessage = validationErrors
|
|
999
|
+
.map(err => err.message)
|
|
1000
|
+
.filter(msg => msg && msg.trim())
|
|
1001
|
+
.join(separator) || 'Validation failed';
|
|
1002
|
+
return new KoattyValidationError(validationErrors, combinedMessage);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
802
1005
|
/*
|
|
803
1006
|
* @Description:
|
|
804
1007
|
* @Usage:
|
|
@@ -858,10 +1061,11 @@ class ValidateClass {
|
|
|
858
1061
|
* @param {*} Clazz
|
|
859
1062
|
* @param {*} data
|
|
860
1063
|
* @param {boolean} [convert=false] auto convert parameters type
|
|
1064
|
+
* @param {ValidationOptions} [options] validation options (returnAllErrors, errorSeparator)
|
|
861
1065
|
* @returns {Promise<any>}
|
|
862
1066
|
* @memberof ValidateClass
|
|
863
1067
|
*/
|
|
864
|
-
async valid(Clazz, data, convert = false) {
|
|
1068
|
+
async valid(Clazz, data, convert = false, options) {
|
|
865
1069
|
let obj = {};
|
|
866
1070
|
if (data instanceof Clazz) {
|
|
867
1071
|
obj = data;
|
|
@@ -877,7 +1081,21 @@ class ValidateClass {
|
|
|
877
1081
|
errors = await validate(obj, { skipMissingProperties: true });
|
|
878
1082
|
}
|
|
879
1083
|
if (errors.length > 0) {
|
|
880
|
-
|
|
1084
|
+
// Check if user wants all errors or just the first one
|
|
1085
|
+
if (options === null || options === void 0 ? void 0 : options.returnAllErrors) {
|
|
1086
|
+
// Throw KoattyValidationError with all error details
|
|
1087
|
+
throw createValidationErrors(errors.map(e => ({
|
|
1088
|
+
field: e.property,
|
|
1089
|
+
value: e.value,
|
|
1090
|
+
constraint: Object.keys(e.constraints || {})[0] || 'unknown',
|
|
1091
|
+
message: Object.values(e.constraints || {})[0] || 'Validation failed',
|
|
1092
|
+
context: e.constraints
|
|
1093
|
+
})), options.errorSeparator || '; ');
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
// Default behavior (backward compatible): return only first error
|
|
1097
|
+
throw new Error(Object.values(errors[0].constraints)[0]);
|
|
1098
|
+
}
|
|
881
1099
|
}
|
|
882
1100
|
return obj;
|
|
883
1101
|
}
|
|
@@ -1114,24 +1332,24 @@ const FunctionValidator = {
|
|
|
1114
1332
|
};
|
|
1115
1333
|
|
|
1116
1334
|
/**
|
|
1117
|
-
*
|
|
1335
|
+
* Decorator Factory - Eliminate decorator code duplication
|
|
1118
1336
|
* @author richen
|
|
1119
1337
|
*/
|
|
1120
1338
|
/**
|
|
1121
|
-
*
|
|
1122
|
-
* @param options
|
|
1123
|
-
* @returns
|
|
1339
|
+
* Factory function to create validation decorators
|
|
1340
|
+
* @param options Decorator configuration options
|
|
1341
|
+
* @returns Decorator factory function
|
|
1124
1342
|
*/
|
|
1125
1343
|
function createValidationDecorator(options) {
|
|
1126
1344
|
const { name, validator, defaultMessage, requiresValue = false } = options;
|
|
1127
1345
|
return function decoratorFactory(...args) {
|
|
1128
|
-
//
|
|
1346
|
+
// Handle parameters: last parameter is ValidationOptions, previous ones are validator function parameters
|
|
1129
1347
|
const validationOptions = args[args.length - 1];
|
|
1130
1348
|
const validatorArgs = requiresValue ? args.slice(0, -1) : [];
|
|
1131
1349
|
return function propertyDecorator(object, propertyName) {
|
|
1132
|
-
//
|
|
1350
|
+
// Set property as exportable
|
|
1133
1351
|
setExpose(object, propertyName);
|
|
1134
|
-
//
|
|
1352
|
+
// Register validation decorator
|
|
1135
1353
|
registerDecorator({
|
|
1136
1354
|
name,
|
|
1137
1355
|
target: object.constructor,
|
|
@@ -1149,8 +1367,9 @@ function createValidationDecorator(options) {
|
|
|
1149
1367
|
},
|
|
1150
1368
|
defaultMessage(validationArguments) {
|
|
1151
1369
|
const property = validationArguments.property;
|
|
1152
|
-
|
|
1153
|
-
|
|
1370
|
+
const customMessage = (validationOptions === null || validationOptions === void 0 ? void 0 : validationOptions.message) || defaultMessage;
|
|
1371
|
+
return customMessage
|
|
1372
|
+
? customMessage.replace('$property', property)
|
|
1154
1373
|
: `Invalid value for ${property}`;
|
|
1155
1374
|
}
|
|
1156
1375
|
}
|
|
@@ -1159,11 +1378,11 @@ function createValidationDecorator(options) {
|
|
|
1159
1378
|
};
|
|
1160
1379
|
}
|
|
1161
1380
|
/**
|
|
1162
|
-
*
|
|
1163
|
-
* @param name
|
|
1164
|
-
* @param validator
|
|
1165
|
-
* @param defaultMessage
|
|
1166
|
-
* @returns
|
|
1381
|
+
* Create simple validation decorator (no additional parameters required)
|
|
1382
|
+
* @param name Decorator name
|
|
1383
|
+
* @param validator Validation function
|
|
1384
|
+
* @param defaultMessage Default error message
|
|
1385
|
+
* @returns Decorator function
|
|
1167
1386
|
*/
|
|
1168
1387
|
function createSimpleDecorator(name, validator, defaultMessage) {
|
|
1169
1388
|
return createValidationDecorator({
|
|
@@ -1174,11 +1393,11 @@ function createSimpleDecorator(name, validator, defaultMessage) {
|
|
|
1174
1393
|
});
|
|
1175
1394
|
}
|
|
1176
1395
|
/**
|
|
1177
|
-
*
|
|
1178
|
-
* @param name
|
|
1179
|
-
* @param validator
|
|
1180
|
-
* @param defaultMessage
|
|
1181
|
-
* @returns
|
|
1396
|
+
* Create parameterized validation decorator
|
|
1397
|
+
* @param name Decorator name
|
|
1398
|
+
* @param validator Validation function
|
|
1399
|
+
* @param defaultMessage Default error message
|
|
1400
|
+
* @returns Decorator factory function
|
|
1182
1401
|
*/
|
|
1183
1402
|
function createParameterizedDecorator(name, validator, defaultMessage) {
|
|
1184
1403
|
return createValidationDecorator({
|
|
@@ -1190,217 +1409,19 @@ function createParameterizedDecorator(name, validator, defaultMessage) {
|
|
|
1190
1409
|
}
|
|
1191
1410
|
|
|
1192
1411
|
/**
|
|
1193
|
-
*
|
|
1194
|
-
* @author richen
|
|
1195
|
-
*/
|
|
1196
|
-
/**
|
|
1197
|
-
* 错误信息国际化
|
|
1198
|
-
*/
|
|
1199
|
-
const ERROR_MESSAGES = {
|
|
1200
|
-
zh: {
|
|
1201
|
-
// 中国本土化验证
|
|
1202
|
-
IsCnName: '必须是有效的中文姓名',
|
|
1203
|
-
IsIdNumber: '必须是有效的身份证号码',
|
|
1204
|
-
IsZipCode: '必须是有效的邮政编码',
|
|
1205
|
-
IsMobile: '必须是有效的手机号码',
|
|
1206
|
-
IsPlateNumber: '必须是有效的车牌号码',
|
|
1207
|
-
// 基础验证
|
|
1208
|
-
IsNotEmpty: '不能为空',
|
|
1209
|
-
IsDate: '必须是有效的日期',
|
|
1210
|
-
IsEmail: '必须是有效的邮箱地址',
|
|
1211
|
-
IsIP: '必须是有效的IP地址',
|
|
1212
|
-
IsPhoneNumber: '必须是有效的电话号码',
|
|
1213
|
-
IsUrl: '必须是有效的URL地址',
|
|
1214
|
-
IsHash: '必须是有效的哈希值',
|
|
1215
|
-
// 比较验证
|
|
1216
|
-
Equals: '必须等于 {comparison}',
|
|
1217
|
-
NotEquals: '不能等于 {comparison}',
|
|
1218
|
-
Contains: '必须包含 {seed}',
|
|
1219
|
-
IsIn: '必须是以下值之一: {possibleValues}',
|
|
1220
|
-
IsNotIn: '不能是以下值之一: {possibleValues}',
|
|
1221
|
-
Gt: '必须大于 {min}',
|
|
1222
|
-
Gte: '必须大于或等于 {min}',
|
|
1223
|
-
Lt: '必须小于 {max}',
|
|
1224
|
-
Lte: '必须小于或等于 {max}',
|
|
1225
|
-
// 通用错误
|
|
1226
|
-
invalidParameter: '参数 {field} 无效',
|
|
1227
|
-
validationFailed: '验证失败',
|
|
1228
|
-
},
|
|
1229
|
-
en: {
|
|
1230
|
-
// Chinese localization validators
|
|
1231
|
-
IsCnName: 'must be a valid Chinese name',
|
|
1232
|
-
IsIdNumber: 'must be a valid ID number',
|
|
1233
|
-
IsZipCode: 'must be a valid zip code',
|
|
1234
|
-
IsMobile: 'must be a valid mobile number',
|
|
1235
|
-
IsPlateNumber: 'must be a valid plate number',
|
|
1236
|
-
// Basic validators
|
|
1237
|
-
IsNotEmpty: 'should not be empty',
|
|
1238
|
-
IsDate: 'must be a valid date',
|
|
1239
|
-
IsEmail: 'must be a valid email',
|
|
1240
|
-
IsIP: 'must be a valid IP address',
|
|
1241
|
-
IsPhoneNumber: 'must be a valid phone number',
|
|
1242
|
-
IsUrl: 'must be a valid URL',
|
|
1243
|
-
IsHash: 'must be a valid hash',
|
|
1244
|
-
// Comparison validators
|
|
1245
|
-
Equals: 'must equal to {comparison}',
|
|
1246
|
-
NotEquals: 'should not equal to {comparison}',
|
|
1247
|
-
Contains: 'must contain {seed}',
|
|
1248
|
-
IsIn: 'must be one of the following values: {possibleValues}',
|
|
1249
|
-
IsNotIn: 'should not be one of the following values: {possibleValues}',
|
|
1250
|
-
Gt: 'must be greater than {min}',
|
|
1251
|
-
Gte: 'must be greater than or equal to {min}',
|
|
1252
|
-
Lt: 'must be less than {max}',
|
|
1253
|
-
Lte: 'must be less than or equal to {max}',
|
|
1254
|
-
// Common errors
|
|
1255
|
-
invalidParameter: 'invalid parameter {field}',
|
|
1256
|
-
validationFailed: 'validation failed',
|
|
1257
|
-
}
|
|
1258
|
-
};
|
|
1259
|
-
/**
|
|
1260
|
-
* 增强的验证错误类
|
|
1261
|
-
*/
|
|
1262
|
-
class KoattyValidationError extends Error {
|
|
1263
|
-
constructor(errors, message) {
|
|
1264
|
-
const errorMessage = message || 'Validation failed';
|
|
1265
|
-
super(errorMessage);
|
|
1266
|
-
this.name = 'KoattyValidationError';
|
|
1267
|
-
this.errors = errors;
|
|
1268
|
-
this.statusCode = 400;
|
|
1269
|
-
this.timestamp = new Date();
|
|
1270
|
-
// 确保正确的原型链
|
|
1271
|
-
Object.setPrototypeOf(this, KoattyValidationError.prototype);
|
|
1272
|
-
}
|
|
1273
|
-
/**
|
|
1274
|
-
* 获取第一个错误信息
|
|
1275
|
-
*/
|
|
1276
|
-
getFirstError() {
|
|
1277
|
-
return this.errors[0];
|
|
1278
|
-
}
|
|
1279
|
-
/**
|
|
1280
|
-
* 获取指定字段的错误
|
|
1281
|
-
*/
|
|
1282
|
-
getFieldErrors(field) {
|
|
1283
|
-
return this.errors.filter(error => error.field === field);
|
|
1284
|
-
}
|
|
1285
|
-
/**
|
|
1286
|
-
* 转换为JSON格式
|
|
1287
|
-
*/
|
|
1288
|
-
toJSON() {
|
|
1289
|
-
return {
|
|
1290
|
-
name: this.name,
|
|
1291
|
-
message: this.message,
|
|
1292
|
-
statusCode: this.statusCode,
|
|
1293
|
-
timestamp: this.timestamp,
|
|
1294
|
-
errors: this.errors
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
/**
|
|
1299
|
-
* 错误信息格式化器
|
|
1300
|
-
*/
|
|
1301
|
-
class ErrorMessageFormatter {
|
|
1302
|
-
constructor(language = 'zh') {
|
|
1303
|
-
this.language = 'zh';
|
|
1304
|
-
this.language = language;
|
|
1305
|
-
}
|
|
1306
|
-
/**
|
|
1307
|
-
* 设置语言
|
|
1308
|
-
*/
|
|
1309
|
-
setLanguage(language) {
|
|
1310
|
-
this.language = language;
|
|
1311
|
-
}
|
|
1312
|
-
/**
|
|
1313
|
-
* 格式化错误消息
|
|
1314
|
-
*/
|
|
1315
|
-
formatMessage(constraint, field, value, context) {
|
|
1316
|
-
const messages = ERROR_MESSAGES[this.language];
|
|
1317
|
-
let template = messages[constraint] || messages.invalidParameter;
|
|
1318
|
-
// 替换占位符
|
|
1319
|
-
template = template.replace('{field}', field);
|
|
1320
|
-
// 优先使用上下文中的值,然后是传入的value
|
|
1321
|
-
if (context) {
|
|
1322
|
-
Object.entries(context).forEach(([key, val]) => {
|
|
1323
|
-
template = template.replace(`{${key}}`, this.formatValue(val));
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1326
|
-
// 如果还有{value}占位符且传入了value,则替换
|
|
1327
|
-
if (value !== undefined && template.includes('{value}')) {
|
|
1328
|
-
template = template.replace('{value}', this.formatValue(value));
|
|
1329
|
-
}
|
|
1330
|
-
return template;
|
|
1331
|
-
}
|
|
1332
|
-
/**
|
|
1333
|
-
* 格式化值用于消息显示
|
|
1334
|
-
* @private
|
|
1335
|
-
*/
|
|
1336
|
-
formatValue(value) {
|
|
1337
|
-
if (value === null)
|
|
1338
|
-
return 'null';
|
|
1339
|
-
if (value === undefined)
|
|
1340
|
-
return 'undefined';
|
|
1341
|
-
if (typeof value === 'number')
|
|
1342
|
-
return String(value);
|
|
1343
|
-
if (typeof value === 'string')
|
|
1344
|
-
return `"${value}"`;
|
|
1345
|
-
if (Array.isArray(value))
|
|
1346
|
-
return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
|
|
1347
|
-
if (typeof value === 'object') {
|
|
1348
|
-
try {
|
|
1349
|
-
return JSON.stringify(value);
|
|
1350
|
-
}
|
|
1351
|
-
catch {
|
|
1352
|
-
// 处理循环引用
|
|
1353
|
-
return '[Circular Reference]';
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
return String(value);
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* 全局错误信息格式化器实例
|
|
1361
|
-
*/
|
|
1362
|
-
const errorFormatter = new ErrorMessageFormatter();
|
|
1363
|
-
/**
|
|
1364
|
-
* 设置全局语言
|
|
1365
|
-
*/
|
|
1366
|
-
function setValidationLanguage(language) {
|
|
1367
|
-
errorFormatter.setLanguage(language);
|
|
1368
|
-
}
|
|
1369
|
-
/**
|
|
1370
|
-
* 创建验证错误
|
|
1371
|
-
*/
|
|
1372
|
-
function createValidationError(field, value, constraint, customMessage, context) {
|
|
1373
|
-
const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
|
|
1374
|
-
return {
|
|
1375
|
-
field,
|
|
1376
|
-
value,
|
|
1377
|
-
constraint,
|
|
1378
|
-
message,
|
|
1379
|
-
context
|
|
1380
|
-
};
|
|
1381
|
-
}
|
|
1382
|
-
/**
|
|
1383
|
-
* 批量创建验证错误
|
|
1384
|
-
*/
|
|
1385
|
-
function createValidationErrors(errors) {
|
|
1386
|
-
const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
|
|
1387
|
-
return new KoattyValidationError(validationErrors);
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
/**
|
|
1391
|
-
* 重构后的装饰器定义 - 使用工厂函数消除重复
|
|
1412
|
+
* Refactored decorator definitions - Using factory functions to eliminate duplication
|
|
1392
1413
|
* @author richen
|
|
1393
1414
|
*/
|
|
1394
|
-
//
|
|
1415
|
+
// Chinese localization validation decorators
|
|
1395
1416
|
const IsCnName = createSimpleDecorator('IsCnName', (value) => helper.isString(value) && cnName(value), 'must be a valid Chinese name');
|
|
1396
1417
|
const IsIdNumber = createSimpleDecorator('IsIdNumber', (value) => helper.isString(value) && idNumber(value), 'must be a valid ID number');
|
|
1397
1418
|
const IsZipCode = createSimpleDecorator('IsZipCode', (value) => helper.isString(value) && zipCode(value), 'must be a valid zip code');
|
|
1398
1419
|
const IsMobile = createSimpleDecorator('IsMobile', (value) => helper.isString(value) && mobile(value), 'must be a valid mobile number');
|
|
1399
1420
|
const IsPlateNumber = createSimpleDecorator('IsPlateNumber', (value) => helper.isString(value) && plateNumber(value), 'must be a valid plate number');
|
|
1400
|
-
//
|
|
1421
|
+
// Basic validation decorators
|
|
1401
1422
|
const IsNotEmpty = createSimpleDecorator('IsNotEmpty', (value) => !helper.isEmpty(value), 'should not be empty');
|
|
1402
1423
|
const IsDate = createSimpleDecorator('IsDate', (value) => helper.isDate(value), 'must be a valid date');
|
|
1403
|
-
//
|
|
1424
|
+
// Parameterized validation decorators
|
|
1404
1425
|
const Equals = createParameterizedDecorator('Equals', (value, comparison) => value === comparison, 'must equal to $constraint1');
|
|
1405
1426
|
const NotEquals = createParameterizedDecorator('NotEquals', (value, comparison) => value !== comparison, 'should not equal to $constraint1');
|
|
1406
1427
|
const Contains = createParameterizedDecorator('Contains', (value, seed) => helper.isString(value) && value.includes(seed), 'must contain $constraint1');
|
|
@@ -1413,7 +1434,10 @@ const Lt = createParameterizedDecorator('Lt', (value, max) => helper.toNumber(va
|
|
|
1413
1434
|
const Lte = createParameterizedDecorator('Lte', (value, max) => helper.toNumber(value) <= max, 'must be less than or equal to $constraint1');
|
|
1414
1435
|
// 复杂验证装饰器(需要特殊处理)
|
|
1415
1436
|
function IsEmail(options, validationOptions) {
|
|
1416
|
-
|
|
1437
|
+
// Handle case where options is actually ValidationOptions (message only)
|
|
1438
|
+
const actualOptions = (options === null || options === void 0 ? void 0 : options.message) ? {} : options;
|
|
1439
|
+
const actualValidationOptions = (options === null || options === void 0 ? void 0 : options.message) ? options : validationOptions;
|
|
1440
|
+
return createParameterizedDecorator('IsEmail', (value) => isEmail(value, actualOptions), 'must be a valid email')(actualValidationOptions);
|
|
1417
1441
|
}
|
|
1418
1442
|
function IsIP(version, validationOptions) {
|
|
1419
1443
|
return createParameterizedDecorator('IsIP', (value) => isIP(value, version), 'must be a valid IP address')(validationOptions);
|
|
@@ -1427,9 +1451,9 @@ function IsUrl(options, validationOptions) {
|
|
|
1427
1451
|
function IsHash(algorithm, validationOptions) {
|
|
1428
1452
|
return createParameterizedDecorator('IsHash', (value) => isHash(value, algorithm), 'must be a valid hash')(validationOptions);
|
|
1429
1453
|
}
|
|
1430
|
-
//
|
|
1454
|
+
// Basic utility decorators (migrated from original decorator.ts)
|
|
1431
1455
|
/**
|
|
1432
|
-
*
|
|
1456
|
+
* Mark property as exportable
|
|
1433
1457
|
*/
|
|
1434
1458
|
function Expose() {
|
|
1435
1459
|
return function (object, propertyName) {
|
|
@@ -1437,7 +1461,7 @@ function Expose() {
|
|
|
1437
1461
|
};
|
|
1438
1462
|
}
|
|
1439
1463
|
/**
|
|
1440
|
-
* Expose
|
|
1464
|
+
* Alias for Expose
|
|
1441
1465
|
*/
|
|
1442
1466
|
function IsDefined() {
|
|
1443
1467
|
return function (object, propertyName) {
|
|
@@ -1445,63 +1469,91 @@ function IsDefined() {
|
|
|
1445
1469
|
};
|
|
1446
1470
|
}
|
|
1447
1471
|
/**
|
|
1448
|
-
*
|
|
1472
|
+
* Parameter validation decorator
|
|
1449
1473
|
*/
|
|
1450
1474
|
function Valid(rule, options) {
|
|
1451
1475
|
return function (object, propertyName, parameterIndex) {
|
|
1452
|
-
//
|
|
1476
|
+
// Keep consistent with original implementation
|
|
1453
1477
|
const existingRules = Reflect.getOwnMetadata("validate", object, propertyName) || {};
|
|
1454
1478
|
existingRules[parameterIndex] = { rule, options };
|
|
1455
1479
|
Reflect.defineMetadata("validate", existingRules, object, propertyName);
|
|
1456
1480
|
};
|
|
1457
1481
|
}
|
|
1458
1482
|
/**
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1483
|
+
* Synchronous validation function - Executes the actual validation logic
|
|
1484
|
+
* @param args Method parameters
|
|
1485
|
+
* @param paramTypes Parameter type metadata
|
|
1486
|
+
* @returns Validated parameters and validation targets
|
|
1461
1487
|
*/
|
|
1462
|
-
function
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
constraint: Object.keys(e.constraints || {})[0] || 'unknown',
|
|
1489
|
-
message: Object.values(e.constraints || {})[0] || 'Validation failed',
|
|
1490
|
-
context: e.constraints
|
|
1491
|
-
})));
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
catch (error) {
|
|
1495
|
-
// 如果验证失败,重新抛出错误
|
|
1496
|
-
throw error;
|
|
1497
|
-
}
|
|
1488
|
+
async function checkValidated(args, paramTypes) {
|
|
1489
|
+
const validationTargets = [];
|
|
1490
|
+
// Validate each parameter
|
|
1491
|
+
for (let i = 0; i < args.length; i++) {
|
|
1492
|
+
const arg = args[i];
|
|
1493
|
+
const paramType = paramTypes[i];
|
|
1494
|
+
// If it's a class type and not a basic type, perform validation
|
|
1495
|
+
if (paramType && typeof paramType === 'function' &&
|
|
1496
|
+
paramType !== String && paramType !== Number &&
|
|
1497
|
+
paramType !== Boolean && paramType !== Array &&
|
|
1498
|
+
paramType !== Object && paramType !== Date) {
|
|
1499
|
+
try {
|
|
1500
|
+
// If parameter is not an instance of the target type, convert it to an instance
|
|
1501
|
+
let validationTarget = arg;
|
|
1502
|
+
if (!(arg instanceof paramType)) {
|
|
1503
|
+
validationTarget = Object.assign(new paramType(), arg);
|
|
1504
|
+
}
|
|
1505
|
+
const errors = await validate(validationTarget);
|
|
1506
|
+
if (errors.length > 0) {
|
|
1507
|
+
throw createValidationErrors(errors.map(e => ({
|
|
1508
|
+
field: e.property,
|
|
1509
|
+
value: e.value,
|
|
1510
|
+
constraint: Object.keys(e.constraints || {})[0] || 'unknown',
|
|
1511
|
+
message: Object.values(e.constraints || {})[0] || 'Validation failed',
|
|
1512
|
+
context: e.constraints
|
|
1513
|
+
})));
|
|
1498
1514
|
}
|
|
1515
|
+
validationTargets.push(validationTarget);
|
|
1499
1516
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1517
|
+
catch (error) {
|
|
1518
|
+
// If validation fails, rethrow the error
|
|
1519
|
+
throw error;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
validationTargets.push(arg);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return { validatedArgs: args, validationTargets };
|
|
1527
|
+
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Method validation decorator
|
|
1530
|
+
* Automatically validates DTO objects in method parameters
|
|
1531
|
+
* @param isAsync Whether to use async validation mode, default is true
|
|
1532
|
+
* - true: Async mode, validation is handled by IOC container in the framework (suitable for scenarios where parameter values need to be obtained asynchronously)
|
|
1533
|
+
* - false: Sync mode, validation is performed immediately when the method is called (suitable for scenarios where parameter values are already prepared)
|
|
1534
|
+
*/
|
|
1535
|
+
function Validated(isAsync = true) {
|
|
1536
|
+
return function (target, propertyKey, descriptor) {
|
|
1537
|
+
if (isAsync) {
|
|
1538
|
+
// Async mode: Save metadata, validation will be performed by the framework after async parameter retrieval
|
|
1539
|
+
IOCContainer.savePropertyData(PARAM_CHECK_KEY, {
|
|
1540
|
+
dtoCheck: 1
|
|
1541
|
+
}, target, propertyKey);
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
// Sync mode: Perform validation immediately when the method is called
|
|
1545
|
+
const originalMethod = descriptor.value;
|
|
1546
|
+
descriptor.value = async function (...args) {
|
|
1547
|
+
// Get parameter type metadata
|
|
1548
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey) || [];
|
|
1549
|
+
// Execute validation
|
|
1550
|
+
await checkValidated(args, paramTypes);
|
|
1551
|
+
// Execute original method
|
|
1552
|
+
return originalMethod.apply(this, args);
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1503
1555
|
return descriptor;
|
|
1504
1556
|
};
|
|
1505
1557
|
}
|
|
1506
1558
|
|
|
1507
|
-
export { ClassValidator, Contains, ENABLE_VALIDATED, ERROR_MESSAGES, Equals, ErrorMessageFormatter, Expose, FunctionValidator, Gt, Gte, IsCnName, IsDate, IsDefined, IsEmail, IsHash, IsIP, IsIdNumber, IsIn, IsMobile, IsNotEmpty, IsNotIn, IsPhoneNumber, IsPlateNumber, IsUrl, IsZipCode, KoattyValidationError, Lt, Lte, NotEquals, PARAM_CHECK_KEY, PARAM_RULE_KEY, PARAM_TYPE_KEY, Valid, ValidFuncs, Validated, cached, checkParamsType, clearAllCaches, configureCaches, convertDtoParamsType, convertParamsType, createParameterizedDecorator, createSimpleDecorator, createValidationDecorator, createValidationError, createValidationErrors, errorFormatter, getAllCacheStats, metadataCache, paramterTypes, performanceMonitor, plainToClass, regexCache, setValidationLanguage, validationCache, warmupCaches };
|
|
1559
|
+
export { ClassValidator, Contains, ENABLE_VALIDATED, ERROR_MESSAGES, Equals, ErrorMessageFormatter, Expose, FunctionValidator, Gt, Gte, IsCnName, IsDate, IsDefined, IsEmail, IsHash, IsIP, IsIdNumber, IsIn, IsMobile, IsNotEmpty, IsNotIn, IsPhoneNumber, IsPlateNumber, IsUrl, IsZipCode, KoattyValidationError, Lt, Lte, NotEquals, PARAM_CHECK_KEY, PARAM_RULE_KEY, PARAM_TYPE_KEY, Valid, ValidFuncs, Validated, cached, checkParamsType, checkValidated, clearAllCaches, configureCaches, convertDtoParamsType, convertParamsType, createParameterizedDecorator, createSimpleDecorator, createValidationDecorator, createValidationError, createValidationErrors, errorFormatter, getAllCacheStats, metadataCache, paramterTypes, performanceMonitor, plainToClass, regexCache, setValidationLanguage, validationCache, warmupCaches };
|