schema-dsl 1.1.0 → 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 +131 -11
- package/README.md +244 -271
- package/STATUS.md +65 -3
- package/docs/conditional-api.md +257 -11
- package/examples/i18n-error.examples.js +181 -0
- package/index.d.ts +324 -1
- package/index.js +35 -2
- package/lib/core/ConditionalBuilder.js +115 -13
- package/lib/core/Validator.js +7 -3
- package/lib/errors/I18nError.js +222 -0
- package/lib/locales/en-US.js +25 -0
- package/lib/locales/zh-CN.js +25 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -181,12 +181,13 @@ if (result.valid) {
|
|
|
181
181
|
|------|-----------|------|
|
|
182
182
|
| **基本验证** | ✅ | string、number、boolean、date、email、url... |
|
|
183
183
|
| **高级验证** | ✅ | 正则、自定义、条件、嵌套、数组... |
|
|
184
|
-
| **🆕 跨类型联合** | ✅ | `types:string|number` 一个字段支持多种类型 (v1.1.
|
|
184
|
+
| **🆕 跨类型联合** | ✅ | `types:string|number` 一个字段支持多种类型 (v1.1.1) |
|
|
185
185
|
| **错误格式化** | ✅ | 自动多语言翻译 |
|
|
186
|
+
| **🆕 多语言错误** | ✅ | `I18nError` 统一的多语言错误抛出 (v1.1.1) |
|
|
186
187
|
| **数据库导出** | ✅ | MongoDB、MySQL、PostgreSQL |
|
|
187
188
|
| **TypeScript** | ✅ | 完整类型定义 |
|
|
188
189
|
| **性能优化** | ✅ | WeakMap 缓存、智能编译 |
|
|
189
|
-
| **插件系统** | ✅ | 支持自定义类型注册 (v1.1.
|
|
190
|
+
| **插件系统** | ✅ | 支持自定义类型注册 (v1.1.1) |
|
|
190
191
|
| **文档生成** | ✅ | Markdown、HTML |
|
|
191
192
|
|
|
192
193
|
### 🆕 v1.1.0 新特性:跨类型联合验证
|
|
@@ -776,86 +777,6 @@ const vipSchema = dsl({
|
|
|
776
777
|
validate(vipSchema, { isVip: true, discount: 30 });
|
|
777
778
|
|
|
778
779
|
// ❌ 非 VIP 用户折扣超过 10
|
|
779
|
-
```
|
|
780
|
-
|
|
781
|
-
### 🆕 链式条件判断 - dsl.if() (v1.1.0)
|
|
782
|
-
|
|
783
|
-
**运行时动态条件判断,类似 JavaScript if-else 语句**
|
|
784
|
-
|
|
785
|
-
```javascript
|
|
786
|
-
const { dsl, validate } = require('schema-dsl');
|
|
787
|
-
|
|
788
|
-
// 1. 简单条件 + 错误消息
|
|
789
|
-
const schema1 = dsl({
|
|
790
|
-
age: 'number!',
|
|
791
|
-
status: dsl.if((data) => data.age >= 18)
|
|
792
|
-
.message('未成年用户不能注册') // 不满足自动抛错
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
validate(schema1, { age: 16, status: 'active' });
|
|
796
|
-
// => { valid: false, errors: [{ message: '未成年用户不能注册' }] }
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
// 2. 条件 + then/else(动态Schema)
|
|
800
|
-
const schema2 = dsl({
|
|
801
|
-
userType: 'string!',
|
|
802
|
-
email: dsl.if((data) => data.userType === 'admin')
|
|
803
|
-
.then('email!') // 管理员必填
|
|
804
|
-
.else('email') // 普通用户可选
|
|
805
|
-
});
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
// 3. 多条件 AND
|
|
809
|
-
const schema3 = dsl({
|
|
810
|
-
age: 'number!',
|
|
811
|
-
userType: 'string!',
|
|
812
|
-
email: dsl.if((data) => data.age >= 18)
|
|
813
|
-
.and((data) => data.userType === 'admin')
|
|
814
|
-
.then('email!')
|
|
815
|
-
.else('email')
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
// 4. 多条件 OR
|
|
820
|
-
const schema4 = dsl({
|
|
821
|
-
age: 'number!',
|
|
822
|
-
status: 'string!',
|
|
823
|
-
reason: dsl.if((data) => data.age < 18)
|
|
824
|
-
.or((data) => data.status === 'blocked')
|
|
825
|
-
.message('不允许注册')
|
|
826
|
-
});
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// 5. elseIf 多分支
|
|
830
|
-
const schema5 = dsl({
|
|
831
|
-
userType: 'string!',
|
|
832
|
-
permissions: dsl.if((data) => data.userType === 'admin')
|
|
833
|
-
.then('array<string>!')
|
|
834
|
-
.elseIf((data) => data.userType === 'vip')
|
|
835
|
-
.then('array<string>')
|
|
836
|
-
.else(null) // 游客不验证
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
// 6. else 可选(不写 else 就不验证)
|
|
841
|
-
const schema6 = dsl({
|
|
842
|
-
userType: 'string!',
|
|
843
|
-
vipLevel: dsl.if((data) => data.userType === 'vip')
|
|
844
|
-
.then('enum:gold|silver|bronze!')
|
|
845
|
-
// 不写 else,非 vip 用户不验证
|
|
846
|
-
});
|
|
847
|
-
```
|
|
848
|
-
|
|
849
|
-
**核心特性**:
|
|
850
|
-
- ✅ **运行时执行** - 在验证时根据实际数据判断(不是Schema定义时)
|
|
851
|
-
- ✅ **多条件组合** - 支持 and/or 逻辑组合
|
|
852
|
-
- ✅ **elseIf 分支** - 支持多层条件判断
|
|
853
|
-
- ✅ **else 可选** - 不写 else 就不验证
|
|
854
|
-
- ✅ **简化设计** - message 自动抛错,无需 throwError()
|
|
855
|
-
|
|
856
|
-
📖 [完整链式条件判断文档](./docs/conditional-api.md)
|
|
857
|
-
|
|
858
|
-
---
|
|
859
780
|
validate(vipSchema, { isVip: false, discount: 15 });
|
|
860
781
|
|
|
861
782
|
|
|
@@ -1179,241 +1100,293 @@ try {
|
|
|
1179
1100
|
|
|
1180
1101
|
---
|
|
1181
1102
|
|
|
1182
|
-
##
|
|
1183
|
-
|
|
1184
|
-
### ✅ 特别适合
|
|
1103
|
+
## ❓ 常见问题 FAQ
|
|
1185
1104
|
|
|
1186
|
-
|
|
1187
|
-
- 🌍 **国际化项目** - 需要完整的多语言错误消息支持
|
|
1188
|
-
- 🗄️ **全栈开发** - 需要从 Schema 自动生成数据库表结构
|
|
1189
|
-
- 📋 **配置驱动** - 验证规则需要从配置文件或数据库动态读取
|
|
1190
|
-
- 🏢 **中小型项目** - Node.js + Express/Koa/Egg.js 后端项目
|
|
1105
|
+
### Q1: 如何判断数据不能为空?(类似 `if(!data)`)
|
|
1191
1106
|
|
|
1192
|
-
|
|
1107
|
+
**方案1:使用必填标记**(推荐)
|
|
1108
|
+
```javascript
|
|
1109
|
+
const schema = dsl({
|
|
1110
|
+
username: 'string!', // 必填,不能为空
|
|
1111
|
+
email: 'email!'
|
|
1112
|
+
});
|
|
1113
|
+
```
|
|
1193
1114
|
|
|
1194
|
-
|
|
1115
|
+
**方案2:使用条件验证 + 抛错**
|
|
1195
1116
|
```javascript
|
|
1196
|
-
//
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
next();
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
next(error);
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
};
|
|
1117
|
+
// 验证失败自动抛错
|
|
1118
|
+
dsl.if(d => !d)
|
|
1119
|
+
.message('数据不能为空')
|
|
1120
|
+
.assert(data);
|
|
1121
|
+
```
|
|
1207
1122
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
)
|
|
1123
|
+
**方案3:异步验证**
|
|
1124
|
+
```javascript
|
|
1125
|
+
// Express/Koa 推荐
|
|
1126
|
+
await dsl.if(d => !d)
|
|
1127
|
+
.message('数据不能为空')
|
|
1128
|
+
.validateAsync(data);
|
|
1212
1129
|
```
|
|
1213
1130
|
|
|
1214
|
-
|
|
1131
|
+
---
|
|
1132
|
+
|
|
1133
|
+
### Q2: 如何判断数据是否是对象?(类似 `typeof data === 'object'`)
|
|
1134
|
+
|
|
1135
|
+
**方案1:使用内置 object 类型**(推荐)
|
|
1215
1136
|
```javascript
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
username: 'string:3-32!',
|
|
1219
|
-
email: 'email!',
|
|
1220
|
-
password: 'string:8-32!',
|
|
1221
|
-
confirmPassword: 'string!'
|
|
1137
|
+
const schema = dsl({
|
|
1138
|
+
data: 'object!' // 必须是对象(排除 null 和 array)
|
|
1222
1139
|
});
|
|
1223
1140
|
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
showErrors(result.errors);
|
|
1228
|
-
}
|
|
1141
|
+
validate(schema, { data: { name: 'John' } }); // ✅ 通过
|
|
1142
|
+
validate(schema, { data: 'string' }); // ❌ 失败
|
|
1143
|
+
validate(schema, { data: [] }); // ❌ 失败
|
|
1229
1144
|
```
|
|
1230
1145
|
|
|
1231
|
-
|
|
1146
|
+
**方案2:条件验证 + 抛错**
|
|
1232
1147
|
```javascript
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
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); // 验证对象结构
|
|
1243
1164
|
```
|
|
1244
1165
|
|
|
1245
1166
|
---
|
|
1246
1167
|
|
|
1247
|
-
|
|
1168
|
+
### Q3: 如何验证嵌套对象?
|
|
1248
1169
|
|
|
1249
|
-
|
|
1170
|
+
```javascript
|
|
1171
|
+
const schema = dsl({
|
|
1172
|
+
user: {
|
|
1173
|
+
profile: 'object!', // profile 必须是对象
|
|
1174
|
+
settings: {
|
|
1175
|
+
theme: 'string',
|
|
1176
|
+
notifications: 'object!' // 嵌套对象验证
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
```
|
|
1250
1181
|
|
|
1251
|
-
|
|
1252
|
-
|------|---------------|---------|
|
|
1253
|
-
| Ajv | 2,000,000 | 🥇 最快 |
|
|
1254
|
-
| Zod | 526,316 | 🥈 很快 |
|
|
1255
|
-
| **schema-dsl** | **277,778** | 🥉 **快** |
|
|
1256
|
-
| Joi | 97,087 | 中等 |
|
|
1257
|
-
| Yup | 60,241 | 较慢 |
|
|
1182
|
+
---
|
|
1258
1183
|
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1184
|
+
### Q4: 如何在 Express/Koa 中使用?
|
|
1185
|
+
|
|
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
|
+
```
|
|
1264
1210
|
|
|
1265
1211
|
---
|
|
1266
1212
|
|
|
1267
|
-
|
|
1213
|
+
### Q5: 如何自定义错误消息?
|
|
1268
1214
|
|
|
1269
|
-
|
|
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
|
+
```
|
|
1270
1232
|
|
|
1271
|
-
|
|
1272
|
-
|---------|---------|------|
|
|
1273
|
-
| 快速开发,减少代码量 | **schema-dsl** | 代码量最少,学习成本最低 |
|
|
1274
|
-
| TypeScript 强类型推断 | Zod | 最佳的 TypeScript 支持 |
|
|
1275
|
-
| 极致性能要求 | Ajv | 性能最强 |
|
|
1276
|
-
| 企业级成熟方案 | Joi | 社区最大,经过大规模验证 |
|
|
1277
|
-
| 多语言 + 数据库导出 | **schema-dsl** | 独家功能 |
|
|
1233
|
+
---
|
|
1278
1234
|
|
|
1279
|
-
###
|
|
1235
|
+
### Q6: 类型对照表
|
|
1280
1236
|
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
<td><strong>语法简洁度</strong></td>
|
|
1292
|
-
<td>⭐⭐⭐⭐⭐<br>一行代码</td>
|
|
1293
|
-
<td>⭐⭐<br>链式调用冗长</td>
|
|
1294
|
-
<td>⭐⭐<br>链式调用冗长</td>
|
|
1295
|
-
<td>⭐⭐⭐<br>相对简洁</td>
|
|
1296
|
-
<td>⭐⭐<br>JSON 配置繁琐</td>
|
|
1297
|
-
</tr>
|
|
1298
|
-
<tr>
|
|
1299
|
-
<td><strong>学习成本</strong></td>
|
|
1300
|
-
<td>⭐⭐⭐⭐⭐<br>5分钟</td>
|
|
1301
|
-
<td>⭐⭐⭐<br>30分钟</td>
|
|
1302
|
-
<td>⭐⭐⭐<br>30分钟</td>
|
|
1303
|
-
<td>⭐⭐⭐⭐<br>15分钟</td>
|
|
1304
|
-
<td>⭐⭐⭐<br>20分钟</td>
|
|
1305
|
-
</tr>
|
|
1306
|
-
<tr>
|
|
1307
|
-
<td><strong>性能(简单验证)</strong></td>
|
|
1308
|
-
<td>⭐⭐⭐⭐<br>55.6万/秒</td>
|
|
1309
|
-
<td>⭐⭐⭐<br>23.3万/秒</td>
|
|
1310
|
-
<td>⭐⭐<br>18.9万/秒</td>
|
|
1311
|
-
<td>⭐⭐⭐⭐⭐<br>100万/秒</td>
|
|
1312
|
-
<td>⭐⭐⭐⭐⭐<br>250万/秒</td>
|
|
1313
|
-
</tr>
|
|
1314
|
-
<tr>
|
|
1315
|
-
<td><strong>性能(复杂验证)</strong></td>
|
|
1316
|
-
<td>⭐⭐⭐⭐⭐<br>62.5万/秒</td>
|
|
1317
|
-
<td>⭐⭐⭐<br>12.5万/秒</td>
|
|
1318
|
-
<td>⭐⭐<br>5.5万/秒</td>
|
|
1319
|
-
<td>⭐⭐⭐⭐<br>38.5万/秒</td>
|
|
1320
|
-
<td>⭐⭐⭐⭐⭐<br>250万/秒</td>
|
|
1321
|
-
</tr>
|
|
1322
|
-
<tr>
|
|
1323
|
-
<td><strong>TypeScript 支持</strong></td>
|
|
1324
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1325
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1326
|
-
<td>⭐⭐⭐<br>.d.ts 类型定义</td>
|
|
1327
|
-
<td>⭐⭐⭐⭐⭐<br>完美类型推断</td>
|
|
1328
|
-
<td>⭐⭐<br>基础支持</td>
|
|
1329
|
-
</tr>
|
|
1330
|
-
<tr>
|
|
1331
|
-
<td><strong>数据库导出</strong></td>
|
|
1332
|
-
<td>✅ MongoDB<br>✅ MySQL<br>✅ PostgreSQL</td>
|
|
1333
|
-
<td>❌</td>
|
|
1334
|
-
<td>❌</td>
|
|
1335
|
-
<td>❌</td>
|
|
1336
|
-
<td>❌</td>
|
|
1337
|
-
</tr>
|
|
1338
|
-
<tr>
|
|
1339
|
-
<td><strong>多语言支持</strong></td>
|
|
1340
|
-
<td>✅ 完整支持<br>可自定义语言包</td>
|
|
1341
|
-
<td>⚠️ 基础支持</td>
|
|
1342
|
-
<td>⚠️ 基础支持</td>
|
|
1343
|
-
<td>⚠️ 基础支持</td>
|
|
1344
|
-
<td>⚠️ 基础支持</td>
|
|
1345
|
-
</tr>
|
|
1346
|
-
<tr>
|
|
1347
|
-
<td><strong>文档生成</strong></td>
|
|
1348
|
-
<td>✅ Markdown<br>✅ HTML</td>
|
|
1349
|
-
<td>❌</td>
|
|
1350
|
-
<td>❌</td>
|
|
1351
|
-
<td>❌</td>
|
|
1352
|
-
<td>❌</td>
|
|
1353
|
-
</tr>
|
|
1354
|
-
<tr>
|
|
1355
|
-
<td><strong>社区规模</strong></td>
|
|
1356
|
-
<td>⭐⭐⭐<br>成长中</td>
|
|
1357
|
-
<td>⭐⭐⭐⭐⭐<br>最大</td>
|
|
1358
|
-
<td>⭐⭐⭐⭐<br>很大</td>
|
|
1359
|
-
<td>⭐⭐⭐⭐<br>快速增长</td>
|
|
1360
|
-
<td>⭐⭐⭐⭐<br>成熟</td>
|
|
1361
|
-
</tr>
|
|
1362
|
-
</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-!'` |
|
|
1363
1247
|
|
|
1364
1248
|
---
|
|
1365
1249
|
|
|
1366
|
-
|
|
1250
|
+
### Q7: 如何合并多个 dsl.if() 验证?
|
|
1367
1251
|
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1252
|
+
**原代码(多个独立验证)**:
|
|
1253
|
+
```javascript
|
|
1254
|
+
dsl.if(d => !d)
|
|
1255
|
+
.message('ACCOUNT_NOT_FOUND')
|
|
1256
|
+
.assert(account);
|
|
1373
1257
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
- [错误处理](./docs/error-handling.md) - ValidationError 详解
|
|
1379
|
-
- [多语言支持](./docs/i18n.md) - 国际化配置指南
|
|
1380
|
-
- [插件开发](./docs/plugin-system.md) - 自定义插件教程
|
|
1258
|
+
dsl.if(d => d.tradable_credits < amount)
|
|
1259
|
+
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
1260
|
+
.assert(account.tradable_credits);
|
|
1261
|
+
```
|
|
1381
1262
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
+
```
|
|
1387
1277
|
|
|
1388
|
-
|
|
1389
|
-
|
|
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)
|
|
1390
1322
|
|
|
1391
1323
|
---
|
|
1392
1324
|
|
|
1393
|
-
|
|
1325
|
+
### Q8: 如何统一抛出多语言错误?(v1.1.1+)
|
|
1394
1326
|
|
|
1395
|
-
|
|
1327
|
+
**问题**: 业务代码中抛出的错误无法多语言,与 `.message()` 和 `.label()` 不一致
|
|
1396
1328
|
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1329
|
+
**✅ 解决方案:使用 `I18nError` 或 `dsl.error`**
|
|
1330
|
+
|
|
1331
|
+
```javascript
|
|
1332
|
+
const { I18nError, dsl } = require('schema-dsl');
|
|
1400
1333
|
|
|
1401
|
-
|
|
1402
|
-
|
|
1334
|
+
// 方式1:直接抛出
|
|
1335
|
+
I18nError.throw('account.notFound');
|
|
1336
|
+
// 中文: "账户不存在"
|
|
1337
|
+
// 英文: "Account not found"
|
|
1403
1338
|
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
+
);
|
|
1406
1353
|
|
|
1407
|
-
|
|
1408
|
-
|
|
1354
|
+
// 方式4:快捷方法
|
|
1355
|
+
dsl.error.throw('user.noPermission');
|
|
1356
|
+
dsl.error.assert(user.role === 'admin', 'user.noPermission');
|
|
1357
|
+
```
|
|
1409
1358
|
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
+
});
|
|
1412
1368
|
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
+
});
|
|
1415
1380
|
```
|
|
1416
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
|
+
|
|
1417
1390
|
---
|
|
1418
1391
|
|
|
1419
1392
|
## 🤝 贡献指南
|
package/STATUS.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# schema-dsl 项目状态
|
|
2
2
|
|
|
3
|
-
> **最后更新**: 2026-01-
|
|
4
|
-
> **当前版本**: v1.
|
|
5
|
-
> **项目状态**: ✅ 全部完成,测试100%通过(
|
|
3
|
+
> **最后更新**: 2026-01-06
|
|
4
|
+
> **当前版本**: v1.1.1
|
|
5
|
+
> **项目状态**: ✅ 全部完成,测试100%通过(921个测试)
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 📋 目录
|
|
10
10
|
|
|
11
|
+
- [v1.1.1](#v111) - 2026-01-06 ✅ 已完成
|
|
12
|
+
- [v1.1.0](#v110) - 2026-01-05 ✅ 已完成
|
|
11
13
|
- [v1.0.9](#v109) - 2026-01-04 ✅ 已完成
|
|
12
14
|
- [v1.0.8](#v108) - 2026-01-04 ✅ 已完成
|
|
13
15
|
- [v1.0.7](#v107) - 2026-01-04 ✅ 已完成
|
|
@@ -23,6 +25,66 @@
|
|
|
23
25
|
|
|
24
26
|
## 版本发布计划
|
|
25
27
|
|
|
28
|
+
### v1.1.1
|
|
29
|
+
|
|
30
|
+
**发布日期**: 2026-01-06
|
|
31
|
+
**状态**: ✅ 已完成
|
|
32
|
+
**类型**: 🎉 新功能 - ConditionalBuilder 独立消息 + I18nError 多语言错误
|
|
33
|
+
**进度**: 100%完成 | 测试: 921个通过 | 新增: 52个测试
|
|
34
|
+
|
|
35
|
+
| 需求标题 | 状态 | 优先级 | 详细 |
|
|
36
|
+
|---------|------|--------|------|
|
|
37
|
+
| ConditionalBuilder 独立消息 | ✅ 完成 | P1 | `.and()/.or()` 后可调用 `.message()` |
|
|
38
|
+
| I18nError 多语言错误 | ✅ 完成 | P1 | 统一的多语言错误抛出机制 |
|
|
39
|
+
| dsl.error 快捷方法 | ✅ 完成 | P1 | create/throw/assert 三个方法 |
|
|
40
|
+
| TypeScript 类型定义 | ✅ 完成 | P0 | I18nError + dsl.error 类型 |
|
|
41
|
+
| 测试用例 | ✅ 完成 | P0 | 52个新测试(24+28) |
|
|
42
|
+
| 文档更新 | ✅ 完成 | P0 | README + CHANGELOG + examples |
|
|
43
|
+
| 语言包扩充 | ✅ 完成 | P1 | 中英文 20+ 错误消息 |
|
|
44
|
+
|
|
45
|
+
**实现状态统计**:
|
|
46
|
+
- ✅ 完成: 7个
|
|
47
|
+
- 🔄 进行中: 0个
|
|
48
|
+
- ⏳ 待完成: 0个
|
|
49
|
+
|
|
50
|
+
**核心变更**:
|
|
51
|
+
- ✅ **新增**: ConditionalBuilder 支持 `.and()/.or()` 独立消息
|
|
52
|
+
- ✅ **新增**: I18nError 类 (lib/errors/I18nError.js)
|
|
53
|
+
- ✅ **新增**: dsl.error 快捷方法 (create/throw/assert)
|
|
54
|
+
- ✅ **新增**: 20+ 内置错误代码(通用/账户/用户/订单)
|
|
55
|
+
- ✅ **新增**: examples/i18n-error.examples.js 示例文件
|
|
56
|
+
- ✅ **更新**: README FAQ Q7/Q8
|
|
57
|
+
- ✅ **更新**: CHANGELOG v1.1.1 完整记录
|
|
58
|
+
- ✅ **更新**: index.d.ts TypeScript 类型定义
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### v1.1.0
|
|
63
|
+
|
|
64
|
+
**发布日期**: 2026-01-05
|
|
65
|
+
**状态**: ✅ 已完成
|
|
66
|
+
**类型**: 🎉 重大功能 - 跨类型联合验证 + 插件系统增强
|
|
67
|
+
**进度**: 100%完成
|
|
68
|
+
|
|
69
|
+
| 需求标题 | 状态 | 优先级 | 详细 |
|
|
70
|
+
|---------|------|--------|------|
|
|
71
|
+
| 跨类型联合验证 | ✅ 完成 | P1 | `types:string|number` 语法 |
|
|
72
|
+
| 插件 DSL 类型注册 | ✅ 完成 | P1 | 插件可注册自定义 DSL 类型 |
|
|
73
|
+
| DslBuilder.registerType | ✅ 完成 | P0 | 静态方法供插件使用 |
|
|
74
|
+
| 文档更新 | ✅ 完成 | P0 | README + CHANGELOG |
|
|
75
|
+
|
|
76
|
+
**实现状态统计**:
|
|
77
|
+
- ✅ 完成: 4个
|
|
78
|
+
- 🔄 进行中: 0个
|
|
79
|
+
- ⏳ 待完成: 0个
|
|
80
|
+
|
|
81
|
+
**核心变更**:
|
|
82
|
+
- ✅ **新增**: `types:` 语法支持跨类型联合验证
|
|
83
|
+
- ✅ **增强**: 插件系统支持 DSL 类型注册
|
|
84
|
+
- ✅ **新增**: DslBuilder.registerType() 静态方法
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
26
88
|
### v1.0.9
|
|
27
89
|
|
|
28
90
|
**发布日期**: 2026-01-04
|