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/README.md
CHANGED
|
@@ -181,12 +181,50 @@ if (result.valid) {
|
|
|
181
181
|
|------|-----------|------|
|
|
182
182
|
| **基本验证** | ✅ | string、number、boolean、date、email、url... |
|
|
183
183
|
| **高级验证** | ✅ | 正则、自定义、条件、嵌套、数组... |
|
|
184
|
+
| **🆕 跨类型联合** | ✅ | `types:string|number` 一个字段支持多种类型 (v1.1.1) |
|
|
184
185
|
| **错误格式化** | ✅ | 自动多语言翻译 |
|
|
186
|
+
| **🆕 多语言错误** | ✅ | `I18nError` 统一的多语言错误抛出 (v1.1.1) |
|
|
185
187
|
| **数据库导出** | ✅ | MongoDB、MySQL、PostgreSQL |
|
|
186
188
|
| **TypeScript** | ✅ | 完整类型定义 |
|
|
187
189
|
| **性能优化** | ✅ | WeakMap 缓存、智能编译 |
|
|
190
|
+
| **插件系统** | ✅ | 支持自定义类型注册 (v1.1.1) |
|
|
188
191
|
| **文档生成** | ✅ | Markdown、HTML |
|
|
189
192
|
|
|
193
|
+
### 🆕 v1.1.0 新特性:跨类型联合验证
|
|
194
|
+
|
|
195
|
+
**一行代码支持多种类型**
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const { dsl, validate } = require('schema-dsl');
|
|
199
|
+
|
|
200
|
+
// 字段可以是字符串或数字
|
|
201
|
+
const schema = dsl({
|
|
202
|
+
value: 'types:string|number'
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
validate(schema, { value: 'hello' }); // ✅ 通过
|
|
206
|
+
validate(schema, { value: 123 }); // ✅ 通过
|
|
207
|
+
validate(schema, { value: true }); // ❌ 失败
|
|
208
|
+
|
|
209
|
+
// 带约束的联合类型
|
|
210
|
+
const advancedSchema = dsl({
|
|
211
|
+
contact: 'types:email|phone!', // 邮箱或手机号
|
|
212
|
+
price: 'types:number:0-|string:1-20' // 数字价格或"面议"
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**实际场景示例**:
|
|
217
|
+
```javascript
|
|
218
|
+
// 用户注册:支持邮箱或手机号
|
|
219
|
+
const registerSchema = dsl({
|
|
220
|
+
username: 'string:3-20!',
|
|
221
|
+
contact: 'types:email|phone!', // 灵活的联系方式
|
|
222
|
+
age: 'types:integer:1-150|null' // 年龄可选
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
📖 [完整文档](./docs/union-types.md) | [插件开发指南](./docs/plugin-type-registration.md)
|
|
227
|
+
|
|
190
228
|
---
|
|
191
229
|
|
|
192
230
|
## 📦 安装
|
|
@@ -377,6 +415,146 @@ const registerSchema = SchemaUtils
|
|
|
377
415
|
// partial - 变为可选(用于更新接口)
|
|
378
416
|
```
|
|
379
417
|
|
|
418
|
+
### 条件验证 - 一行代码搞定
|
|
419
|
+
|
|
420
|
+
**问题场景**:不同情况需要不同的验证规则
|
|
421
|
+
|
|
422
|
+
```javascript
|
|
423
|
+
const { dsl } = require('schema-dsl');
|
|
424
|
+
|
|
425
|
+
// 场景1:年龄限制 - 未成年不能注册
|
|
426
|
+
// ❌ 传统做法:先验证,再判断,写两次
|
|
427
|
+
const result = validate(schema, userData);
|
|
428
|
+
if (!result.valid) return;
|
|
429
|
+
if (userData.age < 18) {
|
|
430
|
+
throw new Error('未成年用户不能注册');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ✅ 新做法:一行代码搞定
|
|
434
|
+
dsl.if(d => d.age < 18)
|
|
435
|
+
.message('未成年用户不能注册')
|
|
436
|
+
.assert(userData); // 失败自动抛错
|
|
437
|
+
|
|
438
|
+
// 场景2:权限检查 - 快速判断
|
|
439
|
+
// ❌ 传统做法:写 if 判断
|
|
440
|
+
if (user.role !== 'admin' && user.role !== 'moderator') {
|
|
441
|
+
return res.status(403).json({ error: '权限不足' });
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ✅ 新做法:一行搞定
|
|
445
|
+
if (!dsl.if(d => d.role === 'admin' || d.role === 'moderator')
|
|
446
|
+
.message('权限不足')
|
|
447
|
+
.check(user)) {
|
|
448
|
+
return res.status(403).json({ error: '权限不足' });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// 场景3:批量过滤 - 筛选符合条件的数据
|
|
452
|
+
// ❌ 传统做法:写 filter 函数
|
|
453
|
+
const adults = users.filter(u => u.age >= 18);
|
|
454
|
+
|
|
455
|
+
// ✅ 新做法:语义更清晰
|
|
456
|
+
const adults = users.filter(u =>
|
|
457
|
+
!dsl.if(d => d.age < 18).message('未成年').check(u)
|
|
458
|
+
);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### 四种方法,满足不同场景
|
|
462
|
+
|
|
463
|
+
| 方法 | 什么时候用 | 返回什么 | 示例 |
|
|
464
|
+
|------|-----------|---------|------|
|
|
465
|
+
| **`.validate()`** | 需要知道错误详情 | `{ valid, errors, data }` | 表单验证 |
|
|
466
|
+
| **`.validateAsync()`** | async/await 场景 | Promise(失败抛错) | Express 中间件 |
|
|
467
|
+
| **`.assert()`** | 快速失败,不想写 if | 失败直接抛错 | 函数入口检查 |
|
|
468
|
+
| **`.check()`** | 只需要判断真假 | `true/false` | 数据过滤 |
|
|
469
|
+
|
|
470
|
+
#### 实际例子
|
|
471
|
+
|
|
472
|
+
**表单验证 - 需要显示错误**
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
// 使用 .validate() 获取错误详情
|
|
476
|
+
const result = dsl.if(d => d.age < 18)
|
|
477
|
+
.message('未成年用户不能注册')
|
|
478
|
+
.validate(formData);
|
|
479
|
+
|
|
480
|
+
if (!result.valid) {
|
|
481
|
+
showError(result.errors[0].message); // 显示给用户
|
|
482
|
+
}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Express 中间件 - 异步验证**
|
|
486
|
+
|
|
487
|
+
```javascript
|
|
488
|
+
// 使用 .validateAsync() 失败自动抛错
|
|
489
|
+
app.post('/register', async (req, res, next) => {
|
|
490
|
+
try {
|
|
491
|
+
await dsl.if(d => d.age < 18)
|
|
492
|
+
.message('未成年用户不能注册')
|
|
493
|
+
.validateAsync(req.body);
|
|
494
|
+
|
|
495
|
+
// 验证通过,继续处理
|
|
496
|
+
const user = await createUser(req.body);
|
|
497
|
+
res.json(user);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
next(error); // 自动传递给错误处理中间件
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**函数参数检查 - 快速断言**
|
|
505
|
+
|
|
506
|
+
```javascript
|
|
507
|
+
// 使用 .assert() 不满足直接抛错
|
|
508
|
+
function registerUser(userData) {
|
|
509
|
+
// 入口检查,不满足直接抛错,代码更清晰
|
|
510
|
+
dsl.if(d => d.age < 18).message('未成年不能注册').assert(userData);
|
|
511
|
+
dsl.if(d => !d.email).message('邮箱必填').assert(userData);
|
|
512
|
+
dsl.if(d => !d.phone).message('手机号必填').assert(userData);
|
|
513
|
+
|
|
514
|
+
// 检查通过,继续业务逻辑
|
|
515
|
+
return createUser(userData);
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
**批量数据处理 - 快速过滤**
|
|
520
|
+
|
|
521
|
+
```javascript
|
|
522
|
+
// 使用 .check() 只返回 true/false
|
|
523
|
+
const canRegister = dsl.if(d => d.age < 18)
|
|
524
|
+
.or(d => d.status === 'blocked')
|
|
525
|
+
.message('不允许注册');
|
|
526
|
+
|
|
527
|
+
// 过滤出可以注册的用户
|
|
528
|
+
const validUsers = users.filter(u => !canRegister.check(u));
|
|
529
|
+
|
|
530
|
+
// 统计未成年用户数量
|
|
531
|
+
const minorCount = users.filter(u =>
|
|
532
|
+
dsl.if(d => d.age < 18).message('未成年').check(u)
|
|
533
|
+
).length;
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**复用验证器**
|
|
537
|
+
|
|
538
|
+
```javascript
|
|
539
|
+
// 创建一次,到处使用
|
|
540
|
+
const ageValidator = dsl.if(d => d.age < 18)
|
|
541
|
+
.message('未成年用户不能注册');
|
|
542
|
+
|
|
543
|
+
// 不同场景使用不同方法
|
|
544
|
+
const r1 = ageValidator.validate({ age: 16 }); // 同步,返回详情
|
|
545
|
+
const r2 = await ageValidator.validateAsync(data); // 异步,失败抛错
|
|
546
|
+
const r3 = ageValidator.check({ age: 20 }); // 快速判断
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### 💡 选择建议
|
|
550
|
+
|
|
551
|
+
- 🎯 **表单验证**:用 `.validate()` - 需要显示错误给用户
|
|
552
|
+
- 🚀 **API 接口**:用 `.validateAsync()` - 配合 try/catch
|
|
553
|
+
- ⚡ **函数入口**:用 `.assert()` - 快速失败,代码简洁
|
|
554
|
+
- 🔍 **数据过滤**:用 `.check()` - 只需要判断真假
|
|
555
|
+
|
|
556
|
+
**完整文档**: [ConditionalBuilder API](./docs/conditional-api.md)
|
|
557
|
+
|
|
380
558
|
---
|
|
381
559
|
|
|
382
560
|
## 📖 DSL 语法速查
|
|
@@ -922,241 +1100,293 @@ try {
|
|
|
922
1100
|
|
|
923
1101
|
---
|
|
924
1102
|
|
|
925
|
-
##
|
|
926
|
-
|
|
927
|
-
### ✅ 特别适合
|
|
1103
|
+
## ❓ 常见问题 FAQ
|
|
928
1104
|
|
|
929
|
-
|
|
930
|
-
- 🌍 **国际化项目** - 需要完整的多语言错误消息支持
|
|
931
|
-
- 🗄️ **全栈开发** - 需要从 Schema 自动生成数据库表结构
|
|
932
|
-
- 📋 **配置驱动** - 验证规则需要从配置文件或数据库动态读取
|
|
933
|
-
- 🏢 **中小型项目** - Node.js + Express/Koa/Egg.js 后端项目
|
|
1105
|
+
### Q1: 如何判断数据不能为空?(类似 `if(!data)`)
|
|
934
1106
|
|
|
935
|
-
|
|
1107
|
+
**方案1:使用必填标记**(推荐)
|
|
1108
|
+
```javascript
|
|
1109
|
+
const schema = dsl({
|
|
1110
|
+
username: 'string!', // 必填,不能为空
|
|
1111
|
+
email: 'email!'
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
936
1114
|
|
|
937
|
-
|
|
1115
|
+
**方案2:使用条件验证 + 抛错**
|
|
938
1116
|
```javascript
|
|
939
|
-
//
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
next();
|
|
945
|
-
} catch (error) {
|
|
946
|
-
next(error);
|
|
947
|
-
}
|
|
948
|
-
};
|
|
949
|
-
};
|
|
1117
|
+
// 验证失败自动抛错
|
|
1118
|
+
dsl.if(d => !d)
|
|
1119
|
+
.message('数据不能为空')
|
|
1120
|
+
.assert(data);
|
|
1121
|
+
```
|
|
950
1122
|
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
)
|
|
1123
|
+
**方案3:异步验证**
|
|
1124
|
+
```javascript
|
|
1125
|
+
// Express/Koa 推荐
|
|
1126
|
+
await dsl.if(d => !d)
|
|
1127
|
+
.message('数据不能为空')
|
|
1128
|
+
.validateAsync(data);
|
|
955
1129
|
```
|
|
956
1130
|
|
|
957
|
-
|
|
1131
|
+
---
|
|
1132
|
+
|
|
1133
|
+
### Q2: 如何判断数据是否是对象?(类似 `typeof data === 'object'`)
|
|
1134
|
+
|
|
1135
|
+
**方案1:使用内置 object 类型**(推荐)
|
|
958
1136
|
```javascript
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
username: 'string:3-32!',
|
|
962
|
-
email: 'email!',
|
|
963
|
-
password: 'string:8-32!',
|
|
964
|
-
confirmPassword: 'string!'
|
|
1137
|
+
const schema = dsl({
|
|
1138
|
+
data: 'object!' // 必须是对象(排除 null 和 array)
|
|
965
1139
|
});
|
|
966
1140
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
showErrors(result.errors);
|
|
971
|
-
}
|
|
1141
|
+
validate(schema, { data: { name: 'John' } }); // ✅ 通过
|
|
1142
|
+
validate(schema, { data: 'string' }); // ❌ 失败
|
|
1143
|
+
validate(schema, { data: [] }); // ❌ 失败
|
|
972
1144
|
```
|
|
973
1145
|
|
|
974
|
-
|
|
1146
|
+
**方案2:条件验证 + 抛错**
|
|
975
1147
|
```javascript
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1148
|
+
dsl.if(d => typeof d !== 'object' || d === null || Array.isArray(d))
|
|
1149
|
+
.message('data 必须是一个对象')
|
|
1150
|
+
.assert(data);
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
**方案3:带结构验证**
|
|
1154
|
+
```javascript
|
|
1155
|
+
const schema = dsl({
|
|
1156
|
+
data: {
|
|
1157
|
+
name: 'string!',
|
|
1158
|
+
age: 'integer!',
|
|
1159
|
+
email: 'email'
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
await validateAsync(schema, input); // 验证对象结构
|
|
986
1164
|
```
|
|
987
1165
|
|
|
988
1166
|
---
|
|
989
1167
|
|
|
990
|
-
|
|
1168
|
+
### Q3: 如何验证嵌套对象?
|
|
1169
|
+
|
|
1170
|
+
```javascript
|
|
1171
|
+
const schema = dsl({
|
|
1172
|
+
user: {
|
|
1173
|
+
profile: 'object!', // profile 必须是对象
|
|
1174
|
+
settings: {
|
|
1175
|
+
theme: 'string',
|
|
1176
|
+
notifications: 'object!' // 嵌套对象验证
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
```
|
|
991
1181
|
|
|
992
|
-
|
|
1182
|
+
---
|
|
993
1183
|
|
|
994
|
-
|
|
995
|
-
|------|---------------|---------|
|
|
996
|
-
| Ajv | 2,000,000 | 🥇 最快 |
|
|
997
|
-
| Zod | 526,316 | 🥈 很快 |
|
|
998
|
-
| **schema-dsl** | **277,778** | 🥉 **快** |
|
|
999
|
-
| Joi | 97,087 | 中等 |
|
|
1000
|
-
| Yup | 60,241 | 较慢 |
|
|
1184
|
+
### Q4: 如何在 Express/Koa 中使用?
|
|
1001
1185
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1186
|
+
```javascript
|
|
1187
|
+
app.post('/api/user', async (req, res) => {
|
|
1188
|
+
try {
|
|
1189
|
+
// 1. 验证请求体是对象
|
|
1190
|
+
await dsl.if(d => typeof d !== 'object' || d === null)
|
|
1191
|
+
.message('请求体必须是对象')
|
|
1192
|
+
.validateAsync(req.body);
|
|
1193
|
+
|
|
1194
|
+
// 2. 验证字段
|
|
1195
|
+
const schema = dsl({
|
|
1196
|
+
username: 'string:3-32!',
|
|
1197
|
+
email: 'email!',
|
|
1198
|
+
password: 'string:8-!'
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
const validData = await validateAsync(schema, req.body);
|
|
1202
|
+
|
|
1203
|
+
// 继续处理...
|
|
1204
|
+
res.json({ success: true, data: validData });
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
res.status(400).json({ error: error.message });
|
|
1207
|
+
}
|
|
1208
|
+
});
|
|
1209
|
+
```
|
|
1007
1210
|
|
|
1008
1211
|
---
|
|
1009
1212
|
|
|
1010
|
-
|
|
1213
|
+
### Q5: 如何自定义错误消息?
|
|
1011
1214
|
|
|
1012
|
-
|
|
1215
|
+
```javascript
|
|
1216
|
+
const schema = dsl({
|
|
1217
|
+
username: dsl('string:3-32!')
|
|
1218
|
+
.label('用户名')
|
|
1219
|
+
.messages({
|
|
1220
|
+
minLength: '用户名至少需要 {{#limit}} 个字符',
|
|
1221
|
+
required: '用户名不能为空'
|
|
1222
|
+
}),
|
|
1223
|
+
|
|
1224
|
+
email: dsl('email!')
|
|
1225
|
+
.label('邮箱地址')
|
|
1226
|
+
.messages({
|
|
1227
|
+
format: '请输入有效的邮箱地址',
|
|
1228
|
+
required: '邮箱不能为空'
|
|
1229
|
+
})
|
|
1230
|
+
});
|
|
1231
|
+
```
|
|
1013
1232
|
|
|
1014
|
-
|
|
1015
|
-
|---------|---------|------|
|
|
1016
|
-
| 快速开发,减少代码量 | **schema-dsl** | 代码量最少,学习成本最低 |
|
|
1017
|
-
| TypeScript 强类型推断 | Zod | 最佳的 TypeScript 支持 |
|
|
1018
|
-
| 极致性能要求 | Ajv | 性能最强 |
|
|
1019
|
-
| 企业级成熟方案 | Joi | 社区最大,经过大规模验证 |
|
|
1020
|
-
| 多语言 + 数据库导出 | **schema-dsl** | 独家功能 |
|
|
1233
|
+
---
|
|
1021
1234
|
|
|
1022
|
-
###
|
|
1235
|
+
### Q6: 类型对照表
|
|
1023
1236
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
<td><strong>语法简洁度</strong></td>
|
|
1035
|
-
<td>⭐⭐⭐⭐⭐<br>一行代码</td>
|
|
1036
|
-
<td>⭐⭐<br>链式调用冗长</td>
|
|
1037
|
-
<td>⭐⭐<br>链式调用冗长</td>
|
|
1038
|
-
<td>⭐⭐⭐<br>相对简洁</td>
|
|
1039
|
-
<td>⭐⭐<br>JSON 配置繁琐</td>
|
|
1040
|
-
</tr>
|
|
1041
|
-
<tr>
|
|
1042
|
-
<td><strong>学习成本</strong></td>
|
|
1043
|
-
<td>⭐⭐⭐⭐⭐<br>5分钟</td>
|
|
1044
|
-
<td>⭐⭐⭐<br>30分钟</td>
|
|
1045
|
-
<td>⭐⭐⭐<br>30分钟</td>
|
|
1046
|
-
<td>⭐⭐⭐⭐<br>15分钟</td>
|
|
1047
|
-
<td>⭐⭐⭐<br>20分钟</td>
|
|
1048
|
-
</tr>
|
|
1049
|
-
<tr>
|
|
1050
|
-
<td><strong>性能(简单验证)</strong></td>
|
|
1051
|
-
<td>⭐⭐⭐⭐<br>55.6万/秒</td>
|
|
1052
|
-
<td>⭐⭐⭐<br>23.3万/秒</td>
|
|
1053
|
-
<td>⭐⭐<br>18.9万/秒</td>
|
|
1054
|
-
<td>⭐⭐⭐⭐⭐<br>100万/秒</td>
|
|
1055
|
-
<td>⭐⭐⭐⭐⭐<br>250万/秒</td>
|
|
1056
|
-
</tr>
|
|
1057
|
-
<tr>
|
|
1058
|
-
<td><strong>性能(复杂验证)</strong></td>
|
|
1059
|
-
<td>⭐⭐⭐⭐⭐<br>62.5万/秒</td>
|
|
1060
|
-
<td>⭐⭐⭐<br>12.5万/秒</td>
|
|
1061
|
-
<td>⭐⭐<br>5.5万/秒</td>
|
|
1062
|
-
<td>⭐⭐⭐⭐<br>38.5万/秒</td>
|
|
1063
|
-
<td>⭐⭐⭐⭐⭐<br>250万/秒</td>
|
|
1064
|
-
</tr>
|
|
1065
|
-
<tr>
|
|
1066
|
-
<td><strong>TypeScript 支持</strong></td>
|
|
1067
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1068
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1069
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1070
|
-
<td>⭐⭐⭐⭐⭐<br>完美类型推断</td>
|
|
1071
|
-
<td>⭐⭐<br>基础支持</td>
|
|
1072
|
-
</tr>
|
|
1073
|
-
<tr>
|
|
1074
|
-
<td><strong>数据库导出</strong></td>
|
|
1075
|
-
<td>✅ MongoDB<br>✅ MySQL<br>✅ PostgreSQL</td>
|
|
1076
|
-
<td>❌</td>
|
|
1077
|
-
<td>❌</td>
|
|
1078
|
-
<td>❌</td>
|
|
1079
|
-
<td>❌</td>
|
|
1080
|
-
</tr>
|
|
1081
|
-
<tr>
|
|
1082
|
-
<td><strong>多语言支持</strong></td>
|
|
1083
|
-
<td>✅ 完整支持<br>可自定义语言包</td>
|
|
1084
|
-
<td>⚠️ 基础支持</td>
|
|
1085
|
-
<td>⚠️ 基础支持</td>
|
|
1086
|
-
<td>⚠️ 基础支持</td>
|
|
1087
|
-
<td>⚠️ 基础支持</td>
|
|
1088
|
-
</tr>
|
|
1089
|
-
<tr>
|
|
1090
|
-
<td><strong>文档生成</strong></td>
|
|
1091
|
-
<td>✅ Markdown<br>✅ HTML</td>
|
|
1092
|
-
<td>❌</td>
|
|
1093
|
-
<td>❌</td>
|
|
1094
|
-
<td>❌</td>
|
|
1095
|
-
<td>❌</td>
|
|
1096
|
-
</tr>
|
|
1097
|
-
<tr>
|
|
1098
|
-
<td><strong>社区规模</strong></td>
|
|
1099
|
-
<td>⭐⭐⭐<br>成长中</td>
|
|
1100
|
-
<td>⭐⭐⭐⭐⭐<br>最大</td>
|
|
1101
|
-
<td>⭐⭐⭐⭐<br>很大</td>
|
|
1102
|
-
<td>⭐⭐⭐⭐<br>快速增长</td>
|
|
1103
|
-
<td>⭐⭐⭐⭐<br>成熟</td>
|
|
1104
|
-
</tr>
|
|
1105
|
-
</table>
|
|
1237
|
+
| JavaScript 条件 | schema-dsl 写法 |
|
|
1238
|
+
|----------------|----------------|
|
|
1239
|
+
| `if (!data)` | `'string!'` 或 `.assert(data)` |
|
|
1240
|
+
| `if (typeof data === 'object')` | `'object!'` |
|
|
1241
|
+
| `if (typeof data === 'string')` | `'string!'` |
|
|
1242
|
+
| `if (typeof data === 'number')` | `'number!'` |
|
|
1243
|
+
| `if (Array.isArray(data))` | `'array!'` |
|
|
1244
|
+
| `if (data === null)` | `'null!'` |
|
|
1245
|
+
| `if (data > 0)` | `'number:0-!'` |
|
|
1246
|
+
| `if (data.length >= 3)` | `'string:3-!'` |
|
|
1106
1247
|
|
|
1107
1248
|
---
|
|
1108
1249
|
|
|
1109
|
-
|
|
1250
|
+
### Q7: 如何合并多个 dsl.if() 验证?
|
|
1110
1251
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1252
|
+
**原代码(多个独立验证)**:
|
|
1253
|
+
```javascript
|
|
1254
|
+
dsl.if(d => !d)
|
|
1255
|
+
.message('ACCOUNT_NOT_FOUND')
|
|
1256
|
+
.assert(account);
|
|
1116
1257
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
- [错误处理](./docs/error-handling.md) - ValidationError 详解
|
|
1122
|
-
- [多语言支持](./docs/i18n.md) - 国际化配置指南
|
|
1123
|
-
- [插件开发](./docs/plugin-system.md) - 自定义插件教程
|
|
1258
|
+
dsl.if(d => d.tradable_credits < amount)
|
|
1259
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1260
|
+
.assert(account.tradable_credits);
|
|
1261
|
+
```
|
|
1124
1262
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1263
|
+
**✅ 方案1:使用 .and() 链式合并(v1.1.1 推荐)**
|
|
1264
|
+
```javascript
|
|
1265
|
+
// ✅ 每个条件都有独立的错误消息
|
|
1266
|
+
dsl.if(d => !d)
|
|
1267
|
+
.message('ACCOUNT_NOT_FOUND')
|
|
1268
|
+
.and(d => d.tradable_credits < amount)
|
|
1269
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1270
|
+
.assert(account);
|
|
1271
|
+
|
|
1272
|
+
// 工作原理:
|
|
1273
|
+
// - 第一个条件失败 → 返回 'ACCOUNT_NOT_FOUND'
|
|
1274
|
+
// - 第二个条件失败 → 返回 'INSUFFICIENT_TRADABLE_CREDITS'
|
|
1275
|
+
// - 所有条件通过 → 验证成功
|
|
1276
|
+
```
|
|
1130
1277
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1278
|
+
**✅ 方案2:使用 .elseIf() 分支验证**
|
|
1279
|
+
```javascript
|
|
1280
|
+
// ✅ 按优先级检查,找到第一个失败的
|
|
1281
|
+
dsl.if(d => !d)
|
|
1282
|
+
.message('ACCOUNT_NOT_FOUND')
|
|
1283
|
+
.elseIf(d => d.tradable_credits < amount)
|
|
1284
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1285
|
+
.assert(account);
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
**✅ 方案3:保持独立验证**(最清晰)
|
|
1289
|
+
```javascript
|
|
1290
|
+
// ✅ 两个独立的验证器
|
|
1291
|
+
dsl.if(d => !d).message('ACCOUNT_NOT_FOUND').assert(account);
|
|
1292
|
+
dsl.if(d => d.tradable_credits < amount)
|
|
1293
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1294
|
+
.assert(account.tradable_credits);
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
**⚠️ 注意事项**:
|
|
1298
|
+
- `.and()` 用于组合多个条件,每个条件可以有**独立的** `.message()` (v1.1.1)
|
|
1299
|
+
- 如果 `.and()` 后不调用 `.message()`,则使用前一个条件的消息
|
|
1300
|
+
- `.elseIf()` 按顺序检查,找到第一个失败的就停止(if-else-if 逻辑)
|
|
1301
|
+
|
|
1302
|
+
**何时使用**:
|
|
1303
|
+
- ✅ 使用 `.and()` - 多个条件,每个有不同错误消息(v1.1.1)
|
|
1304
|
+
- ✅ 使用 `.elseIf()` - 不同分支有不同验证规则
|
|
1305
|
+
- ✅ 独立验证 - 最清晰,最可靠
|
|
1306
|
+
|
|
1307
|
+
**实际应用示例**:
|
|
1308
|
+
```javascript
|
|
1309
|
+
// 账户验证:检查存在性 + 余额 + 状态
|
|
1310
|
+
dsl.if(d => !d)
|
|
1311
|
+
.message('ACCOUNT_NOT_FOUND')
|
|
1312
|
+
.and(d => d.status !== 'active')
|
|
1313
|
+
.message('ACCOUNT_INACTIVE')
|
|
1314
|
+
.and(d => d.tradable_credits < amount)
|
|
1315
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1316
|
+
.assert(account);
|
|
1317
|
+
|
|
1318
|
+
// 每个失败条件都有清晰的错误消息!
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
📖 更多示例请查看 [完整文档](./docs/INDEX.md)
|
|
1133
1322
|
|
|
1134
1323
|
---
|
|
1135
1324
|
|
|
1136
|
-
|
|
1325
|
+
### Q8: 如何统一抛出多语言错误?(v1.1.1+)
|
|
1137
1326
|
|
|
1138
|
-
|
|
1327
|
+
**问题**: 业务代码中抛出的错误无法多语言,与 `.message()` 和 `.label()` 不一致
|
|
1139
1328
|
|
|
1140
|
-
|
|
1141
|
-
# 安装依赖(首次运行)
|
|
1142
|
-
npm install
|
|
1329
|
+
**✅ 解决方案:使用 `I18nError` 或 `dsl.error`**
|
|
1143
1330
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1331
|
+
```javascript
|
|
1332
|
+
const { I18nError, dsl } = require('schema-dsl');
|
|
1146
1333
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1334
|
+
// 方式1:直接抛出
|
|
1335
|
+
I18nError.throw('account.notFound');
|
|
1336
|
+
// 中文: "账户不存在"
|
|
1337
|
+
// 英文: "Account not found"
|
|
1149
1338
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1339
|
+
// 方式2:带参数插值
|
|
1340
|
+
I18nError.throw('account.insufficientBalance', {
|
|
1341
|
+
balance: 50,
|
|
1342
|
+
required: 100
|
|
1343
|
+
});
|
|
1344
|
+
// 输出: "余额不足,当前余额50,需要100"
|
|
1345
|
+
|
|
1346
|
+
// 方式3:断言风格(推荐)
|
|
1347
|
+
I18nError.assert(account, 'account.notFound');
|
|
1348
|
+
I18nError.assert(
|
|
1349
|
+
account.balance >= 100,
|
|
1350
|
+
'account.insufficientBalance',
|
|
1351
|
+
{ balance: account.balance, required: 100 }
|
|
1352
|
+
);
|
|
1152
1353
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1354
|
+
// 方式4:快捷方法
|
|
1355
|
+
dsl.error.throw('user.noPermission');
|
|
1356
|
+
dsl.error.assert(user.role === 'admin', 'user.noPermission');
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
**Express/Koa 集成**:
|
|
1360
|
+
```javascript
|
|
1361
|
+
// 错误处理中间件
|
|
1362
|
+
app.use((error, req, res, next) => {
|
|
1363
|
+
if (error instanceof I18nError) {
|
|
1364
|
+
return res.status(error.statusCode).json(error.toJSON());
|
|
1365
|
+
}
|
|
1366
|
+
next(error);
|
|
1367
|
+
});
|
|
1155
1368
|
|
|
1156
|
-
|
|
1157
|
-
|
|
1369
|
+
// 业务代码中使用
|
|
1370
|
+
app.post('/withdraw', (req, res) => {
|
|
1371
|
+
const account = getAccount(req.user.id);
|
|
1372
|
+
I18nError.assert(account, 'account.notFound');
|
|
1373
|
+
I18nError.assert(
|
|
1374
|
+
account.balance >= req.body.amount,
|
|
1375
|
+
'account.insufficientBalance',
|
|
1376
|
+
{ balance: account.balance, required: req.body.amount }
|
|
1377
|
+
);
|
|
1378
|
+
// ...
|
|
1379
|
+
});
|
|
1158
1380
|
```
|
|
1159
1381
|
|
|
1382
|
+
**内置错误代码**:
|
|
1383
|
+
- 通用: `error.notFound`, `error.forbidden`, `error.unauthorized`
|
|
1384
|
+
- 账户: `account.notFound`, `account.insufficientBalance`
|
|
1385
|
+
- 用户: `user.notFound`, `user.noPermission`
|
|
1386
|
+
- 订单: `order.notPaid`, `order.paymentMissing`
|
|
1387
|
+
|
|
1388
|
+
📖 完整文档请查看 [examples/i18n-error.examples.js](./examples/i18n-error.examples.js)
|
|
1389
|
+
|
|
1160
1390
|
---
|
|
1161
1391
|
|
|
1162
1392
|
## 🤝 贡献指南
|