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,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validator - JSON Schema验证器
|
|
3
|
+
*
|
|
4
|
+
* 基于ajv实现,支持JSON Schema Draft 7标准
|
|
5
|
+
*
|
|
6
|
+
* @module lib/core/Validator
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const Ajv = require('ajv');
|
|
11
|
+
const addFormats = require('ajv-formats');
|
|
12
|
+
const CacheManager = require('./CacheManager');
|
|
13
|
+
const ErrorFormatter = require('./ErrorFormatter');
|
|
14
|
+
const CustomKeywords = require('../validators/CustomKeywords');
|
|
15
|
+
const Locale = require('./Locale');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 验证器类
|
|
19
|
+
*
|
|
20
|
+
* @class Validator
|
|
21
|
+
* @description 封装ajv,提供统一的验证接口
|
|
22
|
+
*/
|
|
23
|
+
class Validator {
|
|
24
|
+
/**
|
|
25
|
+
* 构造函数
|
|
26
|
+
* @param {Object} options - ajv配置选项
|
|
27
|
+
* @param {boolean} options.allErrors - 是否返回所有错误(默认true)
|
|
28
|
+
* @param {boolean} options.useDefaults - 是否使用默认值(默认true)
|
|
29
|
+
* @param {boolean} options.coerceTypes - 是否自动类型转换(默认false)
|
|
30
|
+
* @param {boolean} options.removeAdditional - 是否移除额外属性(默认false)
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
// ajv配置
|
|
34
|
+
this.ajvOptions = {
|
|
35
|
+
allErrors: options.allErrors !== false,
|
|
36
|
+
useDefaults: options.useDefaults !== false,
|
|
37
|
+
coerceTypes: options.coerceTypes || false,
|
|
38
|
+
removeAdditional: options.removeAdditional || false,
|
|
39
|
+
verbose: true, // 启用详细模式,以便访问 parentSchema
|
|
40
|
+
...options
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// 创建ajv实例
|
|
44
|
+
this.ajv = new Ajv(this.ajvOptions);
|
|
45
|
+
|
|
46
|
+
// 添加格式支持(email、uri、date-time等)
|
|
47
|
+
addFormats(this.ajv);
|
|
48
|
+
|
|
49
|
+
// 注册自定义关键字
|
|
50
|
+
CustomKeywords.registerAll(this.ajv);
|
|
51
|
+
|
|
52
|
+
// 编译缓存
|
|
53
|
+
this.cache = new CacheManager({
|
|
54
|
+
maxSize: 100,
|
|
55
|
+
ttl: 3600000 // 1小时
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 错误格式化器
|
|
59
|
+
this.errorFormatter = new ErrorFormatter();
|
|
60
|
+
|
|
61
|
+
// 自定义关键字注册表
|
|
62
|
+
this.customKeywords = new Map();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 编译Schema
|
|
67
|
+
* @param {Object} schema - JSON Schema对象
|
|
68
|
+
* @param {string} cacheKey - 缓存键(可选)
|
|
69
|
+
* @returns {Function} ajv验证函数
|
|
70
|
+
*/
|
|
71
|
+
compile(schema, cacheKey = null) {
|
|
72
|
+
// 尝试从缓存获取
|
|
73
|
+
if (cacheKey) {
|
|
74
|
+
const cached = this.cache.get(cacheKey);
|
|
75
|
+
if (cached) {
|
|
76
|
+
return cached;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// 编译Schema
|
|
82
|
+
const validate = this.ajv.compile(schema);
|
|
83
|
+
|
|
84
|
+
// 缓存编译结果
|
|
85
|
+
if (cacheKey) {
|
|
86
|
+
this.cache.set(cacheKey, validate);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return validate;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
throw new Error(`Schema compilation failed: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 验证数据
|
|
97
|
+
* @param {Object} schema - JSON Schema对象或已编译的验证函数
|
|
98
|
+
* @param {*} data - 待验证的数据
|
|
99
|
+
* @param {Object} options - 验证选项
|
|
100
|
+
* @param {boolean} options.format - 是否格式化错误(默认true)
|
|
101
|
+
* @param {string} options.locale - 动态指定语言(如 'zh-CN', 'en-US')
|
|
102
|
+
* @returns {Object} 验证结果 { valid: boolean, errors: Array, data: * }
|
|
103
|
+
*/
|
|
104
|
+
validate(schema, data, options = {}) {
|
|
105
|
+
const shouldFormat = options.format !== false;
|
|
106
|
+
const locale = options.locale || Locale.getLocale();
|
|
107
|
+
|
|
108
|
+
// 如果指定了语言,临时切换
|
|
109
|
+
const originalLocale = options.locale ? Locale.getLocale() : null;
|
|
110
|
+
if (options.locale) {
|
|
111
|
+
Locale.setLocale(options.locale);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 如果schema是DslBuilder实例,转换为JSON Schema
|
|
115
|
+
if (schema && typeof schema.toSchema === 'function') {
|
|
116
|
+
schema = schema.toSchema();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 检查是否需要移除额外字段 (clean 模式)
|
|
120
|
+
if (schema && schema._removeAdditional) {
|
|
121
|
+
// 创建新的 Validator 实例,启用 removeAdditional
|
|
122
|
+
const tempValidator = new Validator({
|
|
123
|
+
...this.ajvOptions,
|
|
124
|
+
removeAdditional: true
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 移除标记字段并深拷贝,避免修改原 schema
|
|
128
|
+
const cleanSchema = JSON.parse(JSON.stringify(schema));
|
|
129
|
+
delete cleanSchema._removeAdditional;
|
|
130
|
+
|
|
131
|
+
return tempValidator.validate(cleanSchema, data, options);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// 如果schema是函数,说明已编译
|
|
136
|
+
let validate;
|
|
137
|
+
if (typeof schema === 'function') {
|
|
138
|
+
validate = schema;
|
|
139
|
+
} else {
|
|
140
|
+
// 生成缓存键
|
|
141
|
+
const cacheKey = this._generateCacheKey(schema);
|
|
142
|
+
validate = this.compile(schema, cacheKey);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 执行验证
|
|
146
|
+
const valid = validate(data);
|
|
147
|
+
|
|
148
|
+
// 格式化错误
|
|
149
|
+
const errors = valid ? [] : (
|
|
150
|
+
shouldFormat
|
|
151
|
+
? this.errorFormatter.format(validate.errors, locale)
|
|
152
|
+
: validate.errors
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
valid,
|
|
157
|
+
errors,
|
|
158
|
+
data // 可能被useDefaults修改过
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
valid: false,
|
|
163
|
+
errors: [{
|
|
164
|
+
message: `Validation error: ${error.message}`,
|
|
165
|
+
path: '',
|
|
166
|
+
keyword: 'error'
|
|
167
|
+
}],
|
|
168
|
+
data
|
|
169
|
+
};
|
|
170
|
+
} finally {
|
|
171
|
+
// 恢复原语言
|
|
172
|
+
if (originalLocale) {
|
|
173
|
+
Locale.setLocale(originalLocale);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 快速验证(不缓存)
|
|
180
|
+
* @param {Object} schema - JSON Schema对象
|
|
181
|
+
* @param {*} data - 待验证的数据
|
|
182
|
+
* @returns {boolean} 是否有效
|
|
183
|
+
*/
|
|
184
|
+
quickValidate(schema, data) {
|
|
185
|
+
try {
|
|
186
|
+
return this.ajv.validate(schema, data);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 异步验证方法(验证失败自动抛出异常)
|
|
194
|
+
*
|
|
195
|
+
* @param {Object|Function} schema - JSON Schema对象或DslBuilder实例
|
|
196
|
+
* @param {*} data - 待验证的数据
|
|
197
|
+
* @param {Object} options - 验证选项(可选)
|
|
198
|
+
* @param {boolean} options.format - 是否格式化错误(默认true)
|
|
199
|
+
* @param {string} options.locale - 语言环境(如 'zh-CN', 'en-US')
|
|
200
|
+
* @returns {Promise<*>} 验证通过返回处理后的数据
|
|
201
|
+
* @throws {ValidationError} 验证失败抛出异常
|
|
202
|
+
*
|
|
203
|
+
* @example
|
|
204
|
+
* // 基础使用
|
|
205
|
+
* try {
|
|
206
|
+
* const data = await validator.validateAsync(userSchema, inputData);
|
|
207
|
+
* console.log('验证通过:', data);
|
|
208
|
+
* } catch (error) {
|
|
209
|
+
* if (error instanceof ValidationError) {
|
|
210
|
+
* console.error('验证失败:', error.errors);
|
|
211
|
+
* }
|
|
212
|
+
* }
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* // Express 中使用
|
|
216
|
+
* app.post('/users', async (req, res, next) => {
|
|
217
|
+
* try {
|
|
218
|
+
* const data = await validator.validateAsync(userSchema, req.body);
|
|
219
|
+
* const user = await db.users.insert(data);
|
|
220
|
+
* res.json(user);
|
|
221
|
+
* } catch (error) {
|
|
222
|
+
* next(error);
|
|
223
|
+
* }
|
|
224
|
+
* });
|
|
225
|
+
*/
|
|
226
|
+
async validateAsync(schema, data, options = {}) {
|
|
227
|
+
const result = this.validate(schema, data, options);
|
|
228
|
+
|
|
229
|
+
if (!result.valid) {
|
|
230
|
+
const ValidationError = require('../errors/ValidationError');
|
|
231
|
+
throw new ValidationError(result.errors, data);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result.data;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 批量验证
|
|
239
|
+
* @param {Object} schema - JSON Schema对象
|
|
240
|
+
* @param {Array} dataArray - 数据数组
|
|
241
|
+
* @returns {Array} 验证结果数组
|
|
242
|
+
*/
|
|
243
|
+
validateBatch(schema, dataArray) {
|
|
244
|
+
if (!Array.isArray(dataArray)) {
|
|
245
|
+
throw new Error('Data must be an array');
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 编译一次,复用验证函数
|
|
249
|
+
const cacheKey = this._generateCacheKey(schema);
|
|
250
|
+
const validate = this.compile(schema, cacheKey);
|
|
251
|
+
|
|
252
|
+
return dataArray.map(data => this.validate(validate, data));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 添加自定义关键字
|
|
257
|
+
* @param {string} keyword - 关键字名称
|
|
258
|
+
* @param {Object} definition - 关键字定义
|
|
259
|
+
* @returns {Validator} 返回this支持链式调用
|
|
260
|
+
*/
|
|
261
|
+
addKeyword(keyword, definition) {
|
|
262
|
+
try {
|
|
263
|
+
this.ajv.addKeyword(keyword, definition);
|
|
264
|
+
this.customKeywords.set(keyword, definition);
|
|
265
|
+
return this;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error(`Failed to add keyword '${keyword}': ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 添加自定义格式
|
|
273
|
+
* @param {string} name - 格式名称
|
|
274
|
+
* @param {Function|RegExp} validator - 验证函数或正则表达式
|
|
275
|
+
* @returns {Validator} 返回this支持链式调用
|
|
276
|
+
*/
|
|
277
|
+
addFormat(name, validator) {
|
|
278
|
+
try {
|
|
279
|
+
this.ajv.addFormat(name, validator);
|
|
280
|
+
return this;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
throw new Error(`Failed to add format '${name}': ${error.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 添加Schema引用
|
|
288
|
+
* @param {string} uri - Schema URI
|
|
289
|
+
* @param {Object} schema - JSON Schema对象
|
|
290
|
+
* @returns {Validator} 返回this支持链式调用
|
|
291
|
+
*/
|
|
292
|
+
addSchema(uri, schema) {
|
|
293
|
+
try {
|
|
294
|
+
this.ajv.addSchema(schema, uri);
|
|
295
|
+
return this;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new Error(`Failed to add schema '${uri}': ${error.message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* 移除Schema引用
|
|
303
|
+
* @param {string} uri - Schema URI
|
|
304
|
+
* @returns {Validator} 返回this支持链式调用
|
|
305
|
+
*/
|
|
306
|
+
removeSchema(uri) {
|
|
307
|
+
this.ajv.removeSchema(uri);
|
|
308
|
+
return this;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 获取ajv实例(高级用法)
|
|
313
|
+
* @returns {Ajv} ajv实例
|
|
314
|
+
*/
|
|
315
|
+
getAjv() {
|
|
316
|
+
return this.ajv;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 清空缓存
|
|
321
|
+
*/
|
|
322
|
+
clearCache() {
|
|
323
|
+
this.cache.clear();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 获取缓存统计
|
|
328
|
+
* @returns {Object} 缓存统计信息
|
|
329
|
+
*/
|
|
330
|
+
getCacheStats() {
|
|
331
|
+
return this.cache.stats();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 生成Schema的缓存键
|
|
336
|
+
* @private
|
|
337
|
+
* @param {Object} schema - JSON Schema对象
|
|
338
|
+
* @returns {string} 缓存键
|
|
339
|
+
*/
|
|
340
|
+
_generateCacheKey(schema) {
|
|
341
|
+
// 简单实现:使用JSON字符串的hash
|
|
342
|
+
// 生产环境建议使用更高效的hash算法
|
|
343
|
+
return JSON.stringify(schema);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* 静态方法:创建验证器实例
|
|
348
|
+
* @static
|
|
349
|
+
* @param {Object} options - 配置选项
|
|
350
|
+
* @returns {Validator} Validator实例
|
|
351
|
+
*/
|
|
352
|
+
static create(options) {
|
|
353
|
+
return new Validator(options);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 静态方法:快速验证(不创建实例)
|
|
358
|
+
* @static
|
|
359
|
+
* @param {Object} schema - JSON Schema对象
|
|
360
|
+
* @param {*} data - 待验证的数据
|
|
361
|
+
* @returns {boolean} 是否有效
|
|
362
|
+
*/
|
|
363
|
+
static quickValidate(schema, data) {
|
|
364
|
+
const ajv = new Ajv();
|
|
365
|
+
return ajv.validate(schema, data);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Support calling without new
|
|
370
|
+
const ValidatorProxy = new Proxy(Validator, {
|
|
371
|
+
apply: function(target, thisArg, argumentsList) {
|
|
372
|
+
return new target(...argumentsList);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
module.exports = ValidatorProxy;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ValidationError - 验证错误类
|
|
3
|
+
*
|
|
4
|
+
* 用于 validateAsync() 方法,验证失败时自动抛出
|
|
5
|
+
*
|
|
6
|
+
* @module lib/errors/ValidationError
|
|
7
|
+
* @version 2.1.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 验证错误类
|
|
12
|
+
*
|
|
13
|
+
* @class ValidationError
|
|
14
|
+
* @extends Error
|
|
15
|
+
*
|
|
16
|
+
* @property {string} name - 错误名称(固定为 'ValidationError')
|
|
17
|
+
* @property {string} message - 错误消息(所有错误的汇总)
|
|
18
|
+
* @property {Array<Object>} errors - 详细错误列表
|
|
19
|
+
* @property {*} data - 原始验证数据
|
|
20
|
+
* @property {number} statusCode - HTTP 状态码(默认 400)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* // 创建错误
|
|
24
|
+
* const errors = [
|
|
25
|
+
* { path: '/name', message: '字段必填', keyword: 'required' }
|
|
26
|
+
* ];
|
|
27
|
+
* const error = new ValidationError(errors, inputData);
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // 在 Express 中使用
|
|
31
|
+
* app.use((error, req, res, next) => {
|
|
32
|
+
* if (error instanceof ValidationError) {
|
|
33
|
+
* return res.status(error.statusCode).json(error.toJSON());
|
|
34
|
+
* }
|
|
35
|
+
* next(error);
|
|
36
|
+
* });
|
|
37
|
+
*/
|
|
38
|
+
class ValidationError extends Error {
|
|
39
|
+
/**
|
|
40
|
+
* 构造函数
|
|
41
|
+
* @param {Array<Object>} errors - 错误列表
|
|
42
|
+
* @param {string} errors[].path - 字段路径(如 '/name')
|
|
43
|
+
* @param {string} errors[].message - 错误消息
|
|
44
|
+
* @param {string} errors[].keyword - 验证关键字(如 'required', 'format')
|
|
45
|
+
* @param {Object} errors[].params - 错误参数
|
|
46
|
+
* @param {*} data - 原始数据
|
|
47
|
+
*/
|
|
48
|
+
constructor(errors, data) {
|
|
49
|
+
// 生成友好的错误消息
|
|
50
|
+
const messages = errors.map(e => {
|
|
51
|
+
if (e.path) {
|
|
52
|
+
const field = e.path.replace(/^\//, '');
|
|
53
|
+
// 只有字段名非空时才添加前缀
|
|
54
|
+
return field ? `${field}: ${e.message}` : e.message;
|
|
55
|
+
}
|
|
56
|
+
return e.message;
|
|
57
|
+
}).join('; ');
|
|
58
|
+
|
|
59
|
+
// 检查是否所有错误都完全没有 path 属性(而不是空路径)
|
|
60
|
+
const hasNoPath = errors.every(e => e.path === undefined || e.path === null);
|
|
61
|
+
|
|
62
|
+
// 如果都是无 path 属性的错误,使用简单格式;否则使用标准格式
|
|
63
|
+
super(hasNoPath ? `Validation failed - ${messages}` : `Validation failed: ${messages}`);
|
|
64
|
+
|
|
65
|
+
this.name = 'ValidationError';
|
|
66
|
+
this.errors = errors;
|
|
67
|
+
this.data = data;
|
|
68
|
+
this.statusCode = 400;
|
|
69
|
+
|
|
70
|
+
// 保持堆栈跟踪
|
|
71
|
+
if (Error.captureStackTrace) {
|
|
72
|
+
Error.captureStackTrace(this, ValidationError);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 转换为 JSON 格式(用于 API 响应)
|
|
78
|
+
*
|
|
79
|
+
* @returns {Object} JSON 对象
|
|
80
|
+
* @returns {string} return.error - 错误名称
|
|
81
|
+
* @returns {string} return.message - 错误消息
|
|
82
|
+
* @returns {number} return.statusCode - 状态码
|
|
83
|
+
* @returns {Array<Object>} return.details - 详细错误列表
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* const json = error.toJSON();
|
|
87
|
+
* res.status(400).json(json);
|
|
88
|
+
* // {
|
|
89
|
+
* // error: 'ValidationError',
|
|
90
|
+
* // message: 'Validation failed: name: 字段必填',
|
|
91
|
+
* // statusCode: 400,
|
|
92
|
+
* // details: [...]
|
|
93
|
+
* // }
|
|
94
|
+
*/
|
|
95
|
+
toJSON() {
|
|
96
|
+
return {
|
|
97
|
+
error: this.name,
|
|
98
|
+
message: this.message,
|
|
99
|
+
statusCode: this.statusCode,
|
|
100
|
+
details: this.errors.map(e => ({
|
|
101
|
+
field: e.path ? e.path.replace(/^\//, '') : null,
|
|
102
|
+
message: e.message,
|
|
103
|
+
keyword: e.keyword,
|
|
104
|
+
params: e.params
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 获取指定字段的错误
|
|
111
|
+
*
|
|
112
|
+
* @param {string} field - 字段名称(如 'name' 或 '/name')
|
|
113
|
+
* @returns {Object|null} 错误对象或 null
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* const nameError = error.getFieldError('name');
|
|
117
|
+
* if (nameError) {
|
|
118
|
+
* console.log('姓名错误:', nameError.message);
|
|
119
|
+
* }
|
|
120
|
+
*/
|
|
121
|
+
getFieldError(field) {
|
|
122
|
+
// 规范化字段名(移除前导斜杠)
|
|
123
|
+
const normalizedField = field.replace(/^\//, '');
|
|
124
|
+
|
|
125
|
+
// 查找匹配的错误(支持多种路径格式)
|
|
126
|
+
return this.errors.find(e => {
|
|
127
|
+
if (!e.path) return false;
|
|
128
|
+
const errorField = e.path.replace(/^\//, '');
|
|
129
|
+
return errorField === normalizedField;
|
|
130
|
+
}) || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 获取所有字段的错误映射
|
|
135
|
+
*
|
|
136
|
+
* @returns {Object} 字段错误映射 { fieldName: errorMessage }
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* const fieldErrors = error.getFieldErrors();
|
|
140
|
+
* // { name: '字段必填', email: '邮箱格式错误' }
|
|
141
|
+
*/
|
|
142
|
+
getFieldErrors() {
|
|
143
|
+
const result = {};
|
|
144
|
+
this.errors.forEach(e => {
|
|
145
|
+
if (e.path) {
|
|
146
|
+
const field = e.path.replace(/^\//, '');
|
|
147
|
+
if (field) {
|
|
148
|
+
result[field] = e.message;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 检查是否包含指定字段的错误
|
|
157
|
+
*
|
|
158
|
+
* @param {string} field - 字段名称
|
|
159
|
+
* @returns {boolean} 是否包含错误
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* if (error.hasFieldError('name')) {
|
|
163
|
+
* console.log('姓名字段有错误');
|
|
164
|
+
* }
|
|
165
|
+
*/
|
|
166
|
+
hasFieldError(field) {
|
|
167
|
+
return this.getFieldError(field) !== null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 获取错误数量
|
|
172
|
+
*
|
|
173
|
+
* @returns {number} 错误数量
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* console.log(`共 ${error.getErrorCount()} 个错误`);
|
|
177
|
+
*/
|
|
178
|
+
getErrorCount() {
|
|
179
|
+
return this.errors.length;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Support calling without new
|
|
184
|
+
const ValidationErrorProxy = new Proxy(ValidationError, {
|
|
185
|
+
apply: function(target, thisArg, argumentsList) {
|
|
186
|
+
return new target(...argumentsList);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
module.exports = ValidationErrorProxy;
|
|
191
|
+
|