schema-dsl 1.1.6 → 1.1.7
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 +22 -3
- package/changelogs/v1.1.7.md +317 -0
- package/docs/best-practices-project-structure.md +408 -0
- package/docs/issues-resolved-summary.md +196 -0
- package/docs/performance-quick-reference.md +123 -0
- package/docs/user-questions-answered.md +353 -0
- package/docs/validate-dsl-object-support.md +573 -0
- package/lib/adapters/DslAdapter.js +94 -7
- package/lib/core/ErrorFormatter.js +28 -4
- package/package.json +1 -1
- package/test-three-rounds-check.js +0 -375
|
@@ -134,7 +134,18 @@ class ErrorFormatter {
|
|
|
134
134
|
// 处理 ajv 错误格式
|
|
135
135
|
const keyword = err.keyword || err.type || 'validation';
|
|
136
136
|
const instancePath = err.instancePath || err.path || '';
|
|
137
|
-
|
|
137
|
+
|
|
138
|
+
// ✅ 修复:正确处理 required 错误的字段名
|
|
139
|
+
let fieldName;
|
|
140
|
+
if (keyword === 'required' && err.params && err.params.missingProperty) {
|
|
141
|
+
// required 错误:字段名应该是父路径 + 缺失属性
|
|
142
|
+
const parentPath = instancePath.replace(/^\//, '');
|
|
143
|
+
const missingProp = err.params.missingProperty;
|
|
144
|
+
fieldName = parentPath ? `${parentPath}/${missingProp}` : missingProp;
|
|
145
|
+
} else {
|
|
146
|
+
// 其他错误:直接使用 instancePath
|
|
147
|
+
fieldName = instancePath.replace(/^\//, '') || 'value';
|
|
148
|
+
}
|
|
138
149
|
|
|
139
150
|
// 获取 Schema 中的自定义信息
|
|
140
151
|
let schema = err.parentSchema || err.schema || {};
|
|
@@ -155,14 +166,27 @@ class ErrorFormatter {
|
|
|
155
166
|
label = finalMessages[label];
|
|
156
167
|
}
|
|
157
168
|
} else {
|
|
169
|
+
// ✅ 修复:对于所有错误类型,label 都应该只显示字段名(不含路径)
|
|
170
|
+
let labelKey;
|
|
171
|
+
if (keyword === 'required' && err.params && err.params.missingProperty) {
|
|
172
|
+
// required 错误:使用 missingProperty(缺失的字段名)
|
|
173
|
+
labelKey = err.params.missingProperty;
|
|
174
|
+
} else {
|
|
175
|
+
// 其他错误:从完整路径中提取最后一级字段名
|
|
176
|
+
// 例如: "user/profile/age" -> "age"
|
|
177
|
+
// "user/scores/1" -> "1"
|
|
178
|
+
const parts = fieldName.split('/');
|
|
179
|
+
labelKey = parts[parts.length - 1];
|
|
180
|
+
}
|
|
181
|
+
|
|
158
182
|
// 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
|
|
159
183
|
// 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
|
|
160
|
-
const autoKey = `label.${
|
|
184
|
+
const autoKey = `label.${labelKey.replace(/\//g, '.')}`;
|
|
161
185
|
if (finalMessages[autoKey]) {
|
|
162
186
|
label = finalMessages[autoKey];
|
|
163
187
|
} else {
|
|
164
|
-
//
|
|
165
|
-
label =
|
|
188
|
+
// 没找到翻译,回退到字段名
|
|
189
|
+
label = labelKey;
|
|
166
190
|
}
|
|
167
191
|
}
|
|
168
192
|
|
package/package.json
CHANGED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 三轮深度检查脚本
|
|
3
|
-
*
|
|
4
|
-
* 第一轮:验证所有ajv错误类型的参数映射
|
|
5
|
-
* 第二轮:边界情况和极端值测试
|
|
6
|
-
* 第三轮:并发和性能测试
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const Ajv = require('ajv');
|
|
10
|
-
const addFormats = require('ajv-formats');
|
|
11
|
-
const ErrorFormatter = require('./lib/core/ErrorFormatter');
|
|
12
|
-
|
|
13
|
-
console.log('🔍 开始三轮深度检查...\n');
|
|
14
|
-
console.log('='.repeat(80));
|
|
15
|
-
|
|
16
|
-
// ============================================================================
|
|
17
|
-
// 第一轮:验证所有ajv错误类型的参数映射
|
|
18
|
-
// ============================================================================
|
|
19
|
-
|
|
20
|
-
console.log('\n【第一轮】验证所有ajv错误类型的参数映射\n');
|
|
21
|
-
|
|
22
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
23
|
-
addFormats(ajv);
|
|
24
|
-
|
|
25
|
-
const testCases = [
|
|
26
|
-
{
|
|
27
|
-
name: 'required',
|
|
28
|
-
schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
29
|
-
data: {},
|
|
30
|
-
expectedParams: ['missingProperty']
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: 'minLength',
|
|
34
|
-
schema: { type: 'string', minLength: 3 },
|
|
35
|
-
data: 'ab',
|
|
36
|
-
expectedParams: ['limit']
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
name: 'maxLength',
|
|
40
|
-
schema: { type: 'string', maxLength: 10 },
|
|
41
|
-
data: 'this is too long string',
|
|
42
|
-
expectedParams: ['limit']
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'minimum',
|
|
46
|
-
schema: { type: 'number', minimum: 10 },
|
|
47
|
-
data: 5,
|
|
48
|
-
expectedParams: ['limit', 'comparison']
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
name: 'maximum',
|
|
52
|
-
schema: { type: 'number', maximum: 100 },
|
|
53
|
-
data: 150,
|
|
54
|
-
expectedParams: ['limit', 'comparison']
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'enum',
|
|
58
|
-
schema: { type: 'string', enum: ['pro', 'basic', 'free'] },
|
|
59
|
-
data: 'premium',
|
|
60
|
-
expectedParams: ['allowedValues']
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'pattern',
|
|
64
|
-
schema: { type: 'string', pattern: '^[a-z]+$' },
|
|
65
|
-
data: 'ABC123',
|
|
66
|
-
expectedParams: ['pattern']
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'format (email)',
|
|
70
|
-
schema: { type: 'string', format: 'email' },
|
|
71
|
-
data: 'invalid-email',
|
|
72
|
-
expectedParams: ['format']
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'type',
|
|
76
|
-
schema: { type: 'number' },
|
|
77
|
-
data: 'not a number',
|
|
78
|
-
expectedParams: ['type']
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
name: 'minItems',
|
|
82
|
-
schema: { type: 'array', minItems: 2 },
|
|
83
|
-
data: [1],
|
|
84
|
-
expectedParams: ['limit']
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
name: 'maxItems',
|
|
88
|
-
schema: { type: 'array', maxItems: 5 },
|
|
89
|
-
data: [1, 2, 3, 4, 5, 6],
|
|
90
|
-
expectedParams: ['limit']
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
name: 'minProperties',
|
|
94
|
-
schema: { type: 'object', minProperties: 2 },
|
|
95
|
-
data: { a: 1 },
|
|
96
|
-
expectedParams: ['limit']
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
name: 'maxProperties',
|
|
100
|
-
schema: { type: 'object', maxProperties: 3 },
|
|
101
|
-
data: { a: 1, b: 2, c: 3, d: 4 },
|
|
102
|
-
expectedParams: ['limit']
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'additionalProperties',
|
|
106
|
-
schema: { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false },
|
|
107
|
-
data: { name: 'John', age: 30 },
|
|
108
|
-
expectedParams: ['additionalProperty']
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
name: 'uniqueItems',
|
|
112
|
-
schema: { type: 'array', uniqueItems: true },
|
|
113
|
-
data: [1, 2, 2, 3],
|
|
114
|
-
expectedParams: ['i', 'j']
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: 'const',
|
|
118
|
-
schema: { const: 'fixed_value' },
|
|
119
|
-
data: 'wrong_value',
|
|
120
|
-
expectedParams: ['allowedValue']
|
|
121
|
-
}
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
let round1Passed = 0;
|
|
125
|
-
let round1Failed = 0;
|
|
126
|
-
|
|
127
|
-
testCases.forEach(({ name, schema, data, expectedParams }) => {
|
|
128
|
-
const validate = ajv.compile(schema);
|
|
129
|
-
const valid = validate(data);
|
|
130
|
-
|
|
131
|
-
if (!valid && validate.errors) {
|
|
132
|
-
const formatter = new ErrorFormatter('en-US');
|
|
133
|
-
const formatted = formatter.formatDetailed(validate.errors);
|
|
134
|
-
|
|
135
|
-
const hasTemplateVars = formatted.some(err =>
|
|
136
|
-
err.message.includes('{{#') || err.message.includes('}}')
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
if (hasTemplateVars) {
|
|
140
|
-
console.log(`❌ ${name}: 错误消息包含未替换的模板变量`);
|
|
141
|
-
console.log(` 消息: ${formatted[0].message}`);
|
|
142
|
-
console.log(` ajv参数: ${JSON.stringify(validate.errors[0].params)}`);
|
|
143
|
-
round1Failed++;
|
|
144
|
-
} else {
|
|
145
|
-
console.log(`✅ ${name}: 参数映射正确`);
|
|
146
|
-
round1Passed++;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
console.log(`\n第一轮结果: ${round1Passed} 通过, ${round1Failed} 失败`);
|
|
152
|
-
|
|
153
|
-
// ============================================================================
|
|
154
|
-
// 第二轮:边界情况和极端值测试
|
|
155
|
-
// ============================================================================
|
|
156
|
-
|
|
157
|
-
console.log('\n' + '='.repeat(80));
|
|
158
|
-
console.log('\n【第二轮】边界情况和极端值测试\n');
|
|
159
|
-
|
|
160
|
-
const boundaryTests = [
|
|
161
|
-
{
|
|
162
|
-
name: '空字符串枚举',
|
|
163
|
-
schema: { type: 'string', enum: ['', 'a', 'b'] },
|
|
164
|
-
data: 'c'
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: '超长枚举列表',
|
|
168
|
-
schema: { type: 'string', enum: Array(100).fill(0).map((_, i) => `value${i}`) },
|
|
169
|
-
data: 'invalid'
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: '数字0作为枚举值',
|
|
173
|
-
schema: { type: 'number', enum: [0, 1, 2] },
|
|
174
|
-
data: 3
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
name: 'null作为枚举值',
|
|
178
|
-
schema: { enum: [null, 'a', 'b'] },
|
|
179
|
-
data: 'c'
|
|
180
|
-
},
|
|
181
|
-
{
|
|
182
|
-
name: 'undefined作为数据',
|
|
183
|
-
schema: { type: 'string' },
|
|
184
|
-
data: undefined
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
name: '非常深的嵌套对象',
|
|
188
|
-
schema: {
|
|
189
|
-
type: 'object',
|
|
190
|
-
properties: {
|
|
191
|
-
level1: {
|
|
192
|
-
type: 'object',
|
|
193
|
-
properties: {
|
|
194
|
-
level2: {
|
|
195
|
-
type: 'object',
|
|
196
|
-
properties: {
|
|
197
|
-
level3: {
|
|
198
|
-
type: 'object',
|
|
199
|
-
properties: {
|
|
200
|
-
value: { type: 'string', enum: ['a', 'b'] }
|
|
201
|
-
},
|
|
202
|
-
required: ['value']
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
},
|
|
210
|
-
data: { level1: { level2: { level3: { value: 'c' } } } }
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
name: '包含特殊字符的属性名',
|
|
214
|
-
schema: {
|
|
215
|
-
type: 'object',
|
|
216
|
-
properties: {
|
|
217
|
-
'user-name': { type: 'string' },
|
|
218
|
-
'user.email': { type: 'string' }
|
|
219
|
-
},
|
|
220
|
-
additionalProperties: false
|
|
221
|
-
},
|
|
222
|
-
data: { 'user-name': 'John', 'user.email': 'john@example.com', 'extra-field': 'value' }
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
name: '极大数值',
|
|
226
|
-
schema: { type: 'number', maximum: 1000 },
|
|
227
|
-
data: Number.MAX_SAFE_INTEGER
|
|
228
|
-
},
|
|
229
|
-
{
|
|
230
|
-
name: '极小数值',
|
|
231
|
-
schema: { type: 'number', minimum: -1000 },
|
|
232
|
-
data: Number.MIN_SAFE_INTEGER
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
name: 'NaN值',
|
|
236
|
-
schema: { type: 'number' },
|
|
237
|
-
data: NaN
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'Infinity值',
|
|
241
|
-
schema: { type: 'number', maximum: 1000 },
|
|
242
|
-
data: Infinity
|
|
243
|
-
}
|
|
244
|
-
];
|
|
245
|
-
|
|
246
|
-
let round2Passed = 0;
|
|
247
|
-
let round2Failed = 0;
|
|
248
|
-
|
|
249
|
-
boundaryTests.forEach(({ name, schema, data }) => {
|
|
250
|
-
try {
|
|
251
|
-
const validate = ajv.compile(schema);
|
|
252
|
-
const valid = validate(data);
|
|
253
|
-
|
|
254
|
-
if (!valid && validate.errors) {
|
|
255
|
-
const formatter = new ErrorFormatter('en-US');
|
|
256
|
-
const formatted = formatter.formatDetailed(validate.errors);
|
|
257
|
-
|
|
258
|
-
const hasTemplateVars = formatted.some(err =>
|
|
259
|
-
err.message.includes('{{#') || err.message.includes('}}')
|
|
260
|
-
);
|
|
261
|
-
|
|
262
|
-
const hasUndefined = formatted.some(err =>
|
|
263
|
-
err.message.includes('undefined')
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
if (hasTemplateVars) {
|
|
267
|
-
console.log(`❌ ${name}: 包含未替换的模板变量`);
|
|
268
|
-
console.log(` 消息: ${formatted[0].message}`);
|
|
269
|
-
round2Failed++;
|
|
270
|
-
} else if (hasUndefined && !name.includes('undefined')) {
|
|
271
|
-
console.log(`⚠️ ${name}: 包含undefined(可能正常)`);
|
|
272
|
-
console.log(` 消息: ${formatted[0].message}`);
|
|
273
|
-
round2Passed++;
|
|
274
|
-
} else {
|
|
275
|
-
console.log(`✅ ${name}: 处理正确`);
|
|
276
|
-
round2Passed++;
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
} catch (error) {
|
|
280
|
-
console.log(`❌ ${name}: 抛出异常 - ${error.message}`);
|
|
281
|
-
round2Failed++;
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
console.log(`\n第二轮结果: ${round2Passed} 通过, ${round2Failed} 失败`);
|
|
286
|
-
|
|
287
|
-
// ============================================================================
|
|
288
|
-
// 第三轮:多语言一致性测试
|
|
289
|
-
// ============================================================================
|
|
290
|
-
|
|
291
|
-
console.log('\n' + '='.repeat(80));
|
|
292
|
-
console.log('\n【第三轮】多语言一致性测试\n');
|
|
293
|
-
|
|
294
|
-
const languages = ['en-US', 'zh-CN', 'es-ES', 'fr-FR', 'ja-JP'];
|
|
295
|
-
const multiLangTests = [
|
|
296
|
-
{
|
|
297
|
-
name: 'enum错误',
|
|
298
|
-
schema: { type: 'string', enum: ['pro', 'basic', 'free'] },
|
|
299
|
-
data: 'premium'
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
name: 'additionalProperties错误',
|
|
303
|
-
schema: { type: 'object', properties: { name: { type: 'string' } }, additionalProperties: false },
|
|
304
|
-
data: { name: 'John', age: 30 }
|
|
305
|
-
},
|
|
306
|
-
{
|
|
307
|
-
name: 'required错误',
|
|
308
|
-
schema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] },
|
|
309
|
-
data: {}
|
|
310
|
-
},
|
|
311
|
-
{
|
|
312
|
-
name: 'type错误',
|
|
313
|
-
schema: { type: 'number' },
|
|
314
|
-
data: 'not a number'
|
|
315
|
-
}
|
|
316
|
-
];
|
|
317
|
-
|
|
318
|
-
let round3Passed = 0;
|
|
319
|
-
let round3Failed = 0;
|
|
320
|
-
|
|
321
|
-
multiLangTests.forEach(({ name, schema, data }) => {
|
|
322
|
-
const validate = ajv.compile(schema);
|
|
323
|
-
const valid = validate(data);
|
|
324
|
-
|
|
325
|
-
if (!valid && validate.errors) {
|
|
326
|
-
let allLangsPassed = true;
|
|
327
|
-
|
|
328
|
-
languages.forEach(lang => {
|
|
329
|
-
const formatter = new ErrorFormatter(lang);
|
|
330
|
-
const formatted = formatter.formatDetailed(validate.errors);
|
|
331
|
-
|
|
332
|
-
const hasTemplateVars = formatted.some(err =>
|
|
333
|
-
err.message.includes('{{#') || err.message.includes('}}')
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
if (hasTemplateVars) {
|
|
337
|
-
console.log(`❌ ${name} (${lang}): 包含未替换的模板变量`);
|
|
338
|
-
console.log(` 消息: ${formatted[0].message}`);
|
|
339
|
-
allLangsPassed = false;
|
|
340
|
-
}
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
if (allLangsPassed) {
|
|
344
|
-
console.log(`✅ ${name}: 所有语言正确`);
|
|
345
|
-
round3Passed++;
|
|
346
|
-
} else {
|
|
347
|
-
round3Failed++;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
console.log(`\n第三轮结果: ${round3Passed} 通过, ${round3Failed} 失败`);
|
|
353
|
-
|
|
354
|
-
// ============================================================================
|
|
355
|
-
// 总结
|
|
356
|
-
// ============================================================================
|
|
357
|
-
|
|
358
|
-
console.log('\n' + '='.repeat(80));
|
|
359
|
-
console.log('\n【总结】三轮深度检查结果\n');
|
|
360
|
-
|
|
361
|
-
const totalPassed = round1Passed + round2Passed + round3Passed;
|
|
362
|
-
const totalFailed = round1Failed + round2Failed + round3Failed;
|
|
363
|
-
const totalTests = totalPassed + totalFailed;
|
|
364
|
-
|
|
365
|
-
console.log(`第一轮(参数映射): ${round1Passed}/${round1Passed + round1Failed} 通过`);
|
|
366
|
-
console.log(`第二轮(边界情况): ${round2Passed}/${round2Passed + round2Failed} 通过`);
|
|
367
|
-
console.log(`第三轮(多语言): ${round3Passed}/${round3Passed + round3Failed} 通过`);
|
|
368
|
-
console.log(`\n总计: ${totalPassed}/${totalTests} 通过`);
|
|
369
|
-
|
|
370
|
-
if (totalFailed === 0) {
|
|
371
|
-
console.log('\n✅ 所有检查通过!没有发现问题。');
|
|
372
|
-
} else {
|
|
373
|
-
console.log(`\n⚠️ 发现 ${totalFailed} 个问题需要修复。`);
|
|
374
|
-
process.exit(1);
|
|
375
|
-
}
|