schema-dsl 2.3.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 +35 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1122 -0
- package/STATUS.md +273 -0
- package/docs/FEATURE-INDEX.md +521 -0
- package/docs/INDEX.md +224 -0
- package/docs/api-reference.md +1098 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +602 -0
- package/docs/dsl-syntax.md +654 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +459 -0
- package/docs/faq.md +576 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +488 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +398 -0
- package/docs/mongodb-exporter.md +279 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +257 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +290 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils.md +492 -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.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/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/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/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 +270 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +653 -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 +316 -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 +313 -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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 错误码定义
|
|
3
|
+
*
|
|
4
|
+
* 定义所有内置的错误码和默认错误消息
|
|
5
|
+
* v2.0.1: 支持简化格式(无类型前缀)
|
|
6
|
+
*
|
|
7
|
+
* @module lib/core/ErrorCodes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const ERROR_CODES = {
|
|
11
|
+
// ========== 字符串类型错误 ==========
|
|
12
|
+
// v2.0.1: 简化格式(推荐,用户友好)
|
|
13
|
+
'min': {
|
|
14
|
+
code: 'TOO_SHORT',
|
|
15
|
+
message: '{{#label}} length must be at least {{#limit}} characters'
|
|
16
|
+
},
|
|
17
|
+
'max': {
|
|
18
|
+
code: 'TOO_LONG',
|
|
19
|
+
message: '{{#label}} length must be at most {{#limit}} characters'
|
|
20
|
+
},
|
|
21
|
+
// ajv 实际使用的关键字(映射到简化版本)
|
|
22
|
+
'minLength': {
|
|
23
|
+
code: 'TOO_SHORT',
|
|
24
|
+
message: '{{#label}} length must be at least {{#limit}} characters'
|
|
25
|
+
},
|
|
26
|
+
'maxLength': {
|
|
27
|
+
code: 'TOO_LONG',
|
|
28
|
+
message: '{{#label}} length must be at most {{#limit}} characters'
|
|
29
|
+
},
|
|
30
|
+
'email': {
|
|
31
|
+
code: 'INVALID_EMAIL',
|
|
32
|
+
message: '{{#label}} must be a valid email address'
|
|
33
|
+
},
|
|
34
|
+
'url': {
|
|
35
|
+
code: 'INVALID_URL',
|
|
36
|
+
message: '{{#label}} must be a valid URL'
|
|
37
|
+
},
|
|
38
|
+
'pattern': {
|
|
39
|
+
code: 'INVALID_PATTERN',
|
|
40
|
+
message: '{{#label}} format is invalid'
|
|
41
|
+
},
|
|
42
|
+
'format': {
|
|
43
|
+
code: 'INVALID_FORMAT',
|
|
44
|
+
message: '{{#label}} must match format "{{#format}}"'
|
|
45
|
+
},
|
|
46
|
+
'required': {
|
|
47
|
+
code: 'REQUIRED',
|
|
48
|
+
message: '{{#label}} is required'
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// Formats (Standardized)
|
|
52
|
+
'format.email': {
|
|
53
|
+
code: 'INVALID_EMAIL',
|
|
54
|
+
message: '{{#label}} must be a valid email address'
|
|
55
|
+
},
|
|
56
|
+
'format.url': {
|
|
57
|
+
code: 'INVALID_URL',
|
|
58
|
+
message: '{{#label}} must be a valid URL'
|
|
59
|
+
},
|
|
60
|
+
'format.uuid': {
|
|
61
|
+
code: 'INVALID_UUID',
|
|
62
|
+
message: '{{#label}} must be a valid UUID'
|
|
63
|
+
},
|
|
64
|
+
'format.ipv4': {
|
|
65
|
+
code: 'INVALID_IPV4',
|
|
66
|
+
message: '{{#label}} must be a valid IPv4 address'
|
|
67
|
+
},
|
|
68
|
+
'format.ipv6': {
|
|
69
|
+
code: 'INVALID_IPV6',
|
|
70
|
+
message: '{{#label}} must be a valid IPv6 address'
|
|
71
|
+
},
|
|
72
|
+
'string.hostname': {
|
|
73
|
+
code: 'STRING_INVALID_HOSTNAME',
|
|
74
|
+
message: '{{#label}} must be a valid hostname'
|
|
75
|
+
},
|
|
76
|
+
'string.pattern': {
|
|
77
|
+
code: 'STRING_PATTERN_MISMATCH',
|
|
78
|
+
message: '{{#label}} format is invalid'
|
|
79
|
+
},
|
|
80
|
+
'string.enum': {
|
|
81
|
+
code: 'STRING_INVALID_ENUM',
|
|
82
|
+
message: '{{#label}} must be one of: {{#valids}}'
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ========== 数字类型错误 ==========
|
|
86
|
+
'number.base': {
|
|
87
|
+
code: 'NUMBER_INVALID_TYPE',
|
|
88
|
+
message: '{{#label}} must be a number'
|
|
89
|
+
},
|
|
90
|
+
'number.min': {
|
|
91
|
+
code: 'NUMBER_TOO_SMALL',
|
|
92
|
+
message: '{{#label}} must be greater than or equal to {{#limit}}'
|
|
93
|
+
},
|
|
94
|
+
'number.max': {
|
|
95
|
+
code: 'NUMBER_TOO_LARGE',
|
|
96
|
+
message: '{{#label}} must be less than or equal to {{#limit}}'
|
|
97
|
+
},
|
|
98
|
+
'number.integer': {
|
|
99
|
+
code: 'NUMBER_NOT_INTEGER',
|
|
100
|
+
message: '{{#label}} must be an integer'
|
|
101
|
+
},
|
|
102
|
+
'number.positive': {
|
|
103
|
+
code: 'NUMBER_NOT_POSITIVE',
|
|
104
|
+
message: '{{#label}} must be a positive number'
|
|
105
|
+
},
|
|
106
|
+
'number.negative': {
|
|
107
|
+
code: 'NUMBER_NOT_NEGATIVE',
|
|
108
|
+
message: '{{#label}} must be a negative number'
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
// ========== 布尔类型错误 ==========
|
|
112
|
+
'boolean.base': {
|
|
113
|
+
code: 'BOOLEAN_INVALID_TYPE',
|
|
114
|
+
message: '{{#label}} must be a boolean'
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
// ========== 对象类型错误 ==========
|
|
118
|
+
'object.base': {
|
|
119
|
+
code: 'OBJECT_INVALID_TYPE',
|
|
120
|
+
message: '{{#label}} must be an object'
|
|
121
|
+
},
|
|
122
|
+
'object.min': {
|
|
123
|
+
code: 'OBJECT_TOO_FEW_KEYS',
|
|
124
|
+
message: '{{#label}} must have at least {{#limit}} keys'
|
|
125
|
+
},
|
|
126
|
+
'object.max': {
|
|
127
|
+
code: 'OBJECT_TOO_MANY_KEYS',
|
|
128
|
+
message: '{{#label}} must have at most {{#limit}} keys'
|
|
129
|
+
},
|
|
130
|
+
'object.unknown': {
|
|
131
|
+
code: 'OBJECT_UNKNOWN_KEY',
|
|
132
|
+
message: '{{#label}} contains unknown key: {{#key}}'
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// ========== 数组类型错误 ==========
|
|
136
|
+
'array.base': {
|
|
137
|
+
code: 'ARRAY_INVALID_TYPE',
|
|
138
|
+
message: '{{#label}} must be an array'
|
|
139
|
+
},
|
|
140
|
+
'array.min': {
|
|
141
|
+
code: 'ARRAY_TOO_FEW_ITEMS',
|
|
142
|
+
message: '{{#label}} must have at least {{#limit}} items'
|
|
143
|
+
},
|
|
144
|
+
'array.max': {
|
|
145
|
+
code: 'ARRAY_TOO_MANY_ITEMS',
|
|
146
|
+
message: '{{#label}} must have at most {{#limit}} items'
|
|
147
|
+
},
|
|
148
|
+
'array.length': {
|
|
149
|
+
code: 'ARRAY_INVALID_LENGTH',
|
|
150
|
+
message: '{{#label}} must have exactly {{#limit}} items'
|
|
151
|
+
},
|
|
152
|
+
'array.unique': {
|
|
153
|
+
code: 'ARRAY_NOT_UNIQUE',
|
|
154
|
+
message: '{{#label}} must contain unique items'
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// ========== 日期类型错误 ==========
|
|
158
|
+
'date.base': {
|
|
159
|
+
code: 'DATE_INVALID_TYPE',
|
|
160
|
+
message: '{{#label}} must be a valid date'
|
|
161
|
+
},
|
|
162
|
+
'date.min': {
|
|
163
|
+
code: 'DATE_TOO_EARLY',
|
|
164
|
+
message: '{{#label}} must be on or after {{#limit}}'
|
|
165
|
+
},
|
|
166
|
+
'date.max': {
|
|
167
|
+
code: 'DATE_TOO_LATE',
|
|
168
|
+
message: '{{#label}} must be on or before {{#limit}}'
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// ========== 通用错误 ==========
|
|
172
|
+
'type': {
|
|
173
|
+
code: 'INVALID_TYPE',
|
|
174
|
+
message: '{{#label}} must be of type {{#expected}}'
|
|
175
|
+
},
|
|
176
|
+
'any.required': {
|
|
177
|
+
code: 'FIELD_REQUIRED',
|
|
178
|
+
message: '{{#label}} is required'
|
|
179
|
+
},
|
|
180
|
+
'any.invalid': {
|
|
181
|
+
code: 'FIELD_INVALID',
|
|
182
|
+
message: '{{#label}} contains an invalid value'
|
|
183
|
+
},
|
|
184
|
+
'any.only': {
|
|
185
|
+
code: 'FIELD_NOT_MATCH',
|
|
186
|
+
message: '{{#label}} must match {{#valids}}'
|
|
187
|
+
},
|
|
188
|
+
'any.unknown': {
|
|
189
|
+
code: 'FIELD_UNKNOWN',
|
|
190
|
+
message: '{{#key}} is not allowed'
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// ========== 自定义验证错误 ==========
|
|
194
|
+
'custom.validation': {
|
|
195
|
+
code: 'CUSTOM_VALIDATION_FAILED',
|
|
196
|
+
message: 'Validation failed'
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 获取错误信息
|
|
202
|
+
* @param {string} type - 错误类型
|
|
203
|
+
* @returns {Object} 错误信息
|
|
204
|
+
*/
|
|
205
|
+
function getErrorInfo(type) {
|
|
206
|
+
const errorInfo = ERROR_CODES[type];
|
|
207
|
+
if (!errorInfo) {
|
|
208
|
+
return {
|
|
209
|
+
code: 'UNKNOWN_ERROR',
|
|
210
|
+
message: 'Unknown validation error'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
code: errorInfo.code,
|
|
216
|
+
message: errorInfo.message
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 获取所有错误码
|
|
222
|
+
* @returns {Object} 所有错误码
|
|
223
|
+
*/
|
|
224
|
+
function getAllErrorCodes() {
|
|
225
|
+
return ERROR_CODES;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
ERROR_CODES,
|
|
230
|
+
getErrorInfo,
|
|
231
|
+
getAllErrorCodes
|
|
232
|
+
};
|
|
233
|
+
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// lib/core/ErrorFormatter.js
|
|
2
|
+
|
|
3
|
+
const CONSTANTS = require('../config/constants');
|
|
4
|
+
const defaultLocales = require('../locales');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 错误格式化器
|
|
8
|
+
* 将验证错误格式化为友好的消息
|
|
9
|
+
*
|
|
10
|
+
* @class ErrorFormatter
|
|
11
|
+
*/
|
|
12
|
+
class ErrorFormatter {
|
|
13
|
+
constructor(locale = 'zh-CN') {
|
|
14
|
+
this.locale = locale;
|
|
15
|
+
this.messages = this._loadMessages(locale);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 加载错误消息模板
|
|
20
|
+
* @private
|
|
21
|
+
* @param {string} locale - 语言环境
|
|
22
|
+
* @returns {Object} 消息模板
|
|
23
|
+
*/
|
|
24
|
+
_loadMessages(locale) {
|
|
25
|
+
// 优先使用 Locale 类中注册的语言包
|
|
26
|
+
const Locale = require('./Locale');
|
|
27
|
+
const registered = Locale.locales[locale];
|
|
28
|
+
const defaults = defaultLocales[locale] || defaultLocales['en-US'];
|
|
29
|
+
|
|
30
|
+
if (registered) {
|
|
31
|
+
return { ...defaults, ...registered };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 其次使用默认语言包
|
|
35
|
+
return defaults;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 格式化单个错误或错误数组
|
|
40
|
+
* @param {Object|Array<Object>} error - 错误对象或错误数组
|
|
41
|
+
* @param {string} [locale] - 动态指定语言(可选)
|
|
42
|
+
* @returns {string|Array<Object>} 格式化后的错误消息或错误对象数组
|
|
43
|
+
*/
|
|
44
|
+
format(error, locale) {
|
|
45
|
+
// 如果是数组,格式化为详细对象数组
|
|
46
|
+
if (Array.isArray(error)) {
|
|
47
|
+
return this.formatDetailed(error, locale);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 获取当前使用的消息模板
|
|
51
|
+
const messages = locale ? this._loadMessages(locale) : this.messages;
|
|
52
|
+
|
|
53
|
+
// 单个错误对象格式化
|
|
54
|
+
const template = messages[error.type] || error.message;
|
|
55
|
+
return this._interpolate(template, {
|
|
56
|
+
...error,
|
|
57
|
+
path: error.path || 'value',
|
|
58
|
+
allowed: Array.isArray(error.allowed) ? error.allowed.join(', ') : error.allowed
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 格式化所有错误
|
|
64
|
+
* @param {Array<Object>} errors - 错误数组
|
|
65
|
+
* @param {string} [locale] - 动态指定语言(可选)
|
|
66
|
+
* @returns {Array<string>} 格式化后的错误消息数组
|
|
67
|
+
*/
|
|
68
|
+
formatAll(errors, locale) {
|
|
69
|
+
return errors.map(err => this.format(err, locale));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 格式化为详细对象
|
|
74
|
+
* @param {Array<Object>} errors - ajv错误数组
|
|
75
|
+
* @param {string} [locale] - 动态指定语言(可选)
|
|
76
|
+
* @returns {Array<Object>} 详细错误对象数组
|
|
77
|
+
*/
|
|
78
|
+
formatDetailed(errors, locale) {
|
|
79
|
+
if (!Array.isArray(errors)) {
|
|
80
|
+
errors = [errors];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 获取当前使用的消息模板
|
|
84
|
+
const messages = locale ? this._loadMessages(locale) : this.messages;
|
|
85
|
+
|
|
86
|
+
return errors.map(err => {
|
|
87
|
+
// 处理 ajv 错误格式
|
|
88
|
+
const keyword = err.keyword || err.type || 'validation';
|
|
89
|
+
const instancePath = err.instancePath || err.path || '';
|
|
90
|
+
const fieldName = instancePath.replace(/^\//, '') || (err.params && err.params.missingProperty) || 'value';
|
|
91
|
+
|
|
92
|
+
// 获取 Schema 中的自定义信息
|
|
93
|
+
let schema = err.parentSchema || err.schema || {};
|
|
94
|
+
|
|
95
|
+
// 特殊处理 required 错误
|
|
96
|
+
if (keyword === 'required' && err.params && err.params.missingProperty) {
|
|
97
|
+
const prop = err.params.missingProperty;
|
|
98
|
+
if (schema.properties && schema.properties[prop]) {
|
|
99
|
+
schema = schema.properties[prop];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let label = schema._label;
|
|
104
|
+
|
|
105
|
+
if (label) {
|
|
106
|
+
// 如果显式设置了 label,尝试翻译它
|
|
107
|
+
if (messages[label]) {
|
|
108
|
+
label = messages[label];
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
|
|
112
|
+
// 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
|
|
113
|
+
const autoKey = `label.${fieldName.replace(/\//g, '.')}`;
|
|
114
|
+
if (messages[autoKey]) {
|
|
115
|
+
label = messages[autoKey];
|
|
116
|
+
} else {
|
|
117
|
+
// 没找到翻译,回退到 fieldName
|
|
118
|
+
label = fieldName;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const customMessages = schema._customMessages || {};
|
|
123
|
+
|
|
124
|
+
// 关键字映射 (ajv keyword -> schema-dsl 简写)
|
|
125
|
+
// 支持 min/max 作为 minLength/maxLength 的简写
|
|
126
|
+
const keywordMap = {
|
|
127
|
+
'minLength': 'min',
|
|
128
|
+
'maxLength': 'max',
|
|
129
|
+
'minimum': 'min',
|
|
130
|
+
'maximum': 'max',
|
|
131
|
+
'minItems': 'min',
|
|
132
|
+
'maxItems': 'max',
|
|
133
|
+
'pattern': 'pattern',
|
|
134
|
+
'format': 'format',
|
|
135
|
+
'required': 'required',
|
|
136
|
+
'enum': 'enum'
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const mappedKeyword = keywordMap[keyword] || keyword;
|
|
140
|
+
const type = schema.type || 'string';
|
|
141
|
+
|
|
142
|
+
// 查找自定义消息
|
|
143
|
+
// 优先级:
|
|
144
|
+
// 1. type.keyword (如 string.pattern)
|
|
145
|
+
// 2. type.mappedKeyword (如 string.min)
|
|
146
|
+
// 3. mappedKeyword (如 min)
|
|
147
|
+
// 4. keyword (如 minLength,向后兼容)
|
|
148
|
+
// 5. default
|
|
149
|
+
let message = customMessages[`${type}.${keyword}`] ||
|
|
150
|
+
customMessages[`${type}.${mappedKeyword}`] ||
|
|
151
|
+
customMessages[mappedKeyword] ||
|
|
152
|
+
customMessages[keyword] ||
|
|
153
|
+
customMessages['default'];
|
|
154
|
+
|
|
155
|
+
// 自动查找 format 类型的消息 (例如 format.email)
|
|
156
|
+
if (!message && mappedKeyword === 'format' && err.params && err.params.format) {
|
|
157
|
+
let formatName = err.params.format;
|
|
158
|
+
// 映射 uri -> url
|
|
159
|
+
if (formatName === 'uri') formatName = 'url';
|
|
160
|
+
|
|
161
|
+
const formatKey = `format.${formatName}`;
|
|
162
|
+
|
|
163
|
+
// 优先查找 customMessages 中的 format.email
|
|
164
|
+
if (customMessages[formatKey]) {
|
|
165
|
+
message = customMessages[formatKey];
|
|
166
|
+
}
|
|
167
|
+
// 其次查找全局/语言包中的 format.email
|
|
168
|
+
else if (messages[formatKey]) {
|
|
169
|
+
message = messages[formatKey];
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!message) {
|
|
174
|
+
// 使用默认模板
|
|
175
|
+
const template = messages[mappedKeyword] || messages[keyword] || err.message || 'Validation error';
|
|
176
|
+
message = template;
|
|
177
|
+
} else {
|
|
178
|
+
// 检查 message 是否为 key (包含点号且无空格,或者是已知的 key)
|
|
179
|
+
// 如果是 key,尝试从 messages 中查找
|
|
180
|
+
if (typeof message === 'string' && (message.includes('.') || messages[message])) {
|
|
181
|
+
let translated = messages[message];
|
|
182
|
+
|
|
183
|
+
// 尝试回退查找 (例如 pattern.phone.cn -> pattern.phone)
|
|
184
|
+
if (!translated && message.includes('.')) {
|
|
185
|
+
const parts = message.split('.');
|
|
186
|
+
while (parts.length > 1 && !translated) {
|
|
187
|
+
parts.pop();
|
|
188
|
+
const fallbackKey = parts.join('.');
|
|
189
|
+
translated = messages[fallbackKey];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (translated) {
|
|
194
|
+
message = translated;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 插值替换
|
|
200
|
+
const limit = err.params ? (err.params.limit || err.params.limitLength || err.params.comparison) : undefined;
|
|
201
|
+
|
|
202
|
+
const interpolateData = {
|
|
203
|
+
...err.params,
|
|
204
|
+
path: label, // 使用 label 替换 path
|
|
205
|
+
label: label,
|
|
206
|
+
value: err.data,
|
|
207
|
+
limit: limit,
|
|
208
|
+
// 映射 min/max 以匹配模板
|
|
209
|
+
min: limit,
|
|
210
|
+
max: limit,
|
|
211
|
+
expected: err.params ? err.params.type : undefined
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
message = this._interpolate(message, interpolateData);
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
path: fieldName,
|
|
218
|
+
message: message,
|
|
219
|
+
keyword: keyword,
|
|
220
|
+
params: err.params
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 格式化为分组对象
|
|
227
|
+
* @param {Array<Object>} errors - 错误数组
|
|
228
|
+
* @returns {Object} 按路径分组的错误
|
|
229
|
+
*/
|
|
230
|
+
formatGrouped(errors) {
|
|
231
|
+
const grouped = {};
|
|
232
|
+
|
|
233
|
+
for (const error of errors) {
|
|
234
|
+
const path = error.path || 'value';
|
|
235
|
+
if (!grouped[path]) {
|
|
236
|
+
grouped[path] = [];
|
|
237
|
+
}
|
|
238
|
+
grouped[path].push(this.format(error));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return grouped;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 格式化为纯文本
|
|
246
|
+
* @param {Array<Object>} errors - 错误数组
|
|
247
|
+
* @param {string} [separator='\n'] - 分隔符
|
|
248
|
+
* @returns {string} 纯文本错误消息
|
|
249
|
+
*/
|
|
250
|
+
formatText(errors, separator = '\n') {
|
|
251
|
+
return this.formatAll(errors).join(separator);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 格式化为JSON字符串
|
|
256
|
+
* @param {Array<Object>} errors - 错误数组
|
|
257
|
+
* @param {boolean} [pretty=false] - 是否美化
|
|
258
|
+
* @returns {string} JSON字符串
|
|
259
|
+
*/
|
|
260
|
+
formatJSON(errors, pretty = false) {
|
|
261
|
+
const formatted = this.formatDetailed(errors);
|
|
262
|
+
return pretty ? JSON.stringify(formatted, null, 2) : JSON.stringify(formatted);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 格式化为HTML列表
|
|
267
|
+
* @param {Array<Object>} errors - 错误数组
|
|
268
|
+
* @returns {string} HTML字符串
|
|
269
|
+
*/
|
|
270
|
+
formatHTML(errors) {
|
|
271
|
+
const items = this.formatAll(errors)
|
|
272
|
+
.map(msg => ` <li>${this._escapeHTML(msg)}</li>`)
|
|
273
|
+
.join('\n');
|
|
274
|
+
|
|
275
|
+
return `<ul class="validation-errors">\n${items}\n</ul>`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 插值替换
|
|
280
|
+
* @private
|
|
281
|
+
* @param {string} template - 模板字符串
|
|
282
|
+
* @param {Object} data - 数据对象
|
|
283
|
+
* @returns {string} 替换后的字符串
|
|
284
|
+
*/
|
|
285
|
+
_interpolate(template, data) {
|
|
286
|
+
if (!template || typeof template !== 'string') {
|
|
287
|
+
return data.message || 'Validation error';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// 支持 {{#key}} 和 {key} 两种格式
|
|
292
|
+
return template.replace(/\{\{?#?(\w+)\}?\}/g, (match, key) => {
|
|
293
|
+
return data[key] !== undefined ? data[key] : match;
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* HTML转义
|
|
299
|
+
* @private
|
|
300
|
+
* @param {string} text - 文本
|
|
301
|
+
* @returns {string} 转义后的文本
|
|
302
|
+
*/
|
|
303
|
+
_escapeHTML(text) {
|
|
304
|
+
const map = {
|
|
305
|
+
'&': '&',
|
|
306
|
+
'<': '<',
|
|
307
|
+
'>': '>',
|
|
308
|
+
'"': '"',
|
|
309
|
+
"'": '''
|
|
310
|
+
};
|
|
311
|
+
return text.replace(/[&<>"']/g, char => map[char]);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* 设置语言环境
|
|
316
|
+
* @param {string} locale - 语言环境
|
|
317
|
+
*/
|
|
318
|
+
setLocale(locale) {
|
|
319
|
+
this.locale = locale;
|
|
320
|
+
this.messages = this._loadMessages(locale);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 添加自定义消息模板
|
|
325
|
+
* @param {string} type - 错误类型
|
|
326
|
+
* @param {string} template - 消息模板
|
|
327
|
+
*/
|
|
328
|
+
addMessage(type, template) {
|
|
329
|
+
this.messages[type] = template;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 批量添加自定义消息模板
|
|
334
|
+
* @param {Object} messages - 消息模板对象
|
|
335
|
+
*/
|
|
336
|
+
addMessages(messages) {
|
|
337
|
+
Object.assign(this.messages, messages);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
module.exports = ErrorFormatter;
|
|
342
|
+
|