schema-dsl 2.3.0 → 2.3.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/.github/workflows/ci.yml +1 -3
- package/README.md +69 -7
- package/docs/FEATURE-INDEX.md +1 -1
- package/docs/INDEX.md +31 -2
- package/docs/export-guide.md +3 -0
- package/docs/export-limitations.md +551 -0
- package/docs/mongodb-exporter.md +16 -0
- package/docs/mysql-exporter.md +16 -0
- package/docs/postgresql-exporter.md +14 -0
- package/docs/schema-utils-chaining.md +146 -0
- package/docs/schema-utils.md +7 -9
- package/docs/validate-async.md +480 -0
- package/examples/array-dsl-example.js +1 -1
- package/examples/express-integration.js +376 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +2 -2
- package/index.js +13 -1
- package/lib/adapters/DslAdapter.js +47 -1
- package/lib/core/Validator.js +60 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/utils/SchemaUtils.js +170 -38
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SchemaUtils 核心方法示例
|
|
3
|
+
*
|
|
4
|
+
* 展示 v2.1.0 简化后的核心 4 个方法:
|
|
5
|
+
* 1. omit() - 排除字段
|
|
6
|
+
* 2. pick() - 保留字段
|
|
7
|
+
* 3. partial() - 部分验证
|
|
8
|
+
* 4. extend() - 扩展字段
|
|
9
|
+
*
|
|
10
|
+
* @version 2.1.0 (简化版)
|
|
11
|
+
* @date 2025-12-29
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { dsl, validate, SchemaUtils } = require('../index');
|
|
15
|
+
|
|
16
|
+
console.log('========================================');
|
|
17
|
+
console.log(' SchemaUtils 核心方法示例');
|
|
18
|
+
console.log('========================================\n');
|
|
19
|
+
|
|
20
|
+
// ===== 定义基础 Schema =====
|
|
21
|
+
|
|
22
|
+
const fullUserSchema = dsl({
|
|
23
|
+
id: 'objectId!',
|
|
24
|
+
name: 'string:1-50!',
|
|
25
|
+
email: 'email!',
|
|
26
|
+
password: 'string:8-32!',
|
|
27
|
+
age: 'integer:18-120',
|
|
28
|
+
role: 'admin|user|guest',
|
|
29
|
+
createdAt: 'date',
|
|
30
|
+
updatedAt: 'date'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
console.log('基础 Schema 字段:', Object.keys(fullUserSchema.properties));
|
|
34
|
+
console.log('必填字段:', fullUserSchema.required);
|
|
35
|
+
console.log('');
|
|
36
|
+
|
|
37
|
+
// ===== 1. omit() - 排除字段 =====
|
|
38
|
+
|
|
39
|
+
console.log('========== 1. omit() - 排除字段 ==========\n');
|
|
40
|
+
|
|
41
|
+
const publicSchema = SchemaUtils.omit(fullUserSchema, ['password', 'createdAt', 'updatedAt']);
|
|
42
|
+
|
|
43
|
+
console.log('原 Schema 字段:', Object.keys(fullUserSchema.properties));
|
|
44
|
+
console.log('omit 后字段:', Object.keys(publicSchema.properties));
|
|
45
|
+
console.log('password 字段被移除:', publicSchema.properties.password === undefined);
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
// ===== 2. pick() - 保留字段 =====
|
|
49
|
+
|
|
50
|
+
console.log('========== 2. pick() - 保留字段 ==========\n');
|
|
51
|
+
|
|
52
|
+
const nameEmailSchema = SchemaUtils.pick(fullUserSchema, ['name', 'email']);
|
|
53
|
+
|
|
54
|
+
console.log('原 Schema 字段:', Object.keys(fullUserSchema.properties));
|
|
55
|
+
console.log('pick 后字段:', Object.keys(nameEmailSchema.properties));
|
|
56
|
+
console.log('只保留 name 和 email');
|
|
57
|
+
console.log('');
|
|
58
|
+
|
|
59
|
+
// ===== 3. partial() - 部分验证 =====
|
|
60
|
+
|
|
61
|
+
console.log('========== 3. partial() - 部分验证 ==========\n');
|
|
62
|
+
|
|
63
|
+
const partialSchema = SchemaUtils.partial(fullUserSchema);
|
|
64
|
+
|
|
65
|
+
console.log('原 Schema 必填字段:', fullUserSchema.required);
|
|
66
|
+
console.log('partial Schema 必填字段:', partialSchema.required);
|
|
67
|
+
|
|
68
|
+
// 测试:缺少必填字段
|
|
69
|
+
const result4 = validate(partialSchema, {
|
|
70
|
+
name: 'John'
|
|
71
|
+
// 缺少其他必填字段
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log('\n验证结果(缺少必填字段):');
|
|
75
|
+
console.log(' valid:', result4.valid);
|
|
76
|
+
if (result4.valid) {
|
|
77
|
+
console.log(' 数据:', result4.data);
|
|
78
|
+
}
|
|
79
|
+
console.log('');
|
|
80
|
+
|
|
81
|
+
// 部分验证 - 只验证指定字段
|
|
82
|
+
const nameAgeSchema = SchemaUtils.partial(fullUserSchema, ['name', 'age']);
|
|
83
|
+
|
|
84
|
+
console.log('partial(schema, [name, age]) 保留字段:', Object.keys(nameAgeSchema.properties));
|
|
85
|
+
|
|
86
|
+
const result5 = validate(nameAgeSchema, {
|
|
87
|
+
name: 'John',
|
|
88
|
+
age: 30
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('验证结果(只验证 name 和 age):');
|
|
92
|
+
console.log(' valid:', result5.valid);
|
|
93
|
+
console.log('');
|
|
94
|
+
|
|
95
|
+
// ===== 4. extend() - 扩展字段 =====
|
|
96
|
+
|
|
97
|
+
console.log('========== 4. extend() - 扩展字段 ==========\n');
|
|
98
|
+
|
|
99
|
+
const extendedSchema = SchemaUtils.extend(fullUserSchema, {
|
|
100
|
+
avatar: 'url',
|
|
101
|
+
bio: 'string:0-500'
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log('原 Schema 字段:', Object.keys(fullUserSchema.properties));
|
|
105
|
+
console.log('extend 后字段:', Object.keys(extendedSchema.properties));
|
|
106
|
+
console.log('新增字段: avatar, bio');
|
|
107
|
+
console.log('');
|
|
108
|
+
|
|
109
|
+
// ===== 5. 链式调用 =====
|
|
110
|
+
|
|
111
|
+
console.log('========== 5. 链式调用 ==========\n');
|
|
112
|
+
|
|
113
|
+
// 示例 1: omit
|
|
114
|
+
console.log('示例 1: omit (创建用户 Schema)');
|
|
115
|
+
const createSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
116
|
+
|
|
117
|
+
console.log(' 字段:', Object.keys(createSchema.properties));
|
|
118
|
+
console.log('');
|
|
119
|
+
|
|
120
|
+
// 示例 2: pick + partial
|
|
121
|
+
console.log('示例 2: pick + partial (更新用户 Schema)');
|
|
122
|
+
const updateSchema = SchemaUtils
|
|
123
|
+
.pick(fullUserSchema, ['name', 'age'])
|
|
124
|
+
.partial();
|
|
125
|
+
|
|
126
|
+
console.log(' 字段:', Object.keys(updateSchema.properties));
|
|
127
|
+
console.log(' 必填字段:', updateSchema.required);
|
|
128
|
+
console.log('');
|
|
129
|
+
|
|
130
|
+
// 示例 3: pick + extend (用户建议的常见场景)
|
|
131
|
+
console.log('示例 3: pick + extend (自定义用户 Schema)');
|
|
132
|
+
const customSchema = SchemaUtils
|
|
133
|
+
.pick(fullUserSchema, ['name', 'email'])
|
|
134
|
+
.extend({ avatar: 'url', bio: 'string:0-500' });
|
|
135
|
+
|
|
136
|
+
console.log(' 字段:', Object.keys(customSchema.properties));
|
|
137
|
+
console.log('');
|
|
138
|
+
|
|
139
|
+
// 示例 4: omit + extend
|
|
140
|
+
console.log('示例 4: omit + extend (增强用户 Schema)');
|
|
141
|
+
const enhancedSchema = SchemaUtils
|
|
142
|
+
.omit(fullUserSchema, ['password'])
|
|
143
|
+
.extend({ avatar: 'url', bio: 'string:0-500' });
|
|
144
|
+
|
|
145
|
+
console.log(' 字段:', Object.keys(enhancedSchema.properties));
|
|
146
|
+
console.log(' 新增字段: avatar, bio');
|
|
147
|
+
console.log('');
|
|
148
|
+
|
|
149
|
+
// ===== 6. 实际 CRUD 场景 =====
|
|
150
|
+
|
|
151
|
+
console.log('========== 6. 实际 CRUD 场景 ==========\n');
|
|
152
|
+
|
|
153
|
+
console.log('场景 1: POST /users - 创建用户');
|
|
154
|
+
const postSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
155
|
+
|
|
156
|
+
const postData = {
|
|
157
|
+
name: 'John Doe',
|
|
158
|
+
email: 'john@example.com',
|
|
159
|
+
password: 'password123',
|
|
160
|
+
age: 30,
|
|
161
|
+
role: 'user'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const postResult = validate(postSchema, postData);
|
|
165
|
+
console.log(' 验证结果:', postResult.valid);
|
|
166
|
+
console.log(' 数据:', postResult.data);
|
|
167
|
+
console.log('');
|
|
168
|
+
|
|
169
|
+
console.log('场景 2: GET /users/:id - 查询用户');
|
|
170
|
+
const getSchema = SchemaUtils.omit(fullUserSchema, ['password']);
|
|
171
|
+
|
|
172
|
+
const userData = {
|
|
173
|
+
id: '507f1f77bcf86cd799439011',
|
|
174
|
+
name: 'John Doe',
|
|
175
|
+
email: 'john@example.com',
|
|
176
|
+
password: 'password123', // 已从 schema 移除,但验证时会被忽略
|
|
177
|
+
age: 30,
|
|
178
|
+
role: 'user',
|
|
179
|
+
createdAt: new Date(),
|
|
180
|
+
updatedAt: new Date()
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const getResult = validate(getSchema, userData);
|
|
184
|
+
console.log(' 验证结果:', getResult.valid);
|
|
185
|
+
console.log(' 保留字段:', Object.keys(getResult.data));
|
|
186
|
+
console.log('');
|
|
187
|
+
|
|
188
|
+
console.log('场景 3: PATCH /users/:id - 部分更新用户');
|
|
189
|
+
const patchSchema = SchemaUtils
|
|
190
|
+
.pick(fullUserSchema, ['name', 'age'])
|
|
191
|
+
.partial();
|
|
192
|
+
|
|
193
|
+
const patchData = {
|
|
194
|
+
name: 'John Updated'
|
|
195
|
+
// 只更新 name,age 缺失也可以
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const patchResult = validate(patchSchema, patchData);
|
|
199
|
+
console.log(' 验证结果:', patchResult.valid);
|
|
200
|
+
console.log(' 数据:', patchResult.data);
|
|
201
|
+
console.log('');
|
|
202
|
+
|
|
203
|
+
console.log('场景 4: PUT /users/:id - 替换用户');
|
|
204
|
+
const putSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
205
|
+
|
|
206
|
+
const putData = {
|
|
207
|
+
name: 'John Replaced',
|
|
208
|
+
email: 'john.new@example.com',
|
|
209
|
+
password: 'newpassword123',
|
|
210
|
+
age: 31,
|
|
211
|
+
role: 'admin'
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const putResult = validate(putSchema, putData);
|
|
215
|
+
console.log(' 验证结果:', putResult.valid);
|
|
216
|
+
console.log(' 数据:', putResult.data);
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
// ===== 7. 不可变性验证 =====
|
|
220
|
+
|
|
221
|
+
console.log('========== 7. 不可变性验证 ==========\n');
|
|
222
|
+
|
|
223
|
+
const original = dsl({
|
|
224
|
+
name: 'string!',
|
|
225
|
+
email: 'email!'
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const modified = SchemaUtils.omit(original, ['email']);
|
|
229
|
+
|
|
230
|
+
console.log('原 Schema 字段:', Object.keys(original.properties));
|
|
231
|
+
console.log('修改后 Schema 字段:', Object.keys(modified.properties));
|
|
232
|
+
console.log('原 Schema 未被修改:', original.properties.email !== undefined);
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
235
|
+
console.log('========================================');
|
|
236
|
+
console.log(' 所有示例运行完成!');
|
|
237
|
+
console.log('========================================\n');
|
|
238
|
+
|
|
239
|
+
console.log('核心方法总结:');
|
|
240
|
+
console.log(' 1. omit() - 排除字段(50%场景)');
|
|
241
|
+
console.log(' 2. pick() - 保留字段(30%场景)');
|
|
242
|
+
console.log(' 3. partial() - 部分验证(25%场景)');
|
|
243
|
+
console.log(' 4. extend() - 扩展字段(15%场景)');
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log('常见组合:');
|
|
246
|
+
console.log(' - POST: omit(系统字段)');
|
|
247
|
+
console.log(' - GET: omit(敏感字段)');
|
|
248
|
+
console.log(' - PATCH: pick(可修改字段).partial()');
|
|
249
|
+
console.log(' - 自定义: pick(基础字段).extend(新字段)');
|
|
250
|
+
|
|
@@ -69,7 +69,7 @@ console.log('✨ 5. Schema合并');
|
|
|
69
69
|
const baseUser = dsl({ name: 'string!', email: 'email!' });
|
|
70
70
|
const withAge = dsl({ age: 'number:18-120' });
|
|
71
71
|
|
|
72
|
-
const fullUser = SchemaUtils.
|
|
72
|
+
const fullUser = SchemaUtils.extend(baseUser, withAge);
|
|
73
73
|
|
|
74
74
|
console.log('合并后字段:', Object.keys(fullUser.properties));
|
|
75
75
|
console.log('');
|
|
@@ -114,7 +114,7 @@ console.log(`
|
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
3. 强大
|
|
117
|
-
SchemaUtils.
|
|
117
|
+
SchemaUtils.extend() // 扩展字段
|
|
118
118
|
validateBatch() // 快50倍
|
|
119
119
|
|
|
120
120
|
🎉 简洁 + 直观 + 强大 = 完美验证库!
|
package/index.js
CHANGED
|
@@ -220,6 +220,12 @@ dsl.config = function(options = {}) {
|
|
|
220
220
|
|
|
221
221
|
// ========== 导出 ==========
|
|
222
222
|
|
|
223
|
+
// 导入 ValidationError
|
|
224
|
+
const ValidationError = require('./lib/errors/ValidationError');
|
|
225
|
+
|
|
226
|
+
// 导入 validateAsync
|
|
227
|
+
const { validateAsync } = require('./lib/adapters/DslAdapter');
|
|
228
|
+
|
|
223
229
|
module.exports = {
|
|
224
230
|
// 统一DSL API
|
|
225
231
|
dsl,
|
|
@@ -235,10 +241,14 @@ module.exports = {
|
|
|
235
241
|
|
|
236
242
|
// 便捷方法(推荐)
|
|
237
243
|
validate, // 便捷验证(单例)
|
|
244
|
+
validateAsync, // v2.1.0 新增:异步验证
|
|
238
245
|
getDefaultValidator, // 获取单例Validator
|
|
239
246
|
ErrorFormatter,
|
|
240
247
|
CacheManager,
|
|
241
248
|
|
|
249
|
+
// 错误类 (v2.1.0 新增)
|
|
250
|
+
ValidationError,
|
|
251
|
+
|
|
242
252
|
// 错误消息系统
|
|
243
253
|
ErrorCodes,
|
|
244
254
|
MessageTemplate,
|
|
@@ -266,5 +276,7 @@ module.exports = {
|
|
|
266
276
|
CONSTANTS,
|
|
267
277
|
|
|
268
278
|
// 版本信息
|
|
269
|
-
VERSION: '2.
|
|
279
|
+
VERSION: '2.1.0'
|
|
270
280
|
};
|
|
281
|
+
|
|
282
|
+
|
|
@@ -646,8 +646,54 @@ function dsl(definition) {
|
|
|
646
646
|
throw new Error('Invalid DSL definition: must be string or object');
|
|
647
647
|
}
|
|
648
648
|
|
|
649
|
+
/**
|
|
650
|
+
* 便捷验证函数(同步)
|
|
651
|
+
*
|
|
652
|
+
* @param {Object|DslBuilder} schema - JSON Schema或DslBuilder实例
|
|
653
|
+
* @param {*} data - 待验证数据
|
|
654
|
+
* @param {Object} options - 验证选项
|
|
655
|
+
* @returns {Object} 验证结果 { valid, errors, data }
|
|
656
|
+
*
|
|
657
|
+
* @example
|
|
658
|
+
* const result = validate(userSchema, inputData);
|
|
659
|
+
* if (!result.valid) {
|
|
660
|
+
* console.error(result.errors);
|
|
661
|
+
* }
|
|
662
|
+
*/
|
|
663
|
+
function validate(schema, data, options = {}) {
|
|
664
|
+
const Validator = require('../core/Validator');
|
|
665
|
+
const validator = new Validator(options);
|
|
666
|
+
return validator.validate(schema, data, options);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* 便捷异步验证函数
|
|
671
|
+
*
|
|
672
|
+
* @param {Object|DslBuilder} schema - JSON Schema或DslBuilder实例
|
|
673
|
+
* @param {*} data - 待验证数据
|
|
674
|
+
* @param {Object} options - 验证选项
|
|
675
|
+
* @returns {Promise<*>} 验证通过返回数据
|
|
676
|
+
* @throws {ValidationError} 验证失败抛出异常
|
|
677
|
+
*
|
|
678
|
+
* @example
|
|
679
|
+
* try {
|
|
680
|
+
* const data = await validateAsync(userSchema, inputData);
|
|
681
|
+
* console.log('验证通过:', data);
|
|
682
|
+
* } catch (error) {
|
|
683
|
+
* if (error instanceof ValidationError) {
|
|
684
|
+
* console.error('验证失败:', error.errors);
|
|
685
|
+
* }
|
|
686
|
+
* }
|
|
687
|
+
*/
|
|
688
|
+
async function validateAsync(schema, data, options = {}) {
|
|
689
|
+
const Validator = require('../core/Validator');
|
|
690
|
+
const validator = new Validator(options);
|
|
691
|
+
return validator.validateAsync(schema, data, options);
|
|
692
|
+
}
|
|
693
|
+
|
|
649
694
|
// 导出
|
|
650
695
|
module.exports = dsl;
|
|
651
696
|
module.exports.DslAdapter = DslAdapter;
|
|
652
697
|
module.exports.DslBuilder = DslBuilder;
|
|
653
|
-
|
|
698
|
+
module.exports.validate = validate;
|
|
699
|
+
module.exports.validateAsync = validateAsync;
|
package/lib/core/Validator.js
CHANGED
|
@@ -116,6 +116,21 @@ class Validator {
|
|
|
116
116
|
schema = schema.toSchema();
|
|
117
117
|
}
|
|
118
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
|
+
|
|
119
134
|
try {
|
|
120
135
|
// 如果schema是函数,说明已编译
|
|
121
136
|
let validate;
|
|
@@ -174,6 +189,51 @@ class Validator {
|
|
|
174
189
|
}
|
|
175
190
|
}
|
|
176
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
|
+
|
|
177
237
|
/**
|
|
178
238
|
* 批量验证
|
|
179
239
|
* @param {Object} schema - JSON Schema对象
|
|
@@ -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
|
+
|