schema-dsl 1.0.0 → 1.0.4
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 +263 -529
- package/README.md +814 -896
- package/STATUS.md +135 -2
- package/docs/INDEX.md +1 -2
- package/docs/api-reference.md +1 -292
- package/docs/custom-extensions-guide.md +411 -0
- package/docs/enum.md +475 -0
- package/docs/i18n.md +394 -0
- package/docs/performance-benchmark-report.md +179 -0
- package/docs/plugin-system.md +8 -8
- package/docs/typescript-guide.md +554 -0
- package/docs/validate-async.md +1 -1
- package/docs/validation-rules-v1.0.2.md +1601 -0
- package/examples/README.md +81 -0
- package/examples/enum.examples.js +324 -0
- package/examples/express-integration.js +54 -54
- package/examples/i18n-full-demo.js +15 -24
- package/examples/schema-utils-chaining.examples.js +2 -2
- package/examples/slug.examples.js +179 -0
- package/index.d.ts +246 -17
- package/index.js +30 -34
- package/lib/config/constants.js +1 -1
- package/lib/config/patterns/common.js +47 -0
- package/lib/config/patterns/index.js +2 -1
- package/lib/core/DslBuilder.js +500 -8
- package/lib/core/StringExtensions.js +31 -0
- package/lib/core/Validator.js +42 -15
- package/lib/errors/ValidationError.js +3 -3
- package/lib/locales/en-US.js +79 -19
- package/lib/locales/es-ES.js +60 -19
- package/lib/locales/fr-FR.js +84 -43
- package/lib/locales/ja-JP.js +83 -42
- package/lib/locales/zh-CN.js +32 -0
- package/lib/validators/CustomKeywords.js +405 -0
- package/package.json +1 -1
- package/.github/CODE_OF_CONDUCT.md +0 -45
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -57
- package/.github/ISSUE_TEMPLATE/config.yml +0 -11
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -45
- package/.github/ISSUE_TEMPLATE/question.md +0 -31
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -70
- package/.github/SECURITY.md +0 -184
- package/.github/workflows/ci.yml +0 -33
- package/plugins/custom-format.js +0 -101
- package/plugins/custom-validator.js +0 -200
package/lib/core/DslBuilder.js
CHANGED
|
@@ -97,12 +97,36 @@ class DslBuilder {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
101
|
-
if (dsl.includes('|')
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
100
|
+
// 处理枚举(支持多种格式)
|
|
101
|
+
if (dsl.includes('|')) {
|
|
102
|
+
let enumType = 'string'; // 默认字符串
|
|
103
|
+
let enumValues = dsl;
|
|
104
|
+
|
|
105
|
+
// 识别 enum:type:values 或 enum:values 格式
|
|
106
|
+
if (dsl.startsWith('enum:')) {
|
|
107
|
+
const parts = dsl.slice(5).split(':');
|
|
108
|
+
|
|
109
|
+
if (parts.length === 2) {
|
|
110
|
+
// enum:type:values
|
|
111
|
+
enumType = parts[0];
|
|
112
|
+
enumValues = parts[1];
|
|
113
|
+
} else if (parts.length === 1) {
|
|
114
|
+
// enum:values (默认 string)
|
|
115
|
+
enumValues = parts[0];
|
|
116
|
+
}
|
|
117
|
+
} else if (dsl.includes(':') && !this._isKnownType(dsl.split(':')[0])) {
|
|
118
|
+
// 如果有冒号但不是已知类型(如 string:3-32),不作为枚举
|
|
119
|
+
// 让后续逻辑处理
|
|
120
|
+
} else {
|
|
121
|
+
// 简写形式:value1|value2
|
|
122
|
+
// 自动识别类型
|
|
123
|
+
enumType = this._detectEnumType(enumValues);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 如果是枚举,解析值
|
|
127
|
+
if (enumValues.includes('|')) {
|
|
128
|
+
return this._parseEnum(enumType, enumValues);
|
|
129
|
+
}
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
// 处理类型:约束格式
|
|
@@ -230,7 +254,14 @@ class DslBuilder {
|
|
|
230
254
|
'hexColor': { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', _customMessages: { 'pattern': 'pattern.hexColor' } },
|
|
231
255
|
'macAddress': { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', _customMessages: { 'pattern': 'pattern.macAddress' } },
|
|
232
256
|
'cron': { type: 'string', pattern: '^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) (\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) (\\*|([0-6])|\\*\\/([0-6]))$', _customMessages: { 'pattern': 'pattern.cron' } },
|
|
233
|
-
'
|
|
257
|
+
'slug': { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$', _customMessages: { 'pattern': 'pattern.slug' } },
|
|
258
|
+
'any': {},
|
|
259
|
+
// v1.0.2 新增类型
|
|
260
|
+
'alphanum': { type: 'string', alphanum: true },
|
|
261
|
+
'lower': { type: 'string', lowercase: true },
|
|
262
|
+
'upper': { type: 'string', uppercase: true },
|
|
263
|
+
'json': { type: 'string', jsonString: true },
|
|
264
|
+
'port': { type: 'integer', port: true }
|
|
234
265
|
};
|
|
235
266
|
|
|
236
267
|
return typeMap[type] || { type: 'string' };
|
|
@@ -260,8 +291,10 @@ class DslBuilder {
|
|
|
260
291
|
const value = constraint.trim();
|
|
261
292
|
if (value) {
|
|
262
293
|
if (type === 'string') {
|
|
263
|
-
|
|
294
|
+
// 🔴 String单值 = 精确长度(常用于验证码、国家代码等)
|
|
295
|
+
result.exactLength = parseInt(value);
|
|
264
296
|
} else {
|
|
297
|
+
// Number单值 = 最大值(符合直觉:不超过某值)
|
|
265
298
|
result.maximum = parseFloat(value);
|
|
266
299
|
}
|
|
267
300
|
}
|
|
@@ -271,6 +304,76 @@ class DslBuilder {
|
|
|
271
304
|
return result;
|
|
272
305
|
}
|
|
273
306
|
|
|
307
|
+
/**
|
|
308
|
+
* 检查是否为已知类型
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
_isKnownType(type) {
|
|
312
|
+
const knownTypes = [
|
|
313
|
+
'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
|
|
314
|
+
'email', 'url', 'uuid', 'date', 'datetime', 'time', 'ipv4', 'ipv6',
|
|
315
|
+
'binary', 'objectId', 'hexColor', 'macAddress', 'cron', 'any',
|
|
316
|
+
'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport',
|
|
317
|
+
// v1.0.2 新增
|
|
318
|
+
'alphanum', 'lower', 'upper', 'json', 'port'
|
|
319
|
+
];
|
|
320
|
+
return knownTypes.includes(type);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 自动检测枚举类型
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
327
|
+
_detectEnumType(enumValues) {
|
|
328
|
+
const values = enumValues.split('|').map(v => v.trim());
|
|
329
|
+
|
|
330
|
+
// 检查是否全部为布尔值
|
|
331
|
+
const allBoolean = values.every(v => v === 'true' || v === 'false');
|
|
332
|
+
if (allBoolean) return 'boolean';
|
|
333
|
+
|
|
334
|
+
// 检查是否全部为数字
|
|
335
|
+
const allNumber = values.every(v => !isNaN(parseFloat(v)) && isFinite(v));
|
|
336
|
+
if (allNumber) return 'number';
|
|
337
|
+
|
|
338
|
+
// 默认字符串
|
|
339
|
+
return 'string';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 解析枚举值
|
|
344
|
+
* @private
|
|
345
|
+
*/
|
|
346
|
+
_parseEnum(enumType, enumValues) {
|
|
347
|
+
let values = enumValues.split('|').map(v => v.trim());
|
|
348
|
+
|
|
349
|
+
// 类型转换
|
|
350
|
+
if (enumType === 'boolean') {
|
|
351
|
+
values = values.map(v => {
|
|
352
|
+
if (v === 'true') return true;
|
|
353
|
+
if (v === 'false') return false;
|
|
354
|
+
throw new Error(`Invalid boolean enum value: ${v}. Must be 'true' or 'false'`);
|
|
355
|
+
});
|
|
356
|
+
return { type: 'boolean', enum: values };
|
|
357
|
+
} else if (enumType === 'number') {
|
|
358
|
+
values = values.map(v => {
|
|
359
|
+
const num = parseFloat(v);
|
|
360
|
+
if (isNaN(num)) throw new Error(`Invalid number enum value: ${v}`);
|
|
361
|
+
return num;
|
|
362
|
+
});
|
|
363
|
+
return { type: 'number', enum: values };
|
|
364
|
+
} else if (enumType === 'integer') {
|
|
365
|
+
values = values.map(v => {
|
|
366
|
+
const num = parseInt(v, 10);
|
|
367
|
+
if (isNaN(num)) throw new Error(`Invalid integer enum value: ${v}`);
|
|
368
|
+
return num;
|
|
369
|
+
});
|
|
370
|
+
return { type: 'integer', enum: values };
|
|
371
|
+
} else {
|
|
372
|
+
// 字符串枚举(默认)
|
|
373
|
+
return { type: 'string', enum: values };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
274
377
|
/**
|
|
275
378
|
* 添加正则表达式验证
|
|
276
379
|
* @param {RegExp|string} regex - 正则表达式
|
|
@@ -734,6 +837,395 @@ class DslBuilder {
|
|
|
734
837
|
}
|
|
735
838
|
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
736
839
|
}
|
|
840
|
+
|
|
841
|
+
// ========== v1.0.2 新增验证器方法 ==========
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* String 最小长度(使用AJV原生minLength)
|
|
845
|
+
* @param {number} n - 最小长度
|
|
846
|
+
* @returns {DslBuilder}
|
|
847
|
+
*
|
|
848
|
+
* @example
|
|
849
|
+
* dsl('string!').min(3) // 最少3个字符
|
|
850
|
+
*/
|
|
851
|
+
min(n) {
|
|
852
|
+
if (this._baseSchema.type !== 'string') {
|
|
853
|
+
throw new Error('min() only applies to string type');
|
|
854
|
+
}
|
|
855
|
+
this._baseSchema.minLength = n;
|
|
856
|
+
return this;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* String 最大长度(使用AJV原生maxLength)
|
|
861
|
+
* @param {number} n - 最大长度
|
|
862
|
+
* @returns {DslBuilder}
|
|
863
|
+
*
|
|
864
|
+
* @example
|
|
865
|
+
* dsl('string!').max(32) // 最多32个字符
|
|
866
|
+
*/
|
|
867
|
+
max(n) {
|
|
868
|
+
if (this._baseSchema.type !== 'string') {
|
|
869
|
+
throw new Error('max() only applies to string type');
|
|
870
|
+
}
|
|
871
|
+
this._baseSchema.maxLength = n;
|
|
872
|
+
return this;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/**
|
|
876
|
+
* String 精确长度
|
|
877
|
+
* @param {number} n - 精确长度
|
|
878
|
+
* @returns {DslBuilder}
|
|
879
|
+
*
|
|
880
|
+
* @example
|
|
881
|
+
* dsl('string!').length(11) // 必须是11个字符
|
|
882
|
+
*/
|
|
883
|
+
length(n) {
|
|
884
|
+
if (this._baseSchema.type !== 'string') {
|
|
885
|
+
throw new Error('length() only applies to string type');
|
|
886
|
+
}
|
|
887
|
+
this._baseSchema.exactLength = n;
|
|
888
|
+
return this;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* String 只能包含字母和数字
|
|
893
|
+
* @returns {DslBuilder}
|
|
894
|
+
*
|
|
895
|
+
* @example
|
|
896
|
+
* dsl('string!').alphanum() // 只能是字母和数字
|
|
897
|
+
*/
|
|
898
|
+
alphanum() {
|
|
899
|
+
if (this._baseSchema.type !== 'string') {
|
|
900
|
+
throw new Error('alphanum() only applies to string type');
|
|
901
|
+
}
|
|
902
|
+
this._baseSchema.alphanum = true;
|
|
903
|
+
return this;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* String 不能包含前后空格
|
|
908
|
+
* @returns {DslBuilder}
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* dsl('string!').trim() // 不能有前后空格
|
|
912
|
+
*/
|
|
913
|
+
trim() {
|
|
914
|
+
if (this._baseSchema.type !== 'string') {
|
|
915
|
+
throw new Error('trim() only applies to string type');
|
|
916
|
+
}
|
|
917
|
+
this._baseSchema.trim = true;
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* String 必须是小写
|
|
923
|
+
* @returns {DslBuilder}
|
|
924
|
+
*
|
|
925
|
+
* @example
|
|
926
|
+
* dsl('string!').lowercase() // 必须全小写
|
|
927
|
+
*/
|
|
928
|
+
lowercase() {
|
|
929
|
+
if (this._baseSchema.type !== 'string') {
|
|
930
|
+
throw new Error('lowercase() only applies to string type');
|
|
931
|
+
}
|
|
932
|
+
this._baseSchema.lowercase = true;
|
|
933
|
+
return this;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* String 必须是大写
|
|
938
|
+
* @returns {DslBuilder}
|
|
939
|
+
*
|
|
940
|
+
* @example
|
|
941
|
+
* dsl('string!').uppercase() // 必须全大写
|
|
942
|
+
*/
|
|
943
|
+
uppercase() {
|
|
944
|
+
if (this._baseSchema.type !== 'string') {
|
|
945
|
+
throw new Error('uppercase() only applies to string type');
|
|
946
|
+
}
|
|
947
|
+
this._baseSchema.uppercase = true;
|
|
948
|
+
return this;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Number 小数位数限制
|
|
953
|
+
* @param {number} n - 最大小数位数
|
|
954
|
+
* @returns {DslBuilder}
|
|
955
|
+
*
|
|
956
|
+
* @example
|
|
957
|
+
* dsl('number!').precision(2) // 最多2位小数
|
|
958
|
+
*/
|
|
959
|
+
precision(n) {
|
|
960
|
+
if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
|
|
961
|
+
throw new Error('precision() only applies to number type');
|
|
962
|
+
}
|
|
963
|
+
this._baseSchema.precision = n;
|
|
964
|
+
return this;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
/**
|
|
968
|
+
* Number 倍数验证(使用AJV原生multipleOf)
|
|
969
|
+
* @param {number} n - 必须是此数的倍数
|
|
970
|
+
* @returns {DslBuilder}
|
|
971
|
+
*
|
|
972
|
+
* @example
|
|
973
|
+
* dsl('number!').multiple(5) // 必须是5的倍数
|
|
974
|
+
*/
|
|
975
|
+
multiple(n) {
|
|
976
|
+
if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
|
|
977
|
+
throw new Error('multiple() only applies to number type');
|
|
978
|
+
}
|
|
979
|
+
this._baseSchema.multipleOf = n;
|
|
980
|
+
return this;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Number 端口号验证(1-65535)
|
|
985
|
+
* @returns {DslBuilder}
|
|
986
|
+
*
|
|
987
|
+
* @example
|
|
988
|
+
* dsl('integer!').port() // 必须是有效端口号
|
|
989
|
+
*/
|
|
990
|
+
port() {
|
|
991
|
+
if (this._baseSchema.type !== 'number' && this._baseSchema.type !== 'integer') {
|
|
992
|
+
throw new Error('port() only applies to number type');
|
|
993
|
+
}
|
|
994
|
+
this._baseSchema.port = true;
|
|
995
|
+
return this;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Object 要求所有属性都必须存在
|
|
1000
|
+
* @returns {DslBuilder}
|
|
1001
|
+
*
|
|
1002
|
+
* @example
|
|
1003
|
+
* dsl({ name: 'string', age: 'number' }).requireAll()
|
|
1004
|
+
*/
|
|
1005
|
+
requireAll() {
|
|
1006
|
+
if (this._baseSchema.type !== 'object') {
|
|
1007
|
+
throw new Error('requireAll() only applies to object type');
|
|
1008
|
+
}
|
|
1009
|
+
this._baseSchema.requiredAll = true;
|
|
1010
|
+
return this;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Object 严格模式,不允许额外属性
|
|
1015
|
+
* @returns {DslBuilder}
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* dsl({ name: 'string!' }).strict()
|
|
1019
|
+
*/
|
|
1020
|
+
strict() {
|
|
1021
|
+
if (this._baseSchema.type !== 'object') {
|
|
1022
|
+
throw new Error('strict() only applies to object type');
|
|
1023
|
+
}
|
|
1024
|
+
this._baseSchema.strictSchema = true;
|
|
1025
|
+
return this;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/**
|
|
1029
|
+
* Array 不允许稀疏数组
|
|
1030
|
+
* @returns {DslBuilder}
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* dsl('array<string>').noSparse()
|
|
1034
|
+
*/
|
|
1035
|
+
noSparse() {
|
|
1036
|
+
if (this._baseSchema.type !== 'array') {
|
|
1037
|
+
throw new Error('noSparse() only applies to array type');
|
|
1038
|
+
}
|
|
1039
|
+
this._baseSchema.noSparse = true;
|
|
1040
|
+
return this;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Array 必须包含指定元素
|
|
1045
|
+
* @param {Array} items - 必须包含的元素
|
|
1046
|
+
* @returns {DslBuilder}
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* dsl('array<string>').includesRequired(['admin', 'user'])
|
|
1050
|
+
*/
|
|
1051
|
+
includesRequired(items) {
|
|
1052
|
+
if (this._baseSchema.type !== 'array') {
|
|
1053
|
+
throw new Error('includesRequired() only applies to array type');
|
|
1054
|
+
}
|
|
1055
|
+
if (!Array.isArray(items)) {
|
|
1056
|
+
throw new Error('includesRequired() requires an array parameter');
|
|
1057
|
+
}
|
|
1058
|
+
this._baseSchema.includesRequired = items;
|
|
1059
|
+
return this;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/**
|
|
1063
|
+
* Date 自定义日期格式验证
|
|
1064
|
+
* @param {string} fmt - 日期格式(YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY, ISO8601)
|
|
1065
|
+
* @returns {DslBuilder}
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* dsl('string!').dateFormat('YYYY-MM-DD')
|
|
1069
|
+
*/
|
|
1070
|
+
dateFormat(fmt) {
|
|
1071
|
+
if (this._baseSchema.type !== 'string') {
|
|
1072
|
+
throw new Error('dateFormat() only applies to string type');
|
|
1073
|
+
}
|
|
1074
|
+
this._baseSchema.dateFormat = fmt;
|
|
1075
|
+
return this;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* Date 必须晚于指定日期
|
|
1080
|
+
* @param {string} date - 比较日期
|
|
1081
|
+
* @returns {DslBuilder}
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* dsl('date!').after('2024-01-01')
|
|
1085
|
+
*/
|
|
1086
|
+
after(date) {
|
|
1087
|
+
if (this._baseSchema.type !== 'string') {
|
|
1088
|
+
throw new Error('after() only applies to string type');
|
|
1089
|
+
}
|
|
1090
|
+
this._baseSchema.dateGreater = date;
|
|
1091
|
+
return this;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* Date 必须早于指定日期
|
|
1096
|
+
* @param {string} date - 比较日期
|
|
1097
|
+
* @returns {DslBuilder}
|
|
1098
|
+
*
|
|
1099
|
+
* @example
|
|
1100
|
+
* dsl('date!').before('2025-12-31')
|
|
1101
|
+
*/
|
|
1102
|
+
before(date) {
|
|
1103
|
+
if (this._baseSchema.type !== 'string') {
|
|
1104
|
+
throw new Error('before() only applies to string type');
|
|
1105
|
+
}
|
|
1106
|
+
this._baseSchema.dateLess = date;
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Pattern 域名验证
|
|
1112
|
+
* @returns {DslBuilder}
|
|
1113
|
+
*
|
|
1114
|
+
* @example
|
|
1115
|
+
* dsl('string!').domain()
|
|
1116
|
+
*/
|
|
1117
|
+
domain() {
|
|
1118
|
+
if (this._baseSchema.type !== 'string') {
|
|
1119
|
+
throw new Error('domain() only applies to string type');
|
|
1120
|
+
}
|
|
1121
|
+
const config = patterns.common.domain;
|
|
1122
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* Pattern IP地址验证(IPv4或IPv6)
|
|
1127
|
+
* @returns {DslBuilder}
|
|
1128
|
+
*
|
|
1129
|
+
* @example
|
|
1130
|
+
* dsl('string!').ip()
|
|
1131
|
+
*/
|
|
1132
|
+
ip() {
|
|
1133
|
+
if (this._baseSchema.type !== 'string') {
|
|
1134
|
+
throw new Error('ip() only applies to string type');
|
|
1135
|
+
}
|
|
1136
|
+
const config = patterns.common.ip;
|
|
1137
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* Pattern Base64编码验证
|
|
1142
|
+
* @returns {DslBuilder}
|
|
1143
|
+
*
|
|
1144
|
+
* @example
|
|
1145
|
+
* dsl('string!').base64()
|
|
1146
|
+
*/
|
|
1147
|
+
base64() {
|
|
1148
|
+
if (this._baseSchema.type !== 'string') {
|
|
1149
|
+
throw new Error('base64() only applies to string type');
|
|
1150
|
+
}
|
|
1151
|
+
const config = patterns.common.base64;
|
|
1152
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Pattern JWT令牌验证
|
|
1157
|
+
* @returns {DslBuilder}
|
|
1158
|
+
*
|
|
1159
|
+
* @example
|
|
1160
|
+
* dsl('string!').jwt()
|
|
1161
|
+
*/
|
|
1162
|
+
jwt() {
|
|
1163
|
+
if (this._baseSchema.type !== 'string') {
|
|
1164
|
+
throw new Error('jwt() only applies to string type');
|
|
1165
|
+
}
|
|
1166
|
+
const config = patterns.common.jwt;
|
|
1167
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Pattern JSON字符串验证
|
|
1172
|
+
* @returns {DslBuilder}
|
|
1173
|
+
*
|
|
1174
|
+
* @example
|
|
1175
|
+
* dsl('string!').json()
|
|
1176
|
+
*/
|
|
1177
|
+
json() {
|
|
1178
|
+
if (this._baseSchema.type !== 'string') {
|
|
1179
|
+
throw new Error('json() only applies to string type');
|
|
1180
|
+
}
|
|
1181
|
+
this._baseSchema.jsonString = true;
|
|
1182
|
+
return this;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Pattern URL slug验证 (v1.0.3)
|
|
1187
|
+
* URL slug只能包含小写字母、数字和连字符
|
|
1188
|
+
* @returns {DslBuilder}
|
|
1189
|
+
*
|
|
1190
|
+
* @example
|
|
1191
|
+
* dsl('string!').slug() // my-blog-post, hello-world-123
|
|
1192
|
+
*/
|
|
1193
|
+
slug() {
|
|
1194
|
+
if (this._baseSchema.type !== 'string') {
|
|
1195
|
+
throw new Error('slug() only applies to string type');
|
|
1196
|
+
}
|
|
1197
|
+
this._baseSchema.pattern = '^[a-z0-9]+(?:-[a-z0-9]+)*$';
|
|
1198
|
+
this._baseSchema._customMessages = this._baseSchema._customMessages || {};
|
|
1199
|
+
this._baseSchema._customMessages['pattern'] = 'pattern.slug';
|
|
1200
|
+
return this;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* 日期大于验证 (v1.0.2)
|
|
1206
|
+
* @param {string} date - 对比日期
|
|
1207
|
+
* @returns {DslBuilder}
|
|
1208
|
+
*
|
|
1209
|
+
* @example
|
|
1210
|
+
* dsl('string!').dateGreater('2025-01-01')
|
|
1211
|
+
*/
|
|
1212
|
+
dateGreater(date) {
|
|
1213
|
+
this._baseSchema.dateGreater = date;
|
|
1214
|
+
return this;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* 日期小于验证 (v1.0.2)
|
|
1219
|
+
* @param {string} date - 对比日期
|
|
1220
|
+
* @returns {DslBuilder}
|
|
1221
|
+
*
|
|
1222
|
+
* @example
|
|
1223
|
+
* dsl('string!').dateLess('2025-12-31')
|
|
1224
|
+
*/
|
|
1225
|
+
dateLess(date) {
|
|
1226
|
+
this._baseSchema.dateLess = date;
|
|
1227
|
+
return this;
|
|
1228
|
+
}
|
|
737
1229
|
}
|
|
738
1230
|
|
|
739
1231
|
module.exports = DslBuilder;
|
|
@@ -172,6 +172,32 @@ function installStringExtensions(dslFunction) {
|
|
|
172
172
|
return dslFunction(String(this)).passport(country);
|
|
173
173
|
};
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* v1.0.2 新增方法(dateGreater和dateLess)
|
|
177
|
+
* v1.0.3 新增方法(slug)
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* URL slug验证
|
|
182
|
+
*/
|
|
183
|
+
String.prototype.slug = function() {
|
|
184
|
+
return dslFunction(String(this)).slug();
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 日期大于验证
|
|
189
|
+
*/
|
|
190
|
+
String.prototype.dateGreater = function(date) {
|
|
191
|
+
return dslFunction(String(this)).dateGreater(date);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* 日期小于验证
|
|
196
|
+
*/
|
|
197
|
+
String.prototype.dateLess = function(date) {
|
|
198
|
+
return dslFunction(String(this)).dateLess(date);
|
|
199
|
+
};
|
|
200
|
+
|
|
175
201
|
// 标记已安装
|
|
176
202
|
String.prototype._dslExtensionsInstalled = true;
|
|
177
203
|
}
|
|
@@ -200,6 +226,11 @@ function uninstallStringExtensions() {
|
|
|
200
226
|
delete String.prototype.licensePlate;
|
|
201
227
|
delete String.prototype.postalCode;
|
|
202
228
|
delete String.prototype.passport;
|
|
229
|
+
// v1.0.2 新增
|
|
230
|
+
delete String.prototype.dateGreater;
|
|
231
|
+
delete String.prototype.dateLess;
|
|
232
|
+
// v1.0.3 新增
|
|
233
|
+
delete String.prototype.slug;
|
|
203
234
|
delete String.prototype._dslExtensionsInstalled;
|
|
204
235
|
}
|
|
205
236
|
|
package/lib/core/Validator.js
CHANGED
|
@@ -58,6 +58,13 @@ class Validator {
|
|
|
58
58
|
// 错误格式化器
|
|
59
59
|
this.errorFormatter = new ErrorFormatter();
|
|
60
60
|
|
|
61
|
+
// ✅ 性能优化:WeakMap 缓存键(避免 JSON.stringify)
|
|
62
|
+
this.schemaMap = new WeakMap();
|
|
63
|
+
this.schemaKeyCounter = 0;
|
|
64
|
+
|
|
65
|
+
// ✅ 性能优化:缓存 DslBuilder 转换结果
|
|
66
|
+
this.dslSchemaCache = new WeakMap();
|
|
67
|
+
|
|
61
68
|
// 自定义关键字注册表
|
|
62
69
|
this.customKeywords = new Map();
|
|
63
70
|
}
|
|
@@ -102,18 +109,35 @@ class Validator {
|
|
|
102
109
|
* @returns {Object} 验证结果 { valid: boolean, errors: Array, data: * }
|
|
103
110
|
*/
|
|
104
111
|
validate(schema, data, options = {}) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// 如果指定了语言,临时切换
|
|
109
|
-
const originalLocale = options.locale ? Locale.getLocale() : null;
|
|
110
|
-
if (options.locale) {
|
|
112
|
+
// ✅ 性能优化:提前检查语言切换,避免99%的无效操作
|
|
113
|
+
if (options.locale && options.locale !== Locale.getLocale()) {
|
|
114
|
+
const originalLocale = Locale.getLocale();
|
|
111
115
|
Locale.setLocale(options.locale);
|
|
116
|
+
try {
|
|
117
|
+
return this._validateInternal(schema, data, options);
|
|
118
|
+
} finally {
|
|
119
|
+
Locale.setLocale(originalLocale);
|
|
120
|
+
}
|
|
112
121
|
}
|
|
113
122
|
|
|
114
|
-
//
|
|
123
|
+
// 正常流程(无需切换语言)
|
|
124
|
+
return this._validateInternal(schema, data, options);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 内部验证方法
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
_validateInternal(schema, data, options = {}) {
|
|
132
|
+
const shouldFormat = options.format !== false;
|
|
133
|
+
const locale = options.locale || Locale.getLocale();
|
|
134
|
+
|
|
135
|
+
// ✅ 性能优化:缓存 DslBuilder 转换结果
|
|
115
136
|
if (schema && typeof schema.toSchema === 'function') {
|
|
116
|
-
|
|
137
|
+
if (!this.dslSchemaCache.has(schema)) {
|
|
138
|
+
this.dslSchemaCache.set(schema, schema.toSchema());
|
|
139
|
+
}
|
|
140
|
+
schema = this.dslSchemaCache.get(schema);
|
|
117
141
|
}
|
|
118
142
|
|
|
119
143
|
// 检查是否需要移除额外字段 (clean 模式)
|
|
@@ -167,11 +191,6 @@ class Validator {
|
|
|
167
191
|
}],
|
|
168
192
|
data
|
|
169
193
|
};
|
|
170
|
-
} finally {
|
|
171
|
-
// 恢复原语言
|
|
172
|
-
if (originalLocale) {
|
|
173
|
-
Locale.setLocale(originalLocale);
|
|
174
|
-
}
|
|
175
194
|
}
|
|
176
195
|
}
|
|
177
196
|
|
|
@@ -338,8 +357,16 @@ class Validator {
|
|
|
338
357
|
* @returns {string} 缓存键
|
|
339
358
|
*/
|
|
340
359
|
_generateCacheKey(schema) {
|
|
341
|
-
//
|
|
342
|
-
//
|
|
360
|
+
// ✅ 性能优化:使用 WeakMap 避免昂贵的 JSON.stringify
|
|
361
|
+
// 对于对象类型的 schema,使用 WeakMap 存储唯一标识符
|
|
362
|
+
if (typeof schema === 'object' && schema !== null) {
|
|
363
|
+
if (!this.schemaMap.has(schema)) {
|
|
364
|
+
this.schemaMap.set(schema, `schema_${++this.schemaKeyCounter}`);
|
|
365
|
+
}
|
|
366
|
+
return this.schemaMap.get(schema);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 对于原始类型或 null,降级到字符串化
|
|
343
370
|
return JSON.stringify(schema);
|
|
344
371
|
}
|
|
345
372
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 用于 validateAsync() 方法,验证失败时自动抛出
|
|
5
5
|
*
|
|
6
6
|
* @module lib/errors/ValidationError
|
|
7
|
-
* @version
|
|
7
|
+
* @version 1.0.3
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -119,7 +119,7 @@ class ValidationError extends Error {
|
|
|
119
119
|
* }
|
|
120
120
|
*/
|
|
121
121
|
getFieldError(field) {
|
|
122
|
-
|
|
122
|
+
// 规范化字段名(移除前导斜杠)
|
|
123
123
|
const normalizedField = field.replace(/^\//, '');
|
|
124
124
|
|
|
125
125
|
// 查找匹配的错误(支持多种路径格式)
|
|
@@ -182,7 +182,7 @@ class ValidationError extends Error {
|
|
|
182
182
|
|
|
183
183
|
// Support calling without new
|
|
184
184
|
const ValidationErrorProxy = new Proxy(ValidationError, {
|
|
185
|
-
apply: function(target, thisArg, argumentsList) {
|
|
185
|
+
apply: function (target, thisArg, argumentsList) {
|
|
186
186
|
return new target(...argumentsList);
|
|
187
187
|
}
|
|
188
188
|
});
|