schema-dsl 1.0.8 → 1.1.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/CHANGELOG.md +338 -3
- package/README.md +296 -17
- package/STATUS.md +74 -3
- package/docs/FEATURE-INDEX.md +1 -1
- package/docs/add-custom-locale.md +395 -0
- package/docs/best-practices.md +3 -3
- package/docs/cache-manager.md +1 -1
- package/docs/conditional-api.md +1032 -0
- package/docs/dsl-syntax.md +1 -1
- package/docs/dynamic-locale.md +76 -30
- package/docs/error-handling.md +2 -2
- package/docs/export-guide.md +2 -2
- package/docs/export-limitations.md +3 -3
- package/docs/faq.md +6 -6
- package/docs/frontend-i18n-guide.md +19 -16
- package/docs/i18n-user-guide.md +7 -9
- package/docs/i18n.md +65 -2
- package/docs/mongodb-exporter.md +3 -3
- package/docs/multi-type-support.md +12 -2
- package/docs/mysql-exporter.md +1 -1
- package/docs/plugin-system.md +4 -4
- package/docs/postgresql-exporter.md +1 -1
- package/docs/quick-start.md +4 -4
- package/docs/troubleshooting.md +2 -2
- package/docs/type-reference.md +5 -5
- package/docs/typescript-guide.md +5 -6
- package/docs/union-type-guide.md +147 -0
- package/docs/union-types.md +277 -0
- package/docs/validate-async.md +1 -1
- package/examples/array-dsl-example.js +1 -1
- package/examples/conditional-example.js +288 -0
- package/examples/conditional-non-object.js +129 -0
- package/examples/conditional-validate-example.js +321 -0
- package/examples/union-type-example.js +127 -0
- package/examples/union-types-example.js +77 -0
- package/index.d.ts +395 -12
- package/index.js +31 -4
- package/lib/adapters/DslAdapter.js +14 -5
- package/lib/core/ConditionalBuilder.js +401 -0
- package/lib/core/DslBuilder.js +113 -0
- package/lib/core/ErrorFormatter.js +81 -33
- package/lib/core/Locale.js +13 -8
- package/lib/core/Validator.js +252 -16
- package/lib/locales/en-US.js +14 -0
- package/lib/locales/es-ES.js +4 -0
- package/lib/locales/fr-FR.js +4 -0
- package/lib/locales/ja-JP.js +9 -0
- package/lib/locales/zh-CN.js +14 -0
- package/package.json +5 -2
|
@@ -38,20 +38,41 @@ class ErrorFormatter {
|
|
|
38
38
|
/**
|
|
39
39
|
* 格式化单个错误或错误数组
|
|
40
40
|
* @param {Object|Array<Object>} error - 错误对象或错误数组
|
|
41
|
-
* @param {string} [
|
|
41
|
+
* @param {string|Object} [localeOrOptions] - 语言代码字符串或选项对象
|
|
42
|
+
* @param {string} [localeOrOptions.locale] - 动态指定语言(可选)
|
|
43
|
+
* @param {Object} [localeOrOptions.messages] - 自定义错误消息(可选)
|
|
42
44
|
* @returns {string|Array<Object>} 格式化后的错误消息或错误对象数组
|
|
43
45
|
*/
|
|
44
|
-
format(error,
|
|
46
|
+
format(error, localeOrOptions) {
|
|
47
|
+
|
|
48
|
+
// ✅ 支持两种调用方式:
|
|
49
|
+
// 1. format(errors, 'zh-CN') - 旧版兼容
|
|
50
|
+
// 2. format(errors, { locale: 'zh-CN', messages: {...} }) - 新版增强
|
|
51
|
+
let locale;
|
|
52
|
+
let customMessages;
|
|
53
|
+
|
|
54
|
+
if (typeof localeOrOptions === 'string') {
|
|
55
|
+
// 旧版:直接传入语言代码
|
|
56
|
+
locale = localeOrOptions;
|
|
57
|
+
} else if (typeof localeOrOptions === 'object' && localeOrOptions !== null) {
|
|
58
|
+
// 新版:传入选项对象
|
|
59
|
+
locale = localeOrOptions.locale;
|
|
60
|
+
customMessages = localeOrOptions.messages;
|
|
61
|
+
}
|
|
62
|
+
|
|
45
63
|
// 如果是数组,格式化为详细对象数组
|
|
46
64
|
if (Array.isArray(error)) {
|
|
47
|
-
return this.formatDetailed(error, locale);
|
|
65
|
+
return this.formatDetailed(error, locale, customMessages);
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
// 获取当前使用的消息模板
|
|
51
69
|
const messages = locale ? this._loadMessages(locale) : this.messages;
|
|
52
70
|
|
|
71
|
+
// 合并自定义消息
|
|
72
|
+
const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
|
|
73
|
+
|
|
53
74
|
// 单个错误对象格式化
|
|
54
|
-
const template =
|
|
75
|
+
const template = finalMessages[error.type] || error.message;
|
|
55
76
|
return this._interpolate(template, {
|
|
56
77
|
...error,
|
|
57
78
|
path: error.path || 'value',
|
|
@@ -73,9 +94,11 @@ class ErrorFormatter {
|
|
|
73
94
|
* 格式化为详细对象
|
|
74
95
|
* @param {Array<Object>} errors - ajv错误数组
|
|
75
96
|
* @param {string} [locale] - 动态指定语言(可选)
|
|
97
|
+
* @param {Object} [customMessages] - 自定义错误消息(可选)
|
|
76
98
|
* @returns {Array<Object>} 详细错误对象数组
|
|
77
99
|
*/
|
|
78
|
-
formatDetailed(errors, locale) {
|
|
100
|
+
formatDetailed(errors, locale, customMessages) {
|
|
101
|
+
|
|
79
102
|
if (!Array.isArray(errors)) {
|
|
80
103
|
errors = [errors];
|
|
81
104
|
}
|
|
@@ -104,6 +127,9 @@ class ErrorFormatter {
|
|
|
104
127
|
// 获取当前使用的消息模板
|
|
105
128
|
const messages = locale ? this._loadMessages(locale) : this.messages;
|
|
106
129
|
|
|
130
|
+
// ✅ 合并参数传入的自定义消息(在整个 map 循环中使用)
|
|
131
|
+
const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
|
|
132
|
+
|
|
107
133
|
return errors.map(err => {
|
|
108
134
|
// 处理 ajv 错误格式
|
|
109
135
|
const keyword = err.keyword || err.type || 'validation';
|
|
@@ -125,22 +151,27 @@ class ErrorFormatter {
|
|
|
125
151
|
|
|
126
152
|
if (label) {
|
|
127
153
|
// 如果显式设置了 label,尝试翻译它
|
|
128
|
-
if (
|
|
129
|
-
label =
|
|
154
|
+
if (finalMessages[label]) {
|
|
155
|
+
label = finalMessages[label];
|
|
130
156
|
}
|
|
131
157
|
} else {
|
|
132
158
|
// 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
|
|
133
159
|
// 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
|
|
134
160
|
const autoKey = `label.${fieldName.replace(/\//g, '.')}`;
|
|
135
|
-
if (
|
|
136
|
-
label =
|
|
161
|
+
if (finalMessages[autoKey]) {
|
|
162
|
+
label = finalMessages[autoKey];
|
|
137
163
|
} else {
|
|
138
164
|
// 没找到翻译,回退到 fieldName
|
|
139
165
|
label = fieldName;
|
|
140
166
|
}
|
|
141
167
|
}
|
|
142
168
|
|
|
143
|
-
|
|
169
|
+
// Schema 中的自定义消息
|
|
170
|
+
const schemaCustomMessages = schema._customMessages || {};
|
|
171
|
+
|
|
172
|
+
// ✅ 合并优先级:schemaCustomMessages > finalMessages
|
|
173
|
+
// schemaCustomMessages 是字段级的自定义消息,优先级最高
|
|
174
|
+
const mergedMessages = { ...finalMessages, ...schemaCustomMessages };
|
|
144
175
|
|
|
145
176
|
// 关键字映射 (ajv keyword -> schema-dsl 简写)
|
|
146
177
|
// 支持 min/max 作为 minLength/maxLength 的简写
|
|
@@ -160,18 +191,38 @@ class ErrorFormatter {
|
|
|
160
191
|
const mappedKeyword = keywordMap[keyword] || keyword;
|
|
161
192
|
const type = schema.type || 'string';
|
|
162
193
|
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
|
|
195
|
+
// ✅ 优化:如果 schema 中有自定义消息,优先使用
|
|
196
|
+
// 支持三种自定义消息格式:
|
|
197
|
+
// 1. 键引用:_customMessages.pattern = "pattern.objectId" → 查找语言包中的 "pattern.objectId"
|
|
198
|
+
// 2. 模板字符串:_customMessages.min = "{{#label}}必须大于{{#limit}}" → 直接使用并插值
|
|
199
|
+
// 3. 最终消息:_customMessages.pattern = "手机号格式不正确" → 直接使用
|
|
200
|
+
let message;
|
|
201
|
+
|
|
202
|
+
// 1. 首先检查 schema 是否为该 keyword 定义了自定义消息
|
|
203
|
+
let customValue = schemaCustomMessages[keyword] || schemaCustomMessages[mappedKeyword];
|
|
204
|
+
|
|
205
|
+
if (customValue) {
|
|
206
|
+
// 尝试从 mergedMessages 中查找这个键
|
|
207
|
+
const lookupResult = mergedMessages[customValue];
|
|
208
|
+
|
|
209
|
+
if (lookupResult) {
|
|
210
|
+
// 找到了,说明它是一个键引用
|
|
211
|
+
message = lookupResult;
|
|
212
|
+
} else {
|
|
213
|
+
// 没找到,说明它本身就是模板或最终消息,直接使用
|
|
214
|
+
message = customValue;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 2. 如果没有自定义消息,按照通用查找顺序
|
|
219
|
+
if (!message) {
|
|
220
|
+
message = mergedMessages[`${type}.${keyword}`] ||
|
|
221
|
+
mergedMessages[`${type}.${mappedKeyword}`] ||
|
|
222
|
+
mergedMessages[mappedKeyword] ||
|
|
223
|
+
mergedMessages[keyword] ||
|
|
224
|
+
mergedMessages['default'];
|
|
225
|
+
}
|
|
175
226
|
|
|
176
227
|
// 自动查找 format 类型的消息 (例如 format.email)
|
|
177
228
|
if (!message && mappedKeyword === 'format' && err.params && err.params.format) {
|
|
@@ -181,25 +232,22 @@ class ErrorFormatter {
|
|
|
181
232
|
|
|
182
233
|
const formatKey = `format.${formatName}`;
|
|
183
234
|
|
|
184
|
-
// 优先查找
|
|
185
|
-
if (
|
|
186
|
-
message =
|
|
187
|
-
}
|
|
188
|
-
// 其次查找全局/语言包中的 format.email
|
|
189
|
-
else if (messages[formatKey]) {
|
|
190
|
-
message = messages[formatKey];
|
|
235
|
+
// 优先查找 mergedMessages 中的 format.email
|
|
236
|
+
if (mergedMessages[formatKey]) {
|
|
237
|
+
message = mergedMessages[formatKey];
|
|
191
238
|
}
|
|
192
239
|
}
|
|
193
240
|
|
|
194
241
|
if (!message) {
|
|
195
242
|
// 使用默认模板
|
|
196
|
-
const template =
|
|
243
|
+
const template = mergedMessages[mappedKeyword] || mergedMessages[keyword] || err.message || 'Validation error';
|
|
197
244
|
message = template;
|
|
198
245
|
} else {
|
|
199
246
|
// 检查 message 是否为 key (包含点号且无空格,或者是已知的 key)
|
|
200
247
|
// 如果是 key,尝试从 messages 中查找
|
|
201
|
-
if (typeof message === 'string' && (message.includes('.') ||
|
|
202
|
-
|
|
248
|
+
if (typeof message === 'string' && (message.includes('.') || mergedMessages[message])) {
|
|
249
|
+
|
|
250
|
+
let translated = mergedMessages[message];
|
|
203
251
|
|
|
204
252
|
// 尝试回退查找 (例如 pattern.phone.cn -> pattern.phone)
|
|
205
253
|
if (!translated && message.includes('.')) {
|
|
@@ -207,7 +255,7 @@ class ErrorFormatter {
|
|
|
207
255
|
while (parts.length > 1 && !translated) {
|
|
208
256
|
parts.pop();
|
|
209
257
|
const fallbackKey = parts.join('.');
|
|
210
|
-
translated =
|
|
258
|
+
translated = mergedMessages[fallbackKey];
|
|
211
259
|
}
|
|
212
260
|
}
|
|
213
261
|
|
package/lib/core/Locale.js
CHANGED
|
@@ -63,12 +63,16 @@ class Locale {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* 获取错误消息模板
|
|
66
|
-
* @param {string} type -
|
|
66
|
+
* @param {string} type - 错误类型或消息字符串
|
|
67
67
|
* @param {Object} [customMessages] - 自定义消息
|
|
68
|
+
* @param {string} [locale] - 指定语言(可选,默认使用当前语言)
|
|
68
69
|
* @returns {string} 消息模板
|
|
69
70
|
*/
|
|
70
|
-
static getMessage(type, customMessages = {}) {
|
|
71
|
-
//
|
|
71
|
+
static getMessage(type, customMessages = {}, locale = null) {
|
|
72
|
+
// 使用指定的语言或当前全局语言
|
|
73
|
+
const targetLocale = locale || this.currentLocale;
|
|
74
|
+
|
|
75
|
+
// 优先级: 自定义消息 > 全局自定义消息 > 语言包 > ErrorCodes > 原字符串
|
|
72
76
|
|
|
73
77
|
// 1. 自定义消息
|
|
74
78
|
if (customMessages[type]) {
|
|
@@ -80,8 +84,8 @@ class Locale {
|
|
|
80
84
|
return this.customMessages[type];
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
// 3.
|
|
84
|
-
const localeMessages = this.locales[
|
|
87
|
+
// 3. 语言包(使用指定的语言)
|
|
88
|
+
const localeMessages = this.locales[targetLocale];
|
|
85
89
|
if (localeMessages && localeMessages[type]) {
|
|
86
90
|
return localeMessages[type];
|
|
87
91
|
}
|
|
@@ -89,9 +93,10 @@ class Locale {
|
|
|
89
93
|
// 4. 默认消息(从ErrorCodes获取)
|
|
90
94
|
const errorInfo = getErrorInfo(type);
|
|
91
95
|
|
|
92
|
-
// 5.
|
|
93
|
-
if (errorInfo.code === 'UNKNOWN_ERROR'
|
|
94
|
-
|
|
96
|
+
// 5. 如果是未知错误,说明不是预定义的错误码
|
|
97
|
+
if (errorInfo.code === 'UNKNOWN_ERROR') {
|
|
98
|
+
// ✅ 向后兼容:直接返回原字符串(支持硬编码消息)
|
|
99
|
+
return type;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
return errorInfo.message;
|
package/lib/core/Validator.js
CHANGED
|
@@ -115,18 +115,7 @@ class Validator {
|
|
|
115
115
|
* @returns {Object} 验证结果 { valid: boolean, errors: Array, data: * }
|
|
116
116
|
*/
|
|
117
117
|
validate(schema, data, options = {}) {
|
|
118
|
-
// ✅
|
|
119
|
-
if (options.locale && options.locale !== Locale.getLocale()) {
|
|
120
|
-
const originalLocale = Locale.getLocale();
|
|
121
|
-
Locale.setLocale(options.locale);
|
|
122
|
-
try {
|
|
123
|
-
return this._validateInternal(schema, data, options);
|
|
124
|
-
} finally {
|
|
125
|
-
Locale.setLocale(originalLocale);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 正常流程(无需切换语言)
|
|
118
|
+
// ✅ 优化:直接传递 locale 到内部方法,不再切换全局状态
|
|
130
119
|
return this._validateInternal(schema, data, options);
|
|
131
120
|
}
|
|
132
121
|
|
|
@@ -146,6 +135,24 @@ class Validator {
|
|
|
146
135
|
schema = this.dslSchemaCache.get(schema);
|
|
147
136
|
}
|
|
148
137
|
|
|
138
|
+
// ✅ 处理 ConditionalBuilder 条件链(运行时条件判断)
|
|
139
|
+
if (schema && schema._isConditional) {
|
|
140
|
+
// 顶层 ConditionalBuilder(直接作为 Schema 使用)
|
|
141
|
+
// 此时验证的是整个数据对象
|
|
142
|
+
return this._validateConditional(schema, data, null, data, options);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ✅ 预处理包含 ConditionalBuilder 的 Schema
|
|
146
|
+
if (schema && schema.properties) {
|
|
147
|
+
const hasConditional = Object.values(schema.properties).some(
|
|
148
|
+
fieldSchema => fieldSchema && fieldSchema._isConditional
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (hasConditional) {
|
|
152
|
+
return this._validateWithConditionals(schema, data, options);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
149
156
|
// 检查是否需要移除额外字段 (clean 模式)
|
|
150
157
|
if (schema && schema._removeAdditional) {
|
|
151
158
|
// 创建新的 Validator 实例,启用 removeAdditional
|
|
@@ -175,10 +182,13 @@ class Validator {
|
|
|
175
182
|
// 执行验证
|
|
176
183
|
const valid = validate(data);
|
|
177
184
|
|
|
178
|
-
//
|
|
185
|
+
// ✅ 优化:直接传递 locale 和 messages 到格式化器,不修改全局状态
|
|
179
186
|
const errors = valid ? [] : (
|
|
180
187
|
shouldFormat
|
|
181
|
-
? this.errorFormatter.format(validate.errors,
|
|
188
|
+
? this.errorFormatter.format(validate.errors, {
|
|
189
|
+
locale,
|
|
190
|
+
messages: options.messages
|
|
191
|
+
})
|
|
182
192
|
: validate.errors
|
|
183
193
|
);
|
|
184
194
|
|
|
@@ -377,9 +387,235 @@ class Validator {
|
|
|
377
387
|
}
|
|
378
388
|
|
|
379
389
|
/**
|
|
380
|
-
*
|
|
390
|
+
* 验证包含 ConditionalBuilder 字段的 Schema
|
|
391
|
+
* @private
|
|
392
|
+
* @param {Object} schema - Schema 对象
|
|
393
|
+
* @param {*} data - 待验证的数据
|
|
394
|
+
* @param {Object} options - 验证选项
|
|
395
|
+
* @returns {Object} 验证结果
|
|
396
|
+
*/
|
|
397
|
+
_validateWithConditionals(schema, data, options = {}) {
|
|
398
|
+
const errors = [];
|
|
399
|
+
|
|
400
|
+
// 深拷贝 schema,避免修改原始对象
|
|
401
|
+
const cleanSchema = JSON.parse(JSON.stringify(schema));
|
|
402
|
+
const conditionalFields = {};
|
|
403
|
+
|
|
404
|
+
// 提取所有条件字段
|
|
405
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties || {})) {
|
|
406
|
+
if (fieldSchema && fieldSchema._isConditional) {
|
|
407
|
+
conditionalFields[fieldName] = fieldSchema;
|
|
408
|
+
// 从 cleanSchema 中删除条件字段
|
|
409
|
+
delete cleanSchema.properties[fieldName];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// 先验证非条件字段
|
|
414
|
+
const baseResult = this._validateInternal(cleanSchema, data, options);
|
|
415
|
+
if (!baseResult.valid) {
|
|
416
|
+
errors.push(...baseResult.errors);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 然后验证每个条件字段
|
|
420
|
+
for (const [fieldName, conditionalSchema] of Object.entries(conditionalFields)) {
|
|
421
|
+
// ✅ 传递字段名和完整数据对象
|
|
422
|
+
const fieldResult = this._validateConditional(
|
|
423
|
+
conditionalSchema,
|
|
424
|
+
data,
|
|
425
|
+
fieldName,
|
|
426
|
+
data[fieldName],
|
|
427
|
+
options
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
if (!fieldResult.valid) {
|
|
431
|
+
// 添加字段路径信息
|
|
432
|
+
fieldResult.errors.forEach(error => {
|
|
433
|
+
if (!error.path || error.path === '') {
|
|
434
|
+
error.path = fieldName;
|
|
435
|
+
}
|
|
436
|
+
errors.push(error);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
valid: errors.length === 0,
|
|
443
|
+
errors,
|
|
444
|
+
data
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 验证条件链(ConditionalBuilder)
|
|
450
|
+
* @private
|
|
451
|
+
* @param {Object} conditionalSchema - 条件 Schema 对象
|
|
452
|
+
* @param {*} data - 完整数据对象(用于条件判断)
|
|
453
|
+
* @param {string} fieldName - 字段名
|
|
454
|
+
* @param {*} fieldValue - 字段值(用于验证)
|
|
455
|
+
* @param {Object} options - 验证选项
|
|
456
|
+
* @returns {Object} 验证结果
|
|
457
|
+
*/
|
|
458
|
+
_validateConditional(conditionalSchema, data, fieldName, fieldValue, options = {}) {
|
|
459
|
+
const locale = options.locale || Locale.getLocale();
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
// ✅ 遍历条件链,执行第一个匹配的条件
|
|
463
|
+
// 对于 if/elseIf 链,只执行第一个满足条件的分支
|
|
464
|
+
for (let i = 0; i < conditionalSchema.conditions.length; i++) {
|
|
465
|
+
const cond = conditionalSchema.conditions[i];
|
|
466
|
+
|
|
467
|
+
// 执行组合条件(支持 and/or)
|
|
468
|
+
const matched = conditionalSchema._evaluateCondition(cond, data);
|
|
469
|
+
|
|
470
|
+
if (cond.action === 'throw') {
|
|
471
|
+
// ✅ message 模式:条件为 true 时抛错,条件为 false 时通过
|
|
472
|
+
if (matched) {
|
|
473
|
+
// ✅ 条件满足(true),抛出错误
|
|
474
|
+
// 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
|
|
475
|
+
// 传递 locale 参数以支持动态语言切换
|
|
476
|
+
const errorMessage = Locale.getMessage(cond.message, options.messages || {}, locale);
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
valid: false,
|
|
480
|
+
errors: [{
|
|
481
|
+
message: errorMessage,
|
|
482
|
+
path: '',
|
|
483
|
+
keyword: 'conditional',
|
|
484
|
+
params: { condition: cond.type }
|
|
485
|
+
}],
|
|
486
|
+
data
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
// 条件不满足(false),继续验证
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ✅ then/else 模式:找到第一个满足的条件就执行并返回
|
|
494
|
+
if (matched) {
|
|
495
|
+
// 条件满足,执行 then Schema
|
|
496
|
+
if (cond.then !== undefined && cond.then !== null) {
|
|
497
|
+
return this._executeThenBranch(cond.then, data, fieldValue, fieldName, options);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 条件满足但没有 then,表示验证通过
|
|
501
|
+
return {
|
|
502
|
+
valid: true,
|
|
503
|
+
errors: [],
|
|
504
|
+
data
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ✅ 如果是 if/elseIf 条件不满足,继续检查下一个 elseIf
|
|
509
|
+
// 这样就支持了 if...elseIf...elseIf...else 链
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ✅ 所有条件都不满足,执行 else
|
|
513
|
+
if (conditionalSchema.else !== undefined) {
|
|
514
|
+
if (conditionalSchema.else === null) {
|
|
515
|
+
// else 为 null,表示跳过验证
|
|
516
|
+
return {
|
|
517
|
+
valid: true,
|
|
518
|
+
errors: [],
|
|
519
|
+
data
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// 执行 else Schema
|
|
524
|
+
return this._executeThenBranch(conditionalSchema.else, data, fieldValue, fieldName, options);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 没有 else,表示不验证
|
|
528
|
+
return {
|
|
529
|
+
valid: true,
|
|
530
|
+
errors: [],
|
|
531
|
+
data
|
|
532
|
+
};
|
|
533
|
+
} catch (error) {
|
|
534
|
+
return {
|
|
535
|
+
valid: false,
|
|
536
|
+
errors: [{
|
|
537
|
+
message: `Conditional validation error: ${error.message}`,
|
|
538
|
+
path: '',
|
|
539
|
+
keyword: 'conditional'
|
|
540
|
+
}],
|
|
541
|
+
data
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* 执行 then 分支(提取公共逻辑)
|
|
548
|
+
* @private
|
|
549
|
+
* @param {*} thenSchema - then 分支的 Schema
|
|
550
|
+
* @param {*} data - 完整数据对象(用于嵌套条件判断)
|
|
551
|
+
* @param {*} fieldValue - 字段值(用于验证)
|
|
552
|
+
* @param {string} fieldName - 字段名
|
|
553
|
+
* @param {Object} options - 验证选项
|
|
554
|
+
*/
|
|
555
|
+
_executeThenBranch(thenSchema, data, fieldValue, fieldName, options) {
|
|
556
|
+
const DslBuilder = require('./DslBuilder');
|
|
557
|
+
|
|
558
|
+
// ✅ 如果是 ConditionalBuilder 实例(未调用 toSchema),先转换
|
|
559
|
+
if (thenSchema && typeof thenSchema.toSchema === 'function') {
|
|
560
|
+
thenSchema = thenSchema.toSchema();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ✅ 处理嵌套的 ConditionalBuilder(已转换为 Schema 对象)
|
|
564
|
+
if (thenSchema && thenSchema._isConditional) {
|
|
565
|
+
// 嵌套的条件构建器,递归处理
|
|
566
|
+
// 传递完整数据对象用于条件判断,传递字段值用于验证
|
|
567
|
+
return this._validateConditional(thenSchema, data, fieldName, fieldValue, options);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 如果是字符串,解析为 Schema
|
|
571
|
+
if (typeof thenSchema === 'string') {
|
|
572
|
+
const builder = new DslBuilder(thenSchema);
|
|
573
|
+
thenSchema = builder.toSchema();
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ✅ 验证字段值
|
|
577
|
+
return this._validateFieldValue(thenSchema, fieldValue, fieldName, options);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 验证字段值(处理空字符串和 undefined)
|
|
582
|
+
* @private
|
|
583
|
+
* @param {Object} schema - Schema 对象
|
|
584
|
+
* @param {*} fieldValue - 字段值
|
|
585
|
+
* @param {string} fieldName - 字段名
|
|
586
|
+
* @param {Object} options - 验证选项
|
|
587
|
+
* @returns {Object} 验证结果
|
|
588
|
+
*/
|
|
589
|
+
_validateFieldValue(schema, fieldValue, fieldName, options = {}) {
|
|
590
|
+
// ✅ 检查字段是否必填
|
|
591
|
+
const isRequired = schema && schema._required === true;
|
|
592
|
+
|
|
593
|
+
// ✅ 处理 undefined:可选字段缺失时跳过验证
|
|
594
|
+
if (!isRequired && fieldValue === undefined) {
|
|
595
|
+
return {
|
|
596
|
+
valid: true,
|
|
597
|
+
errors: [],
|
|
598
|
+
data: fieldValue
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ✅ 处理空字符串:可选字段的空字符串视为未提供
|
|
603
|
+
if (!isRequired && fieldValue === '') {
|
|
604
|
+
return {
|
|
605
|
+
valid: true,
|
|
606
|
+
errors: [],
|
|
607
|
+
data: fieldValue
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 正常验证
|
|
612
|
+
return this._validateInternal(schema, fieldValue, options);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* 静态工厂方法
|
|
381
617
|
* @static
|
|
382
|
-
* @param {Object} options - 配置选项
|
|
618
|
+
* @param {Object} options - ajv配置选项
|
|
383
619
|
* @returns {Validator} Validator实例
|
|
384
620
|
*/
|
|
385
621
|
static create(options) {
|
package/lib/locales/en-US.js
CHANGED
|
@@ -12,6 +12,11 @@ module.exports = {
|
|
|
12
12
|
'max-depth': 'Maximum recursion depth ({{#depth}}) exceeded at {{#label}}',
|
|
13
13
|
exception: '{{#label}} validation exception: {{#message}}',
|
|
14
14
|
|
|
15
|
+
// Conditional (ConditionalBuilder)
|
|
16
|
+
'conditional.underAge': 'Minors cannot register',
|
|
17
|
+
'conditional.blocked': 'Account has been blocked',
|
|
18
|
+
'conditional.notAllowed': 'Registration not allowed',
|
|
19
|
+
|
|
15
20
|
// Formats
|
|
16
21
|
'format.email': '{{#label}} must be a valid email address',
|
|
17
22
|
'format.url': '{{#label}} must be a valid URL',
|
|
@@ -115,6 +120,15 @@ module.exports = {
|
|
|
115
120
|
'pattern.password.strong': 'Password must be at least 8 characters and contain uppercase, lowercase letters and numbers',
|
|
116
121
|
'pattern.password.veryStrong': 'Password must be at least 10 characters and contain uppercase, lowercase letters, numbers and special characters',
|
|
117
122
|
|
|
123
|
+
// Union Type
|
|
124
|
+
'pattern.emailOrPhone': 'Must be an email or phone number',
|
|
125
|
+
'pattern.usernameOrEmail': 'Must be a username or email',
|
|
126
|
+
'pattern.httpOrHttps': 'Must be a URL starting with http or https',
|
|
127
|
+
|
|
128
|
+
// oneOf (cross-type union) - v1.1.0
|
|
129
|
+
oneOf: '{{#label}} must match one of the following types',
|
|
130
|
+
'oneOf.invalid': '{{#label}} value does not match any allowed type',
|
|
131
|
+
|
|
118
132
|
// Unknown error fallback
|
|
119
133
|
'UNKNOWN_ERROR': 'Unknown validation error',
|
|
120
134
|
|
package/lib/locales/es-ES.js
CHANGED
|
@@ -99,6 +99,10 @@ module.exports = {
|
|
|
99
99
|
// Unknown error fallback
|
|
100
100
|
'UNKNOWN_ERROR': 'Error de validación desconocido',
|
|
101
101
|
|
|
102
|
+
// oneOf (unión entre tipos) - v1.1.0
|
|
103
|
+
oneOf: '{{#label}} debe coincidir con uno de los siguientes tipos',
|
|
104
|
+
'oneOf.invalid': 'El valor de {{#label}} no coincide con ningún tipo permitido',
|
|
105
|
+
|
|
102
106
|
// Custom validation
|
|
103
107
|
'CUSTOM_VALIDATION_FAILED': 'Validación fallida',
|
|
104
108
|
'ASYNC_VALIDATION_NOT_SUPPORTED': 'La validación asíncrona no es compatible en validate() síncrono',
|
package/lib/locales/fr-FR.js
CHANGED
|
@@ -99,6 +99,10 @@ module.exports = {
|
|
|
99
99
|
// Unknown error fallback
|
|
100
100
|
'UNKNOWN_ERROR': 'Erreur de validation inconnue',
|
|
101
101
|
|
|
102
|
+
// oneOf (union de types) - v1.1.0
|
|
103
|
+
oneOf: '{{#label}} doit correspondre à l\'un des types suivants',
|
|
104
|
+
'oneOf.invalid': 'La valeur de {{#label}} ne correspond à aucun type autorisé',
|
|
105
|
+
|
|
102
106
|
// Custom validation
|
|
103
107
|
'CUSTOM_VALIDATION_FAILED': 'Validation échouée',
|
|
104
108
|
'ASYNC_VALIDATION_NOT_SUPPORTED': 'La validation asynchrone n\'est pas prise en charge dans validate() synchrone',
|
package/lib/locales/ja-JP.js
CHANGED
|
@@ -12,6 +12,11 @@ module.exports = {
|
|
|
12
12
|
'max-depth': '{{#label}}で最大再帰深度 ({{#depth}}) を超えました',
|
|
13
13
|
exception: '{{#label}}の検証例外: {{#message}}',
|
|
14
14
|
|
|
15
|
+
// Conditional (ConditionalBuilder)
|
|
16
|
+
'conditional.underAge': '未成年者は登録できません',
|
|
17
|
+
'conditional.blocked': 'アカウントがブロックされています',
|
|
18
|
+
'conditional.notAllowed': '登録は許可されていません',
|
|
19
|
+
|
|
15
20
|
// Formats
|
|
16
21
|
'format.email': '{{#label}}は有効なメールアドレスである必要があります',
|
|
17
22
|
'format.url': '{{#label}}は有効なURLである必要があります',
|
|
@@ -96,6 +101,10 @@ module.exports = {
|
|
|
96
101
|
'pattern.password.strong': 'パスワードは少なくとも8文字で、大文字、小文字、数字を含む必要があります',
|
|
97
102
|
'pattern.password.veryStrong': 'パスワードは少なくとも10文字で、大文字、小文字、数字、特殊文字を含む必要があります',
|
|
98
103
|
|
|
104
|
+
// oneOf (型の結合) - v1.1.0
|
|
105
|
+
oneOf: '{{#label}}は次のいずれかの型に一致する必要があります',
|
|
106
|
+
'oneOf.invalid': '{{#label}}の値は許可された型のいずれとも一致しません',
|
|
107
|
+
|
|
99
108
|
// Unknown error fallback
|
|
100
109
|
'UNKNOWN_ERROR': '不明な検証エラー',
|
|
101
110
|
|
package/lib/locales/zh-CN.js
CHANGED
|
@@ -12,6 +12,11 @@ module.exports = {
|
|
|
12
12
|
'max-depth': '超过最大递归深度 ({{#depth}}) at {{#label}}',
|
|
13
13
|
exception: '{{#label}}验证异常: {{#message}}',
|
|
14
14
|
|
|
15
|
+
// Conditional (ConditionalBuilder)
|
|
16
|
+
'conditional.underAge': '未成年用户不能注册',
|
|
17
|
+
'conditional.blocked': '账号已被封禁',
|
|
18
|
+
'conditional.notAllowed': '不允许注册',
|
|
19
|
+
|
|
15
20
|
// Formats
|
|
16
21
|
'format.email': '{{#label}}必须是有效的邮箱地址',
|
|
17
22
|
'format.url': '{{#label}}必须是有效的URL地址',
|
|
@@ -115,6 +120,15 @@ module.exports = {
|
|
|
115
120
|
'pattern.password.strong': '密码至少8位,需包含大小写字母和数字',
|
|
116
121
|
'pattern.password.veryStrong': '密码至少10位,需包含大小写字母、数字和特殊字符',
|
|
117
122
|
|
|
123
|
+
// Union Type (联合类型)
|
|
124
|
+
'pattern.emailOrPhone': '必须是邮箱或手机号',
|
|
125
|
+
'pattern.usernameOrEmail': '必须是用户名或邮箱',
|
|
126
|
+
'pattern.httpOrHttps': '必须是 http 或 https 开头的 URL',
|
|
127
|
+
|
|
128
|
+
// oneOf (跨类型联合) - v1.1.0 新增
|
|
129
|
+
oneOf: '{{#label}}必须匹配以下类型之一',
|
|
130
|
+
'oneOf.invalid': '{{#label}}的值不匹配任何允许的类型',
|
|
131
|
+
|
|
118
132
|
// Unknown error fallback
|
|
119
133
|
'UNKNOWN_ERROR': '未知的验证错误',
|
|
120
134
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "schema-dsl",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "简洁强大的JSON Schema验证库 - DSL语法 + String扩展 + 便捷validate",
|
|
5
5
|
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
9
|
"require": "./index.js",
|
|
@@ -57,8 +58,10 @@
|
|
|
57
58
|
"ajv-formats": "^2.1.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
61
|
+
"benchmark": "^2.1.4",
|
|
60
62
|
"chai": "^4.5.0",
|
|
61
63
|
"eslint": "^8.57.1",
|
|
64
|
+
"express": "^5.2.1",
|
|
62
65
|
"joi": "^18.0.2",
|
|
63
66
|
"mocha": "^10.8.2",
|
|
64
67
|
"monsqlize": "^1.0.1",
|
|
@@ -67,4 +70,4 @@
|
|
|
67
70
|
"yup": "^1.7.1",
|
|
68
71
|
"zod": "^4.2.1"
|
|
69
72
|
}
|
|
70
|
-
}
|
|
73
|
+
}
|