koatty_validation 1.4.0 → 1.6.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/CHANGELOG.md +16 -0
- package/README.md +106 -3
- package/dist/README.md +106 -3
- package/dist/index.d.ts +112 -67
- package/dist/index.js +1336 -1228
- package/dist/index.mjs +1336 -1230
- package/dist/package.json +2 -2
- package/examples/validated-async-sync-example.ts +213 -0
- package/package.json +2 -2
- package/.vscode/launch.json +0 -81
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @Author: richen
|
|
3
|
-
* @Date: 2025-
|
|
3
|
+
* @Date: 2025-10-23 20:54:39
|
|
4
4
|
* @License: BSD (3-Clause)
|
|
5
5
|
* @Copyright (c) - <richenlin(at)gmail.com>
|
|
6
6
|
* @HomePage: https://koatty.org/
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
var helper = require('koatty_lib');
|
|
11
11
|
require('reflect-metadata');
|
|
12
12
|
var koatty_container = require('koatty_container');
|
|
13
|
-
var classValidator = require('class-validator');
|
|
14
13
|
var lruCache = require('lru-cache');
|
|
14
|
+
var classValidator = require('class-validator');
|
|
15
15
|
|
|
16
16
|
function _interopNamespaceDefault(e) {
|
|
17
17
|
var n = Object.create(null);
|
|
@@ -33,1418 +33,1524 @@ function _interopNamespaceDefault(e) {
|
|
|
33
33
|
var helper__namespace = /*#__PURE__*/_interopNamespaceDefault(helper);
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
* @
|
|
38
|
-
* @
|
|
39
|
-
* @
|
|
36
|
+
* koatty_validation type definitions
|
|
37
|
+
* @author richen
|
|
38
|
+
* @copyright Copyright (c) - <richenlin(at)gmail.com>
|
|
39
|
+
* @license MIT
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Parameter type key constant
|
|
40
43
|
*/
|
|
41
|
-
// tslint:disable-next-line: no-import-side-effect
|
|
42
|
-
// 参数类型键常量
|
|
43
44
|
const PARAM_TYPE_KEY = 'PARAM_TYPE_KEY';
|
|
45
|
+
|
|
44
46
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* @export
|
|
48
|
-
* @param {Object} object
|
|
49
|
-
* @param {(string | symbol)} propertyName
|
|
47
|
+
* Performance cache module - Provides multi-level caching and performance monitoring
|
|
48
|
+
* @author richen
|
|
50
49
|
*/
|
|
51
|
-
function setExpose(object, propertyName) {
|
|
52
|
-
const types = Reflect.getMetadata("design:type", object, propertyName);
|
|
53
|
-
if (types) {
|
|
54
|
-
const originMap = koatty_container.getOriginMetadata(PARAM_TYPE_KEY, object);
|
|
55
|
-
originMap.set(propertyName, types.name);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
50
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* @export
|
|
62
|
-
* @param {*} clazz
|
|
63
|
-
* @param {*} data
|
|
64
|
-
* @param {boolean} [convert=false]
|
|
65
|
-
* @returns
|
|
51
|
+
* Metadata cache
|
|
66
52
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (
|
|
73
|
-
|
|
53
|
+
class MetadataCache {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.cache = new WeakMap();
|
|
56
|
+
}
|
|
57
|
+
static getInstance() {
|
|
58
|
+
if (!MetadataCache.instance) {
|
|
59
|
+
MetadataCache.instance = new MetadataCache();
|
|
74
60
|
}
|
|
75
|
-
return
|
|
61
|
+
return MetadataCache.instance;
|
|
76
62
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* @param convert
|
|
84
|
-
* @returns
|
|
85
|
-
*/
|
|
86
|
-
function assignDtoParams(clazz, data, convert = false) {
|
|
87
|
-
const cls = Reflect.construct(clazz, []);
|
|
88
|
-
if (convert) {
|
|
89
|
-
const metaData = getDtoParamsMeta(clazz, cls);
|
|
90
|
-
for (const [key, type] of metaData) {
|
|
91
|
-
if (key && data[key] !== undefined) {
|
|
92
|
-
cls[key] = convertParamsType(data[key], type);
|
|
93
|
-
}
|
|
63
|
+
/**
|
|
64
|
+
* Get metadata cache for a class
|
|
65
|
+
*/
|
|
66
|
+
getClassCache(target) {
|
|
67
|
+
if (!this.cache.has(target)) {
|
|
68
|
+
this.cache.set(target, new Map());
|
|
94
69
|
}
|
|
70
|
+
return this.cache.get(target);
|
|
95
71
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Cache metadata
|
|
74
|
+
*/
|
|
75
|
+
setMetadata(target, key, value) {
|
|
76
|
+
const classCache = this.getClassCache(target);
|
|
77
|
+
classCache.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get cached metadata
|
|
81
|
+
*/
|
|
82
|
+
getMetadata(target, key) {
|
|
83
|
+
const classCache = this.getClassCache(target);
|
|
84
|
+
return classCache.get(key);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Check if metadata is cached
|
|
88
|
+
*/
|
|
89
|
+
hasMetadata(target, key) {
|
|
90
|
+
const classCache = this.getClassCache(target);
|
|
91
|
+
return classCache.has(key);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Clear cache for a specific class
|
|
95
|
+
*/
|
|
96
|
+
clearClassCache(target) {
|
|
97
|
+
if (this.cache.has(target)) {
|
|
98
|
+
this.cache.delete(target);
|
|
102
99
|
}
|
|
103
100
|
}
|
|
104
|
-
return cls;
|
|
105
101
|
}
|
|
106
102
|
/**
|
|
107
|
-
*
|
|
108
|
-
* @param clazz
|
|
109
|
-
* @param cls
|
|
110
|
-
* @returns
|
|
103
|
+
* Validation result cache
|
|
111
104
|
*/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
class ValidationCache {
|
|
106
|
+
constructor(options) {
|
|
107
|
+
this.hits = 0;
|
|
108
|
+
this.misses = 0;
|
|
109
|
+
this.cache = new lruCache.LRUCache({
|
|
110
|
+
max: (options === null || options === void 0 ? void 0 : options.max) || 5000,
|
|
111
|
+
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 10, // 10 minutes
|
|
112
|
+
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
113
|
+
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
114
|
+
});
|
|
118
115
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
configurable: false,
|
|
123
|
-
writable: false,
|
|
124
|
-
value: typeDef,
|
|
125
|
-
});
|
|
126
|
-
return typeDef;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* convertDtoParamsType
|
|
130
|
-
*
|
|
131
|
-
* @param {*} clazz
|
|
132
|
-
* @param {*} cls
|
|
133
|
-
* @returns {*} cls
|
|
134
|
-
*/
|
|
135
|
-
function convertDtoParamsType(clazz, cls) {
|
|
136
|
-
if (Object.prototype.hasOwnProperty.call(cls, "_typeDef")) {
|
|
137
|
-
for (const key in cls) {
|
|
138
|
-
if (Object.prototype.hasOwnProperty.call(cls._typeDef, key) &&
|
|
139
|
-
cls[key] !== undefined) {
|
|
140
|
-
cls[key] = convertParamsType(cls[key], cls._typeDef[key]);
|
|
141
|
-
}
|
|
116
|
+
static getInstance(options) {
|
|
117
|
+
if (!ValidationCache.instance) {
|
|
118
|
+
ValidationCache.instance = new ValidationCache(options);
|
|
142
119
|
}
|
|
120
|
+
return ValidationCache.instance;
|
|
143
121
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
122
|
+
/**
|
|
123
|
+
* 生成缓存键
|
|
124
|
+
*/
|
|
125
|
+
generateKey(validator, value, ...args) {
|
|
126
|
+
const valueStr = this.serializeValue(value);
|
|
127
|
+
const argsStr = args.length > 0 ? JSON.stringify(args) : '';
|
|
128
|
+
return `${validator}:${valueStr}:${argsStr}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 序列化值用于缓存键
|
|
132
|
+
*/
|
|
133
|
+
serializeValue(value) {
|
|
134
|
+
if (value === null)
|
|
135
|
+
return 'null';
|
|
136
|
+
if (value === undefined)
|
|
137
|
+
return 'undefined';
|
|
138
|
+
if (typeof value === 'string')
|
|
139
|
+
return `s:${value}`;
|
|
140
|
+
if (typeof value === 'number')
|
|
141
|
+
return `n:${value}`;
|
|
142
|
+
if (typeof value === 'boolean')
|
|
143
|
+
return `b:${value}`;
|
|
144
|
+
if (Array.isArray(value))
|
|
145
|
+
return `a:${JSON.stringify(value)}`;
|
|
146
|
+
if (typeof value === 'object')
|
|
147
|
+
return `o:${JSON.stringify(value)}`;
|
|
148
|
+
return String(value);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* 获取缓存的验证结果
|
|
152
|
+
*/
|
|
153
|
+
get(validator, value, ...args) {
|
|
154
|
+
const key = this.generateKey(validator, value, ...args);
|
|
155
|
+
const result = this.cache.get(key);
|
|
156
|
+
// Track cache hits and misses
|
|
157
|
+
if (result !== undefined) {
|
|
158
|
+
this.hits++;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
this.misses++;
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* 缓存验证结果
|
|
167
|
+
*/
|
|
168
|
+
set(validator, value, result, ...args) {
|
|
169
|
+
const key = this.generateKey(validator, value, ...args);
|
|
170
|
+
this.cache.set(key, result);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 检查是否存在缓存
|
|
174
|
+
*/
|
|
175
|
+
has(validator, value, ...args) {
|
|
176
|
+
const key = this.generateKey(validator, value, ...args);
|
|
177
|
+
return this.cache.has(key);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 删除特定缓存
|
|
181
|
+
*/
|
|
182
|
+
delete(validator, value, ...args) {
|
|
183
|
+
const key = this.generateKey(validator, value, ...args);
|
|
184
|
+
return this.cache.delete(key);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* 清空缓存
|
|
188
|
+
*/
|
|
189
|
+
clear() {
|
|
190
|
+
this.cache.clear();
|
|
191
|
+
this.hits = 0;
|
|
192
|
+
this.misses = 0;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* 获取缓存统计
|
|
196
|
+
*/
|
|
197
|
+
getStats() {
|
|
198
|
+
const totalRequests = this.hits + this.misses;
|
|
199
|
+
const hitRate = totalRequests > 0 ? this.hits / totalRequests : 0;
|
|
200
|
+
return {
|
|
201
|
+
size: this.cache.size,
|
|
202
|
+
max: this.cache.max,
|
|
203
|
+
calculatedSize: this.cache.calculatedSize,
|
|
204
|
+
keyCount: this.cache.size,
|
|
205
|
+
hits: this.hits,
|
|
206
|
+
misses: this.misses,
|
|
207
|
+
hitRate: Math.round(hitRate * 10000) / 100, // Percentage with 2 decimal places
|
|
208
|
+
totalRequests,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* 设置缓存TTL
|
|
213
|
+
*/
|
|
214
|
+
setTTL(validator, value, ttl, ...args) {
|
|
215
|
+
const key = this.generateKey(validator, value, ...args);
|
|
216
|
+
const existingValue = this.cache.get(key);
|
|
217
|
+
if (existingValue !== undefined) {
|
|
218
|
+
this.cache.set(key, existingValue, { ttl });
|
|
150
219
|
}
|
|
151
220
|
}
|
|
152
|
-
return cls;
|
|
153
221
|
}
|
|
154
222
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* @param {*} param
|
|
158
|
-
* @param {string} type
|
|
159
|
-
* @returns {*}
|
|
223
|
+
* 正则表达式缓存
|
|
160
224
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (helper__namespace.isNumber(param)) {
|
|
170
|
-
return param;
|
|
171
|
-
}
|
|
172
|
-
if (helper__namespace.isNumberString(param)) {
|
|
173
|
-
return helper__namespace.toNumber(param);
|
|
174
|
-
}
|
|
175
|
-
return NaN;
|
|
176
|
-
case "Boolean":
|
|
177
|
-
case "boolean":
|
|
178
|
-
return !!param;
|
|
179
|
-
case "Array":
|
|
180
|
-
case "array":
|
|
181
|
-
case "Tuple":
|
|
182
|
-
case "tuple":
|
|
183
|
-
if (helper__namespace.isArray(param)) {
|
|
184
|
-
return param;
|
|
185
|
-
}
|
|
186
|
-
return helper__namespace.toArray(param);
|
|
187
|
-
case "String":
|
|
188
|
-
case "string":
|
|
189
|
-
if (helper__namespace.isString(param)) {
|
|
190
|
-
return param;
|
|
191
|
-
}
|
|
192
|
-
return helper__namespace.toString(param);
|
|
193
|
-
case "Null":
|
|
194
|
-
case "null":
|
|
195
|
-
return null;
|
|
196
|
-
case "Undefined":
|
|
197
|
-
case "undefined":
|
|
198
|
-
return undefined;
|
|
199
|
-
case "Bigint":
|
|
200
|
-
case "bigint":
|
|
201
|
-
if (typeof param === 'bigint') {
|
|
202
|
-
return param;
|
|
203
|
-
}
|
|
204
|
-
return BigInt(param);
|
|
205
|
-
// case "object":
|
|
206
|
-
// case "enum":
|
|
207
|
-
default: //any
|
|
208
|
-
return param;
|
|
209
|
-
}
|
|
210
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
225
|
+
class RegexCache {
|
|
226
|
+
constructor(options) {
|
|
227
|
+
this.cache = new lruCache.LRUCache({
|
|
228
|
+
max: (options === null || options === void 0 ? void 0 : options.max) || 200,
|
|
229
|
+
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 30, // 30 minutes
|
|
230
|
+
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
231
|
+
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
232
|
+
});
|
|
211
233
|
}
|
|
212
|
-
|
|
213
|
-
|
|
234
|
+
static getInstance(options) {
|
|
235
|
+
if (!RegexCache.instance) {
|
|
236
|
+
RegexCache.instance = new RegexCache(options);
|
|
237
|
+
}
|
|
238
|
+
return RegexCache.instance;
|
|
214
239
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
case "number":
|
|
227
|
-
if (!helper__namespace.isNumber(value) || helper__namespace.isNaN(value)) {
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
return true;
|
|
231
|
-
case "Boolean":
|
|
232
|
-
case "boolean":
|
|
233
|
-
if (!helper__namespace.isBoolean(value)) {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
return true;
|
|
237
|
-
case "Array":
|
|
238
|
-
case "array":
|
|
239
|
-
case "Tuple":
|
|
240
|
-
case "tuple":
|
|
241
|
-
if (!helper__namespace.isArray(value)) {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
return true;
|
|
245
|
-
case "String":
|
|
246
|
-
case "string":
|
|
247
|
-
if (!helper__namespace.isString(value)) {
|
|
248
|
-
return false;
|
|
249
|
-
}
|
|
250
|
-
return true;
|
|
251
|
-
case "Object":
|
|
252
|
-
case "object":
|
|
253
|
-
case "Enum":
|
|
254
|
-
case "enum":
|
|
255
|
-
if (helper__namespace.isTrueEmpty(value)) {
|
|
256
|
-
return false;
|
|
240
|
+
/**
|
|
241
|
+
* 获取缓存的正则表达式
|
|
242
|
+
*/
|
|
243
|
+
get(pattern, flags) {
|
|
244
|
+
const key = flags ? `${pattern}:::${flags}` : pattern;
|
|
245
|
+
let regex = this.cache.get(key);
|
|
246
|
+
if (!regex) {
|
|
247
|
+
try {
|
|
248
|
+
regex = new RegExp(pattern, flags);
|
|
249
|
+
this.cache.set(key, regex);
|
|
250
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
257
251
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (!helper__namespace.isNull(value)) {
|
|
262
|
-
return false;
|
|
252
|
+
catch (_error) {
|
|
253
|
+
// 如果正则表达式无效,抛出错误
|
|
254
|
+
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
263
255
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
256
|
+
}
|
|
257
|
+
return regex;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 预编译常用正则表达式
|
|
261
|
+
*/
|
|
262
|
+
precompile(patterns) {
|
|
263
|
+
patterns.forEach(({ pattern, flags }) => {
|
|
264
|
+
try {
|
|
265
|
+
this.get(pattern, flags);
|
|
269
266
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
case "bigint":
|
|
273
|
-
if (typeof value !== 'bigint') {
|
|
274
|
-
return false;
|
|
267
|
+
catch (error) {
|
|
268
|
+
console.warn(`Failed to precompile regex: ${pattern}`, error);
|
|
275
269
|
}
|
|
276
|
-
|
|
277
|
-
default: //any
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
/**
|
|
282
|
-
* Checks if value is a chinese name.
|
|
283
|
-
*
|
|
284
|
-
* @param {string} value
|
|
285
|
-
* @returns {boolean}
|
|
286
|
-
*/
|
|
287
|
-
function cnName(value) {
|
|
288
|
-
const reg = /^([a-zA-Z0-9\u4e00-\u9fa5\·]{1,10})$/;
|
|
289
|
-
return reg.test(value);
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Checks if value is a idCard number.
|
|
293
|
-
*
|
|
294
|
-
* @param {string} value
|
|
295
|
-
* @returns
|
|
296
|
-
*/
|
|
297
|
-
function idNumber(value) {
|
|
298
|
-
if (/^\d{15}$/.test(value)) {
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
if ((/^\d{17}[0-9X]$/).test(value)) {
|
|
302
|
-
const vs = '1,0,x,9,8,7,6,5,4,3,2'.split(',');
|
|
303
|
-
const ps = '7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2'.split(',');
|
|
304
|
-
const ss = value.toLowerCase().split('');
|
|
305
|
-
let r = 0;
|
|
306
|
-
for (let i = 0; i < 17; i++) {
|
|
307
|
-
r += ps[i] * ss[i];
|
|
308
|
-
}
|
|
309
|
-
const isOk = (vs[r % 11] === ss[17]);
|
|
310
|
-
return isOk;
|
|
270
|
+
});
|
|
311
271
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const reg = /^(13|14|15|16|17|18|19)\d{9}$/;
|
|
322
|
-
return reg.test(value);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Checks if value is a zipCode.
|
|
326
|
-
*
|
|
327
|
-
* @param {string} value
|
|
328
|
-
* @returns {boolean}
|
|
329
|
-
*/
|
|
330
|
-
function zipCode(value) {
|
|
331
|
-
const reg = /^\d{6}$/;
|
|
332
|
-
return reg.test(value);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Checks if value is a plateNumber.
|
|
336
|
-
*
|
|
337
|
-
* @param {string} value
|
|
338
|
-
* @returns {boolean}
|
|
339
|
-
*/
|
|
340
|
-
function plateNumber(value) {
|
|
341
|
-
// let reg = new RegExp('^(([\u4e00-\u9fa5][a-zA-Z]|[\u4e00-\u9fa5]{2}\d{2}|[\u4e00-\u9fa5]{2}[a-zA-Z])[-]?|([wW][Jj][\u4e00-\u9fa5]{1}[-]?)|([a-zA-Z]{2}))([A-Za-z0-9]{5}|[DdFf][A-HJ-NP-Za-hj-np-z0-9][0-9]{4}|[0-9]{5}[DdFf])$');
|
|
342
|
-
// let xReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
|
|
343
|
-
const xReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))/;
|
|
344
|
-
// let cReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
|
|
345
|
-
const cReg = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$/;
|
|
346
|
-
if (value.length === 7) {
|
|
347
|
-
return cReg.test(value);
|
|
272
|
+
/**
|
|
273
|
+
* 获取缓存统计
|
|
274
|
+
*/
|
|
275
|
+
getStats() {
|
|
276
|
+
return {
|
|
277
|
+
size: this.cache.size,
|
|
278
|
+
max: this.cache.max,
|
|
279
|
+
calculatedSize: this.cache.calculatedSize,
|
|
280
|
+
};
|
|
348
281
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
282
|
+
/**
|
|
283
|
+
* 清空缓存
|
|
284
|
+
*/
|
|
285
|
+
clear() {
|
|
286
|
+
this.cache.clear();
|
|
352
287
|
}
|
|
353
288
|
}
|
|
354
|
-
|
|
355
|
-
/*
|
|
356
|
-
* @Description:
|
|
357
|
-
* @Usage:
|
|
358
|
-
* @Author: richen
|
|
359
|
-
* @Date: 2021-11-25 10:47:04
|
|
360
|
-
* @LastEditTime: 2024-01-03 14:32:49
|
|
361
|
-
*/
|
|
362
|
-
// constant
|
|
363
|
-
const PARAM_RULE_KEY = 'PARAM_RULE_KEY';
|
|
364
|
-
const PARAM_CHECK_KEY = 'PARAM_CHECK_KEY';
|
|
365
|
-
const ENABLE_VALIDATED = "ENABLE_VALIDATED";
|
|
366
289
|
/**
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* @export
|
|
370
|
-
* @enum {number}
|
|
290
|
+
* 性能监控
|
|
371
291
|
*/
|
|
372
|
-
|
|
373
|
-
(function (paramterTypes) {
|
|
374
|
-
paramterTypes[paramterTypes["Number"] = 0] = "Number";
|
|
375
|
-
paramterTypes[paramterTypes["number"] = 1] = "number";
|
|
376
|
-
paramterTypes[paramterTypes["String"] = 2] = "String";
|
|
377
|
-
paramterTypes[paramterTypes["string"] = 3] = "string";
|
|
378
|
-
paramterTypes[paramterTypes["Boolean"] = 4] = "Boolean";
|
|
379
|
-
paramterTypes[paramterTypes["boolean"] = 5] = "boolean";
|
|
380
|
-
paramterTypes[paramterTypes["Array"] = 6] = "Array";
|
|
381
|
-
paramterTypes[paramterTypes["array"] = 7] = "array";
|
|
382
|
-
paramterTypes[paramterTypes["Tuple"] = 8] = "Tuple";
|
|
383
|
-
paramterTypes[paramterTypes["tuple"] = 9] = "tuple";
|
|
384
|
-
paramterTypes[paramterTypes["Object"] = 10] = "Object";
|
|
385
|
-
paramterTypes[paramterTypes["object"] = 11] = "object";
|
|
386
|
-
paramterTypes[paramterTypes["Enum"] = 12] = "Enum";
|
|
387
|
-
paramterTypes[paramterTypes["enum"] = 13] = "enum";
|
|
388
|
-
paramterTypes[paramterTypes["Bigint"] = 14] = "Bigint";
|
|
389
|
-
paramterTypes[paramterTypes["bigint"] = 15] = "bigint";
|
|
390
|
-
paramterTypes[paramterTypes["Null"] = 16] = "Null";
|
|
391
|
-
paramterTypes[paramterTypes["null"] = 17] = "null";
|
|
392
|
-
paramterTypes[paramterTypes["Undefined"] = 18] = "Undefined";
|
|
393
|
-
paramterTypes[paramterTypes["undefined"] = 19] = "undefined";
|
|
394
|
-
})(exports.paramterTypes || (exports.paramterTypes = {}));
|
|
395
|
-
class ValidateClass {
|
|
292
|
+
class PerformanceMonitor {
|
|
396
293
|
constructor() {
|
|
294
|
+
this.metrics = new Map();
|
|
397
295
|
}
|
|
398
|
-
/**
|
|
399
|
-
*
|
|
400
|
-
*
|
|
401
|
-
* @static
|
|
402
|
-
* @returns
|
|
403
|
-
* @memberof ValidateUtil
|
|
404
|
-
*/
|
|
405
296
|
static getInstance() {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* validated data vs dto class
|
|
410
|
-
*
|
|
411
|
-
* @param {*} Clazz
|
|
412
|
-
* @param {*} data
|
|
413
|
-
* @param {boolean} [convert=false] auto convert parameters type
|
|
414
|
-
* @returns {Promise<any>}
|
|
415
|
-
* @memberof ValidateClass
|
|
416
|
-
*/
|
|
417
|
-
async valid(Clazz, data, convert = false) {
|
|
418
|
-
let obj = {};
|
|
419
|
-
if (data instanceof Clazz) {
|
|
420
|
-
obj = data;
|
|
421
|
-
}
|
|
422
|
-
else {
|
|
423
|
-
obj = plainToClass(Clazz, data, convert);
|
|
424
|
-
}
|
|
425
|
-
let errors = [];
|
|
426
|
-
if (convert) {
|
|
427
|
-
errors = await classValidator.validate(obj);
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
errors = await classValidator.validate(obj, { skipMissingProperties: true });
|
|
431
|
-
}
|
|
432
|
-
if (errors.length > 0) {
|
|
433
|
-
throw new Error(Object.values(errors[0].constraints)[0]);
|
|
297
|
+
if (!PerformanceMonitor.instance) {
|
|
298
|
+
PerformanceMonitor.instance = new PerformanceMonitor();
|
|
434
299
|
}
|
|
435
|
-
return
|
|
300
|
+
return PerformanceMonitor.instance;
|
|
436
301
|
}
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* ClassValidator for manual
|
|
440
|
-
*/
|
|
441
|
-
const ClassValidator = ValidateClass.getInstance();
|
|
442
|
-
/**
|
|
443
|
-
* Validator Functions
|
|
444
|
-
*/
|
|
445
|
-
const ValidFuncs = {
|
|
446
|
-
/**
|
|
447
|
-
* Checks value is not empty, undefined, null, '', NaN, [], {} and any empty string(including spaces,
|
|
448
|
-
* tabs, formfeeds, etc.), returns false
|
|
449
|
-
*/
|
|
450
|
-
IsNotEmpty: (value) => {
|
|
451
|
-
return !helper__namespace.isEmpty(value);
|
|
452
|
-
},
|
|
453
|
-
/**
|
|
454
|
-
* Checks if a given value is a real date.
|
|
455
|
-
*/
|
|
456
|
-
IsDate: (value) => {
|
|
457
|
-
return helper__namespace.isDate(value);
|
|
458
|
-
},
|
|
459
|
-
/**
|
|
460
|
-
* Checks if the string is an email. If given value is not a string, then it returns false.
|
|
461
|
-
*/
|
|
462
|
-
IsEmail: (value, options) => {
|
|
463
|
-
return classValidator.isEmail(value, options);
|
|
464
|
-
},
|
|
465
|
-
/**
|
|
466
|
-
* Checks if the string is an IP (version 4 or 6). If given value is not a string, then it returns false.
|
|
467
|
-
*/
|
|
468
|
-
IsIP: (value, version) => {
|
|
469
|
-
return classValidator.isIP(value, version);
|
|
470
|
-
},
|
|
471
|
-
/**
|
|
472
|
-
* Checks if the string is a valid phone number.
|
|
473
|
-
* @param value — the potential phone number string to test
|
|
474
|
-
* @param region 2 characters uppercase country code (e.g. DE, US, CH). If users must enter the intl.
|
|
475
|
-
* prefix (e.g. +41), then you may pass "ZZ" or null as region.
|
|
476
|
-
* See [google-libphonenumber, metadata.js:countryCodeToRegionCodeMap on github]
|
|
477
|
-
* {@link https://github.com/ruimarinho/google-libphonenumber/blob/1e46138878cff479aafe2ce62175c6c49cb58720/src/metadata.js#L33}
|
|
478
|
-
*/
|
|
479
|
-
IsPhoneNumber: (value, region) => {
|
|
480
|
-
return classValidator.isPhoneNumber(value, region);
|
|
481
|
-
},
|
|
482
302
|
/**
|
|
483
|
-
*
|
|
484
|
-
*/
|
|
485
|
-
IsUrl: (value, options) => {
|
|
486
|
-
return classValidator.isURL(value, options);
|
|
487
|
-
},
|
|
488
|
-
/**
|
|
489
|
-
* check if the string is a hash of type algorithm. Algorithm is one of
|
|
490
|
-
* ['md4', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'ripemd128', 'ripemd160', 'tiger128', 'tiger160', 'tiger192', 'crc32', 'crc32b']
|
|
303
|
+
* 开始计时
|
|
491
304
|
*/
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
305
|
+
startTimer(name) {
|
|
306
|
+
const start = performance.now();
|
|
307
|
+
return () => {
|
|
308
|
+
const duration = performance.now() - start;
|
|
309
|
+
this.recordMetric(name, duration);
|
|
310
|
+
};
|
|
311
|
+
}
|
|
495
312
|
/**
|
|
496
|
-
*
|
|
313
|
+
* 记录性能指标
|
|
497
314
|
*/
|
|
498
|
-
|
|
499
|
-
if (!
|
|
500
|
-
|
|
315
|
+
recordMetric(name, duration) {
|
|
316
|
+
if (!this.metrics.has(name)) {
|
|
317
|
+
this.metrics.set(name, {
|
|
318
|
+
count: 0,
|
|
319
|
+
totalTime: 0,
|
|
320
|
+
avgTime: 0,
|
|
321
|
+
maxTime: 0,
|
|
322
|
+
minTime: Infinity,
|
|
323
|
+
lastExecutionTime: new Date(),
|
|
324
|
+
});
|
|
501
325
|
}
|
|
502
|
-
|
|
503
|
-
|
|
326
|
+
const metric = this.metrics.get(name);
|
|
327
|
+
metric.count++;
|
|
328
|
+
metric.totalTime += duration;
|
|
329
|
+
metric.avgTime = metric.totalTime / metric.count;
|
|
330
|
+
metric.maxTime = Math.max(metric.maxTime, duration);
|
|
331
|
+
metric.minTime = Math.min(metric.minTime, duration);
|
|
332
|
+
metric.lastExecutionTime = new Date();
|
|
333
|
+
}
|
|
504
334
|
/**
|
|
505
|
-
*
|
|
335
|
+
* 获取性能报告
|
|
506
336
|
*/
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
337
|
+
getReport() {
|
|
338
|
+
const report = {};
|
|
339
|
+
for (const [name, metric] of this.metrics) {
|
|
340
|
+
report[name] = {
|
|
341
|
+
...metric,
|
|
342
|
+
minTime: metric.minTime === Infinity ? 0 : metric.minTime,
|
|
343
|
+
avgTimeFormatted: `${metric.avgTime.toFixed(2)}ms`,
|
|
344
|
+
totalTimeFormatted: `${metric.totalTime.toFixed(2)}ms`,
|
|
345
|
+
};
|
|
510
346
|
}
|
|
511
|
-
return
|
|
512
|
-
}
|
|
347
|
+
return report;
|
|
348
|
+
}
|
|
513
349
|
/**
|
|
514
|
-
*
|
|
350
|
+
* 获取热点分析(执行时间最长的操作)
|
|
515
351
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
352
|
+
getHotspots(limit = 10) {
|
|
353
|
+
return Array.from(this.metrics.entries())
|
|
354
|
+
.map(([name, metric]) => ({
|
|
355
|
+
name,
|
|
356
|
+
avgTime: metric.avgTime,
|
|
357
|
+
count: metric.count,
|
|
358
|
+
}))
|
|
359
|
+
.sort((a, b) => b.avgTime - a.avgTime)
|
|
360
|
+
.slice(0, limit);
|
|
361
|
+
}
|
|
522
362
|
/**
|
|
523
|
-
*
|
|
363
|
+
* 清空指标
|
|
524
364
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
return mobile(value);
|
|
530
|
-
},
|
|
365
|
+
clear() {
|
|
366
|
+
this.metrics.clear();
|
|
367
|
+
}
|
|
531
368
|
/**
|
|
532
|
-
*
|
|
369
|
+
* 导出性能数据为CSV格式
|
|
533
370
|
*/
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
371
|
+
exportToCSV() {
|
|
372
|
+
const headers = ['Name', 'Count', 'Total Time (ms)', 'Avg Time (ms)', 'Max Time (ms)', 'Min Time (ms)', 'Last Execution'];
|
|
373
|
+
const rows = [headers.join(',')];
|
|
374
|
+
for (const [name, metric] of this.metrics) {
|
|
375
|
+
const row = [
|
|
376
|
+
name,
|
|
377
|
+
metric.count.toString(),
|
|
378
|
+
metric.totalTime.toFixed(2),
|
|
379
|
+
metric.avgTime.toFixed(2),
|
|
380
|
+
metric.maxTime.toFixed(2),
|
|
381
|
+
(metric.minTime === Infinity ? 0 : metric.minTime).toFixed(2),
|
|
382
|
+
metric.lastExecutionTime.toISOString(),
|
|
383
|
+
];
|
|
384
|
+
rows.push(row.join(','));
|
|
537
385
|
}
|
|
538
|
-
return
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Checks if value does not match ("!==") the comparison.
|
|
548
|
-
*/
|
|
549
|
-
NotEquals: (value, comparison) => {
|
|
550
|
-
return classValidator.notEquals(value, comparison);
|
|
551
|
-
},
|
|
552
|
-
/**
|
|
553
|
-
* Checks if the string contains the seed. If given value is not a string, then it returns false.
|
|
554
|
-
*/
|
|
555
|
-
Contains: (value, seed) => {
|
|
556
|
-
return classValidator.contains(value, seed);
|
|
557
|
-
},
|
|
558
|
-
/**
|
|
559
|
-
* Checks if given value is in a array of allowed values.
|
|
560
|
-
*/
|
|
561
|
-
IsIn: (value, possibleValues) => {
|
|
562
|
-
return classValidator.isIn(value, possibleValues);
|
|
563
|
-
},
|
|
564
|
-
/**
|
|
565
|
-
* Checks if given value not in a array of allowed values.
|
|
566
|
-
*/
|
|
567
|
-
IsNotIn: (value, possibleValues) => {
|
|
568
|
-
return classValidator.isNotIn(value, possibleValues);
|
|
569
|
-
},
|
|
570
|
-
/**
|
|
571
|
-
* Checks if the first number is greater than or equal to the second.
|
|
572
|
-
*/
|
|
573
|
-
Gt: (num, min) => {
|
|
574
|
-
return helper__namespace.toNumber(num) > min;
|
|
575
|
-
},
|
|
576
|
-
/**
|
|
577
|
-
* Checks if the first number is less than or equal to the second.
|
|
578
|
-
*/
|
|
579
|
-
Lt: (num, max) => {
|
|
580
|
-
return helper__namespace.toNumber(num) < max;
|
|
581
|
-
},
|
|
582
|
-
/**
|
|
583
|
-
* Checks if the first number is greater than or equal to the second.
|
|
584
|
-
*/
|
|
585
|
-
Gte: (num, min) => {
|
|
586
|
-
return helper__namespace.toNumber(num) >= min;
|
|
587
|
-
},
|
|
588
|
-
/**
|
|
589
|
-
* Checks if the first number is less than or equal to the second.
|
|
590
|
-
*/
|
|
591
|
-
Lte: (num, max) => {
|
|
592
|
-
return helper__namespace.toNumber(num) <= max;
|
|
593
|
-
},
|
|
594
|
-
};
|
|
386
|
+
return rows.join('\n');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// 导出单例实例
|
|
390
|
+
const metadataCache = MetadataCache.getInstance();
|
|
391
|
+
const validationCache = ValidationCache.getInstance();
|
|
392
|
+
const regexCache = RegexCache.getInstance();
|
|
393
|
+
const performanceMonitor = PerformanceMonitor.getInstance();
|
|
595
394
|
/**
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* @export
|
|
599
|
-
* @param {ValidRules} rule
|
|
600
|
-
* @param {unknown} value
|
|
601
|
-
* @param {(string | ValidOtpions)} [options]
|
|
602
|
-
* @returns {*}
|
|
395
|
+
* 缓存装饰器 - 用于缓存验证函数结果
|
|
603
396
|
*/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
throw new Error("Function not implemented.");
|
|
632
|
-
},
|
|
633
|
-
IsZipCode: function (_value, _options) {
|
|
634
|
-
throw new Error("Function not implemented.");
|
|
635
|
-
},
|
|
636
|
-
IsMobile: function (_value, _options) {
|
|
637
|
-
throw new Error("Function not implemented.");
|
|
638
|
-
},
|
|
639
|
-
IsPlateNumber: function (_value, _options) {
|
|
640
|
-
throw new Error("Function not implemented.");
|
|
641
|
-
},
|
|
642
|
-
Equals: function (_value, _options) {
|
|
643
|
-
throw new Error("Function not implemented.");
|
|
644
|
-
},
|
|
645
|
-
NotEquals: function (_value, _options) {
|
|
646
|
-
throw new Error("Function not implemented.");
|
|
647
|
-
},
|
|
648
|
-
Contains: function (_value, _options) {
|
|
649
|
-
throw new Error("Function not implemented.");
|
|
650
|
-
},
|
|
651
|
-
IsIn: function (_value, _options) {
|
|
652
|
-
throw new Error("Function not implemented.");
|
|
653
|
-
},
|
|
654
|
-
IsNotIn: function (_value, _options) {
|
|
655
|
-
throw new Error("Function not implemented.");
|
|
656
|
-
},
|
|
657
|
-
Gt: function (_value, _options) {
|
|
658
|
-
throw new Error("Function not implemented.");
|
|
659
|
-
},
|
|
660
|
-
Lt: function (_value, _options) {
|
|
661
|
-
throw new Error("Function not implemented.");
|
|
662
|
-
},
|
|
663
|
-
Gte: function (_value, _options) {
|
|
664
|
-
throw new Error("Function not implemented.");
|
|
665
|
-
},
|
|
666
|
-
Lte: function (_value, _options) {
|
|
667
|
-
throw new Error("Function not implemented.");
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
/* eslint-enable @typescript-eslint/no-unused-vars */
|
|
671
|
-
Object.keys(ValidFuncs).forEach((key) => {
|
|
672
|
-
FunctionValidator[key] = (value, options) => {
|
|
673
|
-
let validOptions;
|
|
674
|
-
if (typeof options === 'string') {
|
|
675
|
-
validOptions = { message: options, value: null };
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
validOptions = options || { message: `ValidatorError: invalid arguments.`, value: null };
|
|
679
|
-
}
|
|
680
|
-
if (!ValidFuncs[key](value, validOptions.value)) {
|
|
681
|
-
throw new Error(validOptions.message || `ValidatorError: invalid arguments.`);
|
|
682
|
-
}
|
|
397
|
+
function cached(validator, ttl) {
|
|
398
|
+
return function (target, propertyName, descriptor) {
|
|
399
|
+
const originalMethod = descriptor.value;
|
|
400
|
+
descriptor.value = function (...args) {
|
|
401
|
+
const value = args[0];
|
|
402
|
+
const additionalArgs = args.slice(1);
|
|
403
|
+
// 尝试从缓存获取结果
|
|
404
|
+
const cachedResult = validationCache.get(validator, value, ...additionalArgs);
|
|
405
|
+
if (cachedResult !== undefined) {
|
|
406
|
+
return cachedResult;
|
|
407
|
+
}
|
|
408
|
+
// 执行验证并缓存结果
|
|
409
|
+
const endTimer = performanceMonitor.startTimer(validator);
|
|
410
|
+
try {
|
|
411
|
+
const result = originalMethod.apply(this, args);
|
|
412
|
+
validationCache.set(validator, value, result, ...additionalArgs);
|
|
413
|
+
// If TTL is specified, set expiration time
|
|
414
|
+
if (ttl && ttl > 0) {
|
|
415
|
+
validationCache.setTTL(validator, value, ttl, ...additionalArgs);
|
|
416
|
+
}
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
finally {
|
|
420
|
+
endTimer();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
return descriptor;
|
|
683
424
|
};
|
|
684
|
-
}
|
|
685
|
-
|
|
425
|
+
}
|
|
686
426
|
/**
|
|
687
|
-
*
|
|
688
|
-
* @author richen
|
|
427
|
+
* 获取所有缓存统计信息
|
|
689
428
|
*/
|
|
429
|
+
function getAllCacheStats() {
|
|
430
|
+
return {
|
|
431
|
+
validation: validationCache.getStats(),
|
|
432
|
+
regex: regexCache.getStats(),
|
|
433
|
+
performance: performanceMonitor.getReport(),
|
|
434
|
+
hotspots: performanceMonitor.getHotspots(),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
690
437
|
/**
|
|
691
|
-
*
|
|
692
|
-
* @param options 装饰器配置选项
|
|
693
|
-
* @returns 装饰器工厂函数
|
|
438
|
+
* 预热缓存 - 预编译常用正则表达式
|
|
694
439
|
*/
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
// 注册验证装饰器
|
|
705
|
-
classValidator.registerDecorator({
|
|
706
|
-
name,
|
|
707
|
-
target: object.constructor,
|
|
708
|
-
propertyName,
|
|
709
|
-
options: validationOptions,
|
|
710
|
-
constraints: validatorArgs,
|
|
711
|
-
validator: {
|
|
712
|
-
validate(value) {
|
|
713
|
-
try {
|
|
714
|
-
return validator(value, ...validatorArgs);
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
return false;
|
|
718
|
-
}
|
|
719
|
-
},
|
|
720
|
-
defaultMessage(validationArguments) {
|
|
721
|
-
const property = validationArguments.property;
|
|
722
|
-
return defaultMessage
|
|
723
|
-
? defaultMessage.replace('$property', property)
|
|
724
|
-
: `Invalid value for ${property}`;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
};
|
|
729
|
-
};
|
|
440
|
+
function warmupCaches() {
|
|
441
|
+
// 预编译中文验证相关的正则表达式
|
|
442
|
+
const commonPatterns = [
|
|
443
|
+
{ pattern: '^[\u4e00-\u9fa5]{2,8}$' }, // 中文姓名
|
|
444
|
+
{ pattern: '^1[3-9]\\d{9}$' }, // 手机号
|
|
445
|
+
{ pattern: '^\\d{6}$' }, // 邮政编码
|
|
446
|
+
{ pattern: '^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]' }, // 车牌号开头
|
|
447
|
+
];
|
|
448
|
+
regexCache.precompile(commonPatterns);
|
|
730
449
|
}
|
|
731
450
|
/**
|
|
732
|
-
*
|
|
733
|
-
* @param name 装饰器名称
|
|
734
|
-
* @param validator 验证函数
|
|
735
|
-
* @param defaultMessage 默认错误信息
|
|
736
|
-
* @returns 装饰器函数
|
|
451
|
+
* 清空所有缓存
|
|
737
452
|
*/
|
|
738
|
-
function
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
defaultMessage,
|
|
743
|
-
requiresValue: false
|
|
744
|
-
});
|
|
453
|
+
function clearAllCaches() {
|
|
454
|
+
validationCache.clear();
|
|
455
|
+
regexCache.clear();
|
|
456
|
+
performanceMonitor.clear();
|
|
745
457
|
}
|
|
746
458
|
/**
|
|
747
|
-
*
|
|
748
|
-
* @param name 装饰器名称
|
|
749
|
-
* @param validator 验证函数
|
|
750
|
-
* @param defaultMessage 默认错误信息
|
|
751
|
-
* @returns 装饰器工厂函数
|
|
459
|
+
* 配置缓存设置
|
|
752
460
|
*/
|
|
753
|
-
function
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
461
|
+
function configureCaches(options) {
|
|
462
|
+
if (options.validation) {
|
|
463
|
+
// 重新创建验证缓存实例
|
|
464
|
+
ValidationCache.instance = new ValidationCache(options.validation);
|
|
465
|
+
}
|
|
466
|
+
if (options.regex) {
|
|
467
|
+
// 重新创建正则缓存实例
|
|
468
|
+
RegexCache.instance = new RegexCache(options.regex);
|
|
469
|
+
}
|
|
760
470
|
}
|
|
761
471
|
|
|
762
472
|
/**
|
|
763
|
-
*
|
|
764
|
-
* @
|
|
473
|
+
* @ author: richen
|
|
474
|
+
* @ copyright: Copyright (c) - <richenlin(at)gmail.com>
|
|
475
|
+
* @ license: MIT
|
|
476
|
+
* @ version: 2020-03-20 11:34:38
|
|
765
477
|
*/
|
|
766
|
-
//
|
|
767
|
-
const IsCnName = createSimpleDecorator('IsCnName', (value) => helper__namespace.isString(value) && cnName(value), 'must be a valid Chinese name');
|
|
768
|
-
const IsIdNumber = createSimpleDecorator('IsIdNumber', (value) => helper__namespace.isString(value) && idNumber(value), 'must be a valid ID number');
|
|
769
|
-
const IsZipCode = createSimpleDecorator('IsZipCode', (value) => helper__namespace.isString(value) && zipCode(value), 'must be a valid zip code');
|
|
770
|
-
const IsMobile = createSimpleDecorator('IsMobile', (value) => helper__namespace.isString(value) && mobile(value), 'must be a valid mobile number');
|
|
771
|
-
const IsPlateNumber = createSimpleDecorator('IsPlateNumber', (value) => helper__namespace.isString(value) && plateNumber(value), 'must be a valid plate number');
|
|
772
|
-
// 基础验证装饰器
|
|
773
|
-
const IsNotEmpty = createSimpleDecorator('IsNotEmpty', (value) => !helper__namespace.isEmpty(value), 'should not be empty');
|
|
774
|
-
const IsDate = createSimpleDecorator('IsDate', (value) => helper__namespace.isDate(value), 'must be a valid date');
|
|
775
|
-
// 带参数的验证装饰器
|
|
776
|
-
const Equals = createParameterizedDecorator('Equals', (value, comparison) => value === comparison, 'must equal to $constraint1');
|
|
777
|
-
const NotEquals = createParameterizedDecorator('NotEquals', (value, comparison) => value !== comparison, 'should not equal to $constraint1');
|
|
778
|
-
const Contains = createParameterizedDecorator('Contains', (value, seed) => helper__namespace.isString(value) && value.includes(seed), 'must contain $constraint1');
|
|
779
|
-
const IsIn = createParameterizedDecorator('IsIn', (value, possibleValues) => possibleValues.includes(value), 'must be one of the following values: $constraint1');
|
|
780
|
-
const IsNotIn = createParameterizedDecorator('IsNotIn', (value, possibleValues) => !possibleValues.includes(value), 'should not be one of the following values: $constraint1');
|
|
781
|
-
// 数值比较装饰器
|
|
782
|
-
const Gt = createParameterizedDecorator('Gt', (value, min) => helper__namespace.toNumber(value) > min, 'must be greater than $constraint1');
|
|
783
|
-
const Gte = createParameterizedDecorator('Gte', (value, min) => helper__namespace.toNumber(value) >= min, 'must be greater than or equal to $constraint1');
|
|
784
|
-
const Lt = createParameterizedDecorator('Lt', (value, max) => helper__namespace.toNumber(value) < max, 'must be less than $constraint1');
|
|
785
|
-
const Lte = createParameterizedDecorator('Lte', (value, max) => helper__namespace.toNumber(value) <= max, 'must be less than or equal to $constraint1');
|
|
786
|
-
// 复杂验证装饰器(需要特殊处理)
|
|
787
|
-
function IsEmail(options, validationOptions) {
|
|
788
|
-
return createParameterizedDecorator('IsEmail', (value) => classValidator.isEmail(value, options), 'must be a valid email')(validationOptions);
|
|
789
|
-
}
|
|
790
|
-
function IsIP(version, validationOptions) {
|
|
791
|
-
return createParameterizedDecorator('IsIP', (value) => classValidator.isIP(value, version), 'must be a valid IP address')(validationOptions);
|
|
792
|
-
}
|
|
793
|
-
function IsPhoneNumber(region, validationOptions) {
|
|
794
|
-
return createParameterizedDecorator('IsPhoneNumber', (value) => classValidator.isPhoneNumber(value, region), 'must be a valid phone number')(validationOptions);
|
|
795
|
-
}
|
|
796
|
-
function IsUrl(options, validationOptions) {
|
|
797
|
-
return createParameterizedDecorator('IsUrl', (value) => classValidator.isURL(value, options), 'must be a valid URL')(validationOptions);
|
|
798
|
-
}
|
|
799
|
-
function IsHash(algorithm, validationOptions) {
|
|
800
|
-
return createParameterizedDecorator('IsHash', (value) => classValidator.isHash(value, algorithm), 'must be a valid hash')(validationOptions);
|
|
801
|
-
}
|
|
802
|
-
// 基础工具装饰器(从原始decorator.ts移植)
|
|
478
|
+
// tslint:disable-next-line: no-import-side-effect
|
|
803
479
|
/**
|
|
804
|
-
*
|
|
480
|
+
* Set property as included in the process of transformation.
|
|
481
|
+
*
|
|
482
|
+
* @export
|
|
483
|
+
* @param {Object} object
|
|
484
|
+
* @param {(string | symbol)} propertyName
|
|
805
485
|
*/
|
|
806
|
-
function
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
486
|
+
function setExpose(object, propertyName) {
|
|
487
|
+
const types = Reflect.getMetadata("design:type", object, propertyName);
|
|
488
|
+
if (types) {
|
|
489
|
+
const originMap = koatty_container.getOriginMetadata(PARAM_TYPE_KEY, object);
|
|
490
|
+
originMap.set(propertyName, types.name);
|
|
491
|
+
}
|
|
810
492
|
}
|
|
811
493
|
/**
|
|
812
|
-
*
|
|
494
|
+
* plain object convert to class instance
|
|
495
|
+
*
|
|
496
|
+
* @export
|
|
497
|
+
* @param {*} clazz
|
|
498
|
+
* @param {*} data
|
|
499
|
+
* @param {boolean} [convert=false]
|
|
500
|
+
* @returns
|
|
813
501
|
*/
|
|
814
|
-
function
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
502
|
+
function plainToClass(clazz, data, convert = false) {
|
|
503
|
+
if (helper__namespace.isClass(clazz)) {
|
|
504
|
+
if (!helper__namespace.isObject(data)) {
|
|
505
|
+
data = {};
|
|
506
|
+
}
|
|
507
|
+
if (data instanceof clazz) {
|
|
508
|
+
return data;
|
|
509
|
+
}
|
|
510
|
+
return assignDtoParams(clazz, data, convert);
|
|
511
|
+
}
|
|
512
|
+
return data;
|
|
818
513
|
}
|
|
819
514
|
/**
|
|
820
|
-
*
|
|
515
|
+
* assign dto params
|
|
516
|
+
* @param clazz
|
|
517
|
+
* @param data
|
|
518
|
+
* @param convert
|
|
519
|
+
* @returns
|
|
821
520
|
*/
|
|
822
|
-
function
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
521
|
+
function assignDtoParams(clazz, data, convert = false) {
|
|
522
|
+
const cls = Reflect.construct(clazz, []);
|
|
523
|
+
if (convert) {
|
|
524
|
+
const metaData = getDtoParamsMeta(clazz, cls);
|
|
525
|
+
for (const [key, type] of metaData) {
|
|
526
|
+
if (key && data[key] !== undefined) {
|
|
527
|
+
cls[key] = convertParamsType(data[key], type);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
for (const key in cls) {
|
|
533
|
+
if (Object.prototype.hasOwnProperty.call(data, key) &&
|
|
534
|
+
data[key] !== undefined) {
|
|
535
|
+
cls[key] = data[key];
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return cls;
|
|
829
540
|
}
|
|
830
541
|
/**
|
|
831
|
-
*
|
|
542
|
+
* get class prototype type def.
|
|
543
|
+
* @param clazz
|
|
544
|
+
* @param cls
|
|
545
|
+
* @returns
|
|
832
546
|
*/
|
|
833
|
-
function
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
547
|
+
function getDtoParamsMeta(clazz, cls) {
|
|
548
|
+
// Non-own properties are inherited from the prototype chain,
|
|
549
|
+
// ensure that properties are not polluted
|
|
550
|
+
if (!Object.prototype.hasOwnProperty.call(cls, "_typeDef") &&
|
|
551
|
+
("_typeDef" in cls)) {
|
|
552
|
+
return cls._typeDef;
|
|
553
|
+
}
|
|
554
|
+
const typeDef = koatty_container.getOriginMetadata(PARAM_TYPE_KEY, clazz);
|
|
555
|
+
Reflect.defineProperty(clazz.prototype, "_typeDef", {
|
|
556
|
+
enumerable: true,
|
|
557
|
+
configurable: false,
|
|
558
|
+
writable: false,
|
|
559
|
+
value: typeDef,
|
|
560
|
+
});
|
|
561
|
+
return typeDef;
|
|
842
562
|
}
|
|
843
|
-
|
|
844
563
|
/**
|
|
845
|
-
*
|
|
846
|
-
*
|
|
564
|
+
* convertDtoParamsType
|
|
565
|
+
*
|
|
566
|
+
* @param {*} clazz
|
|
567
|
+
* @param {*} cls
|
|
568
|
+
* @returns {*} cls
|
|
847
569
|
*/
|
|
570
|
+
function convertDtoParamsType(clazz, cls) {
|
|
571
|
+
if (Object.prototype.hasOwnProperty.call(cls, "_typeDef")) {
|
|
572
|
+
for (const key in cls) {
|
|
573
|
+
if (Object.prototype.hasOwnProperty.call(cls._typeDef, key) &&
|
|
574
|
+
cls[key] !== undefined) {
|
|
575
|
+
cls[key] = convertParamsType(cls[key], cls._typeDef[key]);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
else {
|
|
580
|
+
const originMap = koatty_container.getOriginMetadata(PARAM_TYPE_KEY, clazz);
|
|
581
|
+
for (const [key, type] of originMap) {
|
|
582
|
+
if (key && cls[key] !== undefined) {
|
|
583
|
+
cls[key] = convertParamsType(cls[key], type);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return cls;
|
|
588
|
+
}
|
|
848
589
|
/**
|
|
849
|
-
*
|
|
590
|
+
* 绑定参数类型转换
|
|
591
|
+
*
|
|
592
|
+
* @param {*} param
|
|
593
|
+
* @param {string} type
|
|
594
|
+
* @returns {*}
|
|
850
595
|
*/
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
validationFailed: 'validation failed',
|
|
596
|
+
function convertParamsType(param, type) {
|
|
597
|
+
try {
|
|
598
|
+
switch (type) {
|
|
599
|
+
case "Number":
|
|
600
|
+
case "number":
|
|
601
|
+
if (helper__namespace.isNaN(param)) {
|
|
602
|
+
return NaN;
|
|
603
|
+
}
|
|
604
|
+
if (helper__namespace.isNumber(param)) {
|
|
605
|
+
// Check for safe integer range
|
|
606
|
+
if (Number.isInteger(param) && !Number.isSafeInteger(param)) {
|
|
607
|
+
console.warn(`[koatty_validation] Number ${param} exceeds safe integer range`);
|
|
608
|
+
}
|
|
609
|
+
return param;
|
|
610
|
+
}
|
|
611
|
+
if (helper__namespace.isNumberString(param)) {
|
|
612
|
+
const num = helper__namespace.toNumber(param);
|
|
613
|
+
if (Number.isInteger(num) && !Number.isSafeInteger(num)) {
|
|
614
|
+
console.warn(`[koatty_validation] Number ${param} exceeds safe integer range`);
|
|
615
|
+
}
|
|
616
|
+
return num;
|
|
617
|
+
}
|
|
618
|
+
return NaN;
|
|
619
|
+
case "Boolean":
|
|
620
|
+
case "boolean":
|
|
621
|
+
return !!param;
|
|
622
|
+
case "Array":
|
|
623
|
+
case "array":
|
|
624
|
+
case "Tuple":
|
|
625
|
+
case "tuple":
|
|
626
|
+
if (helper__namespace.isArray(param)) {
|
|
627
|
+
return param;
|
|
628
|
+
}
|
|
629
|
+
return helper__namespace.toArray(param);
|
|
630
|
+
case "String":
|
|
631
|
+
case "string":
|
|
632
|
+
if (helper__namespace.isString(param)) {
|
|
633
|
+
return param;
|
|
634
|
+
}
|
|
635
|
+
return helper__namespace.toString(param);
|
|
636
|
+
case "Null":
|
|
637
|
+
case "null":
|
|
638
|
+
return null;
|
|
639
|
+
case "Undefined":
|
|
640
|
+
case "undefined":
|
|
641
|
+
return undefined;
|
|
642
|
+
case "Bigint":
|
|
643
|
+
case "bigint":
|
|
644
|
+
if (typeof param === 'bigint') {
|
|
645
|
+
return param;
|
|
646
|
+
}
|
|
647
|
+
return BigInt(param);
|
|
648
|
+
// case "object":
|
|
649
|
+
// case "enum":
|
|
650
|
+
default: //any
|
|
651
|
+
return param;
|
|
652
|
+
}
|
|
909
653
|
}
|
|
910
|
-
|
|
654
|
+
catch (error) {
|
|
655
|
+
// Log conversion errors for debugging
|
|
656
|
+
console.warn(`[koatty_validation] Type conversion error for type "${type}":`, error instanceof Error ? error.message : String(error));
|
|
657
|
+
return param;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
911
660
|
/**
|
|
912
|
-
*
|
|
661
|
+
* Check the base types.
|
|
662
|
+
*
|
|
663
|
+
* @param {*} value
|
|
664
|
+
* @param {string} type
|
|
665
|
+
* @returns {*}
|
|
913
666
|
*/
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
667
|
+
function checkParamsType(value, type) {
|
|
668
|
+
switch (type) {
|
|
669
|
+
case "Number":
|
|
670
|
+
case "number":
|
|
671
|
+
if (!helper__namespace.isNumber(value) || helper__namespace.isNaN(value)) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
return true;
|
|
675
|
+
case "Boolean":
|
|
676
|
+
case "boolean":
|
|
677
|
+
if (!helper__namespace.isBoolean(value)) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
return true;
|
|
681
|
+
case "Array":
|
|
682
|
+
case "array":
|
|
683
|
+
case "Tuple":
|
|
684
|
+
case "tuple":
|
|
685
|
+
if (!helper__namespace.isArray(value)) {
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
return true;
|
|
689
|
+
case "String":
|
|
690
|
+
case "string":
|
|
691
|
+
if (!helper__namespace.isString(value)) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
case "Object":
|
|
696
|
+
case "object":
|
|
697
|
+
case "Enum":
|
|
698
|
+
case "enum":
|
|
699
|
+
if (helper__namespace.isTrueEmpty(value)) {
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
return true;
|
|
703
|
+
case "Null":
|
|
704
|
+
case "null":
|
|
705
|
+
if (!helper__namespace.isNull(value)) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
return true;
|
|
709
|
+
case "Undefined":
|
|
710
|
+
case "undefined":
|
|
711
|
+
if (!helper__namespace.isUndefined(value)) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
return true;
|
|
715
|
+
case "Bigint":
|
|
716
|
+
case "bigint":
|
|
717
|
+
if (typeof value !== 'bigint') {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
return true;
|
|
721
|
+
default: //any
|
|
722
|
+
return true;
|
|
924
723
|
}
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Checks if value is a chinese name.
|
|
727
|
+
*
|
|
728
|
+
* @param {string} value
|
|
729
|
+
* @returns {boolean}
|
|
730
|
+
*/
|
|
731
|
+
function cnName(value) {
|
|
732
|
+
// Add length limit for security
|
|
733
|
+
if (value.length > 50) {
|
|
734
|
+
return false;
|
|
930
735
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
736
|
+
// Use regex cache for better performance
|
|
737
|
+
const reg = regexCache.get('^([a-zA-Z0-9\\u4e00-\\u9fa5\\·]{1,10})$');
|
|
738
|
+
return reg.test(value);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Checks if value is a idCard number.
|
|
742
|
+
*
|
|
743
|
+
* @param {string} value
|
|
744
|
+
* @returns
|
|
745
|
+
*/
|
|
746
|
+
function idNumber(value) {
|
|
747
|
+
// Add length limit for security
|
|
748
|
+
if (!value || value.length > 20) {
|
|
749
|
+
return false;
|
|
936
750
|
}
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
751
|
+
// Check 15-digit ID
|
|
752
|
+
const reg15 = regexCache.get('^\\d{15}$');
|
|
753
|
+
if (reg15.test(value)) {
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
// Check 18-digit ID with validation
|
|
757
|
+
const reg18 = regexCache.get('^\\d{17}[0-9X]$');
|
|
758
|
+
if (reg18.test(value)) {
|
|
759
|
+
const vs = '1,0,x,9,8,7,6,5,4,3,2'.split(',');
|
|
760
|
+
const ps = '7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2'.split(',');
|
|
761
|
+
const ss = value.toLowerCase().split('');
|
|
762
|
+
let r = 0;
|
|
763
|
+
for (let i = 0; i < 17; i++) {
|
|
764
|
+
r += ps[i] * ss[i];
|
|
765
|
+
}
|
|
766
|
+
const isOk = (vs[r % 11] === ss[17]);
|
|
767
|
+
return isOk;
|
|
948
768
|
}
|
|
769
|
+
return false;
|
|
949
770
|
}
|
|
950
771
|
/**
|
|
951
|
-
*
|
|
772
|
+
* Checks if value is a mobile phone number.
|
|
773
|
+
*
|
|
774
|
+
* @param {string} value
|
|
775
|
+
* @returns {boolean}
|
|
952
776
|
*/
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
777
|
+
function mobile(value) {
|
|
778
|
+
// Add length limit for security (prevent ReDoS)
|
|
779
|
+
if (!value || value.length > 20) {
|
|
780
|
+
return false;
|
|
957
781
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
782
|
+
const reg = regexCache.get('^(13|14|15|16|17|18|19)\\d{9}$');
|
|
783
|
+
return reg.test(value);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Checks if value is a zipCode.
|
|
787
|
+
*
|
|
788
|
+
* @param {string} value
|
|
789
|
+
* @returns {boolean}
|
|
790
|
+
*/
|
|
791
|
+
function zipCode(value) {
|
|
792
|
+
// Add length limit for security
|
|
793
|
+
if (!value || value.length > 10) {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
const reg = regexCache.get('^\\d{6}$');
|
|
797
|
+
return reg.test(value);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Checks if value is a plateNumber.
|
|
801
|
+
*
|
|
802
|
+
* @param {string} value
|
|
803
|
+
* @returns {boolean}
|
|
804
|
+
*/
|
|
805
|
+
function plateNumber(value) {
|
|
806
|
+
// Add length limit for security
|
|
807
|
+
if (!value || value.length > 15) {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
// New energy vehicle plate number (8 characters)
|
|
811
|
+
const xReg = regexCache.get('^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]{1}[A-Z]{1}(([0-9]{5}[DF]$)|([DF][A-HJ-NP-Z0-9][0-9]{4}$))');
|
|
812
|
+
// Traditional vehicle plate number (7 characters)
|
|
813
|
+
const cReg = regexCache.get('^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]{1}[A-Z]{1}[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]{1}$');
|
|
814
|
+
if (value.length === 7) {
|
|
815
|
+
return cReg.test(value);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
// New energy vehicle plate
|
|
819
|
+
return xReg.test(value);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
/*
|
|
824
|
+
* @Description:
|
|
825
|
+
* @Usage:
|
|
826
|
+
* @Author: richen
|
|
827
|
+
* @Date: 2021-11-25 10:47:04
|
|
828
|
+
* @LastEditTime: 2024-01-03 14:32:49
|
|
829
|
+
*/
|
|
830
|
+
// constant
|
|
831
|
+
const PARAM_RULE_KEY = 'PARAM_RULE_KEY';
|
|
832
|
+
const PARAM_CHECK_KEY = 'PARAM_CHECK_KEY';
|
|
833
|
+
const ENABLE_VALIDATED = "ENABLE_VALIDATED";
|
|
834
|
+
/**
|
|
835
|
+
* paramterTypes
|
|
836
|
+
*
|
|
837
|
+
* @export
|
|
838
|
+
* @enum {number}
|
|
839
|
+
*/
|
|
840
|
+
exports.paramterTypes = void 0;
|
|
841
|
+
(function (paramterTypes) {
|
|
842
|
+
paramterTypes[paramterTypes["Number"] = 0] = "Number";
|
|
843
|
+
paramterTypes[paramterTypes["number"] = 1] = "number";
|
|
844
|
+
paramterTypes[paramterTypes["String"] = 2] = "String";
|
|
845
|
+
paramterTypes[paramterTypes["string"] = 3] = "string";
|
|
846
|
+
paramterTypes[paramterTypes["Boolean"] = 4] = "Boolean";
|
|
847
|
+
paramterTypes[paramterTypes["boolean"] = 5] = "boolean";
|
|
848
|
+
paramterTypes[paramterTypes["Array"] = 6] = "Array";
|
|
849
|
+
paramterTypes[paramterTypes["array"] = 7] = "array";
|
|
850
|
+
paramterTypes[paramterTypes["Tuple"] = 8] = "Tuple";
|
|
851
|
+
paramterTypes[paramterTypes["tuple"] = 9] = "tuple";
|
|
852
|
+
paramterTypes[paramterTypes["Object"] = 10] = "Object";
|
|
853
|
+
paramterTypes[paramterTypes["object"] = 11] = "object";
|
|
854
|
+
paramterTypes[paramterTypes["Enum"] = 12] = "Enum";
|
|
855
|
+
paramterTypes[paramterTypes["enum"] = 13] = "enum";
|
|
856
|
+
paramterTypes[paramterTypes["Bigint"] = 14] = "Bigint";
|
|
857
|
+
paramterTypes[paramterTypes["bigint"] = 15] = "bigint";
|
|
858
|
+
paramterTypes[paramterTypes["Null"] = 16] = "Null";
|
|
859
|
+
paramterTypes[paramterTypes["null"] = 17] = "null";
|
|
860
|
+
paramterTypes[paramterTypes["Undefined"] = 18] = "Undefined";
|
|
861
|
+
paramterTypes[paramterTypes["undefined"] = 19] = "undefined";
|
|
862
|
+
})(exports.paramterTypes || (exports.paramterTypes = {}));
|
|
863
|
+
class ValidateClass {
|
|
864
|
+
constructor() {
|
|
963
865
|
}
|
|
964
866
|
/**
|
|
965
|
-
*
|
|
867
|
+
*
|
|
868
|
+
*
|
|
869
|
+
* @static
|
|
870
|
+
* @returns
|
|
871
|
+
* @memberof ValidateUtil
|
|
966
872
|
*/
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
let template = messages[constraint] || messages.invalidParameter;
|
|
970
|
-
// 替换占位符
|
|
971
|
-
template = template.replace('{field}', field);
|
|
972
|
-
// 优先使用上下文中的值,然后是传入的value
|
|
973
|
-
if (context) {
|
|
974
|
-
Object.entries(context).forEach(([key, val]) => {
|
|
975
|
-
template = template.replace(`{${key}}`, this.formatValue(val));
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
// 如果还有{value}占位符且传入了value,则替换
|
|
979
|
-
if (value !== undefined && template.includes('{value}')) {
|
|
980
|
-
template = template.replace('{value}', this.formatValue(value));
|
|
981
|
-
}
|
|
982
|
-
return template;
|
|
873
|
+
static getInstance() {
|
|
874
|
+
return this.instance || (this.instance = new ValidateClass());
|
|
983
875
|
}
|
|
984
876
|
/**
|
|
985
|
-
*
|
|
986
|
-
*
|
|
877
|
+
* validated data vs dto class
|
|
878
|
+
*
|
|
879
|
+
* @param {*} Clazz
|
|
880
|
+
* @param {*} data
|
|
881
|
+
* @param {boolean} [convert=false] auto convert parameters type
|
|
882
|
+
* @returns {Promise<any>}
|
|
883
|
+
* @memberof ValidateClass
|
|
987
884
|
*/
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
return 'undefined';
|
|
993
|
-
if (typeof value === 'number')
|
|
994
|
-
return String(value);
|
|
995
|
-
if (typeof value === 'string')
|
|
996
|
-
return `"${value}"`;
|
|
997
|
-
if (Array.isArray(value))
|
|
998
|
-
return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
|
|
999
|
-
if (typeof value === 'object') {
|
|
1000
|
-
try {
|
|
1001
|
-
return JSON.stringify(value);
|
|
1002
|
-
}
|
|
1003
|
-
catch {
|
|
1004
|
-
// 处理循环引用
|
|
1005
|
-
return '[Circular Reference]';
|
|
1006
|
-
}
|
|
885
|
+
async valid(Clazz, data, convert = false) {
|
|
886
|
+
let obj = {};
|
|
887
|
+
if (data instanceof Clazz) {
|
|
888
|
+
obj = data;
|
|
1007
889
|
}
|
|
1008
|
-
|
|
890
|
+
else {
|
|
891
|
+
obj = plainToClass(Clazz, data, convert);
|
|
892
|
+
}
|
|
893
|
+
let errors = [];
|
|
894
|
+
if (convert) {
|
|
895
|
+
errors = await classValidator.validate(obj);
|
|
896
|
+
}
|
|
897
|
+
else {
|
|
898
|
+
errors = await classValidator.validate(obj, { skipMissingProperties: true });
|
|
899
|
+
}
|
|
900
|
+
if (errors.length > 0) {
|
|
901
|
+
throw new Error(Object.values(errors[0].constraints)[0]);
|
|
902
|
+
}
|
|
903
|
+
return obj;
|
|
1009
904
|
}
|
|
1010
905
|
}
|
|
1011
906
|
/**
|
|
1012
|
-
*
|
|
1013
|
-
*/
|
|
1014
|
-
const errorFormatter = new ErrorMessageFormatter();
|
|
1015
|
-
/**
|
|
1016
|
-
* 设置全局语言
|
|
907
|
+
* ClassValidator for manual
|
|
1017
908
|
*/
|
|
1018
|
-
|
|
1019
|
-
errorFormatter.setLanguage(language);
|
|
1020
|
-
}
|
|
909
|
+
const ClassValidator = ValidateClass.getInstance();
|
|
1021
910
|
/**
|
|
1022
|
-
*
|
|
911
|
+
* Helper function to wrap validation with cache
|
|
1023
912
|
*/
|
|
1024
|
-
function
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
913
|
+
function withCache(validatorName, validatorFunc) {
|
|
914
|
+
return (value, ...args) => {
|
|
915
|
+
// Check cache first
|
|
916
|
+
const cached = validationCache.get(validatorName, value, ...args);
|
|
917
|
+
if (cached !== undefined) {
|
|
918
|
+
return cached;
|
|
919
|
+
}
|
|
920
|
+
// Execute validation
|
|
921
|
+
const result = validatorFunc(value, ...args);
|
|
922
|
+
// Cache the result
|
|
923
|
+
validationCache.set(validatorName, value, result, ...args);
|
|
924
|
+
return result;
|
|
1032
925
|
};
|
|
1033
926
|
}
|
|
1034
927
|
/**
|
|
1035
|
-
*
|
|
1036
|
-
*/
|
|
1037
|
-
function createValidationErrors(errors) {
|
|
1038
|
-
const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
|
|
1039
|
-
return new KoattyValidationError(validationErrors);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
/**
|
|
1043
|
-
* 性能缓存模块 - 提供多层次缓存和性能监控
|
|
1044
|
-
* @author richen
|
|
1045
|
-
*/
|
|
1046
|
-
/**
|
|
1047
|
-
* 元数据缓存
|
|
928
|
+
* Validator Functions
|
|
1048
929
|
*/
|
|
1049
|
-
|
|
1050
|
-
constructor() {
|
|
1051
|
-
this.cache = new WeakMap();
|
|
1052
|
-
}
|
|
1053
|
-
static getInstance() {
|
|
1054
|
-
if (!MetadataCache.instance) {
|
|
1055
|
-
MetadataCache.instance = new MetadataCache();
|
|
1056
|
-
}
|
|
1057
|
-
return MetadataCache.instance;
|
|
1058
|
-
}
|
|
1059
|
-
/**
|
|
1060
|
-
* 获取类的元数据缓存
|
|
1061
|
-
*/
|
|
1062
|
-
getClassCache(target) {
|
|
1063
|
-
if (!this.cache.has(target)) {
|
|
1064
|
-
this.cache.set(target, new Map());
|
|
1065
|
-
}
|
|
1066
|
-
return this.cache.get(target);
|
|
1067
|
-
}
|
|
1068
|
-
/**
|
|
1069
|
-
* 缓存元数据
|
|
1070
|
-
*/
|
|
1071
|
-
setMetadata(target, key, value) {
|
|
1072
|
-
const classCache = this.getClassCache(target);
|
|
1073
|
-
classCache.set(key, value);
|
|
1074
|
-
}
|
|
930
|
+
const ValidFuncs = {
|
|
1075
931
|
/**
|
|
1076
|
-
*
|
|
932
|
+
* Checks value is not empty, undefined, null, '', NaN, [], {} and any empty string(including spaces,
|
|
933
|
+
* tabs, formfeeds, etc.), returns false
|
|
1077
934
|
*/
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
935
|
+
IsNotEmpty: withCache('IsNotEmpty', (value) => {
|
|
936
|
+
return !helper__namespace.isEmpty(value);
|
|
937
|
+
}),
|
|
1082
938
|
/**
|
|
1083
|
-
*
|
|
939
|
+
* Checks if a given value is a real date.
|
|
1084
940
|
*/
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
}
|
|
941
|
+
IsDate: withCache('IsDate', (value) => {
|
|
942
|
+
return helper__namespace.isDate(value);
|
|
943
|
+
}),
|
|
1089
944
|
/**
|
|
1090
|
-
*
|
|
945
|
+
* Checks if the string is an email. If given value is not a string, then it returns false.
|
|
1091
946
|
*/
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
/**
|
|
1099
|
-
* 验证结果缓存
|
|
1100
|
-
*/
|
|
1101
|
-
class ValidationCache {
|
|
1102
|
-
constructor(options) {
|
|
1103
|
-
this.cache = new lruCache.LRUCache({
|
|
1104
|
-
max: (options === null || options === void 0 ? void 0 : options.max) || 5000,
|
|
1105
|
-
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 10, // 10分钟
|
|
1106
|
-
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
1107
|
-
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
1108
|
-
});
|
|
1109
|
-
}
|
|
1110
|
-
static getInstance(options) {
|
|
1111
|
-
if (!ValidationCache.instance) {
|
|
1112
|
-
ValidationCache.instance = new ValidationCache(options);
|
|
1113
|
-
}
|
|
1114
|
-
return ValidationCache.instance;
|
|
1115
|
-
}
|
|
947
|
+
IsEmail: withCache('IsEmail', (value, options) => {
|
|
948
|
+
return classValidator.isEmail(value, options);
|
|
949
|
+
}),
|
|
1116
950
|
/**
|
|
1117
|
-
*
|
|
951
|
+
* Checks if the string is an IP (version 4 or 6). If given value is not a string, then it returns false.
|
|
1118
952
|
*/
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
return `${validator}:${valueStr}:${argsStr}`;
|
|
1123
|
-
}
|
|
953
|
+
IsIP: withCache('IsIP', (value, version) => {
|
|
954
|
+
return classValidator.isIP(value, version);
|
|
955
|
+
}),
|
|
1124
956
|
/**
|
|
1125
|
-
*
|
|
957
|
+
* Checks if the string is a valid phone number.
|
|
958
|
+
* @param value — the potential phone number string to test
|
|
959
|
+
* @param region 2 characters uppercase country code (e.g. DE, US, CH). If users must enter the intl.
|
|
960
|
+
* prefix (e.g. +41), then you may pass "ZZ" or null as region.
|
|
961
|
+
* See [google-libphonenumber, metadata.js:countryCodeToRegionCodeMap on github]
|
|
962
|
+
* {@link https://github.com/ruimarinho/google-libphonenumber/blob/1e46138878cff479aafe2ce62175c6c49cb58720/src/metadata.js#L33}
|
|
1126
963
|
*/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
if (value === undefined)
|
|
1131
|
-
return 'undefined';
|
|
1132
|
-
if (typeof value === 'string')
|
|
1133
|
-
return `s:${value}`;
|
|
1134
|
-
if (typeof value === 'number')
|
|
1135
|
-
return `n:${value}`;
|
|
1136
|
-
if (typeof value === 'boolean')
|
|
1137
|
-
return `b:${value}`;
|
|
1138
|
-
if (Array.isArray(value))
|
|
1139
|
-
return `a:${JSON.stringify(value)}`;
|
|
1140
|
-
if (typeof value === 'object')
|
|
1141
|
-
return `o:${JSON.stringify(value)}`;
|
|
1142
|
-
return String(value);
|
|
1143
|
-
}
|
|
964
|
+
IsPhoneNumber: withCache('IsPhoneNumber', (value, region) => {
|
|
965
|
+
return classValidator.isPhoneNumber(value, region);
|
|
966
|
+
}),
|
|
1144
967
|
/**
|
|
1145
|
-
*
|
|
968
|
+
* Checks if the string is an url. If given value is not a string, then it returns false.
|
|
1146
969
|
*/
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
}
|
|
970
|
+
IsUrl: withCache('IsUrl', (value, options) => {
|
|
971
|
+
return classValidator.isURL(value, options);
|
|
972
|
+
}),
|
|
1151
973
|
/**
|
|
1152
|
-
*
|
|
974
|
+
* check if the string is a hash of type algorithm. Algorithm is one of
|
|
975
|
+
* ['md4', 'md5', 'sha1', 'sha256', 'sha384', 'sha512', 'ripemd128', 'ripemd160', 'tiger128', 'tiger160', 'tiger192', 'crc32', 'crc32b']
|
|
1153
976
|
*/
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
}
|
|
977
|
+
IsHash: withCache('IsHash', (value, algorithm) => {
|
|
978
|
+
return classValidator.isHash(value, algorithm);
|
|
979
|
+
}),
|
|
1158
980
|
/**
|
|
1159
|
-
*
|
|
981
|
+
* Checks if value is a chinese name.
|
|
1160
982
|
*/
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
983
|
+
IsCnName: withCache('IsCnName', (value) => {
|
|
984
|
+
if (!helper__namespace.isString(value)) {
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
return cnName(value);
|
|
988
|
+
}),
|
|
1165
989
|
/**
|
|
1166
|
-
*
|
|
990
|
+
* Checks if value is a idcard number.
|
|
1167
991
|
*/
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
992
|
+
IsIdNumber: withCache('IsIdNumber', (value) => {
|
|
993
|
+
if (!helper__namespace.isString(value)) {
|
|
994
|
+
return false;
|
|
995
|
+
}
|
|
996
|
+
return idNumber(value);
|
|
997
|
+
}),
|
|
1172
998
|
/**
|
|
1173
|
-
*
|
|
999
|
+
* Checks if value is a zipCode.
|
|
1174
1000
|
*/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1001
|
+
IsZipCode: withCache('IsZipCode', (value) => {
|
|
1002
|
+
if (!helper__namespace.isString(value)) {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
return zipCode(value);
|
|
1006
|
+
}),
|
|
1178
1007
|
/**
|
|
1179
|
-
*
|
|
1008
|
+
* Checks if value is a mobile phone number.
|
|
1180
1009
|
*/
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1010
|
+
IsMobile: withCache('IsMobile', (value) => {
|
|
1011
|
+
if (!helper__namespace.isString(value)) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
return mobile(value);
|
|
1015
|
+
}),
|
|
1189
1016
|
/**
|
|
1190
|
-
*
|
|
1017
|
+
* Checks if value is a plateNumber.
|
|
1191
1018
|
*/
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
if (existingValue !== undefined) {
|
|
1196
|
-
this.cache.set(key, existingValue, { ttl });
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
/**
|
|
1201
|
-
* 正则表达式缓存
|
|
1202
|
-
*/
|
|
1203
|
-
class RegexCache {
|
|
1204
|
-
constructor(options) {
|
|
1205
|
-
this.cache = new lruCache.LRUCache({
|
|
1206
|
-
max: (options === null || options === void 0 ? void 0 : options.max) || 200,
|
|
1207
|
-
ttl: (options === null || options === void 0 ? void 0 : options.ttl) || 1000 * 60 * 30, // 30分钟
|
|
1208
|
-
allowStale: (options === null || options === void 0 ? void 0 : options.allowStale) || false,
|
|
1209
|
-
updateAgeOnGet: (options === null || options === void 0 ? void 0 : options.updateAgeOnGet) || true,
|
|
1210
|
-
});
|
|
1211
|
-
}
|
|
1212
|
-
static getInstance(options) {
|
|
1213
|
-
if (!RegexCache.instance) {
|
|
1214
|
-
RegexCache.instance = new RegexCache(options);
|
|
1019
|
+
IsPlateNumber: withCache('IsPlateNumber', (value) => {
|
|
1020
|
+
if (!helper__namespace.isString(value)) {
|
|
1021
|
+
return false;
|
|
1215
1022
|
}
|
|
1216
|
-
return
|
|
1217
|
-
}
|
|
1023
|
+
return plateNumber(value);
|
|
1024
|
+
}),
|
|
1218
1025
|
/**
|
|
1219
|
-
*
|
|
1026
|
+
* Checks if value matches ("===") the comparison.
|
|
1220
1027
|
*/
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if (!regex) {
|
|
1225
|
-
try {
|
|
1226
|
-
regex = new RegExp(pattern, flags);
|
|
1227
|
-
this.cache.set(key, regex);
|
|
1228
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1229
|
-
}
|
|
1230
|
-
catch (_error) {
|
|
1231
|
-
// 如果正则表达式无效,抛出错误
|
|
1232
|
-
throw new Error(`Invalid regex pattern: ${pattern}`);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return regex;
|
|
1236
|
-
}
|
|
1028
|
+
Equals: withCache('Equals', (value, comparison) => {
|
|
1029
|
+
return classValidator.equals(value, comparison);
|
|
1030
|
+
}),
|
|
1237
1031
|
/**
|
|
1238
|
-
*
|
|
1032
|
+
* Checks if value does not match ("!==") the comparison.
|
|
1239
1033
|
*/
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
this.get(pattern, flags);
|
|
1244
|
-
}
|
|
1245
|
-
catch (error) {
|
|
1246
|
-
console.warn(`Failed to precompile regex: ${pattern}`, error);
|
|
1247
|
-
}
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1034
|
+
NotEquals: withCache('NotEquals', (value, comparison) => {
|
|
1035
|
+
return classValidator.notEquals(value, comparison);
|
|
1036
|
+
}),
|
|
1250
1037
|
/**
|
|
1251
|
-
*
|
|
1038
|
+
* Checks if the string contains the seed. If given value is not a string, then it returns false.
|
|
1252
1039
|
*/
|
|
1253
|
-
|
|
1254
|
-
return
|
|
1255
|
-
|
|
1256
|
-
max: this.cache.max,
|
|
1257
|
-
calculatedSize: this.cache.calculatedSize,
|
|
1258
|
-
};
|
|
1259
|
-
}
|
|
1040
|
+
Contains: withCache('Contains', (value, seed) => {
|
|
1041
|
+
return classValidator.contains(value, seed);
|
|
1042
|
+
}),
|
|
1260
1043
|
/**
|
|
1261
|
-
*
|
|
1044
|
+
* Checks if given value is in a array of allowed values.
|
|
1262
1045
|
*/
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* 性能监控
|
|
1269
|
-
*/
|
|
1270
|
-
class PerformanceMonitor {
|
|
1271
|
-
constructor() {
|
|
1272
|
-
this.metrics = new Map();
|
|
1273
|
-
}
|
|
1274
|
-
static getInstance() {
|
|
1275
|
-
if (!PerformanceMonitor.instance) {
|
|
1276
|
-
PerformanceMonitor.instance = new PerformanceMonitor();
|
|
1277
|
-
}
|
|
1278
|
-
return PerformanceMonitor.instance;
|
|
1279
|
-
}
|
|
1046
|
+
IsIn: withCache('IsIn', (value, possibleValues) => {
|
|
1047
|
+
return classValidator.isIn(value, possibleValues);
|
|
1048
|
+
}),
|
|
1280
1049
|
/**
|
|
1281
|
-
*
|
|
1050
|
+
* Checks if given value not in a array of allowed values.
|
|
1282
1051
|
*/
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const duration = performance.now() - start;
|
|
1287
|
-
this.recordMetric(name, duration);
|
|
1288
|
-
};
|
|
1289
|
-
}
|
|
1052
|
+
IsNotIn: withCache('IsNotIn', (value, possibleValues) => {
|
|
1053
|
+
return classValidator.isNotIn(value, possibleValues);
|
|
1054
|
+
}),
|
|
1290
1055
|
/**
|
|
1291
|
-
*
|
|
1056
|
+
* Checks if the first number is greater than or equal to the second.
|
|
1292
1057
|
*/
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1058
|
+
Gt: withCache('Gt', (num, min) => {
|
|
1059
|
+
return helper__namespace.toNumber(num) > min;
|
|
1060
|
+
}),
|
|
1061
|
+
/**
|
|
1062
|
+
* Checks if the first number is less than or equal to the second.
|
|
1063
|
+
*/
|
|
1064
|
+
Lt: withCache('Lt', (num, max) => {
|
|
1065
|
+
return helper__namespace.toNumber(num) < max;
|
|
1066
|
+
}),
|
|
1067
|
+
/**
|
|
1068
|
+
* Checks if the first number is greater than or equal to the second.
|
|
1069
|
+
*/
|
|
1070
|
+
Gte: withCache('Gte', (num, min) => {
|
|
1071
|
+
return helper__namespace.toNumber(num) >= min;
|
|
1072
|
+
}),
|
|
1073
|
+
/**
|
|
1074
|
+
* Checks if the first number is less than or equal to the second.
|
|
1075
|
+
*/
|
|
1076
|
+
Lte: withCache('Lte', (num, max) => {
|
|
1077
|
+
return helper__namespace.toNumber(num) <= max;
|
|
1078
|
+
}),
|
|
1079
|
+
};
|
|
1080
|
+
/**
|
|
1081
|
+
* Helper function to create validator function
|
|
1082
|
+
*/
|
|
1083
|
+
function createValidatorFunction(validatorFunc, defaultMessage) {
|
|
1084
|
+
return (value, options) => {
|
|
1085
|
+
let validOptions;
|
|
1086
|
+
if (typeof options === 'string') {
|
|
1087
|
+
validOptions = { message: options, value: null };
|
|
1303
1088
|
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1089
|
+
else {
|
|
1090
|
+
validOptions = options || { message: defaultMessage, value: null };
|
|
1091
|
+
}
|
|
1092
|
+
if (!validatorFunc(value, validOptions.value)) {
|
|
1093
|
+
// Priority: validOptions.message -> defaultMessage -> fallback error
|
|
1094
|
+
let errorMessage = validOptions.message;
|
|
1095
|
+
// If message is empty or whitespace, use defaultMessage
|
|
1096
|
+
if (!errorMessage || !errorMessage.trim()) {
|
|
1097
|
+
errorMessage = defaultMessage;
|
|
1098
|
+
}
|
|
1099
|
+
// If defaultMessage is also empty, use fallback
|
|
1100
|
+
if (!errorMessage || !errorMessage.trim()) {
|
|
1101
|
+
errorMessage = 'ValidatorError: invalid arguments.';
|
|
1102
|
+
}
|
|
1103
|
+
throw new Error(errorMessage);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Use functions or built-in rules for validation.
|
|
1109
|
+
* Throws error if validation fails.
|
|
1110
|
+
*
|
|
1111
|
+
* @export
|
|
1112
|
+
*/
|
|
1113
|
+
const FunctionValidator = {
|
|
1114
|
+
IsNotEmpty: createValidatorFunction(ValidFuncs.IsNotEmpty, 'Value should not be empty'),
|
|
1115
|
+
IsDate: createValidatorFunction(ValidFuncs.IsDate, 'Must be a valid date'),
|
|
1116
|
+
IsEmail: createValidatorFunction(ValidFuncs.IsEmail, 'Must be a valid email'),
|
|
1117
|
+
IsIP: createValidatorFunction(ValidFuncs.IsIP, 'Must be a valid IP address'),
|
|
1118
|
+
IsPhoneNumber: createValidatorFunction(ValidFuncs.IsPhoneNumber, 'Must be a valid phone number'),
|
|
1119
|
+
IsUrl: createValidatorFunction(ValidFuncs.IsUrl, 'Must be a valid URL'),
|
|
1120
|
+
IsHash: createValidatorFunction(ValidFuncs.IsHash, 'Must be a valid hash'),
|
|
1121
|
+
IsCnName: createValidatorFunction(ValidFuncs.IsCnName, 'Must be a valid Chinese name'),
|
|
1122
|
+
IsIdNumber: createValidatorFunction(ValidFuncs.IsIdNumber, 'Must be a valid ID number'),
|
|
1123
|
+
IsZipCode: createValidatorFunction(ValidFuncs.IsZipCode, 'Must be a valid zip code'),
|
|
1124
|
+
IsMobile: createValidatorFunction(ValidFuncs.IsMobile, 'Must be a valid mobile number'),
|
|
1125
|
+
IsPlateNumber: createValidatorFunction(ValidFuncs.IsPlateNumber, 'Must be a valid plate number'),
|
|
1126
|
+
Equals: createValidatorFunction(ValidFuncs.Equals, 'Values must be equal'),
|
|
1127
|
+
NotEquals: createValidatorFunction(ValidFuncs.NotEquals, 'Values must not be equal'),
|
|
1128
|
+
Contains: createValidatorFunction(ValidFuncs.Contains, 'Value must contain specified substring'),
|
|
1129
|
+
IsIn: createValidatorFunction(ValidFuncs.IsIn, 'Value must be in the allowed list'),
|
|
1130
|
+
IsNotIn: createValidatorFunction(ValidFuncs.IsNotIn, 'Value must not be in the forbidden list'),
|
|
1131
|
+
Gt: createValidatorFunction(ValidFuncs.Gt, 'Value must be greater than threshold'),
|
|
1132
|
+
Lt: createValidatorFunction(ValidFuncs.Lt, 'Value must be less than threshold'),
|
|
1133
|
+
Gte: createValidatorFunction(ValidFuncs.Gte, 'Value must be greater than or equal to threshold'),
|
|
1134
|
+
Lte: createValidatorFunction(ValidFuncs.Lte, 'Value must be less than or equal to threshold'),
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Decorator Factory - Eliminate decorator code duplication
|
|
1139
|
+
* @author richen
|
|
1140
|
+
*/
|
|
1141
|
+
/**
|
|
1142
|
+
* Factory function to create validation decorators
|
|
1143
|
+
* @param options Decorator configuration options
|
|
1144
|
+
* @returns Decorator factory function
|
|
1145
|
+
*/
|
|
1146
|
+
function createValidationDecorator(options) {
|
|
1147
|
+
const { name, validator, defaultMessage, requiresValue = false } = options;
|
|
1148
|
+
return function decoratorFactory(...args) {
|
|
1149
|
+
// Handle parameters: last parameter is ValidationOptions, previous ones are validator function parameters
|
|
1150
|
+
const validationOptions = args[args.length - 1];
|
|
1151
|
+
const validatorArgs = requiresValue ? args.slice(0, -1) : [];
|
|
1152
|
+
return function propertyDecorator(object, propertyName) {
|
|
1153
|
+
// Set property as exportable
|
|
1154
|
+
setExpose(object, propertyName);
|
|
1155
|
+
// Register validation decorator
|
|
1156
|
+
classValidator.registerDecorator({
|
|
1157
|
+
name,
|
|
1158
|
+
target: object.constructor,
|
|
1159
|
+
propertyName,
|
|
1160
|
+
options: validationOptions,
|
|
1161
|
+
constraints: validatorArgs,
|
|
1162
|
+
validator: {
|
|
1163
|
+
validate(value) {
|
|
1164
|
+
try {
|
|
1165
|
+
return validator(value, ...validatorArgs);
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
return false;
|
|
1169
|
+
}
|
|
1170
|
+
},
|
|
1171
|
+
defaultMessage(validationArguments) {
|
|
1172
|
+
const property = validationArguments.property;
|
|
1173
|
+
return defaultMessage
|
|
1174
|
+
? defaultMessage.replace('$property', property)
|
|
1175
|
+
: `Invalid value for ${property}`;
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Create simple validation decorator (no additional parameters required)
|
|
1184
|
+
* @param name Decorator name
|
|
1185
|
+
* @param validator Validation function
|
|
1186
|
+
* @param defaultMessage Default error message
|
|
1187
|
+
* @returns Decorator function
|
|
1188
|
+
*/
|
|
1189
|
+
function createSimpleDecorator(name, validator, defaultMessage) {
|
|
1190
|
+
return createValidationDecorator({
|
|
1191
|
+
name,
|
|
1192
|
+
validator,
|
|
1193
|
+
defaultMessage,
|
|
1194
|
+
requiresValue: false
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Create parameterized validation decorator
|
|
1199
|
+
* @param name Decorator name
|
|
1200
|
+
* @param validator Validation function
|
|
1201
|
+
* @param defaultMessage Default error message
|
|
1202
|
+
* @returns Decorator factory function
|
|
1203
|
+
*/
|
|
1204
|
+
function createParameterizedDecorator(name, validator, defaultMessage) {
|
|
1205
|
+
return createValidationDecorator({
|
|
1206
|
+
name,
|
|
1207
|
+
validator,
|
|
1208
|
+
defaultMessage,
|
|
1209
|
+
requiresValue: true
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
/**
|
|
1214
|
+
* Improved error handling mechanism
|
|
1215
|
+
* @author richen
|
|
1216
|
+
*/
|
|
1217
|
+
/**
|
|
1218
|
+
* Error message internationalization
|
|
1219
|
+
*/
|
|
1220
|
+
const ERROR_MESSAGES = {
|
|
1221
|
+
zh: {
|
|
1222
|
+
// Chinese localization validation
|
|
1223
|
+
IsCnName: '必须是有效的中文姓名',
|
|
1224
|
+
IsIdNumber: '必须是有效的身份证号码',
|
|
1225
|
+
IsZipCode: '必须是有效的邮政编码',
|
|
1226
|
+
IsMobile: '必须是有效的手机号码',
|
|
1227
|
+
IsPlateNumber: '必须是有效的车牌号码',
|
|
1228
|
+
// Basic validation
|
|
1229
|
+
IsNotEmpty: '不能为空',
|
|
1230
|
+
IsDate: '必须是有效的日期',
|
|
1231
|
+
IsEmail: '必须是有效的邮箱地址',
|
|
1232
|
+
IsIP: '必须是有效的IP地址',
|
|
1233
|
+
IsPhoneNumber: '必须是有效的电话号码',
|
|
1234
|
+
IsUrl: '必须是有效的URL地址',
|
|
1235
|
+
IsHash: '必须是有效的哈希值',
|
|
1236
|
+
// Comparison validation
|
|
1237
|
+
Equals: '必须等于 {comparison}',
|
|
1238
|
+
NotEquals: '不能等于 {comparison}',
|
|
1239
|
+
Contains: '必须包含 {seed}',
|
|
1240
|
+
IsIn: '必须是以下值之一: {possibleValues}',
|
|
1241
|
+
IsNotIn: '不能是以下值之一: {possibleValues}',
|
|
1242
|
+
Gt: '必须大于 {min}',
|
|
1243
|
+
Gte: '必须大于或等于 {min}',
|
|
1244
|
+
Lt: '必须小于 {max}',
|
|
1245
|
+
Lte: '必须小于或等于 {max}',
|
|
1246
|
+
// Common errors
|
|
1247
|
+
invalidParameter: '参数 {field} 无效',
|
|
1248
|
+
validationFailed: '验证失败',
|
|
1249
|
+
},
|
|
1250
|
+
en: {
|
|
1251
|
+
// Chinese localization validators
|
|
1252
|
+
IsCnName: 'must be a valid Chinese name',
|
|
1253
|
+
IsIdNumber: 'must be a valid ID number',
|
|
1254
|
+
IsZipCode: 'must be a valid zip code',
|
|
1255
|
+
IsMobile: 'must be a valid mobile number',
|
|
1256
|
+
IsPlateNumber: 'must be a valid plate number',
|
|
1257
|
+
// Basic validators
|
|
1258
|
+
IsNotEmpty: 'should not be empty',
|
|
1259
|
+
IsDate: 'must be a valid date',
|
|
1260
|
+
IsEmail: 'must be a valid email',
|
|
1261
|
+
IsIP: 'must be a valid IP address',
|
|
1262
|
+
IsPhoneNumber: 'must be a valid phone number',
|
|
1263
|
+
IsUrl: 'must be a valid URL',
|
|
1264
|
+
IsHash: 'must be a valid hash',
|
|
1265
|
+
// Comparison validators
|
|
1266
|
+
Equals: 'must equal to {comparison}',
|
|
1267
|
+
NotEquals: 'should not equal to {comparison}',
|
|
1268
|
+
Contains: 'must contain {seed}',
|
|
1269
|
+
IsIn: 'must be one of the following values: {possibleValues}',
|
|
1270
|
+
IsNotIn: 'should not be one of the following values: {possibleValues}',
|
|
1271
|
+
Gt: 'must be greater than {min}',
|
|
1272
|
+
Gte: 'must be greater than or equal to {min}',
|
|
1273
|
+
Lt: 'must be less than {max}',
|
|
1274
|
+
Lte: 'must be less than or equal to {max}',
|
|
1275
|
+
// Common errors
|
|
1276
|
+
invalidParameter: 'invalid parameter {field}',
|
|
1277
|
+
validationFailed: 'validation failed',
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
/**
|
|
1281
|
+
* Enhanced validation error class
|
|
1282
|
+
*/
|
|
1283
|
+
class KoattyValidationError extends Error {
|
|
1284
|
+
constructor(errors, message) {
|
|
1285
|
+
const errorMessage = message || 'Validation failed';
|
|
1286
|
+
super(errorMessage);
|
|
1287
|
+
this.name = 'KoattyValidationError';
|
|
1288
|
+
this.errors = errors;
|
|
1289
|
+
this.statusCode = 400;
|
|
1290
|
+
this.timestamp = new Date();
|
|
1291
|
+
// Ensure correct prototype chain
|
|
1292
|
+
Object.setPrototypeOf(this, KoattyValidationError.prototype);
|
|
1311
1293
|
}
|
|
1312
1294
|
/**
|
|
1313
|
-
*
|
|
1295
|
+
* Get the first error message
|
|
1314
1296
|
*/
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
for (const [name, metric] of this.metrics) {
|
|
1318
|
-
report[name] = {
|
|
1319
|
-
...metric,
|
|
1320
|
-
minTime: metric.minTime === Infinity ? 0 : metric.minTime,
|
|
1321
|
-
avgTimeFormatted: `${metric.avgTime.toFixed(2)}ms`,
|
|
1322
|
-
totalTimeFormatted: `${metric.totalTime.toFixed(2)}ms`,
|
|
1323
|
-
};
|
|
1324
|
-
}
|
|
1325
|
-
return report;
|
|
1297
|
+
getFirstError() {
|
|
1298
|
+
return this.errors[0];
|
|
1326
1299
|
}
|
|
1327
1300
|
/**
|
|
1328
|
-
*
|
|
1301
|
+
* Get errors for a specific field
|
|
1329
1302
|
*/
|
|
1330
|
-
|
|
1331
|
-
return
|
|
1332
|
-
.map(([name, metric]) => ({
|
|
1333
|
-
name,
|
|
1334
|
-
avgTime: metric.avgTime,
|
|
1335
|
-
count: metric.count,
|
|
1336
|
-
}))
|
|
1337
|
-
.sort((a, b) => b.avgTime - a.avgTime)
|
|
1338
|
-
.slice(0, limit);
|
|
1303
|
+
getFieldErrors(field) {
|
|
1304
|
+
return this.errors.filter(error => error.field === field);
|
|
1339
1305
|
}
|
|
1340
1306
|
/**
|
|
1341
|
-
*
|
|
1307
|
+
* Convert to JSON format
|
|
1342
1308
|
*/
|
|
1343
|
-
|
|
1344
|
-
|
|
1309
|
+
toJSON() {
|
|
1310
|
+
return {
|
|
1311
|
+
name: this.name,
|
|
1312
|
+
message: this.message,
|
|
1313
|
+
statusCode: this.statusCode,
|
|
1314
|
+
timestamp: this.timestamp,
|
|
1315
|
+
errors: this.errors
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
/**
|
|
1320
|
+
* Error message formatter
|
|
1321
|
+
*/
|
|
1322
|
+
class ErrorMessageFormatter {
|
|
1323
|
+
constructor(language = 'zh') {
|
|
1324
|
+
this.language = 'zh';
|
|
1325
|
+
this.language = language;
|
|
1345
1326
|
}
|
|
1346
1327
|
/**
|
|
1347
|
-
*
|
|
1328
|
+
* Set language
|
|
1348
1329
|
*/
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1330
|
+
setLanguage(language) {
|
|
1331
|
+
this.language = language;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Format error message
|
|
1335
|
+
*/
|
|
1336
|
+
formatMessage(constraint, field, value, context) {
|
|
1337
|
+
const messages = ERROR_MESSAGES[this.language];
|
|
1338
|
+
let template = messages[constraint] || messages.invalidParameter;
|
|
1339
|
+
// Replace placeholders
|
|
1340
|
+
template = template.replace('{field}', field);
|
|
1341
|
+
// Prioritize values from context, then use passed value
|
|
1342
|
+
if (context) {
|
|
1343
|
+
Object.entries(context).forEach(([key, val]) => {
|
|
1344
|
+
template = template.replace(`{${key}}`, this.formatValue(val));
|
|
1345
|
+
});
|
|
1363
1346
|
}
|
|
1364
|
-
|
|
1347
|
+
// If there's still a {value} placeholder and value was passed, replace it
|
|
1348
|
+
if (value !== undefined && template.includes('{value}')) {
|
|
1349
|
+
template = template.replace('{value}', this.formatValue(value));
|
|
1350
|
+
}
|
|
1351
|
+
return template;
|
|
1365
1352
|
}
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const cachedResult = validationCache.get(validator, value, ...additionalArgs);
|
|
1383
|
-
if (cachedResult !== undefined) {
|
|
1384
|
-
return cachedResult;
|
|
1385
|
-
}
|
|
1386
|
-
// 执行验证并缓存结果
|
|
1387
|
-
const endTimer = performanceMonitor.startTimer(validator);
|
|
1353
|
+
/**
|
|
1354
|
+
* Format value for message display
|
|
1355
|
+
* @private
|
|
1356
|
+
*/
|
|
1357
|
+
formatValue(value) {
|
|
1358
|
+
if (value === null)
|
|
1359
|
+
return 'null';
|
|
1360
|
+
if (value === undefined)
|
|
1361
|
+
return 'undefined';
|
|
1362
|
+
if (typeof value === 'number')
|
|
1363
|
+
return String(value);
|
|
1364
|
+
if (typeof value === 'string')
|
|
1365
|
+
return `"${value}"`;
|
|
1366
|
+
if (Array.isArray(value))
|
|
1367
|
+
return `[${value.map(v => this.formatValue(v)).join(', ')}]`;
|
|
1368
|
+
if (typeof value === 'object') {
|
|
1388
1369
|
try {
|
|
1389
|
-
|
|
1390
|
-
validationCache.set(validator, value, result, ...additionalArgs);
|
|
1391
|
-
// 如果指定了TTL,设置过期时间
|
|
1392
|
-
if (ttl && ttl > 0) {
|
|
1393
|
-
validationCache.setTTL(validator, value, ttl, ...additionalArgs);
|
|
1394
|
-
}
|
|
1395
|
-
return result;
|
|
1370
|
+
return JSON.stringify(value);
|
|
1396
1371
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1372
|
+
catch {
|
|
1373
|
+
// Handle circular references
|
|
1374
|
+
return '[Circular Reference]';
|
|
1399
1375
|
}
|
|
1400
|
-
}
|
|
1401
|
-
return
|
|
1402
|
-
}
|
|
1376
|
+
}
|
|
1377
|
+
return String(value);
|
|
1378
|
+
}
|
|
1403
1379
|
}
|
|
1404
1380
|
/**
|
|
1405
|
-
*
|
|
1381
|
+
* Global error message formatter instance
|
|
1406
1382
|
*/
|
|
1407
|
-
|
|
1383
|
+
const errorFormatter = new ErrorMessageFormatter();
|
|
1384
|
+
/**
|
|
1385
|
+
* Set global language
|
|
1386
|
+
*/
|
|
1387
|
+
function setValidationLanguage(language) {
|
|
1388
|
+
errorFormatter.setLanguage(language);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Create validation error
|
|
1392
|
+
*/
|
|
1393
|
+
function createValidationError(field, value, constraint, customMessage, context) {
|
|
1394
|
+
const message = customMessage || errorFormatter.formatMessage(constraint, field, value, context);
|
|
1408
1395
|
return {
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1396
|
+
field,
|
|
1397
|
+
value,
|
|
1398
|
+
constraint,
|
|
1399
|
+
message,
|
|
1400
|
+
context
|
|
1413
1401
|
};
|
|
1414
1402
|
}
|
|
1415
1403
|
/**
|
|
1416
|
-
*
|
|
1404
|
+
* Create validation errors in batch
|
|
1417
1405
|
*/
|
|
1418
|
-
function
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
{ pattern: '^[\u4e00-\u9fa5]{2,8}$' }, // 中文姓名
|
|
1422
|
-
{ pattern: '^1[3-9]\\d{9}$' }, // 手机号
|
|
1423
|
-
{ pattern: '^\\d{6}$' }, // 邮政编码
|
|
1424
|
-
{ pattern: '^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼]' }, // 车牌号开头
|
|
1425
|
-
];
|
|
1426
|
-
regexCache.precompile(commonPatterns);
|
|
1406
|
+
function createValidationErrors(errors) {
|
|
1407
|
+
const validationErrors = errors.map(error => createValidationError(error.field, error.value, error.constraint, error.message, error.context));
|
|
1408
|
+
return new KoattyValidationError(validationErrors);
|
|
1427
1409
|
}
|
|
1410
|
+
|
|
1428
1411
|
/**
|
|
1429
|
-
*
|
|
1412
|
+
* Refactored decorator definitions - Using factory functions to eliminate duplication
|
|
1413
|
+
* @author richen
|
|
1430
1414
|
*/
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1415
|
+
// Chinese localization validation decorators
|
|
1416
|
+
const IsCnName = createSimpleDecorator('IsCnName', (value) => helper__namespace.isString(value) && cnName(value), 'must be a valid Chinese name');
|
|
1417
|
+
const IsIdNumber = createSimpleDecorator('IsIdNumber', (value) => helper__namespace.isString(value) && idNumber(value), 'must be a valid ID number');
|
|
1418
|
+
const IsZipCode = createSimpleDecorator('IsZipCode', (value) => helper__namespace.isString(value) && zipCode(value), 'must be a valid zip code');
|
|
1419
|
+
const IsMobile = createSimpleDecorator('IsMobile', (value) => helper__namespace.isString(value) && mobile(value), 'must be a valid mobile number');
|
|
1420
|
+
const IsPlateNumber = createSimpleDecorator('IsPlateNumber', (value) => helper__namespace.isString(value) && plateNumber(value), 'must be a valid plate number');
|
|
1421
|
+
// Basic validation decorators
|
|
1422
|
+
const IsNotEmpty = createSimpleDecorator('IsNotEmpty', (value) => !helper__namespace.isEmpty(value), 'should not be empty');
|
|
1423
|
+
const IsDate = createSimpleDecorator('IsDate', (value) => helper__namespace.isDate(value), 'must be a valid date');
|
|
1424
|
+
// Parameterized validation decorators
|
|
1425
|
+
const Equals = createParameterizedDecorator('Equals', (value, comparison) => value === comparison, 'must equal to $constraint1');
|
|
1426
|
+
const NotEquals = createParameterizedDecorator('NotEquals', (value, comparison) => value !== comparison, 'should not equal to $constraint1');
|
|
1427
|
+
const Contains = createParameterizedDecorator('Contains', (value, seed) => helper__namespace.isString(value) && value.includes(seed), 'must contain $constraint1');
|
|
1428
|
+
const IsIn = createParameterizedDecorator('IsIn', (value, possibleValues) => possibleValues.includes(value), 'must be one of the following values: $constraint1');
|
|
1429
|
+
const IsNotIn = createParameterizedDecorator('IsNotIn', (value, possibleValues) => !possibleValues.includes(value), 'should not be one of the following values: $constraint1');
|
|
1430
|
+
// 数值比较装饰器
|
|
1431
|
+
const Gt = createParameterizedDecorator('Gt', (value, min) => helper__namespace.toNumber(value) > min, 'must be greater than $constraint1');
|
|
1432
|
+
const Gte = createParameterizedDecorator('Gte', (value, min) => helper__namespace.toNumber(value) >= min, 'must be greater than or equal to $constraint1');
|
|
1433
|
+
const Lt = createParameterizedDecorator('Lt', (value, max) => helper__namespace.toNumber(value) < max, 'must be less than $constraint1');
|
|
1434
|
+
const Lte = createParameterizedDecorator('Lte', (value, max) => helper__namespace.toNumber(value) <= max, 'must be less than or equal to $constraint1');
|
|
1435
|
+
// 复杂验证装饰器(需要特殊处理)
|
|
1436
|
+
function IsEmail(options, validationOptions) {
|
|
1437
|
+
return createParameterizedDecorator('IsEmail', (value) => classValidator.isEmail(value, options), 'must be a valid email')(validationOptions);
|
|
1438
|
+
}
|
|
1439
|
+
function IsIP(version, validationOptions) {
|
|
1440
|
+
return createParameterizedDecorator('IsIP', (value) => classValidator.isIP(value, version), 'must be a valid IP address')(validationOptions);
|
|
1441
|
+
}
|
|
1442
|
+
function IsPhoneNumber(region, validationOptions) {
|
|
1443
|
+
return createParameterizedDecorator('IsPhoneNumber', (value) => classValidator.isPhoneNumber(value, region), 'must be a valid phone number')(validationOptions);
|
|
1435
1444
|
}
|
|
1445
|
+
function IsUrl(options, validationOptions) {
|
|
1446
|
+
return createParameterizedDecorator('IsUrl', (value) => classValidator.isURL(value, options), 'must be a valid URL')(validationOptions);
|
|
1447
|
+
}
|
|
1448
|
+
function IsHash(algorithm, validationOptions) {
|
|
1449
|
+
return createParameterizedDecorator('IsHash', (value) => classValidator.isHash(value, algorithm), 'must be a valid hash')(validationOptions);
|
|
1450
|
+
}
|
|
1451
|
+
// Basic utility decorators (migrated from original decorator.ts)
|
|
1436
1452
|
/**
|
|
1437
|
-
*
|
|
1453
|
+
* Mark property as exportable
|
|
1438
1454
|
*/
|
|
1439
|
-
function
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1455
|
+
function Expose() {
|
|
1456
|
+
return function (object, propertyName) {
|
|
1457
|
+
setExpose(object, propertyName);
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Alias for Expose
|
|
1462
|
+
*/
|
|
1463
|
+
function IsDefined() {
|
|
1464
|
+
return function (object, propertyName) {
|
|
1465
|
+
setExpose(object, propertyName);
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Parameter validation decorator
|
|
1470
|
+
*/
|
|
1471
|
+
function Valid(rule, options) {
|
|
1472
|
+
return function (object, propertyName, parameterIndex) {
|
|
1473
|
+
// Keep consistent with original implementation
|
|
1474
|
+
const existingRules = Reflect.getOwnMetadata("validate", object, propertyName) || {};
|
|
1475
|
+
existingRules[parameterIndex] = { rule, options };
|
|
1476
|
+
Reflect.defineMetadata("validate", existingRules, object, propertyName);
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Synchronous validation function - Executes the actual validation logic
|
|
1481
|
+
* @param args Method parameters
|
|
1482
|
+
* @param paramTypes Parameter type metadata
|
|
1483
|
+
* @returns Validated parameters and validation targets
|
|
1484
|
+
*/
|
|
1485
|
+
async function checkValidated(args, paramTypes) {
|
|
1486
|
+
const validationTargets = [];
|
|
1487
|
+
// Validate each parameter
|
|
1488
|
+
for (let i = 0; i < args.length; i++) {
|
|
1489
|
+
const arg = args[i];
|
|
1490
|
+
const paramType = paramTypes[i];
|
|
1491
|
+
// If it's a class type and not a basic type, perform validation
|
|
1492
|
+
if (paramType && typeof paramType === 'function' &&
|
|
1493
|
+
paramType !== String && paramType !== Number &&
|
|
1494
|
+
paramType !== Boolean && paramType !== Array &&
|
|
1495
|
+
paramType !== Object && paramType !== Date) {
|
|
1496
|
+
try {
|
|
1497
|
+
// If parameter is not an instance of the target type, convert it to an instance
|
|
1498
|
+
let validationTarget = arg;
|
|
1499
|
+
if (!(arg instanceof paramType)) {
|
|
1500
|
+
validationTarget = Object.assign(new paramType(), arg);
|
|
1501
|
+
}
|
|
1502
|
+
const errors = await classValidator.validate(validationTarget);
|
|
1503
|
+
if (errors.length > 0) {
|
|
1504
|
+
throw createValidationErrors(errors.map(e => ({
|
|
1505
|
+
field: e.property,
|
|
1506
|
+
value: e.value,
|
|
1507
|
+
constraint: Object.keys(e.constraints || {})[0] || 'unknown',
|
|
1508
|
+
message: Object.values(e.constraints || {})[0] || 'Validation failed',
|
|
1509
|
+
context: e.constraints
|
|
1510
|
+
})));
|
|
1511
|
+
}
|
|
1512
|
+
validationTargets.push(validationTarget);
|
|
1513
|
+
}
|
|
1514
|
+
catch (error) {
|
|
1515
|
+
// If validation fails, rethrow the error
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
else {
|
|
1520
|
+
validationTargets.push(arg);
|
|
1521
|
+
}
|
|
1447
1522
|
}
|
|
1523
|
+
return { validatedArgs: args, validationTargets };
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Method validation decorator
|
|
1527
|
+
* Automatically validates DTO objects in method parameters
|
|
1528
|
+
* @param isAsync Whether to use async validation mode, default is true
|
|
1529
|
+
* - true: Async mode, validation is handled by IOC container in the framework (suitable for scenarios where parameter values need to be obtained asynchronously)
|
|
1530
|
+
* - false: Sync mode, validation is performed immediately when the method is called (suitable for scenarios where parameter values are already prepared)
|
|
1531
|
+
*/
|
|
1532
|
+
function Validated(isAsync = true) {
|
|
1533
|
+
return function (target, propertyKey, descriptor) {
|
|
1534
|
+
if (isAsync) {
|
|
1535
|
+
// Async mode: Save metadata, validation will be performed by the framework after async parameter retrieval
|
|
1536
|
+
koatty_container.IOCContainer.savePropertyData(PARAM_CHECK_KEY, {
|
|
1537
|
+
dtoCheck: 1
|
|
1538
|
+
}, target, propertyKey);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
// Sync mode: Perform validation immediately when the method is called
|
|
1542
|
+
const originalMethod = descriptor.value;
|
|
1543
|
+
descriptor.value = async function (...args) {
|
|
1544
|
+
// Get parameter type metadata
|
|
1545
|
+
const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey) || [];
|
|
1546
|
+
// Execute validation
|
|
1547
|
+
await checkValidated(args, paramTypes);
|
|
1548
|
+
// Execute original method
|
|
1549
|
+
return originalMethod.apply(this, args);
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
return descriptor;
|
|
1553
|
+
};
|
|
1448
1554
|
}
|
|
1449
1555
|
|
|
1450
1556
|
exports.ClassValidator = ClassValidator;
|
|
@@ -1478,11 +1584,13 @@ exports.Lte = Lte;
|
|
|
1478
1584
|
exports.NotEquals = NotEquals;
|
|
1479
1585
|
exports.PARAM_CHECK_KEY = PARAM_CHECK_KEY;
|
|
1480
1586
|
exports.PARAM_RULE_KEY = PARAM_RULE_KEY;
|
|
1587
|
+
exports.PARAM_TYPE_KEY = PARAM_TYPE_KEY;
|
|
1481
1588
|
exports.Valid = Valid;
|
|
1482
1589
|
exports.ValidFuncs = ValidFuncs;
|
|
1483
1590
|
exports.Validated = Validated;
|
|
1484
1591
|
exports.cached = cached;
|
|
1485
1592
|
exports.checkParamsType = checkParamsType;
|
|
1593
|
+
exports.checkValidated = checkValidated;
|
|
1486
1594
|
exports.clearAllCaches = clearAllCaches;
|
|
1487
1595
|
exports.configureCaches = configureCaches;
|
|
1488
1596
|
exports.convertDtoParamsType = convertDtoParamsType;
|