schema-dsl 1.0.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/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +33 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/STATUS.md +101 -0
- package/docs/FEATURE-INDEX.md +519 -0
- package/docs/INDEX.md +253 -0
- package/docs/api-reference.md +1096 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +601 -0
- package/docs/dsl-syntax.md +653 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +462 -0
- package/docs/export-limitations.md +551 -0
- package/docs/faq.md +577 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +476 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +397 -0
- package/docs/mongodb-exporter.md +295 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +273 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +304 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils-chaining.md +143 -0
- package/docs/schema-utils.md +490 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate-async.md +480 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/express-integration.js +376 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +282 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +699 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +376 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +445 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSL Builder - 统一的Schema构建器
|
|
3
|
+
*
|
|
4
|
+
* 支持链式调用扩展DSL功能
|
|
5
|
+
*
|
|
6
|
+
* @module lib/core/DslBuilder
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* // 简单使用
|
|
11
|
+
* const schema = dsl('email!');
|
|
12
|
+
*
|
|
13
|
+
* // 链式扩展
|
|
14
|
+
* const schema = dsl('email!')
|
|
15
|
+
* .pattern(/custom/)
|
|
16
|
+
* .messages({ 'string.pattern': '格式不正确' })
|
|
17
|
+
* .label('邮箱地址');
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const ErrorCodes = require('./ErrorCodes');
|
|
21
|
+
const MessageTemplate = require('./MessageTemplate');
|
|
22
|
+
const Locale = require('./Locale');
|
|
23
|
+
const patterns = require('../config/patterns');
|
|
24
|
+
|
|
25
|
+
class DslBuilder {
|
|
26
|
+
/**
|
|
27
|
+
* 创建 DslBuilder 实例
|
|
28
|
+
* @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
|
|
29
|
+
*/
|
|
30
|
+
constructor(dslString) {
|
|
31
|
+
if (!dslString || typeof dslString !== 'string') {
|
|
32
|
+
throw new Error('DSL string is required');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 解析DSL字符串
|
|
36
|
+
const trimmed = dslString.trim();
|
|
37
|
+
|
|
38
|
+
// 特殊处理:array!数字 → array:数字 + 必填
|
|
39
|
+
// 例如:array!1-10 → array:1-10!
|
|
40
|
+
let processedDsl = trimmed;
|
|
41
|
+
if (/^array![\d-]/.test(trimmed)) {
|
|
42
|
+
processedDsl = trimmed.replace(/^array!/, 'array:') + '!';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._required = processedDsl.endsWith('!');
|
|
46
|
+
const dslWithoutRequired = this._required ? processedDsl.slice(0, -1) : processedDsl;
|
|
47
|
+
|
|
48
|
+
// 简单解析为基础Schema(避免循环依赖)
|
|
49
|
+
this._baseSchema = this._parseSimple(dslWithoutRequired);
|
|
50
|
+
|
|
51
|
+
// 扩展属性
|
|
52
|
+
this._customMessages = {};
|
|
53
|
+
this._label = null;
|
|
54
|
+
this._customValidators = [];
|
|
55
|
+
this._description = null;
|
|
56
|
+
this._whenConditions = [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 简单解析DSL字符串(避免循环依赖)
|
|
61
|
+
* @private
|
|
62
|
+
* @param {string} dsl - DSL字符串(不含!)
|
|
63
|
+
* @returns {Object} JSON Schema对象
|
|
64
|
+
*/
|
|
65
|
+
_parseSimple(dsl) {
|
|
66
|
+
// 处理数组类型:array:1-10 或 array<string>
|
|
67
|
+
if (dsl.startsWith('array')) {
|
|
68
|
+
const schema = { type: 'array' };
|
|
69
|
+
|
|
70
|
+
// 匹配:array:min-max<itemType> 或 array:constraint<itemType> 或 array<itemType>
|
|
71
|
+
const arrayMatch = dsl.match(/^array(?::([^<]+?))?(?:<(.+)>)?$/);
|
|
72
|
+
|
|
73
|
+
if (arrayMatch) {
|
|
74
|
+
const [, constraint, itemType] = arrayMatch;
|
|
75
|
+
|
|
76
|
+
// 解析约束
|
|
77
|
+
if (constraint) {
|
|
78
|
+
const trimmedConstraint = constraint.trim();
|
|
79
|
+
|
|
80
|
+
if (trimmedConstraint.includes('-')) {
|
|
81
|
+
// 范围约束: min-max, min-, -max
|
|
82
|
+
const [min, max] = trimmedConstraint.split('-').map(v => v.trim());
|
|
83
|
+
if (min) schema.minItems = parseInt(min, 10);
|
|
84
|
+
if (max) schema.maxItems = parseInt(max, 10);
|
|
85
|
+
} else {
|
|
86
|
+
// 单个值 = 最大值
|
|
87
|
+
schema.maxItems = parseInt(trimmedConstraint, 10);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 解析元素类型
|
|
92
|
+
if (itemType) {
|
|
93
|
+
schema.items = this._parseSimple(itemType.trim());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return schema;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 处理枚举
|
|
101
|
+
if (dsl.includes('|') && !dsl.includes(':')) {
|
|
102
|
+
return {
|
|
103
|
+
type: 'string',
|
|
104
|
+
enum: dsl.split('|').map(v => v.trim())
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 处理类型:约束格式
|
|
109
|
+
const colonIndex = dsl.indexOf(':');
|
|
110
|
+
let type, constraint;
|
|
111
|
+
|
|
112
|
+
if (colonIndex === -1) {
|
|
113
|
+
type = dsl;
|
|
114
|
+
constraint = '';
|
|
115
|
+
} else {
|
|
116
|
+
type = dsl.substring(0, colonIndex);
|
|
117
|
+
constraint = dsl.substring(colonIndex + 1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 特殊处理 phone:country
|
|
121
|
+
if (type === 'phone') {
|
|
122
|
+
const country = constraint || 'cn';
|
|
123
|
+
const config = patterns.phone[country];
|
|
124
|
+
if (!config) throw new Error(`Unsupported country: ${country}`);
|
|
125
|
+
return {
|
|
126
|
+
type: 'string',
|
|
127
|
+
pattern: config.pattern.source,
|
|
128
|
+
minLength: config.min,
|
|
129
|
+
maxLength: config.max,
|
|
130
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 特殊处理 idCard:country
|
|
135
|
+
if (type === 'idCard') {
|
|
136
|
+
const country = constraint || 'cn';
|
|
137
|
+
const config = patterns.idCard[country.toLowerCase()];
|
|
138
|
+
if (!config) throw new Error(`Unsupported country for idCard: ${country}`);
|
|
139
|
+
return {
|
|
140
|
+
type: 'string',
|
|
141
|
+
pattern: config.pattern.source,
|
|
142
|
+
minLength: config.min,
|
|
143
|
+
maxLength: config.max,
|
|
144
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 特殊处理 creditCard:type
|
|
149
|
+
if (type === 'creditCard') {
|
|
150
|
+
const cardType = constraint || 'visa';
|
|
151
|
+
const config = patterns.creditCard[cardType.toLowerCase()];
|
|
152
|
+
if (!config) throw new Error(`Unsupported credit card type: ${cardType}`);
|
|
153
|
+
return {
|
|
154
|
+
type: 'string',
|
|
155
|
+
pattern: config.pattern.source,
|
|
156
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 特殊处理 licensePlate:country
|
|
161
|
+
if (type === 'licensePlate') {
|
|
162
|
+
const country = constraint || 'cn';
|
|
163
|
+
const config = patterns.licensePlate[country.toLowerCase()];
|
|
164
|
+
if (!config) throw new Error(`Unsupported country for licensePlate: ${country}`);
|
|
165
|
+
return {
|
|
166
|
+
type: 'string',
|
|
167
|
+
pattern: config.pattern.source,
|
|
168
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 特殊处理 postalCode:country
|
|
173
|
+
if (type === 'postalCode') {
|
|
174
|
+
const country = constraint || 'cn';
|
|
175
|
+
const config = patterns.postalCode[country.toLowerCase()];
|
|
176
|
+
if (!config) throw new Error(`Unsupported country for postalCode: ${country}`);
|
|
177
|
+
return {
|
|
178
|
+
type: 'string',
|
|
179
|
+
pattern: config.pattern.source,
|
|
180
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 特殊处理 passport:country
|
|
185
|
+
if (type === 'passport') {
|
|
186
|
+
const country = constraint || 'cn';
|
|
187
|
+
const config = patterns.passport[country.toLowerCase()];
|
|
188
|
+
if (!config) throw new Error(`Unsupported country for passport: ${country}`);
|
|
189
|
+
return {
|
|
190
|
+
type: 'string',
|
|
191
|
+
pattern: config.pattern.source,
|
|
192
|
+
_customMessages: { 'pattern': config.key || config.msg }
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 获取基础类型
|
|
197
|
+
const schema = this._getBaseType(type);
|
|
198
|
+
|
|
199
|
+
// 处理约束
|
|
200
|
+
if (constraint) {
|
|
201
|
+
Object.assign(schema, this._parseConstraint(schema.type, constraint));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return schema;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 获取基础类型Schema
|
|
209
|
+
* @private
|
|
210
|
+
*/
|
|
211
|
+
_getBaseType(type) {
|
|
212
|
+
const typeMap = {
|
|
213
|
+
'string': { type: 'string' },
|
|
214
|
+
'number': { type: 'number' },
|
|
215
|
+
'integer': { type: 'integer' },
|
|
216
|
+
'boolean': { type: 'boolean' },
|
|
217
|
+
'object': { type: 'object' },
|
|
218
|
+
'array': { type: 'array' },
|
|
219
|
+
'null': { type: 'null' },
|
|
220
|
+
'email': { type: 'string', format: 'email' },
|
|
221
|
+
'url': { type: 'string', format: 'uri' },
|
|
222
|
+
'uuid': { type: 'string', format: 'uuid' },
|
|
223
|
+
'date': { type: 'string', format: 'date' },
|
|
224
|
+
'datetime': { type: 'string', format: 'date-time' },
|
|
225
|
+
'time': { type: 'string', format: 'time' },
|
|
226
|
+
'ipv4': { type: 'string', format: 'ipv4' },
|
|
227
|
+
'ipv6': { type: 'string', format: 'ipv6' },
|
|
228
|
+
'binary': { type: 'string', contentEncoding: 'base64' },
|
|
229
|
+
'objectId': { type: 'string', pattern: '^[0-9a-fA-F]{24}$', _customMessages: { 'pattern': 'pattern.objectId' } },
|
|
230
|
+
'hexColor': { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$', _customMessages: { 'pattern': 'pattern.hexColor' } },
|
|
231
|
+
'macAddress': { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', _customMessages: { 'pattern': 'pattern.macAddress' } },
|
|
232
|
+
'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
|
+
'any': {}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
return typeMap[type] || { type: 'string' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 解析约束
|
|
241
|
+
* @private
|
|
242
|
+
*/
|
|
243
|
+
_parseConstraint(type, constraint) {
|
|
244
|
+
const result = {};
|
|
245
|
+
|
|
246
|
+
if (type === 'string' || type === 'number' || type === 'integer') {
|
|
247
|
+
// 范围约束: min-max
|
|
248
|
+
if (constraint.includes('-')) {
|
|
249
|
+
const [min, max] = constraint.split('-').map(v => v.trim());
|
|
250
|
+
|
|
251
|
+
if (type === 'string') {
|
|
252
|
+
if (min) result.minLength = parseInt(min);
|
|
253
|
+
if (max) result.maxLength = parseInt(max);
|
|
254
|
+
} else {
|
|
255
|
+
if (min) result.minimum = parseFloat(min);
|
|
256
|
+
if (max) result.maximum = parseFloat(max);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
// 单个值
|
|
260
|
+
const value = constraint.trim();
|
|
261
|
+
if (value) {
|
|
262
|
+
if (type === 'string') {
|
|
263
|
+
result.maxLength = parseInt(value);
|
|
264
|
+
} else {
|
|
265
|
+
result.maximum = parseFloat(value);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 添加正则表达式验证
|
|
276
|
+
* @param {RegExp|string} regex - 正则表达式
|
|
277
|
+
* @param {string} [message] - 自定义错误消息
|
|
278
|
+
* @returns {DslBuilder}
|
|
279
|
+
*
|
|
280
|
+
* @example
|
|
281
|
+
* dsl('string:3-32!')
|
|
282
|
+
* .pattern(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字和下划线')
|
|
283
|
+
*/
|
|
284
|
+
pattern(regex, message) {
|
|
285
|
+
this._baseSchema.pattern = regex instanceof RegExp ? regex.source : regex;
|
|
286
|
+
|
|
287
|
+
if (message) {
|
|
288
|
+
this._customMessages['string.pattern'] = message;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return this;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 自定义错误消息
|
|
296
|
+
* @param {Object} messages - 错误消息对象
|
|
297
|
+
* @returns {DslBuilder}
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* dsl('string:3-32!')
|
|
301
|
+
* .messages({
|
|
302
|
+
* 'string.min': '至少{{#limit}}个字符',
|
|
303
|
+
* 'string.max': '最多{{#limit}}个字符'
|
|
304
|
+
* })
|
|
305
|
+
*/
|
|
306
|
+
messages(messages) {
|
|
307
|
+
Object.assign(this._customMessages, messages);
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 设置字段标签(用于错误消息)
|
|
313
|
+
* @param {string} labelText - 标签文本
|
|
314
|
+
* @returns {DslBuilder}
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* dsl('email!').label('邮箱地址')
|
|
318
|
+
*/
|
|
319
|
+
label(labelText) {
|
|
320
|
+
this._label = labelText;
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 添加自定义验证器
|
|
326
|
+
* @param {Function} validatorFn - 验证函数
|
|
327
|
+
* @returns {DslBuilder}
|
|
328
|
+
*
|
|
329
|
+
* 支持多种返回方式:
|
|
330
|
+
* 1. 不返回/返回 undefined → 验证通过
|
|
331
|
+
* 2. 返回 true → 验证通过
|
|
332
|
+
* 3. 返回 false → 验证失败(使用默认消息)
|
|
333
|
+
* 4. 返回字符串 → 验证失败(字符串作为错误消息)
|
|
334
|
+
* 5. 返回对象 { error, message } → 验证失败(自定义错误)
|
|
335
|
+
* 6. 抛出异常 → 验证失败(异常消息作为错误)
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* // 方式1: 不返回任何值(推荐)
|
|
339
|
+
* .custom(async (value) => {
|
|
340
|
+
* const exists = await checkEmailExists(value);
|
|
341
|
+
* if (exists) return '邮箱已被占用';
|
|
342
|
+
* })
|
|
343
|
+
*
|
|
344
|
+
* // 方式2: 返回错误消息字符串
|
|
345
|
+
* .custom((value) => {
|
|
346
|
+
* if (value.includes('admin')) return '不能包含敏感词';
|
|
347
|
+
* })
|
|
348
|
+
*
|
|
349
|
+
* // 方式3: 返回错误对象
|
|
350
|
+
* .custom(async (value) => {
|
|
351
|
+
* const exists = await checkExists(value);
|
|
352
|
+
* if (exists) {
|
|
353
|
+
* return { error: 'email.exists', message: '邮箱已被占用' };
|
|
354
|
+
* }
|
|
355
|
+
* })
|
|
356
|
+
*
|
|
357
|
+
* // 方式4: 抛出异常
|
|
358
|
+
* .custom(async (value) => {
|
|
359
|
+
* const user = await findUser(value);
|
|
360
|
+
* if (!user) throw new Error('用户不存在');
|
|
361
|
+
* })
|
|
362
|
+
*/
|
|
363
|
+
custom(validatorFn) {
|
|
364
|
+
if (typeof validatorFn !== 'function') {
|
|
365
|
+
throw new Error('Custom validator must be a function');
|
|
366
|
+
}
|
|
367
|
+
this._customValidators.push(validatorFn);
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 设置描述
|
|
373
|
+
* @param {string} text - 描述文本
|
|
374
|
+
* @returns {DslBuilder}
|
|
375
|
+
*
|
|
376
|
+
* @example
|
|
377
|
+
* dsl('string:3-32!').description('用户登录名')
|
|
378
|
+
*/
|
|
379
|
+
description(text) {
|
|
380
|
+
this._description = text;
|
|
381
|
+
return this;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 设置默认值
|
|
387
|
+
* @param {*} value - 默认值
|
|
388
|
+
* @returns {DslBuilder}
|
|
389
|
+
*/
|
|
390
|
+
default(value) {
|
|
391
|
+
this._baseSchema.default = value;
|
|
392
|
+
return this;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* 转换为 JSON Schema
|
|
397
|
+
* @returns {Object} JSON Schema对象
|
|
398
|
+
*/
|
|
399
|
+
toSchema() {
|
|
400
|
+
const schema = { ...this._baseSchema };
|
|
401
|
+
|
|
402
|
+
// 添加描述
|
|
403
|
+
if (this._description) {
|
|
404
|
+
schema.description = this._description;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 添加自定义消息
|
|
408
|
+
if (Object.keys(this._customMessages).length > 0) {
|
|
409
|
+
schema._customMessages = this._customMessages;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// 添加标签
|
|
413
|
+
if (this._label) {
|
|
414
|
+
schema._label = this._label;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 添加自定义验证器
|
|
418
|
+
if (this._customValidators.length > 0) {
|
|
419
|
+
schema._customValidators = this._customValidators;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 添加when条件
|
|
423
|
+
if (this._whenConditions.length > 0) {
|
|
424
|
+
schema._whenConditions = this._whenConditions;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 添加必填标记
|
|
428
|
+
schema._required = this._required;
|
|
429
|
+
|
|
430
|
+
return schema;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* 验证数据
|
|
435
|
+
* @param {*} data - 待验证数据
|
|
436
|
+
* @param {Object} [context] - 验证上下文
|
|
437
|
+
* @returns {Promise<Object>} 验证结果
|
|
438
|
+
*/
|
|
439
|
+
async validate(data, context = {}) {
|
|
440
|
+
const Validator = require('./Validator');
|
|
441
|
+
const validator = new Validator();
|
|
442
|
+
const schema = this.toSchema();
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
return validator.validate(schema, data);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 验证Schema嵌套深度
|
|
450
|
+
* @static
|
|
451
|
+
* @param {Object} schema - Schema对象
|
|
452
|
+
* @param {number} maxDepth - 最大深度(默认3)
|
|
453
|
+
* @returns {Object} { valid, depth, path, message }
|
|
454
|
+
*/
|
|
455
|
+
static validateNestingDepth(schema, maxDepth = 3) {
|
|
456
|
+
let maxFound = 0;
|
|
457
|
+
let deepestPath = '';
|
|
458
|
+
|
|
459
|
+
function traverse(obj, depth = 0, path = '', isRoot = false) {
|
|
460
|
+
// 更新最大深度(仅当节点是容器时,即包含 properties 或 items)
|
|
461
|
+
// 这样叶子节点(如 string 字段)不会增加嵌套深度
|
|
462
|
+
if (!isRoot && (obj.properties || obj.items)) {
|
|
463
|
+
if (depth > maxFound) {
|
|
464
|
+
maxFound = depth;
|
|
465
|
+
deepestPath = path;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (obj && typeof obj === 'object') {
|
|
470
|
+
if (obj.properties) {
|
|
471
|
+
const nextDepth = depth + 1;
|
|
472
|
+
Object.keys(obj.properties).forEach(key => {
|
|
473
|
+
traverse(obj.properties[key], nextDepth, `${path}.${key}`.replace(/^\./, ''), false);
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
if (obj.items) {
|
|
477
|
+
// 数组items不增加深度,或者根据需求增加
|
|
478
|
+
// 这里保持原逻辑:数组本身算一层,items内部继续
|
|
479
|
+
traverse(obj.items, depth, `${path}[]`, false);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
traverse(schema, 0, '', true);
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
valid: maxFound <= maxDepth,
|
|
488
|
+
depth: maxFound,
|
|
489
|
+
path: deepestPath,
|
|
490
|
+
message: maxFound > maxDepth
|
|
491
|
+
? `嵌套深度${maxFound}超过限制${maxDepth},路径: ${deepestPath}`
|
|
492
|
+
: `嵌套深度${maxFound}符合要求`
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ========== 默认验证方法 ==========
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* 设置格式
|
|
500
|
+
* @param {string} format - 格式名称 (email, url, uuid, etc.)
|
|
501
|
+
* @returns {DslBuilder}
|
|
502
|
+
*/
|
|
503
|
+
format(format) {
|
|
504
|
+
this._baseSchema.format = format;
|
|
505
|
+
return this;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 手机号别名
|
|
510
|
+
* @param {string} country
|
|
511
|
+
* @returns {DslBuilder}
|
|
512
|
+
*/
|
|
513
|
+
phoneNumber(country) {
|
|
514
|
+
return this.phone(country);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 身份证验证
|
|
519
|
+
* @param {string} country - 国家代码 (目前仅支持 'cn')
|
|
520
|
+
* @returns {DslBuilder}
|
|
521
|
+
*/
|
|
522
|
+
idCard(country = 'cn') {
|
|
523
|
+
if (country.toLowerCase() !== 'cn') {
|
|
524
|
+
throw new Error(`Unsupported country for idCard: ${country}`);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 中国身份证正则 (18位)
|
|
528
|
+
const pattern = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
|
|
529
|
+
|
|
530
|
+
// 自动设置长度
|
|
531
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = 18;
|
|
532
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 18;
|
|
533
|
+
|
|
534
|
+
return this.pattern(pattern)
|
|
535
|
+
.messages({
|
|
536
|
+
'pattern': 'pattern.idCard.cn'
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* URL Slug 验证
|
|
542
|
+
* @returns {DslBuilder}
|
|
543
|
+
*/
|
|
544
|
+
slug() {
|
|
545
|
+
// 只能包含小写字母、数字和连字符,不能以连字符开头或结尾
|
|
546
|
+
const pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
547
|
+
|
|
548
|
+
return this.pattern(pattern)
|
|
549
|
+
.messages({
|
|
550
|
+
'pattern': 'pattern.slug'
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 用户名验证(自动设置合理约束)
|
|
556
|
+
* @param {string|Object} preset - 预设长度或选项
|
|
557
|
+
* - '5-20' → 长度5-20
|
|
558
|
+
* - 'short' → 3-16(短用户名)
|
|
559
|
+
* - 'medium' → 3-32(中等,默认)
|
|
560
|
+
* - 'long' → 3-64(长用户名)
|
|
561
|
+
* - { minLength, maxLength, allowUnderscore, allowNumber }
|
|
562
|
+
* @returns {DslBuilder}
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* // 简洁写法(推荐)
|
|
566
|
+
* username: 'string!'.username() // 自动3-32
|
|
567
|
+
* username: 'string!'.username('5-20') // 长度5-20
|
|
568
|
+
* username: 'string!'.username('short') // 短用户名3-16
|
|
569
|
+
* username: 'string!'.username('long') // 长用户名3-64
|
|
570
|
+
*/
|
|
571
|
+
username(preset = 'medium') {
|
|
572
|
+
let minLength, maxLength, allowUnderscore = true, allowNumber = true;
|
|
573
|
+
|
|
574
|
+
// 解析预设
|
|
575
|
+
if (typeof preset === 'string') {
|
|
576
|
+
// 字符串范围格式:'5-20'
|
|
577
|
+
const rangeMatch = preset.match(/^(\d+)-(\d+)$/);
|
|
578
|
+
if (rangeMatch) {
|
|
579
|
+
minLength = parseInt(rangeMatch[1], 10);
|
|
580
|
+
maxLength = parseInt(rangeMatch[2], 10);
|
|
581
|
+
}
|
|
582
|
+
// 预设枚举
|
|
583
|
+
else {
|
|
584
|
+
const presets = {
|
|
585
|
+
short: { min: 3, max: 16 },
|
|
586
|
+
medium: { min: 3, max: 32 },
|
|
587
|
+
long: { min: 3, max: 64 }
|
|
588
|
+
};
|
|
589
|
+
const p = presets[preset] || presets.medium;
|
|
590
|
+
minLength = p.min;
|
|
591
|
+
maxLength = p.max;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// 对象参数
|
|
595
|
+
else if (typeof preset === 'object') {
|
|
596
|
+
minLength = preset.minLength || 3;
|
|
597
|
+
maxLength = preset.maxLength || 32;
|
|
598
|
+
allowUnderscore = preset.allowUnderscore !== false;
|
|
599
|
+
allowNumber = preset.allowNumber !== false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 自动设置长度约束(如果未设置)
|
|
603
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = minLength;
|
|
604
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = maxLength;
|
|
605
|
+
|
|
606
|
+
// 设置正则验证
|
|
607
|
+
let pattern = '^[a-zA-Z]';
|
|
608
|
+
if (allowUnderscore && allowNumber) {
|
|
609
|
+
pattern += '[a-zA-Z0-9_]*$';
|
|
610
|
+
} else if (allowNumber) {
|
|
611
|
+
pattern += '[a-zA-Z0-9]*$';
|
|
612
|
+
} else {
|
|
613
|
+
pattern += '[a-zA-Z]*$';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
return this.pattern(new RegExp(pattern))
|
|
617
|
+
.messages({
|
|
618
|
+
'pattern': 'pattern.username'
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 密码强度验证(自动设置合理约束)
|
|
624
|
+
* @param {string} strength - 强度级别
|
|
625
|
+
* @returns {DslBuilder}
|
|
626
|
+
*
|
|
627
|
+
* @example
|
|
628
|
+
* password: 'string!'.password('strong') // 自动设置8-64长度
|
|
629
|
+
*/
|
|
630
|
+
password(strength = 'medium') {
|
|
631
|
+
const patterns = {
|
|
632
|
+
weak: /.{6,}/,
|
|
633
|
+
medium: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
|
|
634
|
+
strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
|
|
635
|
+
veryStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{10,}$/
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const minLengths = { weak: 6, medium: 8, strong: 8, veryStrong: 10 };
|
|
639
|
+
|
|
640
|
+
const pattern = patterns[strength];
|
|
641
|
+
if (!pattern) {
|
|
642
|
+
throw new Error(`Invalid password strength: ${strength}`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 自动设置长度约束
|
|
646
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = minLengths[strength];
|
|
647
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 64;
|
|
648
|
+
|
|
649
|
+
return this.pattern(pattern)
|
|
650
|
+
.messages({
|
|
651
|
+
'pattern': `pattern.password.${strength}`
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* 手机号验证(自动设置合理约束)
|
|
657
|
+
* @param {string} country - 国家代码: cn|us|uk|hk|tw|international
|
|
658
|
+
* @returns {DslBuilder}
|
|
659
|
+
*
|
|
660
|
+
* @example
|
|
661
|
+
* phone: 'string!'.phone('cn') // ✅ 推荐
|
|
662
|
+
* phone: 'number!'.phone('cn') // ✅ 自动纠正为 string
|
|
663
|
+
*/
|
|
664
|
+
phone(country = 'cn') {
|
|
665
|
+
// ✨ 自动纠正类型为 string(手机号不应该是 number)
|
|
666
|
+
if (this._baseSchema.type === 'number' || this._baseSchema.type === 'integer') {
|
|
667
|
+
this._baseSchema.type = 'string';
|
|
668
|
+
// 清理 number 类型的属性
|
|
669
|
+
delete this._baseSchema.minimum;
|
|
670
|
+
delete this._baseSchema.maximum;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const config = patterns.phone[country];
|
|
674
|
+
if (!config) {
|
|
675
|
+
throw new Error(`Unsupported country: ${country}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 自动设置长度约束
|
|
679
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = config.min;
|
|
680
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = config.max;
|
|
681
|
+
|
|
682
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* 信用卡验证
|
|
687
|
+
* @param {string} type - 卡类型: visa|mastercard|amex|discover|jcb|unionpay
|
|
688
|
+
* @returns {DslBuilder}
|
|
689
|
+
*/
|
|
690
|
+
creditCard(type = 'visa') {
|
|
691
|
+
const config = patterns.creditCard[type.toLowerCase()];
|
|
692
|
+
if (!config) {
|
|
693
|
+
throw new Error(`Unsupported credit card type: ${type}`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* 车牌号验证
|
|
701
|
+
* @param {string} country - 国家代码
|
|
702
|
+
* @returns {DslBuilder}
|
|
703
|
+
*/
|
|
704
|
+
licensePlate(country = 'cn') {
|
|
705
|
+
const config = patterns.licensePlate[country.toLowerCase()];
|
|
706
|
+
if (!config) {
|
|
707
|
+
throw new Error(`Unsupported country for licensePlate: ${country}`);
|
|
708
|
+
}
|
|
709
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* 邮政编码验证
|
|
714
|
+
* @param {string} country - 国家代码
|
|
715
|
+
* @returns {DslBuilder}
|
|
716
|
+
*/
|
|
717
|
+
postalCode(country = 'cn') {
|
|
718
|
+
const config = patterns.postalCode[country.toLowerCase()];
|
|
719
|
+
if (!config) {
|
|
720
|
+
throw new Error(`Unsupported country for postalCode: ${country}`);
|
|
721
|
+
}
|
|
722
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* 护照号码验证
|
|
727
|
+
* @param {string} country - 国家代码
|
|
728
|
+
* @returns {DslBuilder}
|
|
729
|
+
*/
|
|
730
|
+
passport(country = 'cn') {
|
|
731
|
+
const config = patterns.passport[country.toLowerCase()];
|
|
732
|
+
if (!config) {
|
|
733
|
+
throw new Error(`Unsupported country for passport: ${country}`);
|
|
734
|
+
}
|
|
735
|
+
return this.pattern(config.pattern).messages({ 'pattern': config.key });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
module.exports = DslBuilder;
|
|
740
|
+
|