schema-dsl 1.0.9 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +325 -2
- package/README.md +419 -189
- package/STATUS.md +65 -3
- package/docs/FEATURE-INDEX.md +1 -1
- package/docs/best-practices.md +3 -3
- package/docs/cache-manager.md +1 -1
- package/docs/conditional-api.md +1278 -0
- package/docs/dsl-syntax.md +1 -1
- package/docs/dynamic-locale.md +2 -2
- 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 +1 -1
- 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/i18n-error.examples.js +181 -0
- package/examples/union-type-example.js +127 -0
- package/examples/union-types-example.js +77 -0
- package/index.d.ts +655 -7
- package/index.js +54 -3
- package/lib/adapters/DslAdapter.js +14 -5
- package/lib/core/ConditionalBuilder.js +503 -0
- package/lib/core/DslBuilder.js +113 -0
- package/lib/core/Locale.js +13 -8
- package/lib/core/Validator.js +250 -2
- package/lib/errors/I18nError.js +222 -0
- package/lib/locales/en-US.js +39 -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 +39 -0
- package/package.json +3 -1
package/lib/core/DslBuilder.js
CHANGED
|
@@ -23,6 +23,87 @@ const Locale = require('./Locale');
|
|
|
23
23
|
const patterns = require('../config/patterns');
|
|
24
24
|
|
|
25
25
|
class DslBuilder {
|
|
26
|
+
/**
|
|
27
|
+
* 静态属性:存储用户自定义类型(插件注册)
|
|
28
|
+
* @private
|
|
29
|
+
* @type {Map<string, Object|Function>}
|
|
30
|
+
*/
|
|
31
|
+
static _customTypes = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 注册自定义类型(供插件使用)
|
|
35
|
+
* @param {string} name - 类型名称
|
|
36
|
+
* @param {Object|Function} schema - JSON Schema对象 或 生成函数
|
|
37
|
+
* @throws {Error} 类型名称无效时抛出错误
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* // 插件中注册自定义类型
|
|
41
|
+
* DslBuilder.registerType('phone-cn', {
|
|
42
|
+
* type: 'string',
|
|
43
|
+
* pattern: '^1[3-9]\\d{9}$'
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // 在DSL中使用
|
|
47
|
+
* dsl('phone-cn!') // ✅ 可用
|
|
48
|
+
* dsl('types:string|phone-cn') // ✅ 可用
|
|
49
|
+
*/
|
|
50
|
+
static registerType(name, schema) {
|
|
51
|
+
if (!name || typeof name !== 'string') {
|
|
52
|
+
throw new Error('Type name must be a non-empty string');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!schema || (typeof schema !== 'object' && typeof schema !== 'function')) {
|
|
56
|
+
throw new Error('Schema must be an object or function');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
this._customTypes.set(name, schema);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 检查类型是否已注册(内置或自定义)
|
|
64
|
+
* @param {string} type - 类型名称
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* DslBuilder.hasType('string') // true (内置)
|
|
69
|
+
* DslBuilder.hasType('phone-cn') // false (未注册)
|
|
70
|
+
*
|
|
71
|
+
* DslBuilder.registerType('phone-cn', { ... });
|
|
72
|
+
* DslBuilder.hasType('phone-cn') // true (已注册)
|
|
73
|
+
*/
|
|
74
|
+
static hasType(type) {
|
|
75
|
+
// 检查自定义类型
|
|
76
|
+
if (this._customTypes.has(type)) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 检查内置类型
|
|
81
|
+
const builtInTypes = [
|
|
82
|
+
'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
|
|
83
|
+
'email', 'url', 'uuid', 'date', 'datetime', 'time',
|
|
84
|
+
'ipv4', 'ipv6', 'binary', 'objectId', 'hexColor', 'macAddress',
|
|
85
|
+
'cron', 'slug', 'alphanum', 'lower', 'upper', 'json', 'port',
|
|
86
|
+
'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport', 'any'
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
return builtInTypes.includes(type);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 获取所有已注册的自定义类型
|
|
94
|
+
* @returns {Array<string>}
|
|
95
|
+
*/
|
|
96
|
+
static getCustomTypes() {
|
|
97
|
+
return Array.from(this._customTypes.keys());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 清除所有自定义类型(主要用于测试)
|
|
102
|
+
*/
|
|
103
|
+
static clearCustomTypes() {
|
|
104
|
+
this._customTypes.clear();
|
|
105
|
+
}
|
|
106
|
+
|
|
26
107
|
/**
|
|
27
108
|
* 创建 DslBuilder 实例
|
|
28
109
|
* @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
|
|
@@ -63,6 +144,26 @@ class DslBuilder {
|
|
|
63
144
|
* @returns {Object} JSON Schema对象
|
|
64
145
|
*/
|
|
65
146
|
_parseSimple(dsl) {
|
|
147
|
+
// 🔴 处理跨类型联合:types:type1|type2|type3
|
|
148
|
+
if (dsl.startsWith('types:')) {
|
|
149
|
+
const typesStr = dsl.substring(6); // 去掉 'types:' 前缀
|
|
150
|
+
const types = typesStr.split('|').map(t => t.trim()).filter(t => t);
|
|
151
|
+
|
|
152
|
+
if (types.length === 0) {
|
|
153
|
+
throw new Error('types: requires at least one type');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (types.length === 1) {
|
|
157
|
+
// 只有一个类型,直接解析(避免不必要的oneOf)
|
|
158
|
+
return this._parseSimple(types[0]);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 多个类型,生成oneOf结构
|
|
162
|
+
return {
|
|
163
|
+
oneOf: types.map(type => this._parseSimple(type))
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
66
167
|
// 处理数组类型:array:1-10 或 array<string>
|
|
67
168
|
if (dsl.startsWith('array')) {
|
|
68
169
|
const schema = { type: 'array' };
|
|
@@ -233,6 +334,18 @@ class DslBuilder {
|
|
|
233
334
|
* @private
|
|
234
335
|
*/
|
|
235
336
|
_getBaseType(type) {
|
|
337
|
+
// 🔴 优先查询自定义类型(插件注册的)
|
|
338
|
+
if (DslBuilder._customTypes.has(type)) {
|
|
339
|
+
const customSchema = DslBuilder._customTypes.get(type);
|
|
340
|
+
// 如果是函数,调用它生成Schema
|
|
341
|
+
if (typeof customSchema === 'function') {
|
|
342
|
+
return customSchema();
|
|
343
|
+
}
|
|
344
|
+
// 否则返回Schema对象的深拷贝(避免污染)
|
|
345
|
+
return JSON.parse(JSON.stringify(customSchema));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 🔴 查询内置类型
|
|
236
349
|
const typeMap = {
|
|
237
350
|
'string': { type: 'string' },
|
|
238
351
|
'number': { type: 'number' },
|
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
|
@@ -135,6 +135,24 @@ class Validator {
|
|
|
135
135
|
schema = this.dslSchemaCache.get(schema);
|
|
136
136
|
}
|
|
137
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
|
+
|
|
138
156
|
// 检查是否需要移除额外字段 (clean 模式)
|
|
139
157
|
if (schema && schema._removeAdditional) {
|
|
140
158
|
// 创建新的 Validator 实例,启用 removeAdditional
|
|
@@ -369,9 +387,239 @@ class Validator {
|
|
|
369
387
|
}
|
|
370
388
|
|
|
371
389
|
/**
|
|
372
|
-
*
|
|
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 evaluation = conditionalSchema._evaluateCondition(cond, data);
|
|
469
|
+
const matched = evaluation.result;
|
|
470
|
+
const failedMessage = evaluation.failedMessage;
|
|
471
|
+
|
|
472
|
+
if (cond.action === 'throw') {
|
|
473
|
+
// ✅ message 模式:条件为 true 时抛错,条件为 false 时通过
|
|
474
|
+
if (matched) {
|
|
475
|
+
// ✅ 条件满足(true),抛出错误
|
|
476
|
+
// 优先使用具体的失败消息,如果没有则使用整体消息
|
|
477
|
+
const errorMsg = failedMessage || cond.message;
|
|
478
|
+
// 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
|
|
479
|
+
// 传递 locale 参数以支持动态语言切换
|
|
480
|
+
const errorMessage = Locale.getMessage(errorMsg, options.messages || {}, locale);
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
valid: false,
|
|
484
|
+
errors: [{
|
|
485
|
+
message: errorMessage,
|
|
486
|
+
path: '',
|
|
487
|
+
keyword: 'conditional',
|
|
488
|
+
params: { condition: cond.type }
|
|
489
|
+
}],
|
|
490
|
+
data
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
// 条件不满足(false),继续验证
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ✅ then/else 模式:找到第一个满足的条件就执行并返回
|
|
498
|
+
if (matched) {
|
|
499
|
+
// 条件满足,执行 then Schema
|
|
500
|
+
if (cond.then !== undefined && cond.then !== null) {
|
|
501
|
+
return this._executeThenBranch(cond.then, data, fieldValue, fieldName, options);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 条件满足但没有 then,表示验证通过
|
|
505
|
+
return {
|
|
506
|
+
valid: true,
|
|
507
|
+
errors: [],
|
|
508
|
+
data
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// ✅ 如果是 if/elseIf 条件不满足,继续检查下一个 elseIf
|
|
513
|
+
// 这样就支持了 if...elseIf...elseIf...else 链
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ✅ 所有条件都不满足,执行 else
|
|
517
|
+
if (conditionalSchema.else !== undefined) {
|
|
518
|
+
if (conditionalSchema.else === null) {
|
|
519
|
+
// else 为 null,表示跳过验证
|
|
520
|
+
return {
|
|
521
|
+
valid: true,
|
|
522
|
+
errors: [],
|
|
523
|
+
data
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// 执行 else Schema
|
|
528
|
+
return this._executeThenBranch(conditionalSchema.else, data, fieldValue, fieldName, options);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 没有 else,表示不验证
|
|
532
|
+
return {
|
|
533
|
+
valid: true,
|
|
534
|
+
errors: [],
|
|
535
|
+
data
|
|
536
|
+
};
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return {
|
|
539
|
+
valid: false,
|
|
540
|
+
errors: [{
|
|
541
|
+
message: `Conditional validation error: ${error.message}`,
|
|
542
|
+
path: '',
|
|
543
|
+
keyword: 'conditional'
|
|
544
|
+
}],
|
|
545
|
+
data
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* 执行 then 分支(提取公共逻辑)
|
|
552
|
+
* @private
|
|
553
|
+
* @param {*} thenSchema - then 分支的 Schema
|
|
554
|
+
* @param {*} data - 完整数据对象(用于嵌套条件判断)
|
|
555
|
+
* @param {*} fieldValue - 字段值(用于验证)
|
|
556
|
+
* @param {string} fieldName - 字段名
|
|
557
|
+
* @param {Object} options - 验证选项
|
|
558
|
+
*/
|
|
559
|
+
_executeThenBranch(thenSchema, data, fieldValue, fieldName, options) {
|
|
560
|
+
const DslBuilder = require('./DslBuilder');
|
|
561
|
+
|
|
562
|
+
// ✅ 如果是 ConditionalBuilder 实例(未调用 toSchema),先转换
|
|
563
|
+
if (thenSchema && typeof thenSchema.toSchema === 'function') {
|
|
564
|
+
thenSchema = thenSchema.toSchema();
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ✅ 处理嵌套的 ConditionalBuilder(已转换为 Schema 对象)
|
|
568
|
+
if (thenSchema && thenSchema._isConditional) {
|
|
569
|
+
// 嵌套的条件构建器,递归处理
|
|
570
|
+
// 传递完整数据对象用于条件判断,传递字段值用于验证
|
|
571
|
+
return this._validateConditional(thenSchema, data, fieldName, fieldValue, options);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 如果是字符串,解析为 Schema
|
|
575
|
+
if (typeof thenSchema === 'string') {
|
|
576
|
+
const builder = new DslBuilder(thenSchema);
|
|
577
|
+
thenSchema = builder.toSchema();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ✅ 验证字段值
|
|
581
|
+
return this._validateFieldValue(thenSchema, fieldValue, fieldName, options);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 验证字段值(处理空字符串和 undefined)
|
|
586
|
+
* @private
|
|
587
|
+
* @param {Object} schema - Schema 对象
|
|
588
|
+
* @param {*} fieldValue - 字段值
|
|
589
|
+
* @param {string} fieldName - 字段名
|
|
590
|
+
* @param {Object} options - 验证选项
|
|
591
|
+
* @returns {Object} 验证结果
|
|
592
|
+
*/
|
|
593
|
+
_validateFieldValue(schema, fieldValue, fieldName, options = {}) {
|
|
594
|
+
// ✅ 检查字段是否必填
|
|
595
|
+
const isRequired = schema && schema._required === true;
|
|
596
|
+
|
|
597
|
+
// ✅ 处理 undefined:可选字段缺失时跳过验证
|
|
598
|
+
if (!isRequired && fieldValue === undefined) {
|
|
599
|
+
return {
|
|
600
|
+
valid: true,
|
|
601
|
+
errors: [],
|
|
602
|
+
data: fieldValue
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ✅ 处理空字符串:可选字段的空字符串视为未提供
|
|
607
|
+
if (!isRequired && fieldValue === '') {
|
|
608
|
+
return {
|
|
609
|
+
valid: true,
|
|
610
|
+
errors: [],
|
|
611
|
+
data: fieldValue
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 正常验证
|
|
616
|
+
return this._validateInternal(schema, fieldValue, options);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* 静态工厂方法
|
|
373
621
|
* @static
|
|
374
|
-
* @param {Object} options - 配置选项
|
|
622
|
+
* @param {Object} options - ajv配置选项
|
|
375
623
|
* @returns {Validator} Validator实例
|
|
376
624
|
*/
|
|
377
625
|
static create(options) {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I18nError - 多语言错误工具类
|
|
3
|
+
*
|
|
4
|
+
* 提供统一的多语言错误抛出机制,支持:
|
|
5
|
+
* - 多语言 key 自动翻译
|
|
6
|
+
* - 参数插值(如 {{field}}, {{limit}})
|
|
7
|
+
* - 自定义错误代码
|
|
8
|
+
* - Express/Koa 集成
|
|
9
|
+
*
|
|
10
|
+
* @module lib/errors/I18nError
|
|
11
|
+
* @version 1.1.1
|
|
12
|
+
*
|
|
13
|
+
* @example 基础用法
|
|
14
|
+
* const { I18nError } = require('schema-dsl');
|
|
15
|
+
*
|
|
16
|
+
* // 抛出多语言错误
|
|
17
|
+
* throw I18nError.create('error.notFound', { resource: '账户' });
|
|
18
|
+
* // 中文: "找不到账户"
|
|
19
|
+
* // 英文: "Account not found"
|
|
20
|
+
*
|
|
21
|
+
* @example 业务代码中使用
|
|
22
|
+
* function getAccount(id) {
|
|
23
|
+
* const account = db.findAccount(id);
|
|
24
|
+
* if (!account) {
|
|
25
|
+
* throw I18nError.create('account.notFound', { accountId: id });
|
|
26
|
+
* }
|
|
27
|
+
* if (account.balance < 100) {
|
|
28
|
+
* throw I18nError.create('account.insufficientBalance', {
|
|
29
|
+
* balance: account.balance,
|
|
30
|
+
* required: 100
|
|
31
|
+
* });
|
|
32
|
+
* }
|
|
33
|
+
* return account;
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* @example Express 中间件
|
|
37
|
+
* app.use((error, req, res, next) => {
|
|
38
|
+
* if (error instanceof I18nError) {
|
|
39
|
+
* return res.status(error.statusCode).json(error.toJSON());
|
|
40
|
+
* }
|
|
41
|
+
* next(error);
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
const Locale = require('../core/Locale');
|
|
46
|
+
const MessageTemplate = require('../core/MessageTemplate');
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 多语言错误类
|
|
50
|
+
*
|
|
51
|
+
* @class I18nError
|
|
52
|
+
* @extends Error
|
|
53
|
+
*
|
|
54
|
+
* @property {string} name - 错误名称(固定为 'I18nError')
|
|
55
|
+
* @property {string} message - 错误消息(已翻译)
|
|
56
|
+
* @property {string} code - 错误代码(多语言 key)
|
|
57
|
+
* @property {Object} params - 错误参数(用于插值)
|
|
58
|
+
* @property {number} statusCode - HTTP 状态码(默认 400)
|
|
59
|
+
* @property {string} locale - 使用的语言环境
|
|
60
|
+
*/
|
|
61
|
+
class I18nError extends Error {
|
|
62
|
+
/**
|
|
63
|
+
* 构造函数
|
|
64
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
65
|
+
* @param {Object} params - 错误参数(用于插值)
|
|
66
|
+
* @param {number} statusCode - HTTP 状态码(默认 400)
|
|
67
|
+
* @param {string} locale - 语言环境(默认使用当前语言)
|
|
68
|
+
*/
|
|
69
|
+
constructor(code, params = {}, statusCode = 400, locale = null) {
|
|
70
|
+
// 获取翻译后的消息模板
|
|
71
|
+
const actualLocale = locale || Locale.getLocale();
|
|
72
|
+
const template = Locale.getMessage(code, {}, actualLocale);
|
|
73
|
+
|
|
74
|
+
// 使用 MessageTemplate 进行参数插值
|
|
75
|
+
const messageTemplate = new MessageTemplate(template);
|
|
76
|
+
const message = messageTemplate.render(params || {});
|
|
77
|
+
|
|
78
|
+
super(message);
|
|
79
|
+
|
|
80
|
+
this.name = 'I18nError';
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.params = params || {};
|
|
83
|
+
this.statusCode = statusCode;
|
|
84
|
+
this.locale = actualLocale;
|
|
85
|
+
|
|
86
|
+
// 保持堆栈跟踪
|
|
87
|
+
if (Error.captureStackTrace) {
|
|
88
|
+
Error.captureStackTrace(this, I18nError);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 静态工厂方法 - 创建并抛出错误
|
|
94
|
+
*
|
|
95
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
96
|
+
* @param {Object} params - 错误参数
|
|
97
|
+
* @param {number} statusCode - HTTP 状态码
|
|
98
|
+
* @returns {I18nError} 错误实例
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // 创建错误(不抛出)
|
|
102
|
+
* const error = I18nError.create('error.notFound', { resource: '用户' });
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* // 直接抛出
|
|
106
|
+
* throw I18nError.create('error.notFound', { resource: '用户' });
|
|
107
|
+
*/
|
|
108
|
+
static create(code, params = {}, statusCode = 400) {
|
|
109
|
+
return new I18nError(code, params, statusCode);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 静态工厂方法 - 快速抛出错误
|
|
114
|
+
*
|
|
115
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
116
|
+
* @param {Object} params - 错误参数
|
|
117
|
+
* @param {number} statusCode - HTTP 状态码
|
|
118
|
+
* @throws {I18nError} 直接抛出错误
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* I18nError.throw('error.notFound', { resource: '用户' });
|
|
122
|
+
* // 等同于:throw I18nError.create('error.notFound', { resource: '用户' });
|
|
123
|
+
*/
|
|
124
|
+
static throw(code, params = {}, statusCode = 400) {
|
|
125
|
+
throw new I18nError(code, params, statusCode);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 断言方法 - 条件不满足时抛错
|
|
130
|
+
*
|
|
131
|
+
* @param {boolean} condition - 条件表达式
|
|
132
|
+
* @param {string} code - 错误代码(多语言 key)
|
|
133
|
+
* @param {Object} params - 错误参数
|
|
134
|
+
* @param {number} statusCode - HTTP 状态码
|
|
135
|
+
* @throws {I18nError} 条件为 false 时抛出错误
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* I18nError.assert(account, 'account.notFound', { accountId: id });
|
|
139
|
+
* // 等同于:if (!account) throw I18nError.create('account.notFound', { accountId: id });
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* I18nError.assert(
|
|
143
|
+
* account.balance >= 100,
|
|
144
|
+
* 'account.insufficientBalance',
|
|
145
|
+
* { balance: account.balance, required: 100 }
|
|
146
|
+
* );
|
|
147
|
+
*/
|
|
148
|
+
static assert(condition, code, params = {}, statusCode = 400) {
|
|
149
|
+
if (!condition) {
|
|
150
|
+
throw new I18nError(code, params, statusCode);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 检查错误是否为指定代码
|
|
156
|
+
*
|
|
157
|
+
* @param {string} code - 错误代码
|
|
158
|
+
* @returns {boolean} 是否匹配
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* try {
|
|
162
|
+
* // ...
|
|
163
|
+
* } catch (error) {
|
|
164
|
+
* if (error instanceof I18nError && error.is('account.notFound')) {
|
|
165
|
+
* // 处理账户不存在的情况
|
|
166
|
+
* }
|
|
167
|
+
* }
|
|
168
|
+
*/
|
|
169
|
+
is(code) {
|
|
170
|
+
return this.code === code;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 转换为 JSON 格式(用于 API 响应)
|
|
175
|
+
*
|
|
176
|
+
* @returns {Object} JSON 对象
|
|
177
|
+
* @returns {string} return.error - 错误名称
|
|
178
|
+
* @returns {string} return.code - 错误代码
|
|
179
|
+
* @returns {string} return.message - 错误消息(已翻译)
|
|
180
|
+
* @returns {Object} return.params - 错误参数
|
|
181
|
+
* @returns {number} return.statusCode - 状态码
|
|
182
|
+
* @returns {string} return.locale - 语言环境
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* const json = error.toJSON();
|
|
186
|
+
* res.status(error.statusCode).json(json);
|
|
187
|
+
* // {
|
|
188
|
+
* // error: 'I18nError',
|
|
189
|
+
* // code: 'account.notFound',
|
|
190
|
+
* // message: '找不到账户',
|
|
191
|
+
* // params: { accountId: '123' },
|
|
192
|
+
* // statusCode: 404,
|
|
193
|
+
* // locale: 'zh-CN'
|
|
194
|
+
* // }
|
|
195
|
+
*/
|
|
196
|
+
toJSON() {
|
|
197
|
+
return {
|
|
198
|
+
error: this.name,
|
|
199
|
+
code: this.code,
|
|
200
|
+
message: this.message,
|
|
201
|
+
params: this.params,
|
|
202
|
+
statusCode: this.statusCode,
|
|
203
|
+
locale: this.locale
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 转换为字符串
|
|
209
|
+
*
|
|
210
|
+
* @returns {string} 格式化的错误信息
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* console.log(error.toString());
|
|
214
|
+
* // "I18nError [account.notFound]: 找不到账户"
|
|
215
|
+
*/
|
|
216
|
+
toString() {
|
|
217
|
+
return `${this.name} [${this.code}]: ${this.message}`;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = I18nError;
|
|
222
|
+
|