koatty_validation 1.4.0 → 1.6.0

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