schema-dsl 1.2.5 → 2.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/CHANGELOG.md +87 -212
- package/README.md +391 -2249
- package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
- package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
- package/dist/Validator-C7GsVQOH.d.cts +192 -0
- package/dist/Validator-hFWKGxir.d.ts +192 -0
- package/dist/index.cjs +6594 -0
- package/dist/index.d.cts +1145 -0
- package/dist/index.d.ts +1145 -0
- package/dist/index.js +6528 -0
- package/dist/plugin-CIKtTMtS.d.cts +246 -0
- package/dist/plugin-CIKtTMtS.d.ts +246 -0
- package/dist/plugins/custom-format.cjs +3802 -0
- package/dist/plugins/custom-format.d.cts +12 -0
- package/dist/plugins/custom-format.d.ts +12 -0
- package/dist/plugins/custom-format.js +3772 -0
- package/dist/plugins/custom-type-example.cjs +3795 -0
- package/dist/plugins/custom-type-example.d.cts +8 -0
- package/dist/plugins/custom-type-example.d.ts +8 -0
- package/dist/plugins/custom-type-example.js +3765 -0
- package/dist/plugins/custom-validator.cjs +146 -0
- package/dist/plugins/custom-validator.d.cts +10 -0
- package/dist/plugins/custom-validator.d.ts +10 -0
- package/dist/plugins/custom-validator.js +121 -0
- package/docs/FEATURE-INDEX.md +102 -68
- package/docs/add-custom-locale.md +48 -35
- package/docs/add-keyword.md +24 -0
- package/docs/api-reference.md +396 -154
- package/docs/api.md +13 -0
- package/docs/best-practices-project-structure.md +19 -10
- package/docs/best-practices.md +93 -53
- package/docs/cache-manager.md +23 -15
- package/docs/compile.md +45 -0
- package/docs/conditional-api.md +40 -11
- package/docs/custom-extensions-guide.md +80 -152
- package/docs/design-philosophy.md +76 -71
- package/docs/doc-index.md +324 -0
- package/docs/dsl-syntax.md +69 -19
- package/docs/dynamic-locale.md +24 -14
- package/docs/enum.md +12 -5
- package/docs/error-handling.md +53 -44
- package/docs/export-guide.md +47 -8
- package/docs/export-limitations.md +27 -11
- package/docs/faq.md +86 -67
- package/docs/frontend-i18n-guide.md +26 -12
- package/docs/i18n-user-guide.md +60 -47
- package/docs/i18n.md +51 -32
- package/docs/index.md +48 -0
- package/docs/json-schema-basics.md +40 -0
- package/docs/label-vs-description.md +12 -3
- package/docs/markdown-exporter.md +15 -6
- package/docs/mongodb-exporter.md +11 -4
- package/docs/multi-language.md +26 -0
- package/docs/multi-type-support.md +26 -33
- package/docs/mysql-exporter.md +9 -2
- package/docs/number-operators.md +12 -5
- package/docs/optional-marker-guide.md +28 -23
- package/docs/performance-guide.md +49 -0
- package/docs/plugin-system.md +205 -366
- package/docs/plugin-type-registration.md +34 -0
- package/docs/postgresql-exporter.md +9 -2
- package/docs/public/favicon.svg +5 -0
- package/docs/quick-start.md +37 -363
- package/docs/runtime-locale-support.md +20 -9
- package/docs/schema-helper.md +10 -5
- package/docs/schema-utils-advanced-issues.md +23 -0
- package/docs/schema-utils-best-practices.md +20 -0
- package/docs/schema-utils-chaining.md +7 -0
- package/docs/schema-utils.md +76 -42
- package/docs/security-checklist.md +20 -0
- package/docs/string-extensions.md +17 -9
- package/docs/troubleshooting.md +36 -21
- package/docs/type-converter.md +41 -50
- package/docs/type-reference.md +38 -15
- package/docs/typescript-guide.md +53 -42
- package/docs/union-type-guide.md +11 -1
- package/docs/union-types.md +10 -3
- package/docs/validate-async.md +36 -25
- package/docs/validate-batch.md +49 -0
- package/docs/validate-dsl-object-support.md +33 -28
- package/docs/validate.md +36 -16
- package/docs/validation-guide.md +25 -7
- package/docs/validator.md +39 -0
- package/package.json +85 -27
- package/plugins/custom-format.cjs +8 -0
- package/plugins/custom-type-example.cjs +8 -0
- package/plugins/custom-validator.cjs +8 -0
- package/src/adapters/DslAdapter.ts +111 -0
- package/src/adapters/index.ts +1 -0
- package/src/config/constants.ts +83 -0
- package/src/config/index.ts +2 -0
- package/src/config/patterns.ts +77 -0
- package/src/core/CacheManager.ts +159 -0
- package/src/core/ConditionalBuilder.ts +382 -0
- package/src/core/ConditionalRuntime.ts +28 -0
- package/src/core/ConditionalValidator.ts +255 -0
- package/src/core/DslBuilder.ts +677 -0
- package/src/core/ErrorCodes.ts +38 -0
- package/src/core/ErrorFormatter.ts +271 -0
- package/src/core/JSONSchemaCore.ts +65 -0
- package/src/core/Locale.ts +187 -0
- package/src/core/MessageTemplate.ts +42 -0
- package/src/core/ObjectDslBuilder.ts +64 -0
- package/src/core/PluginManager.ts +326 -0
- package/src/core/StringExtensions.ts +140 -0
- package/src/core/TemplateEngine.ts +44 -0
- package/src/core/Validator.ts +448 -0
- package/src/errors/I18nError.ts +159 -0
- package/src/errors/ValidationError.ts +105 -0
- package/src/exporters/BaseExporter.ts +60 -0
- package/src/exporters/MarkdownExporter.ts +305 -0
- package/src/exporters/MongoDBExporter.ts +126 -0
- package/src/exporters/MySQLExporter.ts +155 -0
- package/src/exporters/PostgreSQLExporter.ts +222 -0
- package/src/exporters/index.ts +18 -0
- package/src/index.ts +633 -0
- package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
- package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
- package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
- package/src/locales/index.ts +103 -0
- package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
- package/src/locales/types.ts +156 -0
- package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
- package/src/parser/ConstraintParser.ts +101 -0
- package/src/parser/DslParser.ts +470 -0
- package/src/parser/SchemaCompiler.ts +66 -0
- package/src/parser/TypeRegistry.ts +250 -0
- package/src/parser/index.ts +6 -0
- package/src/plugins/custom-format.ts +126 -0
- package/src/plugins/custom-type-example.ts +108 -0
- package/src/plugins/custom-validator.ts +140 -0
- package/src/types/conditional.ts +28 -0
- package/src/types/config.ts +59 -0
- package/src/types/dsl.ts +131 -0
- package/src/types/error.ts +60 -0
- package/src/types/index.ts +17 -0
- package/src/types/infer.ts +128 -0
- package/src/types/plugin.ts +58 -0
- package/src/types/safe-regex.d.ts +9 -0
- package/src/types/schema.ts +66 -0
- package/src/types/validate.ts +71 -0
- package/src/utils/SchemaHelper.ts +196 -0
- package/src/utils/SchemaUtils.ts +346 -0
- package/src/utils/TypeConverter.ts +215 -0
- package/src/utils/index.ts +10 -0
- package/src/validators/CustomKeywords.ts +477 -0
- package/.eslintignore +0 -11
- package/.eslintrc.json +0 -27
- package/CONTRIBUTING.md +0 -368
- package/STATUS.md +0 -491
- package/changelogs/v1.0.0.md +0 -328
- package/changelogs/v1.0.9.md +0 -367
- package/changelogs/v1.1.0.md +0 -389
- package/changelogs/v1.1.1.md +0 -308
- package/changelogs/v1.1.2.md +0 -183
- package/changelogs/v1.1.3.md +0 -161
- package/changelogs/v1.1.4.md +0 -432
- package/changelogs/v1.1.5.md +0 -493
- package/changelogs/v1.1.6.md +0 -211
- package/changelogs/v1.1.8.md +0 -376
- package/changelogs/v1.2.3.md +0 -124
- package/docs/INDEX.md +0 -252
- package/docs/issues-resolved-summary.md +0 -196
- package/docs/performance-benchmark-report.md +0 -179
- package/docs/performance-quick-reference.md +0 -123
- package/docs/user-questions-answered.md +0 -353
- package/docs/validation-rules-v1.0.2.md +0 -1608
- package/examples/README.md +0 -81
- package/examples/array-dsl-example.js +0 -227
- package/examples/conditional-example.js +0 -288
- package/examples/conditional-non-object.js +0 -129
- package/examples/conditional-validate-example.js +0 -321
- package/examples/custom-extension.js +0 -85
- package/examples/dsl-match-example.js +0 -74
- package/examples/dsl-style.js +0 -118
- package/examples/dynamic-locale-configuration.js +0 -348
- package/examples/dynamic-locale-example.js +0 -287
- package/examples/enum.examples.js +0 -324
- package/examples/export-demo.js +0 -130
- package/examples/express-integration.js +0 -376
- package/examples/i18n-error-handling-complete.js +0 -381
- package/examples/i18n-error-handling-quickstart.md +0 -0
- package/examples/i18n-error.examples.js +0 -181
- package/examples/i18n-full-demo.js +0 -301
- package/examples/i18n-memory-safety.examples.js +0 -268
- package/examples/markdown-export.js +0 -71
- package/examples/middleware-usage.js +0 -93
- package/examples/new-features-comparison.js +0 -315
- package/examples/password-reset/README.md +0 -153
- package/examples/password-reset/schema.js +0 -26
- package/examples/password-reset/test.js +0 -101
- package/examples/plugin-system.examples.js +0 -205
- package/examples/schema-utils-chaining.examples.js +0 -250
- package/examples/simple-example.js +0 -122
- package/examples/slug.examples.js +0 -179
- package/examples/string-extensions.js +0 -297
- package/examples/union-type-example.js +0 -127
- package/examples/union-types-example.js +0 -77
- package/examples/user-registration/README.md +0 -156
- package/examples/user-registration/routes.js +0 -92
- package/examples/user-registration/schema.js +0 -150
- package/examples/user-registration/server.js +0 -74
- package/index.d.ts +0 -3658
- package/index.js +0 -475
- package/index.mjs +0 -60
- package/lib/adapters/DslAdapter.js +0 -995
- package/lib/adapters/index.js +0 -20
- package/lib/config/constants.js +0 -286
- package/lib/config/patterns/common.js +0 -47
- package/lib/config/patterns/creditCard.js +0 -9
- package/lib/config/patterns/idCard.js +0 -9
- package/lib/config/patterns/index.js +0 -9
- package/lib/config/patterns/licensePlate.js +0 -4
- package/lib/config/patterns/passport.js +0 -4
- package/lib/config/patterns/phone.js +0 -9
- package/lib/config/patterns/postalCode.js +0 -5
- package/lib/core/CacheManager.js +0 -376
- package/lib/core/ConditionalBuilder.js +0 -503
- package/lib/core/DslBuilder.js +0 -1589
- package/lib/core/ErrorCodes.js +0 -233
- package/lib/core/ErrorFormatter.js +0 -445
- package/lib/core/JSONSchemaCore.js +0 -347
- package/lib/core/Locale.js +0 -130
- package/lib/core/MessageTemplate.js +0 -98
- package/lib/core/PluginManager.js +0 -448
- package/lib/core/StringExtensions.js +0 -240
- package/lib/core/Validator.js +0 -654
- package/lib/errors/I18nError.js +0 -328
- package/lib/errors/ValidationError.js +0 -191
- package/lib/exporters/MarkdownExporter.js +0 -420
- package/lib/exporters/MongoDBExporter.js +0 -162
- package/lib/exporters/MySQLExporter.js +0 -212
- package/lib/exporters/PostgreSQLExporter.js +0 -289
- package/lib/exporters/index.js +0 -24
- package/lib/locales/index.js +0 -8
- package/lib/utils/LRUCache.js +0 -174
- package/lib/utils/SchemaHelper.js +0 -240
- package/lib/utils/SchemaUtils.js +0 -445
- package/lib/utils/TypeConverter.js +0 -245
- package/lib/utils/index.js +0 -13
- package/lib/validators/CustomKeywords.js +0 -616
- package/lib/validators/index.js +0 -11
package/README.md
CHANGED
|
@@ -1,314 +1,92 @@
|
|
|
1
|
-
<div align="center">
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
3
|
# 🎯 schema-dsl
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
一行 DSL 替代 10 行链式调用
|
|
5
|
+
**Declare field rules with the simplest DSL — let one schema drive validation, derivation, export, and documentation.**
|
|
8
6
|
|
|
9
7
|
[](https://www.npmjs.com/package/schema-dsl)
|
|
10
8
|
[](https://www.npmjs.com/package/schema-dsl)
|
|
11
9
|
[](https://github.com/vextjs/schema-dsl/actions)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
12
11
|
[](https://opensource.org/licenses/MIT)
|
|
13
12
|
|
|
14
|
-
[
|
|
15
|
-
|
|
16
|
-
</div>
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## ⚡ TL;DR(30秒快速理解)
|
|
21
|
-
|
|
22
|
-
**schema-dsl 是什么?**
|
|
23
|
-
最简洁的数据验证库,一行DSL代替10行链式调用,性能超越Zod/Joi/Yup。
|
|
24
|
-
|
|
25
|
-
**核心优势:**
|
|
26
|
-
- 🎯 **极简语法**: `'string:3-32!'` 代替 8行 Joi 代码(减少 65% 代码量)
|
|
27
|
-
- 🚀 **性能第一**: 2,879,606 ops/s,比 Zod 快 1.58倍,比 Joi 快 9.61倍
|
|
28
|
-
- 🌍 **完整多语言**: 内置5种语言,支持运行时动态切换(v1.1.0+)
|
|
29
|
-
- 🎨 **独家功能**: 从验证规则直接生成 MongoDB/MySQL/PostgreSQL Schema
|
|
30
|
-
|
|
31
|
-
**3行代码上手:**
|
|
32
|
-
```javascript
|
|
33
|
-
const { dsl, validate } = require('schema-dsl');
|
|
34
|
-
const schema = dsl({ email: 'email!', age: 'number:18-' });
|
|
35
|
-
const result = validate(schema, { email: 'test@example.com', age: 25 });
|
|
36
|
-
console.log(result.valid); // true
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
**5分钟教程**: [快速开始](#-快速开始) | **完整文档**: [docs/INDEX.md](./docs/INDEX.md) | **在线体验**: [RunKit](https://runkit.com/npm/schema-dsl)
|
|
40
|
-
|
|
41
|
-
---
|
|
42
|
-
|
|
43
|
-
## 🗺️ 文档导航
|
|
44
|
-
|
|
45
|
-
**新手入门**:
|
|
46
|
-
- [快速开始](#-快速开始) - 5 分钟上手
|
|
47
|
-
- [功能总览](#-功能总览) - 了解所有功能
|
|
48
|
-
- [DSL 语法速查](#-dsl-语法速查) - 语法参考
|
|
49
|
-
|
|
50
|
-
**核心功能**:
|
|
51
|
-
- [基础验证](#1-基础验证javascript) - 表单验证
|
|
52
|
-
- [批量验证](#批量验证) - 性能优化
|
|
53
|
-
- [嵌套对象](#嵌套对象验证) - 复杂结构
|
|
54
|
-
- [条件验证](#条件验证---一行代码搞定) - 动态规则
|
|
55
|
-
- [多语言](#4-多语言支持) - 国际化
|
|
56
|
-
|
|
57
|
-
**框架集成**:
|
|
58
|
-
- [Express](#2-express-集成---自动错误处理)
|
|
59
|
-
- [Koa](#koa-集成)
|
|
60
|
-
- [Fastify](#fastify-集成)
|
|
61
|
-
|
|
62
|
-
**高级功能**:
|
|
63
|
-
- [数据库导出](#3-数据库-schema-导出) - 独家功能
|
|
64
|
-
- [插件系统](#6-插件系统) - 扩展功能
|
|
65
|
-
- [TypeScript](#15-typescript-用法-) - 类型支持
|
|
66
|
-
|
|
67
|
-
**完整文档**: [docs/INDEX.md](./docs/INDEX.md) - 40+ 篇详细文档
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## 🆕 最新特性(v1.1.8)
|
|
72
|
-
|
|
73
|
-
### 🎯 智能参数识别 - 简化语法支持(v1.1.8)
|
|
74
|
-
|
|
75
|
-
**API 更简洁,从4个参数减少到2个参数**
|
|
76
|
-
|
|
77
|
-
```javascript
|
|
78
|
-
const { dsl, Locale } = require('schema-dsl');
|
|
79
|
-
|
|
80
|
-
// 配置语言包
|
|
81
|
-
Locale.addLocale('zh-CN', {
|
|
82
|
-
'account.notFound': {
|
|
83
|
-
code: 40001,
|
|
84
|
-
message: '账户不存在'
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
Locale.addLocale('en-US', {
|
|
89
|
-
'account.notFound': {
|
|
90
|
-
code: 40001,
|
|
91
|
-
message: 'Account not found'
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ✅ 新增:简化语法(推荐)
|
|
96
|
-
dsl.error.throw('account.notFound', 'zh-CN');
|
|
97
|
-
dsl.error.throw('account.notFound', 'zh-CN', 404);
|
|
13
|
+
[Quick Start](#-quick-start) · [Documentation](https://vextjs.github.io/schema-dsl) · [Feature Overview](#-feature-overview) · [Examples](./examples)
|
|
98
14
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
dsl.error.throw('account.notFound', { id: '123' }, 404, 'zh-CN');
|
|
102
|
-
|
|
103
|
-
// 所有方法都支持
|
|
104
|
-
dsl.error.create('account.notFound', 'zh-CN');
|
|
105
|
-
dsl.error.assert(account, 'account.notFound', 'zh-CN');
|
|
15
|
+
```bash
|
|
16
|
+
npm install schema-dsl
|
|
106
17
|
```
|
|
107
18
|
|
|
108
|
-
|
|
109
|
-
- 🎯 **参数更少**: 无需参数对象时从4个参数减少到2个
|
|
110
|
-
- 🎯 **智能识别**: 自动判断第2个参数是语言还是参数对象
|
|
111
|
-
- 🎯 **完全兼容**: 现有代码无需修改,渐进式增强
|
|
112
|
-
- 🎯 **降低错误**: 不再需要传递空对象 `{}`
|
|
113
|
-
|
|
114
|
-
📖 [完整文档](./docs/error-handling.md) · [实现原理](./docs/i18n-implementation-analysis.md) · [变更日志](./changelogs/v1.1.8.md)
|
|
19
|
+
</div>
|
|
115
20
|
|
|
116
21
|
---
|
|
117
22
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
**统一错误代码,多语言共享,前端友好**
|
|
121
|
-
|
|
122
|
-
```javascript
|
|
123
|
-
// 语言包配置(支持对象格式)
|
|
124
|
-
const locales = {
|
|
125
|
-
'zh-CN': {
|
|
126
|
-
'account.notFound': {
|
|
127
|
-
code: 40001, // 统一的数字错误代码
|
|
128
|
-
message: '账户不存在'
|
|
129
|
-
},
|
|
130
|
-
'account.insufficientBalance': {
|
|
131
|
-
code: 40002,
|
|
132
|
-
message: '余额不足,当前{{#balance}},需要{{#required}}'
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
'en-US': {
|
|
136
|
-
'account.notFound': {
|
|
137
|
-
code: 40001, // 相同的数字 code
|
|
138
|
-
message: 'Account not found'
|
|
139
|
-
},
|
|
140
|
-
'account.insufficientBalance': {
|
|
141
|
-
code: 40002,
|
|
142
|
-
message: 'Insufficient balance: {{#balance}}, required: {{#required}}'
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// 使用
|
|
148
|
-
try {
|
|
149
|
-
dsl.error.throw('account.notFound');
|
|
150
|
-
} catch (error) {
|
|
151
|
-
console.log(error.code); // 40001 (统一数字代码)
|
|
152
|
-
console.log(error.originalKey); // 'account.notFound' (原始key)
|
|
153
|
-
console.log(error.message); // 中文: "账户不存在" / 英文: "Account not found"
|
|
154
|
-
|
|
155
|
-
// 增强的 error.is() - 两种方式都支持
|
|
156
|
-
if (error.is('account.notFound')) { } // ✅ 使用 originalKey
|
|
157
|
-
if (error.is(40001)) { } // ✅ 使用数字 code
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// 前端统一处理
|
|
161
|
-
switch (error.code) {
|
|
162
|
-
case 40001: showNotFoundPage(); break; // 不受语言影响
|
|
163
|
-
case 40002: showTopUpDialog(); break;
|
|
164
|
-
}
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
**核心优势**:
|
|
168
|
-
- 🎯 **统一错误代码**: 不同语言使用相同的数字 `code`,便于前端统一处理
|
|
169
|
-
- 🔄 **完全向后兼容**: 字符串格式自动转换,现有代码无需修改
|
|
170
|
-
- 📊 **更好的错误追踪**: `originalKey` 和 `code` 分离,便于日志分析
|
|
171
|
-
- 🌍 **多语言友好**: 前端可以用统一的数字 code 处理,不受语言影响
|
|
172
|
-
|
|
173
|
-
📖 [完整文档](./docs/error-handling.md#v115-新功能对象格式错误配置) · [变更日志](./changelogs/v1.1.5.md)
|
|
23
|
+
## ⚡ TL;DR (30-second intro)
|
|
174
24
|
|
|
175
|
-
|
|
25
|
+
**What is schema-dsl?**
|
|
176
26
|
|
|
177
|
-
|
|
27
|
+
Write field rules like this:
|
|
178
28
|
|
|
179
|
-
|
|
29
|
+
```typescript
|
|
30
|
+
import { dsl, validate } from 'schema-dsl';
|
|
180
31
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
32
|
+
const userSchema = dsl({
|
|
33
|
+
username: 'string:3-32!',
|
|
34
|
+
email: 'email!',
|
|
35
|
+
role: 'admin|user|guest',
|
|
36
|
+
contact: 'types:email|phone'
|
|
186
37
|
});
|
|
187
38
|
|
|
188
|
-
validate(
|
|
189
|
-
validate(schema, { contact: '13800138000' }); // ✅ 通过
|
|
190
|
-
validate(schema, { contact: 12345 }); // ❌ 失败
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**实际场景**:
|
|
194
|
-
- ✅ 用户注册:支持邮箱或手机号登录
|
|
195
|
-
- ✅ 商品价格:数字或"面议"字符串
|
|
196
|
-
- ✅ 可选字段:允许null值
|
|
197
|
-
|
|
198
|
-
📖 [完整文档](./docs/union-types.md)
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
### 🌍 运行时多语言支持
|
|
203
|
-
|
|
204
|
-
**无需修改全局设置,每次调用指定语言**
|
|
205
|
-
|
|
206
|
-
```javascript
|
|
207
|
-
// 根据请求头动态返回不同语言的错误
|
|
208
|
-
app.post('/api/account', (req, res) => {
|
|
209
|
-
const locale = req.headers['accept-language'] || 'en-US';
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
dsl.error.assert(account, 'account.notFound', {}, 404, locale);
|
|
213
|
-
// 中文请求返回: "账户不存在"
|
|
214
|
-
// 英文请求返回: "Account not found"
|
|
215
|
-
} catch (error) {
|
|
216
|
-
res.status(error.statusCode).json(error.toJSON());
|
|
217
|
-
}
|
|
218
|
-
});
|
|
39
|
+
const result = validate(userSchema, req.body);
|
|
219
40
|
```
|
|
220
41
|
|
|
221
|
-
|
|
222
|
-
- ✅ 多语言 API(根据请求头动态返回)
|
|
223
|
-
- ✅ 微服务架构(错误传递保持原语言)
|
|
224
|
-
- ✅ 国际化应用(同一请求多种语言)
|
|
225
|
-
|
|
226
|
-
📖 [运行时多语言文档](./docs/runtime-locale-support.md)
|
|
42
|
+
Then that **same set of rules** continues to power:
|
|
227
43
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
44
|
+
- ✅ **Sync / async validation** — `validate()` / `validateAsync()`
|
|
45
|
+
- ✅ **Schema derivation** — `pick / omit / partial` to tailor schemas per endpoint
|
|
46
|
+
- ✅ **Database schemas** — export directly to MongoDB / MySQL / PostgreSQL
|
|
47
|
+
- ✅ **Field documentation** — auto-generate Markdown
|
|
48
|
+
- ✅ **Unified error model** — `ValidationError` + `I18nError`
|
|
49
|
+
- ✅ **Internationalization** — 5 built-in locales (zh-CN / en-US / ja-JP / es-ES / fr-FR), switchable at runtime
|
|
231
50
|
|
|
232
|
-
-
|
|
233
|
-
- ✅ **统一错误抛出**: `I18nError` 类,支持多语言错误消息(v1.1.1)
|
|
234
|
-
- ✅ **插件系统增强**: 自定义类型注册更简单(v1.1.0)
|
|
235
|
-
- ✅ **TypeScript 类型完善**: 0个类型错误(v1.1.4)
|
|
236
|
-
|
|
237
|
-
[查看完整更新日志](./CHANGELOG.md)
|
|
51
|
+
**5-minute tutorial**: [Quick Start](https://vextjs.github.io/schema-dsl/quick-start) | **Full docs**: [Online Documentation](https://vextjs.github.io/schema-dsl)
|
|
238
52
|
|
|
239
53
|
---
|
|
240
54
|
|
|
241
|
-
##
|
|
242
|
-
|
|
243
|
-
> 方便AI快速理解所有功能
|
|
55
|
+
## 🗺️ Documentation
|
|
244
56
|
|
|
245
|
-
|
|
57
|
+
**Getting started**:
|
|
58
|
+
- [Quick Start](https://vextjs.github.io/schema-dsl/quick-start) — up and running in 5 minutes
|
|
59
|
+
- [DSL Syntax Reference](#-dsl-syntax-reference) — syntax cheatsheet
|
|
60
|
+
- [FAQ](https://vextjs.github.io/schema-dsl/faq) — common questions
|
|
246
61
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
},
|
|
254
|
-
"i18n": {
|
|
255
|
-
"supported": ["zh-CN", "en-US", "ja-JP", "es-ES", "fr-FR"],
|
|
256
|
-
"features": ["配置加载", "运行时切换", "自定义消息", "参数插值"],
|
|
257
|
-
"runtime": "v1.1.0+ 运行时指定语言 (dsl.error.create(code, params, statusCode, locale))"
|
|
258
|
-
},
|
|
259
|
-
"database": {
|
|
260
|
-
"export": ["MongoDB", "MySQL", "PostgreSQL"],
|
|
261
|
-
"unique": "从验证规则直接生成数据库Schema"
|
|
262
|
-
},
|
|
263
|
-
"framework": {
|
|
264
|
-
"integration": ["Express", "Koa", "Fastify"],
|
|
265
|
-
"async": "validateAsync() 失败自动抛出 ValidationError"
|
|
266
|
-
},
|
|
267
|
-
"api": {
|
|
268
|
-
"main": ["dsl()", "validate()", "validateAsync()"],
|
|
269
|
-
"utils": ["SchemaUtils.pick()", "SchemaUtils.omit()", "SchemaUtils.partial()"],
|
|
270
|
-
"conditional": ["dsl.if()", "dsl.match()"],
|
|
271
|
-
"errors": ["ValidationError", "I18nError"]
|
|
272
|
-
},
|
|
273
|
-
"performance": {
|
|
274
|
-
"opsPerSecond": 2879606,
|
|
275
|
-
"vs": {
|
|
276
|
-
"Zod": "1.58x faster",
|
|
277
|
-
"Joi": "9.61x faster",
|
|
278
|
-
"Yup": "27.07x faster"
|
|
279
|
-
},
|
|
280
|
-
"optimization": ["WeakMap缓存", "智能编译", "批量验证优化"]
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
```
|
|
62
|
+
**Core features**:
|
|
63
|
+
- [Validation Guide](https://vextjs.github.io/schema-dsl/validation-guide) — all validation scenarios
|
|
64
|
+
- [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) — schema reuse
|
|
65
|
+
- [Conditional Validation API](https://vextjs.github.io/schema-dsl/conditional-api) — dsl.if / dsl.match
|
|
66
|
+
- [Async Validation & Framework Integration](https://vextjs.github.io/schema-dsl/validate-async) — Express / Koa / Fastify
|
|
67
|
+
- [Error Handling & i18n](https://vextjs.github.io/schema-dsl/error-handling) — error model
|
|
284
68
|
|
|
285
|
-
|
|
69
|
+
**Export & integration**:
|
|
70
|
+
- [Export Guide](https://vextjs.github.io/schema-dsl/export-guide) — MongoDB / MySQL / PostgreSQL
|
|
71
|
+
- [TypeScript Guide](https://vextjs.github.io/schema-dsl/typescript-guide) — type inference and usage
|
|
72
|
+
- [Plugin System](https://vextjs.github.io/schema-dsl/plugin-system) — custom extensions
|
|
286
73
|
|
|
287
|
-
|
|
288
|
-
|-----|------|--------|------|
|
|
289
|
-
| `dsl(schema)` | 创建Schema | Schema对象 | [DSL语法](./docs/dsl-syntax.md) |
|
|
290
|
-
| `validate(schema, data)` | 同步验证 | `{valid, errors, data}` | [验证指南](./docs/validation-guide.md) |
|
|
291
|
-
| `validateAsync(schema, data)` | 异步验证 | Promise(失败抛错) | [异步验证](./docs/validate-async.md) |
|
|
292
|
-
| `dsl.if(condition)` | 条件验证 | ConditionalBuilder | [条件API](./docs/conditional-api.md) |
|
|
293
|
-
| `SchemaUtils.pick()` | 选择字段 | 新Schema | [SchemaUtils](./docs/schema-utils.md) |
|
|
294
|
-
| `I18nError.throw()` | 抛出多语言错误 | never | [I18nError示例](./examples/i18n-error.examples.js) |
|
|
74
|
+
**Full docs**: [Online Documentation](https://vextjs.github.io/schema-dsl) · [Feature Index](https://vextjs.github.io/schema-dsl/FEATURE-INDEX)
|
|
295
75
|
|
|
296
76
|
---
|
|
297
77
|
|
|
298
|
-
## ✨
|
|
299
|
-
|
|
300
|
-
### 🎯 极简 DSL 语法
|
|
78
|
+
## ✨ Why schema-dsl?
|
|
301
79
|
|
|
302
|
-
|
|
80
|
+
### 🎯 Minimal DSL — 65% less code
|
|
303
81
|
|
|
304
82
|
<table>
|
|
305
83
|
<tr>
|
|
306
84
|
<td width="50%" valign="top">
|
|
307
85
|
|
|
308
|
-
**❌
|
|
86
|
+
**❌ Traditional approach** — verbose
|
|
309
87
|
|
|
310
88
|
```javascript
|
|
311
|
-
// Joi
|
|
89
|
+
// Joi — requires 8 lines
|
|
312
90
|
const schema = Joi.object({
|
|
313
91
|
username: Joi.string()
|
|
314
92
|
.min(3).max(32).required(),
|
|
@@ -322,14 +100,14 @@ const schema = Joi.object({
|
|
|
322
100
|
</td>
|
|
323
101
|
<td width="50%" valign="top">
|
|
324
102
|
|
|
325
|
-
**✅ schema-dsl**
|
|
103
|
+
**✅ schema-dsl** — concise and clean
|
|
326
104
|
|
|
327
|
-
```
|
|
328
|
-
//
|
|
105
|
+
```typescript
|
|
106
|
+
// just 3 lines
|
|
329
107
|
const schema = dsl({
|
|
330
108
|
username: 'string:3-32!',
|
|
331
|
-
email:
|
|
332
|
-
age:
|
|
109
|
+
email: 'email!',
|
|
110
|
+
age: 'number:18-120'
|
|
333
111
|
});
|
|
334
112
|
```
|
|
335
113
|
|
|
@@ -337,2150 +115,514 @@ const schema = dsl({
|
|
|
337
115
|
</tr>
|
|
338
116
|
</table>
|
|
339
117
|
|
|
340
|
-
###
|
|
341
|
-
|
|
342
|
-
**测试结果:schema-dsl 是目前性能最优的验证库(基于最新基准测试)**
|
|
343
|
-
|
|
344
|
-
| 验证库 | 性能 (ops/s) | 相对速度 | 评价 |
|
|
345
|
-
|--------|-------------|---------|------|
|
|
346
|
-
| **schema-dsl** | **2,879,606** | **基准 (1.00x)** | **🥇 第一名** |
|
|
347
|
-
| Zod | 1,818,592 | 0.63x | 🥈 慢 58% |
|
|
348
|
-
| Joi | 299,761 | 0.10x | 🥉 慢 861% |
|
|
349
|
-
| Yup | 106,378 | 0.04x | 慢 2607% |
|
|
350
|
-
|
|
351
|
-
**性能优势**:
|
|
352
|
-
- ✅ 比 Zod 快 **1.58倍**
|
|
353
|
-
- ✅ 比 Joi 快 **9.61倍**
|
|
354
|
-
- ✅ 比 Yup 快 **27.07倍**
|
|
355
|
-
|
|
356
|
-
> 📊 **测试环境**: Node.js v20.x, Windows
|
|
357
|
-
> 📊 **测试场景**: 用户注册表单验证(username, email, age, tags)
|
|
358
|
-
> 📊 **测试工具**: [Benchmark.js](https://benchmarkjs.com/)
|
|
359
|
-
> 📊 **运行测试**: `node test/benchmarks/library-comparison.js`
|
|
360
|
-
|
|
361
|
-
### 🌍 完整多语言支持
|
|
362
|
-
|
|
363
|
-
**一行配置,自动加载所有语言包**
|
|
364
|
-
|
|
365
|
-
```javascript
|
|
366
|
-
const { dsl, validate } = require('schema-dsl');
|
|
367
|
-
const path = require('path');
|
|
368
|
-
|
|
369
|
-
// ========== 应用启动时配置(只执行一次)==========
|
|
370
|
-
dsl.config({
|
|
371
|
-
i18n: path.join(__dirname, 'locales') // 自动加载目录下所有语言文件
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// ========== 运行时直接切换语言(无需重新加载)==========
|
|
375
|
-
const schema = dsl({ username: 'string:3-32!' });
|
|
376
|
-
|
|
377
|
-
// 中文错误消息
|
|
378
|
-
validate(schema, { username: 'ab' }, { locale: 'zh-CN' });
|
|
379
|
-
// => "username长度不能少于3个字符"
|
|
380
|
-
|
|
381
|
-
// 英文错误消息
|
|
382
|
-
validate(schema, { username: 'ab' }, { locale: 'en-US' });
|
|
383
|
-
// => "username length must be at least 3"
|
|
384
|
-
|
|
385
|
-
// 日语错误消息
|
|
386
|
-
validate(schema, { username: 'ab' }, { locale: 'ja-JP' });
|
|
387
|
-
// => "usernameは3文字以上である必要があります"
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
**🆕 运行时多语言支持(v1.1.0+)**
|
|
391
|
-
|
|
392
|
-
无需修改全局设置,可在每次调用时指定语言:
|
|
393
|
-
|
|
394
|
-
```javascript
|
|
395
|
-
const { dsl, I18nError } = require('schema-dsl');
|
|
396
|
-
|
|
397
|
-
// 方式1: 业务错误 - 运行时指定语言
|
|
398
|
-
const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
|
|
399
|
-
console.log(error1.message); // "账户不存在"
|
|
400
|
-
|
|
401
|
-
const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
|
|
402
|
-
console.log(error2.message); // "Account not found"
|
|
403
|
-
|
|
404
|
-
// 方式2: 断言风格 - 根据请求头动态指定
|
|
405
|
-
app.post('/api/withdraw', (req, res) => {
|
|
406
|
-
const locale = req.headers['accept-language'] || 'en-US';
|
|
407
|
-
const account = getAccount(req.user.id);
|
|
408
|
-
|
|
409
|
-
// 根据请求头返回对应语言的错误
|
|
410
|
-
I18nError.assert(account, 'account.notFound', {}, 404, locale);
|
|
411
|
-
I18nError.assert(
|
|
412
|
-
account.balance >= req.body.amount,
|
|
413
|
-
'account.insufficientBalance',
|
|
414
|
-
{ balance: account.balance, required: req.body.amount },
|
|
415
|
-
400,
|
|
416
|
-
locale
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
// 验证通过,继续处理...
|
|
420
|
-
});
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
**适用场景**:
|
|
424
|
-
- ✅ 多语言 API(根据请求头返回不同语言)
|
|
425
|
-
- ✅ 微服务架构(错误在服务间传递时保持语言)
|
|
426
|
-
- ✅ 同一请求中需要多种语言的错误消息
|
|
427
|
-
|
|
428
|
-
**内置语言**: 中文、英文、日语、法语、西班牙语
|
|
429
|
-
|
|
430
|
-
📖 [完整多语言文档](./docs/i18n.md)
|
|
431
|
-
📖 [运行时多语言支持](./docs/runtime-locale-support.md)
|
|
432
|
-
|
|
433
|
-
### 🎨 数据库 Schema 导出
|
|
434
|
-
|
|
435
|
-
**一份定义,多处使用**
|
|
436
|
-
|
|
437
|
-
```javascript
|
|
438
|
-
const { dsl, exporters } = require('schema-dsl');
|
|
439
|
-
|
|
440
|
-
const schema = dsl({
|
|
441
|
-
username: 'string:3-32!',
|
|
442
|
-
email: 'email!',
|
|
443
|
-
age: 'number:18-120'
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// 导出 MongoDB Schema
|
|
447
|
-
const mongoExporter = new exporters.MongoDBExporter();
|
|
448
|
-
const mongoSchema = mongoExporter.export(schema);
|
|
449
|
-
|
|
450
|
-
// 导出 MySQL 建表语句
|
|
451
|
-
const mysqlExporter = new exporters.MySQLExporter();
|
|
452
|
-
const mysqlDDL = mysqlExporter.export('users', schema);
|
|
453
|
-
|
|
454
|
-
// 导出 PostgreSQL 建表语句
|
|
455
|
-
const pgExporter = new exporters.PostgreSQLExporter();
|
|
456
|
-
const pgDDL = pgExporter.export('users', schema);
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
**✅ 独家功能**:从验证规则直接生成数据库结构!
|
|
460
|
-
|
|
461
|
-
### ⚡ 5 分钟上手
|
|
462
|
-
|
|
463
|
-
**学习成本极低,立即可用**
|
|
464
|
-
|
|
465
|
-
```javascript
|
|
466
|
-
const { dsl, validate } = require('schema-dsl');
|
|
467
|
-
|
|
468
|
-
// 1️⃣ 定义规则(1 分钟)
|
|
469
|
-
const schema = dsl({
|
|
470
|
-
username: 'string:3-32!',
|
|
471
|
-
email: 'email!',
|
|
472
|
-
password: 'string:8-!'
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
// 2️⃣ 验证数据(30 秒)
|
|
476
|
-
const result = validate(schema, {
|
|
477
|
-
username: 'john',
|
|
478
|
-
email: 'john@example.com',
|
|
479
|
-
password: '12345678'
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// 3️⃣ 处理结果(30 秒)
|
|
483
|
-
if (result.valid) {
|
|
484
|
-
console.log('验证通过!');
|
|
485
|
-
} else {
|
|
486
|
-
console.log('错误:', result.errors);
|
|
487
|
-
}
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
**对比其他库**:
|
|
491
|
-
- Joi/Yup:需要 30 分钟学习链式 API
|
|
492
|
-
- Zod:需要 15 分钟学习 TypeScript 类型
|
|
493
|
-
- Ajv:需要 20 分钟学习 JSON Schema 规范
|
|
494
|
-
|
|
495
|
-
### 💪 功能完整
|
|
496
|
-
|
|
497
|
-
| 功能 | schema-dsl | 说明 |
|
|
498
|
-
|------|-----------|------|
|
|
499
|
-
| **基本验证** | ✅ | string、number、boolean、date、email、url... |
|
|
500
|
-
| **高级验证** | ✅ | 正则、自定义、条件、嵌套、数组... |
|
|
501
|
-
| **🆕 跨类型联合** | ✅ | `types:string|number` 一个字段支持多种类型 (v1.1.1) |
|
|
502
|
-
| **错误格式化** | ✅ | 自动多语言翻译 |
|
|
503
|
-
| **🆕 多语言错误** | ✅ | `I18nError` 统一的多语言错误抛出 (v1.1.1) |
|
|
504
|
-
| **数据库导出** | ✅ | MongoDB、MySQL、PostgreSQL |
|
|
505
|
-
| **TypeScript** | ✅ | 完整类型定义 |
|
|
506
|
-
| **性能优化** | ✅ | WeakMap 缓存、智能编译 |
|
|
507
|
-
| **插件系统** | ✅ | 支持自定义类型注册 (v1.1.1) |
|
|
508
|
-
| **文档生成** | ✅ | Markdown、HTML |
|
|
118
|
+
### 💪 Full-featured
|
|
509
119
|
|
|
510
|
-
|
|
120
|
+
| Feature | schema-dsl | Notes |
|
|
121
|
+
|---------|:----------:|-------|
|
|
122
|
+
| **Basic validation** | ✅ | string, number, boolean, date, email, url, phone… |
|
|
123
|
+
| **Advanced validation** | ✅ | regex, custom functions, conditional branches, nested objects, arrays… |
|
|
124
|
+
| **Cross-type union** | ✅ | `types:email\|phone` — one field accepts multiple types |
|
|
125
|
+
| **Error messages** | ✅ | auto-translated + custom messages + field labels |
|
|
126
|
+
| **i18n business errors** | ✅ | `I18nError` with numeric error codes |
|
|
127
|
+
| **Database export** | ✅ | MongoDB / MySQL / PostgreSQL schema generation |
|
|
128
|
+
| **Documentation generation** | ✅ | Markdown field docs auto-generated |
|
|
129
|
+
| **TypeScript** | ✅ | Written in native TypeScript with full type inference |
|
|
130
|
+
| **Plugin system** | ✅ | Custom types / formats / validators |
|
|
131
|
+
| **Schema reuse** | ✅ | pick / omit / partial / extend |
|
|
511
132
|
|
|
512
|
-
|
|
133
|
+
### 🎨 One schema, many uses (unique capability)
|
|
513
134
|
|
|
514
|
-
```
|
|
515
|
-
|
|
135
|
+
```typescript
|
|
136
|
+
import { dsl, exporters, SchemaUtils } from 'schema-dsl';
|
|
516
137
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
138
|
+
const userSchema = dsl({
|
|
139
|
+
id: 'uuid!',
|
|
140
|
+
username: 'string:3-32!',
|
|
141
|
+
email: 'email!',
|
|
142
|
+
password: 'string:8-64!',
|
|
143
|
+
age: 'number:18-120',
|
|
144
|
+
createdAt: 'string!'
|
|
520
145
|
});
|
|
521
146
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
147
|
+
// 📋 derive scenario-specific schemas
|
|
148
|
+
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
|
|
149
|
+
const updateSchema = SchemaUtils.partial(SchemaUtils.pick(userSchema, ['username', 'email']));
|
|
150
|
+
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
|
|
525
151
|
|
|
526
|
-
//
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
});
|
|
531
|
-
```
|
|
152
|
+
// 🗄️ export the same schema to any database
|
|
153
|
+
const mongoSchema = new exporters.MongoDBExporter().export(userSchema);
|
|
154
|
+
const mysqlDDL = new exporters.MySQLExporter().export('users', userSchema);
|
|
155
|
+
const pgDDL = new exporters.PostgreSQLExporter().export('users', userSchema);
|
|
532
156
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// 用户注册:支持邮箱或手机号
|
|
536
|
-
const registerSchema = dsl({
|
|
537
|
-
username: 'string:3-20!',
|
|
538
|
-
contact: 'types:email|phone!', // 灵活的联系方式
|
|
539
|
-
age: 'types:integer:1-150|null' // 年龄可选
|
|
540
|
-
});
|
|
157
|
+
// 📝 generate field documentation from the same schema
|
|
158
|
+
const markdown = exporters.MarkdownExporter.export(userSchema, { title: 'User Field Reference' });
|
|
541
159
|
```
|
|
542
160
|
|
|
543
|
-
|
|
161
|
+
> ⚠️ SQL exporters only accept `anyOf` / `oneOf` when every branch resolves to the **same** SQL column type (for example `ipv4 | ipv6`). Ambiguous unions such as `string | number` now throw an explicit error instead of silently choosing the first branch.
|
|
544
162
|
|
|
545
163
|
---
|
|
546
164
|
|
|
547
|
-
##
|
|
548
|
-
|
|
549
|
-
> 让 AI 和开发者快速了解所有功能
|
|
550
|
-
|
|
551
|
-
### 核心功能速查表
|
|
552
|
-
|
|
553
|
-
| 分类 | 功能 | 代码示例 | 文档链接 |
|
|
554
|
-
|------|------|---------|----------|
|
|
555
|
-
| **基础验证** | DSL 语法 | `'string:3-32!'` | [DSL 语法](./docs/dsl-syntax.md) |
|
|
556
|
-
| | 链式调用 | `'string!'.label('用户名')` | [String 扩展](./docs/string-extensions.md) |
|
|
557
|
-
| | TypeScript | `dsl('string!')` | [TS 指南](./docs/typescript-guide.md) |
|
|
558
|
-
| **高级验证** | 条件验证 | `dsl.if()/dsl.match()` | [条件 API](./docs/conditional-api.md) |
|
|
559
|
-
| | 嵌套对象 | `{ user: { name: 'string!' } }` | [验证指南](./docs/validation-guide.md) |
|
|
560
|
-
| | 数组验证 | `'array:1-10<string>'` | [类型参考](./docs/type-reference.md) |
|
|
561
|
-
| | 联合类型 | `'types:string\|number'` | [联合类型](./docs/union-types.md) |
|
|
562
|
-
| | 正则验证 | `.pattern(/^[A-Z]+$/)` | [自定义扩展](./docs/custom-extensions-guide.md) |
|
|
563
|
-
| | 自定义验证 | `.custom((v) => ...)` | [自定义扩展](./docs/custom-extensions-guide.md) |
|
|
564
|
-
| **Schema 工具** | 复用字段 | `SchemaUtils.pick()` | [SchemaUtils](./docs/schema-utils.md) |
|
|
565
|
-
| | 批量验证 | `validateBatch(schema, array)` | [批量验证](#批量验证) |
|
|
566
|
-
| | 字段库 | `createLibrary()` | [SchemaUtils](./docs/schema-utils.md) |
|
|
567
|
-
| **框架集成** | Express | `validateAsync + try/catch` | [Express 示例](./examples/express-integration.js) |
|
|
568
|
-
| | Koa | `validateAsync + ctx.throw` | [中间件示例](./examples/middleware-usage.js) |
|
|
569
|
-
| | Fastify | `preValidation hook` | [中间件示例](./examples/middleware-usage.js) |
|
|
570
|
-
| **多语言** | 配置语言 | `dsl.config({ i18n })` | [i18n 指南](./docs/i18n-user-guide.md) |
|
|
571
|
-
| | 错误抛出 | `I18nError.throw()` | [I18nError 示例](./examples/i18n-error.examples.js) |
|
|
572
|
-
| **数据库** | MongoDB | `MongoDBExporter.export()` | [MongoDB 导出](./docs/mongodb-exporter.md) |
|
|
573
|
-
| | MySQL | `MySQLExporter.export()` | [MySQL 导出](./docs/mysql-exporter.md) |
|
|
574
|
-
| | PostgreSQL | `PostgreSQLExporter.export()` | [PostgreSQL 导出](./docs/postgresql-exporter.md) |
|
|
575
|
-
| **插件** | 自定义格式 | `pluginManager.register()` | [插件系统](./docs/plugin-system.md) |
|
|
576
|
-
| **性能** | 缓存配置 | `config({ cache })` | [缓存管理](./docs/cache-manager.md) |
|
|
577
|
-
|
|
578
|
-
### 常见使用场景
|
|
579
|
-
|
|
580
|
-
| 场景 | 代码示例 | 完整示例 |
|
|
581
|
-
|------|---------|----------|
|
|
582
|
-
| **API 参数验证** | [Express 集成](#2-express-集成---自动错误处理) | [完整代码](./examples/express-integration.js) |
|
|
583
|
-
| **用户注册表单** | [基础验证](#1-基础验证javascript) | [完整代码](./examples/user-registration/) |
|
|
584
|
-
| **批量数据处理** | [批量验证](#批量验证) | [完整代码](./examples/simple-example.js) |
|
|
585
|
-
| **多语言应用** | [多语言支持](#4-多语言支持) | [完整代码](./examples/i18n-full-demo.js) |
|
|
586
|
-
| **数据库建表** | [数据库导出](#3-数据库-schema-导出) | [完整代码](./examples/export-demo.js) |
|
|
587
|
-
| **复杂嵌套结构** | [嵌套对象](#嵌套对象验证) | [验证指南](./docs/validation-guide.md) |
|
|
588
|
-
| **正则格式验证** | [正则验证](#正则验证) | [自定义扩展](./docs/custom-extensions-guide.md) |
|
|
589
|
-
| **业务逻辑验证** | [自定义验证器](#自定义验证器) | [自定义扩展](./docs/custom-extensions-guide.md) |
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
## �📦 安装
|
|
165
|
+
## 📦 Installation
|
|
594
166
|
|
|
595
167
|
```bash
|
|
596
168
|
npm install schema-dsl
|
|
597
169
|
```
|
|
598
170
|
|
|
171
|
+
**Runtime requirement**: Node.js >= 18.0.0
|
|
172
|
+
|
|
599
173
|
---
|
|
600
174
|
|
|
601
|
-
## 🚀
|
|
175
|
+
## 🚀 Quick Start
|
|
602
176
|
|
|
603
|
-
### 1.
|
|
177
|
+
### 1. Basic validation
|
|
604
178
|
|
|
605
|
-
```
|
|
606
|
-
|
|
179
|
+
```typescript
|
|
180
|
+
import { dsl, validate } from 'schema-dsl';
|
|
607
181
|
|
|
608
182
|
const userSchema = dsl({
|
|
609
183
|
username: 'string:3-32!',
|
|
610
|
-
email:
|
|
611
|
-
age:
|
|
612
|
-
|
|
184
|
+
email: 'email!',
|
|
185
|
+
age: 'number:18-120',
|
|
186
|
+
role: 'admin|user|guest',
|
|
187
|
+
tags: 'array<string>'
|
|
613
188
|
});
|
|
614
189
|
|
|
615
|
-
// ✅
|
|
616
|
-
const
|
|
190
|
+
// ✅ validation passed
|
|
191
|
+
const result = validate(userSchema, {
|
|
617
192
|
username: 'john_doe',
|
|
618
|
-
email:
|
|
619
|
-
age:
|
|
620
|
-
|
|
193
|
+
email: 'john@example.com',
|
|
194
|
+
age: 25,
|
|
195
|
+
role: 'user',
|
|
196
|
+
tags: ['verified']
|
|
621
197
|
});
|
|
622
198
|
|
|
623
|
-
console.log(
|
|
624
|
-
console.log(
|
|
625
|
-
|
|
626
|
-
// ❌ 验证失败 - 看看如何处理错误
|
|
627
|
-
const result2 = validate(userSchema, {
|
|
628
|
-
username: 'ab', // 太短(最少3个字符)
|
|
629
|
-
email: 'invalid-email', // 格式错误
|
|
630
|
-
age: 15 // 小于最小值18
|
|
631
|
-
});
|
|
199
|
+
console.log(result.valid); // true
|
|
200
|
+
console.log(result.data); // validated data
|
|
632
201
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
[
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
]
|
|
641
|
-
*/
|
|
202
|
+
// ❌ validation failed
|
|
203
|
+
const bad = validate(userSchema, { username: 'ab', email: 'not-email' });
|
|
204
|
+
console.log(bad.errors);
|
|
205
|
+
// [
|
|
206
|
+
// { path: 'username', message: 'username must be at least 3 characters' },
|
|
207
|
+
// { path: 'email', message: 'email must be a valid email address' }
|
|
208
|
+
// ]
|
|
642
209
|
```
|
|
643
210
|
|
|
644
|
-
###
|
|
645
|
-
|
|
646
|
-
**重要**: TypeScript 中**必须**使用 `dsl()` 包裹字符串以获得类型提示(v1.0.6+ 移除了全局 String 类型扩展以避免类型污染):
|
|
211
|
+
### 2. Async validation + Express integration
|
|
647
212
|
|
|
648
213
|
```typescript
|
|
649
214
|
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
|
|
650
215
|
|
|
651
|
-
// ✅ 正确:使用 dsl() 包裹字符串获得完整类型提示
|
|
652
|
-
const userSchema = dsl({
|
|
653
|
-
username: dsl('string:3-32!')
|
|
654
|
-
.pattern(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字和下划线')
|
|
655
|
-
.label('用户名'),
|
|
656
|
-
|
|
657
|
-
email: dsl('email!')
|
|
658
|
-
.label('邮箱地址')
|
|
659
|
-
.messages({ required: '邮箱必填' }),
|
|
660
|
-
|
|
661
|
-
age: dsl('number:18-100')
|
|
662
|
-
.label('年龄')
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
// 异步验证(推荐)
|
|
666
|
-
try {
|
|
667
|
-
const validData = await validateAsync(userSchema, {
|
|
668
|
-
username: 'testuser',
|
|
669
|
-
email: 'test@example.com',
|
|
670
|
-
age: 25
|
|
671
|
-
});
|
|
672
|
-
console.log('验证通过:', validData);
|
|
673
|
-
} catch (error) {
|
|
674
|
-
if (error instanceof ValidationError) {
|
|
675
|
-
error.errors.forEach(err => {
|
|
676
|
-
console.log(`${err.path}: ${err.message}`);
|
|
677
|
-
});
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
**为什么必须用 `dsl()` 包裹?**
|
|
683
|
-
- ✅ 完整的类型推导和 IDE 自动提示
|
|
684
|
-
- ✅ 避免污染原生 String 类型(v1.0.6+ 重要改进)
|
|
685
|
-
- ✅ 保证 `trim()`、`toLowerCase()` 等原生方法类型正确
|
|
686
|
-
- ✅ 更好的开发体验和类型安全
|
|
687
|
-
|
|
688
|
-
**JavaScript 用户不受影响**:在 JavaScript 中仍然可以直接使用 `'email!'.label('邮箱')` 语法。
|
|
689
|
-
|
|
690
|
-
**详细说明**: 请查看 [TypeScript 使用指南](./docs/typescript-guide.md)
|
|
691
|
-
|
|
692
|
-
### 2. Express 集成 - 自动错误处理
|
|
693
|
-
|
|
694
|
-
```javascript
|
|
695
|
-
const { dsl, validateAsync, ValidationError } = require('schema-dsl');
|
|
696
|
-
|
|
697
|
-
// 定义验证 Schema
|
|
698
216
|
const createUserSchema = dsl({
|
|
699
217
|
username: 'string:3-32!',
|
|
700
|
-
email:
|
|
218
|
+
email: 'email!',
|
|
701
219
|
password: 'string:8-32!'
|
|
702
220
|
});
|
|
703
221
|
|
|
704
|
-
// 在路由中使用
|
|
705
222
|
app.post('/api/users', async (req, res, next) => {
|
|
706
223
|
try {
|
|
707
|
-
//
|
|
224
|
+
// throws ValidationError automatically on failure
|
|
708
225
|
const validData = await validateAsync(createUserSchema, req.body);
|
|
709
|
-
|
|
710
226
|
const user = await db.users.create(validData);
|
|
711
227
|
res.json({ success: true, data: user });
|
|
712
228
|
} catch (error) {
|
|
713
|
-
// ValidationError 会被全局错误处理器捕获
|
|
714
229
|
next(error);
|
|
715
230
|
}
|
|
716
231
|
});
|
|
717
232
|
|
|
718
|
-
//
|
|
233
|
+
// global error handler
|
|
719
234
|
app.use((error, req, res, next) => {
|
|
720
235
|
if (error instanceof ValidationError) {
|
|
721
|
-
|
|
722
|
-
return res.status(400).json({
|
|
723
|
-
success: false,
|
|
724
|
-
message: 'Validation failed',
|
|
725
|
-
errors: error.errors // 详细的字段错误列表
|
|
726
|
-
});
|
|
236
|
+
return res.status(400).json({ success: false, errors: error.errors });
|
|
727
237
|
}
|
|
728
|
-
|
|
729
|
-
// 其他错误继续传递
|
|
730
238
|
next(error);
|
|
731
239
|
});
|
|
732
240
|
```
|
|
733
241
|
|
|
734
|
-
### Schema
|
|
242
|
+
### 3. Schema reuse (create / update / public)
|
|
735
243
|
|
|
736
|
-
```
|
|
737
|
-
|
|
244
|
+
```typescript
|
|
245
|
+
import { dsl, SchemaUtils } from 'schema-dsl';
|
|
738
246
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
age: 'number:18-120',
|
|
746
|
-
role: 'admin|user|guest',
|
|
747
|
-
createdAt: 'datetime!',
|
|
748
|
-
updatedAt: 'datetime!'
|
|
247
|
+
const userSchema = dsl({
|
|
248
|
+
id: 'uuid!',
|
|
249
|
+
username: 'string:3-32!',
|
|
250
|
+
email: 'email!',
|
|
251
|
+
password: 'string:8-64!',
|
|
252
|
+
createdAt: 'string!'
|
|
749
253
|
});
|
|
750
254
|
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
const createSchema = SchemaUtils.omit(fullUserSchema, ['id', 'createdAt', 'updatedAt']);
|
|
754
|
-
|
|
755
|
-
// 场景2: 查询用户 - 隐藏敏感字段
|
|
756
|
-
// 使用 omit() 排除敏感信息
|
|
757
|
-
const publicSchema = SchemaUtils.omit(fullUserSchema, ['password']);
|
|
758
|
-
|
|
759
|
-
// 场景3: 更新用户 - 只允许更新部分字段
|
|
760
|
-
// 使用 pick() 选择字段 + partial() 变为可选
|
|
761
|
-
const updateSchema = SchemaUtils
|
|
762
|
-
.pick(fullUserSchema, ['username', 'email', 'age'])
|
|
763
|
-
.partial(); // 所有字段变为可选
|
|
764
|
-
|
|
765
|
-
// 场景4: 注册接口 - 扩展额外字段
|
|
766
|
-
// 使用 pick() + extend() 添加新字段
|
|
767
|
-
const registerSchema = SchemaUtils
|
|
768
|
-
.pick(fullUserSchema, ['username', 'email', 'password'])
|
|
769
|
-
.extend({
|
|
770
|
-
captcha: 'string:4-6!',
|
|
771
|
-
agree: 'boolean!'
|
|
772
|
-
});
|
|
773
|
-
|
|
774
|
-
// 💡 快速记忆:
|
|
775
|
-
// omit - 排除字段(隐藏敏感信息)
|
|
776
|
-
// pick - 挑选字段(限制可修改字段)
|
|
777
|
-
// extend - 扩展字段(添加新字段)
|
|
778
|
-
// partial - 变为可选(用于更新接口)
|
|
779
|
-
```
|
|
255
|
+
// create endpoint: remove server-generated fields
|
|
256
|
+
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
|
|
780
257
|
|
|
781
|
-
|
|
258
|
+
// update endpoint: pick editable fields, all optional
|
|
259
|
+
const updateSchema = SchemaUtils.partial(
|
|
260
|
+
SchemaUtils.pick(userSchema, ['username', 'email'])
|
|
261
|
+
);
|
|
782
262
|
|
|
783
|
-
|
|
263
|
+
// public response: hide sensitive fields
|
|
264
|
+
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
|
|
265
|
+
```
|
|
784
266
|
|
|
785
|
-
|
|
786
|
-
const { dsl } = require('schema-dsl');
|
|
787
|
-
|
|
788
|
-
// 场景1:年龄限制 - 未成年不能注册
|
|
789
|
-
// ❌ 传统做法:先验证,再判断,写两次
|
|
790
|
-
const result = validate(schema, userData);
|
|
791
|
-
if (!result.valid) return;
|
|
792
|
-
if (userData.age < 18) {
|
|
793
|
-
throw new Error('未成年用户不能注册');
|
|
794
|
-
}
|
|
267
|
+
### 4. Database schema export
|
|
795
268
|
|
|
796
|
-
|
|
797
|
-
dsl
|
|
798
|
-
.message('未成年用户不能注册')
|
|
799
|
-
.assert(userData); // 失败自动抛错
|
|
269
|
+
```typescript
|
|
270
|
+
import { dsl, exporters } from 'schema-dsl';
|
|
800
271
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
272
|
+
const productSchema = dsl({
|
|
273
|
+
name: 'string:1-100!',
|
|
274
|
+
price: 'number:>0!',
|
|
275
|
+
stock: 'integer:0-!',
|
|
276
|
+
category: 'string!',
|
|
277
|
+
createdAt: 'datetime!'
|
|
278
|
+
});
|
|
806
279
|
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
280
|
+
// MongoDB $jsonSchema (for db.createCollection() document validation; not a Mongoose model schema)
|
|
281
|
+
const mongoSchema = new exporters.MongoDBExporter().export(productSchema);
|
|
282
|
+
/*
|
|
283
|
+
{
|
|
284
|
+
$jsonSchema: {
|
|
285
|
+
bsonType: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
name: { bsonType: 'string', minLength: 1, maxLength: 100 },
|
|
288
|
+
price: { bsonType: 'double', minimum: 0 },
|
|
289
|
+
stock: { bsonType: 'int', minimum: 0 },
|
|
290
|
+
category: { bsonType: 'string' },
|
|
291
|
+
createdAt: { bsonType: 'string' }
|
|
292
|
+
},
|
|
293
|
+
required: ['name', 'price', 'stock', 'category', 'createdAt']
|
|
294
|
+
}
|
|
812
295
|
}
|
|
296
|
+
*/
|
|
813
297
|
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
298
|
+
// MySQL DDL
|
|
299
|
+
const mysqlDDL = new exporters.MySQLExporter().export('products', productSchema);
|
|
300
|
+
/*
|
|
301
|
+
CREATE TABLE `products` (
|
|
302
|
+
`name` VARCHAR(100) NOT NULL,
|
|
303
|
+
`price` DECIMAL(10, 2) NOT NULL,
|
|
304
|
+
`stock` INT NOT NULL,
|
|
305
|
+
`category` VARCHAR(255) NOT NULL,
|
|
306
|
+
`createdAt` DATETIME NOT NULL
|
|
307
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
308
|
+
*/
|
|
817
309
|
|
|
818
|
-
//
|
|
819
|
-
const
|
|
820
|
-
!dsl.if(d => d.age < 18).message('未成年').check(u)
|
|
821
|
-
);
|
|
310
|
+
// Markdown field documentation
|
|
311
|
+
const markdown = exporters.MarkdownExporter.export(productSchema, { title: 'Product Field Reference' });
|
|
822
312
|
```
|
|
823
313
|
|
|
824
|
-
|
|
314
|
+
---
|
|
825
315
|
|
|
826
|
-
|
|
827
|
-
|------|-----------|---------|------|
|
|
828
|
-
| **`.validate()`** | 需要知道错误详情 | `{ valid, errors, data }` | 表单验证 |
|
|
829
|
-
| **`.validateAsync()`** | async/await 场景 | Promise(失败抛错) | Express 中间件 |
|
|
830
|
-
| **`.assert()`** | 快速失败,不想写 if | 失败直接抛错 | 函数入口检查 |
|
|
831
|
-
| **`.check()`** | 只需要判断真假 | `true/false` | 数据过滤 |
|
|
316
|
+
## 🗒️ Feature Overview
|
|
832
317
|
|
|
833
|
-
|
|
318
|
+
### Common use cases
|
|
834
319
|
|
|
835
|
-
|
|
320
|
+
| Use case | API | Docs |
|
|
321
|
+
|----------|-----|------|
|
|
322
|
+
| API parameter validation | `validateAsync` + `ValidationError` | [Async Validation](https://vextjs.github.io/schema-dsl/validate-async) |
|
|
323
|
+
| Form / script validation | `validate()` | [Validation Guide](https://vextjs.github.io/schema-dsl/validation-guide) |
|
|
324
|
+
| Batch data validation | `SchemaUtils.validateBatch()` | [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) |
|
|
325
|
+
| create / update derivation | `pick / omit / partial` | [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) |
|
|
326
|
+
| Database table creation | `MongoDBExporter / MySQLExporter` | [Export Guide](https://vextjs.github.io/schema-dsl/export-guide) |
|
|
327
|
+
| Field documentation | `MarkdownExporter` | [Export Guide](https://vextjs.github.io/schema-dsl/export-guide) |
|
|
328
|
+
| Multilingual API errors | `I18nError` | [Error Handling](https://vextjs.github.io/schema-dsl/error-handling) |
|
|
329
|
+
| Conditional / dynamic rules | `dsl.if()` / `dsl.match()` | [Conditional API](https://vextjs.github.io/schema-dsl/conditional-api) |
|
|
330
|
+
| Custom type extensions | `PluginManager` | [Plugin System](https://vextjs.github.io/schema-dsl/plugin-system) |
|
|
836
331
|
|
|
837
|
-
|
|
838
|
-
// 使用 .validate() 获取错误详情
|
|
839
|
-
const result = dsl.if(d => d.age < 18)
|
|
840
|
-
.message('未成年用户不能注册')
|
|
841
|
-
.validate(formData);
|
|
332
|
+
---
|
|
842
333
|
|
|
843
|
-
|
|
844
|
-
showError(result.errors[0].message); // 显示给用户
|
|
845
|
-
}
|
|
846
|
-
```
|
|
334
|
+
## 📖 DSL Syntax Reference
|
|
847
335
|
|
|
848
|
-
|
|
336
|
+
### Basic types
|
|
849
337
|
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
338
|
+
```typescript
|
|
339
|
+
dsl({
|
|
340
|
+
// string
|
|
341
|
+
name: 'string!', // required
|
|
342
|
+
code: 'string:6', // exact length 6
|
|
343
|
+
bio: 'string:-500', // max length 500
|
|
344
|
+
username: 'string:3-32', // length range 3–32
|
|
345
|
+
|
|
346
|
+
// number
|
|
347
|
+
age: 'number:18-120', // range 18–120
|
|
348
|
+
score: 'integer:0-100', // integer 0–100
|
|
349
|
+
price: 'number:>0', // strictly greater than 0
|
|
350
|
+
level: 'number:>=1', // greater than or equal to 1
|
|
351
|
+
|
|
352
|
+
// enum
|
|
353
|
+
status: 'active|inactive|pending', // string enum
|
|
354
|
+
tier: 'enum:number:1|2|3', // numeric enum
|
|
355
|
+
|
|
356
|
+
// array
|
|
357
|
+
tags: 'array<string>', // string array
|
|
358
|
+
items: 'array:1-10<number>', // 1–10 numeric elements
|
|
359
|
+
|
|
360
|
+
// boolean
|
|
361
|
+
active: 'boolean!',
|
|
362
|
+
|
|
363
|
+
// union type
|
|
364
|
+
contact: 'types:email|phone!', // email or phone, required
|
|
365
|
+
price2: 'types:number:0-|string', // number or string
|
|
366
|
+
})
|
|
865
367
|
```
|
|
866
368
|
|
|
867
|
-
|
|
369
|
+
### Built-in formats
|
|
868
370
|
|
|
869
|
-
```
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
//
|
|
878
|
-
|
|
879
|
-
}
|
|
371
|
+
```typescript
|
|
372
|
+
dsl({
|
|
373
|
+
email: 'email!', // email address
|
|
374
|
+
website: 'url!', // URL
|
|
375
|
+
birthday: 'date!', // YYYY-MM-DD
|
|
376
|
+
createdAt: 'datetime!', // ISO 8601
|
|
377
|
+
userId: 'uuid!', // UUID
|
|
378
|
+
phone: 'phone:cn!', // Chinese mobile number
|
|
379
|
+
idCard: 'idCard:cn!', // Chinese national ID
|
|
380
|
+
slug: 'slug:3-100!', // URL-friendly string
|
|
381
|
+
})
|
|
880
382
|
```
|
|
881
383
|
|
|
882
|
-
|
|
384
|
+
### Fluent chain API (recommended for TypeScript)
|
|
883
385
|
|
|
884
|
-
```
|
|
885
|
-
|
|
886
|
-
const canRegister = dsl.if(d => d.age < 18)
|
|
887
|
-
.or(d => d.status === 'blocked')
|
|
888
|
-
.message('不允许注册');
|
|
889
|
-
|
|
890
|
-
// 过滤出可以注册的用户
|
|
891
|
-
const validUsers = users.filter(u => !canRegister.check(u));
|
|
892
|
-
|
|
893
|
-
// 统计未成年用户数量
|
|
894
|
-
const minorCount = users.filter(u =>
|
|
895
|
-
dsl.if(d => d.age < 18).message('未成年').check(u)
|
|
896
|
-
).length;
|
|
897
|
-
```
|
|
386
|
+
```typescript
|
|
387
|
+
import { dsl } from 'schema-dsl';
|
|
898
388
|
|
|
899
|
-
|
|
389
|
+
const schema = dsl({
|
|
390
|
+
username: dsl('string:3-32!')
|
|
391
|
+
.username()
|
|
392
|
+
.label('username')
|
|
393
|
+
.messages({ required: 'Username is required' }),
|
|
900
394
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const r1 = ageValidator.validate({ age: 16 }); // 同步,返回详情
|
|
908
|
-
const r2 = await ageValidator.validateAsync(data); // 异步,失败抛错
|
|
909
|
-
const r3 = ageValidator.check({ age: 20 }); // 快速判断
|
|
395
|
+
email: dsl('email!').label('email address'),
|
|
396
|
+
|
|
397
|
+
phone: dsl('string:11!')
|
|
398
|
+
.pattern(/^1[3-9]\d{9}$/)
|
|
399
|
+
.label('phone number'),
|
|
400
|
+
});
|
|
910
401
|
```
|
|
911
402
|
|
|
912
|
-
|
|
403
|
+
### Conditional validation
|
|
913
404
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
405
|
+
```typescript
|
|
406
|
+
// dsl.match — route to different rules based on a field value
|
|
407
|
+
const contactSchema = dsl({
|
|
408
|
+
type: 'email|phone|wechat',
|
|
409
|
+
contact: dsl.match('type', {
|
|
410
|
+
email: 'email!',
|
|
411
|
+
phone: 'string:11!',
|
|
412
|
+
wechat: 'string:6-20!',
|
|
413
|
+
})
|
|
414
|
+
});
|
|
918
415
|
|
|
919
|
-
|
|
416
|
+
// dsl.if — simple conditional branch
|
|
417
|
+
const orderSchema = dsl({
|
|
418
|
+
isVip: 'boolean!',
|
|
419
|
+
discount: dsl.if('isVip', 'number:10-50!', 'number:0-10')
|
|
420
|
+
});
|
|
920
421
|
|
|
921
|
-
|
|
422
|
+
// dsl.if chain assertion
|
|
423
|
+
dsl.if(d => !d.account)
|
|
424
|
+
.message('Account not found')
|
|
425
|
+
.and(d => d.account.balance < amount)
|
|
426
|
+
.message('Insufficient balance')
|
|
427
|
+
.assert(data);
|
|
428
|
+
```
|
|
922
429
|
|
|
923
|
-
|
|
430
|
+
---
|
|
924
431
|
|
|
925
|
-
|
|
432
|
+
## 🌍 Internationalization
|
|
926
433
|
|
|
927
|
-
|
|
434
|
+
```typescript
|
|
435
|
+
import { dsl, validate, Locale, I18nError } from 'schema-dsl';
|
|
928
436
|
|
|
929
|
-
|
|
930
|
-
const
|
|
437
|
+
// built-in locales: zh-CN / en-US / ja-JP / es-ES / fr-FR (auto-loaded, no configuration needed)
|
|
438
|
+
const result = validate(schema, data, { locale: 'en-US' });
|
|
439
|
+
// error messages automatically use the specified locale
|
|
931
440
|
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
441
|
+
// register a custom locale
|
|
442
|
+
Locale.addLocale('zh-CN', {
|
|
443
|
+
'user.notFound': 'User not found',
|
|
444
|
+
'user.forbidden': { code: 40003, message: 'Access forbidden' },
|
|
936
445
|
});
|
|
937
446
|
|
|
938
|
-
//
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
{ username: 'user3', email: 'user3@example.com', age: 30 }
|
|
943
|
-
];
|
|
944
|
-
|
|
945
|
-
// 批量验证
|
|
946
|
-
const validator = new Validator();
|
|
947
|
-
const result = SchemaUtils.validateBatch(userSchema, users, validator);
|
|
447
|
+
// throw i18n business errors
|
|
448
|
+
I18nError.assert(user, 'user.notFound'); // auto-throw when user is falsy
|
|
449
|
+
I18nError.throw('user.forbidden', {}, 403); // throw directly
|
|
450
|
+
I18nError.assert(ok, 'user.notFound', {}, 404, locale); // specify locale at runtime
|
|
948
451
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
452
|
+
// errors carry a numeric code; frontend can branch on it
|
|
453
|
+
try {
|
|
454
|
+
await api.getUser(id);
|
|
455
|
+
} catch (error) {
|
|
456
|
+
switch (error.code) {
|
|
457
|
+
case 40003: showForbiddenPage(); break;
|
|
458
|
+
}
|
|
956
459
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
console.log(result.errors);
|
|
960
|
-
/*
|
|
961
|
-
[
|
|
962
|
-
{ index: 1, errors: [
|
|
963
|
-
{ path: 'username', message: '...' },
|
|
964
|
-
{ path: 'age', message: '...' }
|
|
965
|
-
]}
|
|
966
|
-
]
|
|
967
|
-
*/
|
|
968
|
-
|
|
969
|
-
// 只获取有效数据
|
|
970
|
-
const validUsers = result.results
|
|
971
|
-
.filter(r => r.valid)
|
|
972
|
-
.map(r => r.data);
|
|
973
|
-
```
|
|
974
|
-
|
|
975
|
-
📖 **详细文档**: [SchemaUtils.validateBatch](./docs/schema-utils.md#validatebatch---批量验证)
|
|
976
|
-
|
|
977
|
-
---
|
|
978
|
-
|
|
979
|
-
### 嵌套对象验证
|
|
980
|
-
|
|
981
|
-
**场景**: 验证复杂的用户资料
|
|
982
|
-
|
|
983
|
-
```javascript
|
|
984
|
-
const { dsl, validate } = require('schema-dsl');
|
|
985
|
-
|
|
986
|
-
const profileSchema = dsl({
|
|
987
|
-
user: {
|
|
988
|
-
basic: {
|
|
989
|
-
name: 'string:2-50!',
|
|
990
|
-
email: 'email!',
|
|
991
|
-
phone: 'string:11!'
|
|
992
|
-
},
|
|
993
|
-
address: {
|
|
994
|
-
country: 'string!',
|
|
995
|
-
city: 'string!',
|
|
996
|
-
street: 'string',
|
|
997
|
-
zipCode: 'string:6'
|
|
998
|
-
},
|
|
999
|
-
preferences: {
|
|
1000
|
-
language: 'zh-CN|en-US|ja-JP',
|
|
1001
|
-
timezone: 'string',
|
|
1002
|
-
notifications: {
|
|
1003
|
-
email: 'boolean',
|
|
1004
|
-
sms: 'boolean',
|
|
1005
|
-
push: 'boolean'
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
},
|
|
1009
|
-
metadata: {
|
|
1010
|
-
source: 'web|mobile|api',
|
|
1011
|
-
createdAt: 'datetime!',
|
|
1012
|
-
tags: 'array:0-10<string>'
|
|
1013
|
-
}
|
|
1014
|
-
});
|
|
1015
|
-
|
|
1016
|
-
const result = validate(profileSchema, {
|
|
1017
|
-
user: {
|
|
1018
|
-
basic: {
|
|
1019
|
-
name: 'John Doe',
|
|
1020
|
-
email: 'john@example.com',
|
|
1021
|
-
phone: '13800138000'
|
|
1022
|
-
},
|
|
1023
|
-
address: {
|
|
1024
|
-
country: 'China',
|
|
1025
|
-
city: 'Beijing',
|
|
1026
|
-
zipCode: '100000'
|
|
1027
|
-
},
|
|
1028
|
-
preferences: {
|
|
1029
|
-
language: 'zh-CN',
|
|
1030
|
-
timezone: 'Asia/Shanghai',
|
|
1031
|
-
notifications: {
|
|
1032
|
-
email: true,
|
|
1033
|
-
sms: false,
|
|
1034
|
-
push: true
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
},
|
|
1038
|
-
metadata: {
|
|
1039
|
-
source: 'web',
|
|
1040
|
-
createdAt: new Date().toISOString(),
|
|
1041
|
-
tags: ['vip', 'active']
|
|
1042
|
-
}
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
console.log(result.valid); // true
|
|
1046
|
-
```
|
|
1047
|
-
|
|
1048
|
-
📖 **详细文档**: [嵌套对象验证](./docs/validation-guide.md#嵌套对象验证)
|
|
460
|
+
```
|
|
1049
461
|
|
|
1050
462
|
---
|
|
1051
463
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
**场景**: 验证订单商品列表
|
|
464
|
+
## 🔌 Plugin System
|
|
1055
465
|
|
|
1056
|
-
```
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
// 方式 1: 简单数组
|
|
1060
|
-
const schema1 = dsl({
|
|
1061
|
-
tags: 'array:1-10<string>', // 1-10 个字符串
|
|
1062
|
-
scores: 'array<number:0-100>' // 数字数组,每个 0-100
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
// 方式 2: 对象数组
|
|
1066
|
-
const orderSchema = dsl({
|
|
1067
|
-
orderId: 'string!',
|
|
1068
|
-
items: 'array:1-100!', // 必填,1-100 个商品
|
|
1069
|
-
// 注意:数组元素的验证需要单独定义
|
|
1070
|
-
_itemSchema: { // 约定:用 _ 前缀标记辅助 schema
|
|
1071
|
-
productId: 'string!',
|
|
1072
|
-
name: 'string:1-100!',
|
|
1073
|
-
quantity: 'integer:1-999!',
|
|
1074
|
-
price: 'number:>0!'
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
// 验证订单
|
|
1079
|
-
const order = {
|
|
1080
|
-
orderId: 'ORD-12345',
|
|
1081
|
-
items: [
|
|
1082
|
-
{ productId: 'P001', name: 'iPhone', quantity: 2, price: 5999.00 },
|
|
1083
|
-
{ productId: 'P002', name: 'AirPods', quantity: 1, price: 1299.00 }
|
|
1084
|
-
]
|
|
1085
|
-
};
|
|
1086
|
-
|
|
1087
|
-
// 先验证订单结构
|
|
1088
|
-
const result1 = validate(orderSchema, order);
|
|
1089
|
-
if (!result1.valid) {
|
|
1090
|
-
console.log('订单结构错误:', result1.errors);
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// 再验证每个商品
|
|
1094
|
-
const itemSchema = dsl(orderSchema._itemSchema);
|
|
1095
|
-
for (const [index, item] of order.items.entries()) {
|
|
1096
|
-
const result = validate(itemSchema, item);
|
|
1097
|
-
if (!result.valid) {
|
|
1098
|
-
console.log(`商品 ${index} 错误:`, result.errors);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
```
|
|
1102
|
-
|
|
1103
|
-
📖 **详细文档**: [数组验证](./docs/validation-guide.md#数组验证)
|
|
1104
|
-
|
|
1105
|
-
---
|
|
1106
|
-
|
|
1107
|
-
### 正则验证
|
|
1108
|
-
|
|
1109
|
-
**场景**: 自定义格式验证
|
|
1110
|
-
|
|
1111
|
-
```javascript
|
|
1112
|
-
const { dsl, validate } = require('schema-dsl');
|
|
1113
|
-
|
|
1114
|
-
const schema = dsl({
|
|
1115
|
-
// 车牌号
|
|
1116
|
-
licensePlate: 'string!'
|
|
1117
|
-
.pattern(/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/)
|
|
1118
|
-
.label('车牌号')
|
|
1119
|
-
.messages({
|
|
1120
|
-
pattern: '请输入有效的中国车牌号'
|
|
1121
|
-
}),
|
|
1122
|
-
|
|
1123
|
-
// 身份证号(简化版)
|
|
1124
|
-
idCard: 'string:18!'
|
|
1125
|
-
.pattern(/^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dXx]$/)
|
|
1126
|
-
.label('身份证号')
|
|
1127
|
-
.messages({
|
|
1128
|
-
pattern: '请输入有效的 18 位身份证号'
|
|
1129
|
-
}),
|
|
1130
|
-
|
|
1131
|
-
// 自定义代码格式
|
|
1132
|
-
inviteCode: 'string:8!'
|
|
1133
|
-
.pattern(/^[A-Z]{3}\\d{5}$/)
|
|
1134
|
-
.label('邀请码')
|
|
1135
|
-
.messages({
|
|
1136
|
-
pattern: '邀请码格式:3个大写字母 + 5个数字(如 ABC12345)'
|
|
1137
|
-
})
|
|
1138
|
-
});
|
|
1139
|
-
|
|
1140
|
-
const result = validate(schema, {
|
|
1141
|
-
licensePlate: '京A12345',
|
|
1142
|
-
idCard: '110101199003071234',
|
|
1143
|
-
inviteCode: 'ABC12345'
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
console.log(result.valid); // true
|
|
1147
|
-
```
|
|
1148
|
-
|
|
1149
|
-
📖 **详细文档**: [正则验证](./docs/validation-guide.md#正则验证) | [String 扩展](./docs/string-extensions.md)
|
|
1150
|
-
|
|
1151
|
-
---
|
|
1152
|
-
|
|
1153
|
-
### 自定义验证器
|
|
1154
|
-
|
|
1155
|
-
**场景**: 业务逻辑验证
|
|
1156
|
-
|
|
1157
|
-
```javascript
|
|
1158
|
-
const { dsl, validate, validateAsync } = require('schema-dsl');
|
|
1159
|
-
|
|
1160
|
-
// 同步自定义验证
|
|
1161
|
-
const schema1 = dsl({
|
|
1162
|
-
username: 'string:3-32!'
|
|
1163
|
-
.custom((value) => {
|
|
1164
|
-
// 不能以数字开头
|
|
1165
|
-
if (/^\\d/.test(value)) {
|
|
1166
|
-
return '用户名不能以数字开头';
|
|
1167
|
-
}
|
|
1168
|
-
// 禁用敏感词
|
|
1169
|
-
const blocked = ['admin', 'root', 'system'];
|
|
1170
|
-
if (blocked.includes(value.toLowerCase())) {
|
|
1171
|
-
return '该用户名不可用';
|
|
1172
|
-
}
|
|
1173
|
-
})
|
|
1174
|
-
.label('用户名')
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
// 异步自定义验证(检查唯一性)
|
|
1178
|
-
const schema2 = dsl({
|
|
1179
|
-
email: 'email!'
|
|
1180
|
-
.custom(async (value) => {
|
|
1181
|
-
const exists = await checkEmailExists(value);
|
|
1182
|
-
if (exists) {
|
|
1183
|
-
return '该邮箱已被注册';
|
|
1184
|
-
}
|
|
1185
|
-
})
|
|
1186
|
-
.label('邮箱')
|
|
1187
|
-
});
|
|
1188
|
-
|
|
1189
|
-
// 多字段联合验证
|
|
1190
|
-
const schema3 = dsl({
|
|
1191
|
-
password: 'string:8-32!',
|
|
1192
|
-
confirmPassword: 'string:8-32!'
|
|
1193
|
-
})
|
|
1194
|
-
.custom((data) => {
|
|
1195
|
-
if (data.password !== data.confirmPassword) {
|
|
1196
|
-
return { confirmPassword: '两次密码不一致' };
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
// 使用
|
|
1201
|
-
const result = validate(schema1, { username: 'admin' });
|
|
1202
|
-
console.log(result.errors); // [{ path: 'username', message: '该用户名不可用' }]
|
|
1203
|
-
|
|
1204
|
-
// 模拟数据库查询
|
|
1205
|
-
async function checkEmailExists(email) {
|
|
1206
|
-
// 实际项目中查询数据库
|
|
1207
|
-
return email === 'exists@example.com';
|
|
1208
|
-
}
|
|
1209
|
-
```
|
|
1210
|
-
|
|
1211
|
-
📖 **详细文档**: [自定义验证器](./docs/custom-extensions-guide.md) | [验证指南](./docs/validation-guide.md)
|
|
1212
|
-
|
|
1213
|
-
---
|
|
1214
|
-
|
|
1215
|
-
### 框架集成
|
|
1216
|
-
|
|
1217
|
-
#### Koa 集成
|
|
1218
|
-
|
|
1219
|
-
```javascript
|
|
1220
|
-
const Koa = require('koa');
|
|
1221
|
-
const { dsl, validateAsync, ValidationError } = require('schema-dsl');
|
|
1222
|
-
|
|
1223
|
-
const app = new Koa();
|
|
1224
|
-
|
|
1225
|
-
const createUserSchema = dsl({
|
|
1226
|
-
username: 'string:3-32!',
|
|
1227
|
-
email: 'email!',
|
|
1228
|
-
password: 'string:8-32!'
|
|
1229
|
-
});
|
|
1230
|
-
|
|
1231
|
-
// 路由
|
|
1232
|
-
app.use(async (ctx) => {
|
|
1233
|
-
if (ctx.path === '/api/users' && ctx.method === 'POST') {
|
|
1234
|
-
try {
|
|
1235
|
-
// 验证请求体
|
|
1236
|
-
const validData = await validateAsync(createUserSchema, ctx.request.body);
|
|
1237
|
-
|
|
1238
|
-
// 业务逻辑
|
|
1239
|
-
const user = await createUser(validData);
|
|
1240
|
-
|
|
1241
|
-
ctx.body = { success: true, data: user };
|
|
1242
|
-
} catch (error) {
|
|
1243
|
-
if (error instanceof ValidationError) {
|
|
1244
|
-
ctx.status = 400;
|
|
1245
|
-
ctx.body = {
|
|
1246
|
-
success: false,
|
|
1247
|
-
message: 'Validation failed',
|
|
1248
|
-
errors: error.errors
|
|
1249
|
-
};
|
|
1250
|
-
} else {
|
|
1251
|
-
throw error;
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
app.listen(3000);
|
|
1258
|
-
|
|
1259
|
-
// 模拟用户创建函数
|
|
1260
|
-
async function createUser(data) {
|
|
1261
|
-
return { id: '123', ...data };
|
|
1262
|
-
}
|
|
1263
|
-
```
|
|
1264
|
-
|
|
1265
|
-
#### Fastify 集成
|
|
1266
|
-
|
|
1267
|
-
```javascript
|
|
1268
|
-
const fastify = require('fastify')();
|
|
1269
|
-
const { dsl, validateAsync, ValidationError } = require('schema-dsl');
|
|
1270
|
-
|
|
1271
|
-
const createUserSchema = dsl({
|
|
1272
|
-
username: 'string:3-32!',
|
|
1273
|
-
email: 'email!',
|
|
1274
|
-
password: 'string:8-32!'
|
|
1275
|
-
});
|
|
1276
|
-
|
|
1277
|
-
// 使用 preValidation hook
|
|
1278
|
-
fastify.post('/api/users', {
|
|
1279
|
-
preValidation: async (request, reply) => {
|
|
1280
|
-
try {
|
|
1281
|
-
request.body = await validateAsync(createUserSchema, request.body);
|
|
1282
|
-
} catch (error) {
|
|
1283
|
-
if (error instanceof ValidationError) {
|
|
1284
|
-
reply.code(400).send({
|
|
1285
|
-
success: false,
|
|
1286
|
-
message: 'Validation failed',
|
|
1287
|
-
errors: error.errors
|
|
1288
|
-
});
|
|
1289
|
-
} else {
|
|
1290
|
-
throw error;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
}, async (request, reply) => {
|
|
1295
|
-
// 验证通过,继续处理
|
|
1296
|
-
const user = await createUser(request.body);
|
|
1297
|
-
return { success: true, data: user };
|
|
1298
|
-
});
|
|
1299
|
-
|
|
1300
|
-
fastify.listen({ port: 3000 });
|
|
1301
|
-
|
|
1302
|
-
// 模拟用户创建函数
|
|
1303
|
-
async function createUser(data) {
|
|
1304
|
-
return { id: '123', ...data };
|
|
1305
|
-
}
|
|
1306
|
-
```
|
|
1307
|
-
|
|
1308
|
-
📖 **详细文档**: [中间件使用示例](./examples/middleware-usage.js) | [Express 集成](./examples/express-integration.js)
|
|
1309
|
-
|
|
1310
|
-
---
|
|
1311
|
-
|
|
1312
|
-
### 字段库复用
|
|
1313
|
-
|
|
1314
|
-
**场景**: 大型项目的字段管理
|
|
1315
|
-
|
|
1316
|
-
```javascript
|
|
1317
|
-
// fields/common.js - 定义字段库
|
|
1318
|
-
const { dsl } = require('schema-dsl');
|
|
1319
|
-
|
|
1320
|
-
module.exports = {
|
|
1321
|
-
// 基础字段
|
|
1322
|
-
email: () => 'email!'.label('邮箱地址'),
|
|
1323
|
-
phone: (country = 'cn') => 'string:11!'.phoneNumber(country).label('手机号'),
|
|
1324
|
-
username: () => 'string:3-32!'.username().label('用户名'),
|
|
1325
|
-
password: (strength = 'medium') => 'string:8-32!'.password(strength).label('密码'),
|
|
1326
|
-
|
|
1327
|
-
// 组合字段
|
|
1328
|
-
userAuth: () => ({
|
|
1329
|
-
username: 'string:3-32!'.username().label('用户名'),
|
|
1330
|
-
password: 'string:8-32!'.password('strong').label('密码')
|
|
1331
|
-
}),
|
|
1332
|
-
|
|
1333
|
-
userProfile: () => ({
|
|
1334
|
-
nickname: 'string:2-20!'.label('昵称'),
|
|
1335
|
-
bio: 'string:-500',
|
|
1336
|
-
avatar: 'url',
|
|
1337
|
-
birthday: 'date'
|
|
1338
|
-
}),
|
|
1339
|
-
|
|
1340
|
-
address: () => ({
|
|
1341
|
-
country: 'string!',
|
|
1342
|
-
province: 'string!',
|
|
1343
|
-
city: 'string!',
|
|
1344
|
-
district: 'string',
|
|
1345
|
-
street: 'string',
|
|
1346
|
-
zipCode: 'string:6'
|
|
1347
|
-
})
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
|
-
// schemas/user.js - 使用字段库
|
|
1351
|
-
const { dsl } = require('schema-dsl');
|
|
1352
|
-
const fields = require('../fields/common');
|
|
1353
|
-
|
|
1354
|
-
// 注册 Schema
|
|
1355
|
-
exports.registerSchema = dsl({
|
|
1356
|
-
...fields.userAuth(), // 展开用户认证字段
|
|
1357
|
-
email: fields.email(),
|
|
1358
|
-
phone: fields.phone(),
|
|
1359
|
-
agree: 'boolean!'
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
|
-
// 个人资料 Schema
|
|
1363
|
-
exports.profileSchema = dsl({
|
|
1364
|
-
...fields.userProfile(), // 展开用户资料字段
|
|
1365
|
-
...fields.address() // 展开地址字段
|
|
1366
|
-
});
|
|
1367
|
-
|
|
1368
|
-
// 登录 Schema
|
|
1369
|
-
exports.loginSchema = dsl({
|
|
1370
|
-
account: 'types:email|phone!', // 邮箱或手机号
|
|
1371
|
-
password: fields.password('strong')
|
|
1372
|
-
});
|
|
1373
|
-
```
|
|
1374
|
-
|
|
1375
|
-
📖 **详细文档**: [SchemaUtils 完整指南](./docs/schema-utils.md) | [字段库复用](./docs/schema-utils.md#字段库复用大型项目) | [最佳实践](./docs/best-practices.md)
|
|
1376
|
-
|
|
1377
|
-
---
|
|
1378
|
-
|
|
1379
|
-
## �📖 DSL 语法速查
|
|
1380
|
-
|
|
1381
|
-
### 基础类型
|
|
1382
|
-
|
|
1383
|
-
```javascript
|
|
1384
|
-
dsl({
|
|
1385
|
-
// 字符串
|
|
1386
|
-
name: 'string!', // 必填字符串
|
|
1387
|
-
code: 'string:6', // 🆕 v1.0.3: 精确长度 6(验证码)
|
|
1388
|
-
bio: 'string:-500', // 🆕 v1.0.3: 最大长度 500
|
|
1389
|
-
content: 'string:10-', // 🆕 v1.0.3: 最小长度 10
|
|
1390
|
-
username: 'string:3-32', // 长度范围 3-32
|
|
1391
|
-
|
|
1392
|
-
// 数字
|
|
1393
|
-
age: 'number!', // 必填数字
|
|
1394
|
-
price: 'number:0-9999.99', // 范围 0-9999.99
|
|
1395
|
-
score: 'integer:0-100', // 整数 0-100
|
|
1396
|
-
|
|
1397
|
-
// 🆕 v1.1.2: 数字比较运算符
|
|
1398
|
-
minAge: 'number:>=18', // 大于等于 18
|
|
1399
|
-
maxScore: 'number:<=100', // 小于等于 100
|
|
1400
|
-
positiveNum: 'number:>0', // 大于 0(不包括0)
|
|
1401
|
-
temperature: 'number:<100', // 小于 100(不包括100)
|
|
1402
|
-
exactValue: 'number:=50', // 等于 50
|
|
1403
|
-
negativeOk: 'number:>-10', // 支持负数:大于 -10
|
|
1404
|
-
priceLimit: 'number:<=99.99', // 支持小数:小于等于 99.99
|
|
1405
|
-
|
|
1406
|
-
// 💡 比较运算符 vs 范围语法
|
|
1407
|
-
// 'number:18-120' → 18 <= x <= 120 (包括边界)
|
|
1408
|
-
// 'number:>=18' → x >= 18 (语义更清晰)
|
|
1409
|
-
// 'number:>0' → x > 0 (不包括0,范围语法无法表达)
|
|
1410
|
-
// 'number:<100' → x < 100 (不包括100,范围语法无法表达)
|
|
1411
|
-
|
|
1412
|
-
// 布尔值
|
|
1413
|
-
active: 'boolean!',
|
|
1414
|
-
|
|
1415
|
-
// 枚举 - 限定值只能是特定选项之一
|
|
1416
|
-
status: 'active|inactive|pending', // ✅ 推荐:字符串枚举(简写)
|
|
1417
|
-
role: 'enum:admin|user|guest!', // 等价写法(完整形式)
|
|
1418
|
-
|
|
1419
|
-
isPublic: 'true|false', // ✅ 自动识别布尔值
|
|
1420
|
-
isVerified: 'enum:boolean:true|false', // 显式指定类型(更清晰)
|
|
1421
|
-
|
|
1422
|
-
priority: '1|2|3!', // ✅ 自动识别数字
|
|
1423
|
-
level: 'enum:number:1|2|3|4|5', // 显式指定(避免字符串"1"通过验证)
|
|
1424
|
-
grade: 'enum:integer:1|2|3', // 整数枚举(禁止小数)
|
|
1425
|
-
rating: '1.0|1.5|2.0|2.5', // 小数枚举
|
|
1426
|
-
|
|
1427
|
-
// 💡 使用建议:
|
|
1428
|
-
// - 默认用简写(active|inactive)- 最简洁
|
|
1429
|
-
// - 需要明确类型时用完整形式(enum:number:1|2|3)
|
|
1430
|
-
// - 值可能混淆时用完整形式(避免"1"和1混用)
|
|
1431
|
-
|
|
1432
|
-
// 数组
|
|
1433
|
-
tags: 'array<string>', // 字符串数组
|
|
1434
|
-
items: 'array:1-10<number>', // 1-10 个数字的数组
|
|
1435
|
-
|
|
1436
|
-
// 对象
|
|
1437
|
-
meta: 'object' // 任意对象
|
|
1438
|
-
})
|
|
1439
|
-
```
|
|
1440
|
-
|
|
1441
|
-
### 内置格式
|
|
1442
|
-
|
|
1443
|
-
```javascript
|
|
1444
|
-
dsl({
|
|
1445
|
-
// 邮箱
|
|
1446
|
-
email: 'email!',
|
|
1447
|
-
|
|
1448
|
-
// URL
|
|
1449
|
-
website: 'url!',
|
|
1450
|
-
homepage: 'https-url!', // 必须 HTTPS
|
|
1451
|
-
|
|
1452
|
-
// 日期时间
|
|
1453
|
-
birthday: 'date!', // YYYY-MM-DD
|
|
1454
|
-
createdAt: 'datetime!', // ISO 8601
|
|
1455
|
-
publishTime: 'timestamp!', // Unix 时间戳
|
|
1456
|
-
|
|
1457
|
-
// UUID
|
|
1458
|
-
userId: 'uuid!',
|
|
1459
|
-
requestId: 'uuid:v4!',
|
|
1460
|
-
|
|
1461
|
-
// 中国手机号
|
|
1462
|
-
phone: 'phone:cn!',
|
|
1463
|
-
|
|
1464
|
-
// 身份证号
|
|
1465
|
-
idCard: 'idCard:cn!',
|
|
1466
|
-
|
|
1467
|
-
// 信用卡
|
|
1468
|
-
cardNumber: 'creditCard:visa!',
|
|
1469
|
-
|
|
1470
|
-
// 邮政编码
|
|
1471
|
-
zipCode: 'postalCode:cn!',
|
|
1472
|
-
|
|
1473
|
-
// 车牌号
|
|
1474
|
-
plate: 'licensePlate:cn!',
|
|
1475
|
-
|
|
1476
|
-
// 护照号
|
|
1477
|
-
passport: 'passport:cn!'
|
|
1478
|
-
})
|
|
1479
|
-
```
|
|
1480
|
-
|
|
1481
|
-
### ✨ v1.0.3 新增类型
|
|
1482
|
-
|
|
1483
|
-
#### URL友好字符串(slug)- 用于博客和页面URL
|
|
1484
|
-
|
|
1485
|
-
```javascript
|
|
1486
|
-
dsl({
|
|
1487
|
-
// 博客文章URL: /posts/my-first-blog-post
|
|
1488
|
-
articleSlug: 'slug:3-100!',
|
|
1489
|
-
|
|
1490
|
-
// 分类URL: /category/javascript
|
|
1491
|
-
categorySlug: 'slug!',
|
|
1492
|
-
|
|
1493
|
-
// 链式调用
|
|
1494
|
-
pageSlug: 'string!'.slug()
|
|
1495
|
-
})
|
|
1496
|
-
|
|
1497
|
-
// ✅ 有效格式: my-blog-post, hello-world-123, article
|
|
1498
|
-
// ✅ 只能包含: 小写字母(a-z)、数字(0-9)、连字符(-)
|
|
1499
|
-
// ❌ 不能包含: 大写字母、下划线、空格、特殊字符
|
|
1500
|
-
|
|
1501
|
-
// 查看完整示例: node examples/slug.examples.js
|
|
1502
|
-
```
|
|
1503
|
-
|
|
1504
|
-
#### 字符串验证增强 - 解决常见验证场景
|
|
1505
|
-
|
|
1506
|
-
```javascript
|
|
1507
|
-
dsl({
|
|
1508
|
-
// 用户名 - 只允许字母和数字(不允许下划线)
|
|
1509
|
-
username: 'alphanum:3-20!', // 只允许 john123,不允许 john_123
|
|
1510
|
-
|
|
1511
|
-
// 邮箱 - 统一小写存储
|
|
1512
|
-
email: 'lower!', // 自动转小写
|
|
1513
|
-
|
|
1514
|
-
// 验证码 - 强制大写
|
|
1515
|
-
code: 'upper:6!', // 验证码大写: ABC123
|
|
1516
|
-
|
|
1517
|
-
// JSON配置 - 验证JSON字符串格式
|
|
1518
|
-
config: 'json!', // 存储JSON配置: {"theme":"dark"}
|
|
1519
|
-
|
|
1520
|
-
// 端口号 - 限制有效范围
|
|
1521
|
-
serverPort: 'port!', // 1-65535
|
|
1522
|
-
dbPort: 'port!' // 数据库端口
|
|
1523
|
-
})
|
|
1524
|
-
```
|
|
1525
|
-
|
|
1526
|
-
#### 约束语法优化 ⚠️ 破坏性变更
|
|
1527
|
-
|
|
1528
|
-
**v1.0.3 修复了单值语法**,使其更符合直觉:
|
|
1529
|
-
|
|
1530
|
-
```javascript
|
|
1531
|
-
dsl({
|
|
1532
|
-
code: 'string:6!', // 🆕 精确长度 6(之前是最大长度)
|
|
1533
|
-
bio: 'string:-500', // 🆕 最大长度 500(新语法)
|
|
1534
|
-
content: 'string:10-', // 🆕 最小长度 10(新语法)
|
|
1535
|
-
username: 'string:3-32' // 长度范围 3-32(不变)
|
|
1536
|
-
})
|
|
1537
|
-
```
|
|
1538
|
-
|
|
1539
|
-
**迁移指南**:
|
|
1540
|
-
- 如果你之前用 `'string:N'` 表示最大长度,请改为 `'string:-N'`
|
|
1541
|
-
- 如果你期望精确长度,无需修改(新版本行为正确)
|
|
1542
|
-
|
|
1543
|
-
**查看详细文档**:
|
|
1544
|
-
- [完整验证规则参考](./docs/validation-rules-v1.0.2.md)
|
|
1545
|
-
- [更新日志](./CHANGELOG.md)
|
|
1546
|
-
|
|
1547
|
-
### 高级特性
|
|
1548
|
-
|
|
1549
|
-
```javascript
|
|
1550
|
-
dsl({
|
|
1551
|
-
// 用户名(3-32字符,字母数字下划线)
|
|
1552
|
-
username: 'string:3-32!'.username(),
|
|
1553
|
-
|
|
1554
|
-
// 密码(8-32字符,必须包含大小写字母和数字)
|
|
1555
|
-
password: 'string:8-32!'.password(),
|
|
1556
|
-
|
|
1557
|
-
// 自定义正则
|
|
1558
|
-
code: 'string!'.pattern(/^[A-Z]{3}\d{3}$/),
|
|
1559
|
-
|
|
1560
|
-
// 自定义错误消息
|
|
1561
|
-
age: 'number:18-120!'.messages({
|
|
1562
|
-
'number.min': '年龄必须大于18岁',
|
|
1563
|
-
'number.max': '年龄不能超过120岁'
|
|
1564
|
-
}),
|
|
1565
|
-
|
|
1566
|
-
// 字段标签(用于多语言)
|
|
1567
|
-
email: 'email!'.label('用户邮箱'),
|
|
1568
|
-
|
|
1569
|
-
// 字段描述
|
|
1570
|
-
bio: 'string:10-500'.description('用户简介,10-500字符')
|
|
1571
|
-
})
|
|
1572
|
-
```
|
|
1573
|
-
|
|
1574
|
-
### 条件验证 - dsl.match 和 dsl.if
|
|
1575
|
-
|
|
1576
|
-
**根据其他字段的值动态决定验证规则**
|
|
1577
|
-
|
|
1578
|
-
```javascript
|
|
1579
|
-
const { dsl } = require('schema-dsl');
|
|
1580
|
-
|
|
1581
|
-
// 1. dsl.match - 根据字段值匹配不同规则(类似 switch-case)
|
|
1582
|
-
const contactSchema = dsl({
|
|
1583
|
-
contactType: 'email|phone|wechat',
|
|
1584
|
-
|
|
1585
|
-
// 根据 contactType 的值决定 contact 字段的验证规则
|
|
1586
|
-
contact: dsl.match('contactType', {
|
|
1587
|
-
email: 'email!', // contactType='email' 时验证邮箱格式
|
|
1588
|
-
phone: 'string:11!', // contactType='phone' 时验证11位手机号
|
|
1589
|
-
wechat: 'string:6-20!', // contactType='wechat' 时验证微信号
|
|
1590
|
-
_default: 'string' // 默认规则(可选)
|
|
1591
|
-
})
|
|
1592
|
-
});
|
|
1593
|
-
|
|
1594
|
-
// ✅ 验证通过
|
|
1595
|
-
validate(contactSchema, { contactType: 'email', contact: 'user@example.com' });
|
|
1596
|
-
validate(contactSchema, { contactType: 'phone', contact: '13800138000' });
|
|
1597
|
-
|
|
1598
|
-
// ❌ 验证失败
|
|
1599
|
-
validate(contactSchema, { contactType: 'email', contact: 'invalid' });
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
// 2. dsl.if - 简单条件分支(类似 if-else)
|
|
1603
|
-
const vipSchema = dsl({
|
|
1604
|
-
isVip: 'boolean!',
|
|
1605
|
-
|
|
1606
|
-
// 如果是 VIP,折扣必须在 10-50 之间;否则在 0-10 之间
|
|
1607
|
-
discount: dsl.if('isVip', 'number:10-50!', 'number:0-10')
|
|
1608
|
-
});
|
|
1609
|
-
|
|
1610
|
-
// ✅ VIP 用户
|
|
1611
|
-
validate(vipSchema, { isVip: true, discount: 30 });
|
|
1612
|
-
|
|
1613
|
-
// ❌ 非 VIP 用户折扣超过 10
|
|
1614
|
-
validate(vipSchema, { isVip: false, discount: 15 });
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
// 3. 实际应用场景:订单验证
|
|
1618
|
-
const orderSchema = dsl({
|
|
1619
|
-
paymentMethod: 'alipay|wechat|card|cod', // cod = 货到付款
|
|
1620
|
-
|
|
1621
|
-
// 根据支付方式决定支付信息格式
|
|
1622
|
-
paymentInfo: dsl.match('paymentMethod', {
|
|
1623
|
-
alipay: 'email!', // 支付宝:邮箱
|
|
1624
|
-
wechat: 'string:20-30', // 微信:支付串
|
|
1625
|
-
card: 'string:16-19', // 银行卡:卡号
|
|
1626
|
-
cod: 'string:0-0', // 货到付款:无需支付信息
|
|
1627
|
-
_default: 'string'
|
|
1628
|
-
}),
|
|
1629
|
-
|
|
1630
|
-
// 货到付款需要详细地址
|
|
1631
|
-
address: dsl.if('paymentMethod',
|
|
1632
|
-
'string:10-200!', // cod = 货到付款时地址必填
|
|
1633
|
-
'string:10-200' // 其他支付方式地址可选
|
|
1634
|
-
)
|
|
1635
|
-
});
|
|
1636
|
-
```
|
|
1637
|
-
|
|
1638
|
-
**💡 使用场景**:
|
|
1639
|
-
- ✅ 多种联系方式验证(邮箱/手机/微信)
|
|
1640
|
-
- ✅ VIP 和普通用户不同的折扣范围
|
|
1641
|
-
- ✅ 不同支付方式的支付信息格式
|
|
1642
|
-
- ✅ 根据用户类型决定必填字段
|
|
1643
|
-
|
|
1644
|
-
**查看完整示例**: [examples/dsl-match-example.js](./examples/dsl-match-example.js)
|
|
1645
|
-
|
|
1646
|
-
---
|
|
1647
|
-
|
|
1648
|
-
## 🔧 核心功能
|
|
1649
|
-
|
|
1650
|
-
### 1. String 扩展 - 链式调用
|
|
1651
|
-
|
|
1652
|
-
```javascript
|
|
1653
|
-
// 直接在字符串上调用验证方法
|
|
1654
|
-
const schema = dsl({
|
|
1655
|
-
username: 'string:3-32!'.username().label('用户名'),
|
|
1656
|
-
email: 'email!'.label('邮箱地址'),
|
|
1657
|
-
phone: 'string:11!'.phoneNumber('cn').label('手机号'),
|
|
1658
|
-
password: 'string:8-32!'.password().messages({
|
|
1659
|
-
'string.password': '密码必须包含大小写字母和数字'
|
|
1660
|
-
})
|
|
1661
|
-
});
|
|
1662
|
-
```
|
|
1663
|
-
|
|
1664
|
-
### 2. Schema 复用工具
|
|
1665
|
-
|
|
1666
|
-
```javascript
|
|
1667
|
-
const { SchemaUtils } = require('schema-dsl');
|
|
1668
|
-
|
|
1669
|
-
// 创建可复用的字段片段
|
|
1670
|
-
const fields = SchemaUtils.createLibrary({
|
|
1671
|
-
email: () => 'email!'.label('邮箱'),
|
|
1672
|
-
phone: () => 'string:11!'.phoneNumber('cn').label('手机号'),
|
|
1673
|
-
username: () => 'string:3-32!'.username().label('用户名')
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
|
-
// 在多个 Schema 中复用
|
|
1677
|
-
const loginSchema = dsl({
|
|
1678
|
-
account: fields.email(),
|
|
1679
|
-
password: 'string!'
|
|
1680
|
-
});
|
|
1681
|
-
|
|
1682
|
-
const registerSchema = dsl({
|
|
1683
|
-
username: fields.username(),
|
|
1684
|
-
email: fields.email(),
|
|
1685
|
-
phone: fields.phone(),
|
|
1686
|
-
password: 'string:8-32!'
|
|
1687
|
-
});
|
|
1688
|
-
|
|
1689
|
-
// Schema 组合操作
|
|
1690
|
-
const baseUser = dsl({ name: 'string!', email: 'email!' });
|
|
1691
|
-
|
|
1692
|
-
// 挑选字段
|
|
1693
|
-
const publicUser = SchemaUtils.pick(baseUser, ['name', 'email']);
|
|
1694
|
-
|
|
1695
|
-
// 排除字段
|
|
1696
|
-
const safeUser = SchemaUtils.omit(baseUser, ['password']);
|
|
1697
|
-
|
|
1698
|
-
// 扩展字段
|
|
1699
|
-
const adminUser = SchemaUtils.extend(baseUser, {
|
|
1700
|
-
role: 'admin|superadmin',
|
|
1701
|
-
permissions: 'array<string>'
|
|
1702
|
-
});
|
|
1703
|
-
|
|
1704
|
-
// 部分验证(移除必填限制)
|
|
1705
|
-
const updateUser = SchemaUtils.partial(baseUser, ['name', 'email']);
|
|
1706
|
-
```
|
|
1707
|
-
|
|
1708
|
-
### 3. 数据库 Schema 导出
|
|
1709
|
-
|
|
1710
|
-
**唯一支持数据库 Schema 自动生成的验证库!**
|
|
1711
|
-
|
|
1712
|
-
```javascript
|
|
1713
|
-
const { dsl, exporters } = require('schema-dsl');
|
|
1714
|
-
|
|
1715
|
-
const userSchema = dsl({
|
|
1716
|
-
username: 'string:3-32!',
|
|
1717
|
-
email: 'email!',
|
|
1718
|
-
age: 'number:18-120',
|
|
1719
|
-
tags: 'array<string>',
|
|
1720
|
-
createdAt: 'datetime!'
|
|
1721
|
-
});
|
|
1722
|
-
|
|
1723
|
-
// 导出为 MongoDB Schema
|
|
1724
|
-
const mongoSchema = exporters.MongoDBExporter.export(userSchema);
|
|
1725
|
-
console.log(mongoSchema);
|
|
1726
|
-
/*
|
|
1727
|
-
{
|
|
1728
|
-
username: { type: String, required: true, minlength: 3, maxlength: 32 },
|
|
1729
|
-
email: { type: String, required: true, match: /.../ },
|
|
1730
|
-
age: { type: Number, min: 18, max: 120 },
|
|
1731
|
-
tags: [{ type: String }],
|
|
1732
|
-
createdAt: { type: Date, required: true }
|
|
1733
|
-
}
|
|
1734
|
-
*/
|
|
1735
|
-
|
|
1736
|
-
// 导出为 MySQL DDL
|
|
1737
|
-
const mysqlExporter = new exporters.MySQLExporter();
|
|
1738
|
-
const mysqlDDL = mysqlExporter.export('users', userSchema);
|
|
1739
|
-
console.log(mysqlDDL);
|
|
1740
|
-
/*
|
|
1741
|
-
CREATE TABLE `users` (
|
|
1742
|
-
`username` VARCHAR(32) NOT NULL,
|
|
1743
|
-
`email` VARCHAR(255) NOT NULL,
|
|
1744
|
-
`age` INT,
|
|
1745
|
-
`tags` JSON,
|
|
1746
|
-
`createdAt` DATETIME NOT NULL
|
|
1747
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
1748
|
-
*/
|
|
1749
|
-
|
|
1750
|
-
// 导出为 PostgreSQL DDL
|
|
1751
|
-
const pgExporter = new exporters.PostgreSQLExporter();
|
|
1752
|
-
const pgDDL = pgExporter.export('users', userSchema);
|
|
1753
|
-
|
|
1754
|
-
// 导出为 Markdown 文档
|
|
1755
|
-
const markdown = exporters.MarkdownExporter.export(userSchema, {
|
|
1756
|
-
title: 'User API 文档'
|
|
1757
|
-
});
|
|
1758
|
-
```
|
|
1759
|
-
|
|
1760
|
-
### 4. 多语言支持
|
|
1761
|
-
|
|
1762
|
-
```javascript
|
|
1763
|
-
const { dsl, validate } = require('schema-dsl');
|
|
1764
|
-
const path = require('path');
|
|
1765
|
-
|
|
1766
|
-
// 方式 1: 从目录加载语言包(推荐)
|
|
1767
|
-
dsl.config({
|
|
1768
|
-
i18n: path.join(__dirname, 'i18n/dsl') // 直接传字符串路径
|
|
1769
|
-
});
|
|
1770
|
-
|
|
1771
|
-
// 方式 2: 直接传入语言包对象
|
|
1772
|
-
dsl.config({
|
|
1773
|
-
i18n: {
|
|
1774
|
-
'zh-CN': {
|
|
1775
|
-
'label.username': '用户名',
|
|
1776
|
-
'label.email': '邮箱地址',
|
|
1777
|
-
'required': '{{#label}}不能为空',
|
|
1778
|
-
'string.min': '{{#label}}长度不能少于{{#limit}}个字符'
|
|
1779
|
-
},
|
|
1780
|
-
'en-US': {
|
|
1781
|
-
'label.username': 'Username',
|
|
1782
|
-
'label.email': 'Email Address',
|
|
1783
|
-
'required': '{{#label}} is required',
|
|
1784
|
-
'string.min': '{{#label}} must be at least {{#limit}} characters'
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
});
|
|
1788
|
-
|
|
1789
|
-
// 使用 Label Key
|
|
1790
|
-
const schema = dsl({
|
|
1791
|
-
username: dsl('string:3-32!').label('label.username'),
|
|
1792
|
-
email: dsl('email!').label('label.email')
|
|
1793
|
-
});
|
|
1794
|
-
|
|
1795
|
-
// 验证时指定语言
|
|
1796
|
-
const result1 = validate(schema, data, { locale: 'zh-CN' });
|
|
1797
|
-
// 错误消息:用户名长度不能少于3个字符
|
|
1798
|
-
|
|
1799
|
-
const result2 = validate(schema, data, { locale: 'en-US' });
|
|
1800
|
-
// 错误消息:Username must be at least 3 characters
|
|
1801
|
-
```
|
|
1802
|
-
|
|
1803
|
-
### 5. 缓存配置 (v1.0.4+)
|
|
1804
|
-
|
|
1805
|
-
```javascript
|
|
1806
|
-
const { dsl, config } = require('schema-dsl');
|
|
1807
|
-
|
|
1808
|
-
// 配置缓存选项(推荐在使用 DSL 之前调用)
|
|
1809
|
-
config({
|
|
1810
|
-
cache: {
|
|
1811
|
-
maxSize: 1000, // 最大缓存条目数(默认:100)
|
|
1812
|
-
ttl: 7200000, // 缓存过期时间(毫秒,默认:3600000,即1小时)
|
|
1813
|
-
enabled: true, // 是否启用缓存(默认:true)
|
|
1814
|
-
statsEnabled: true // 是否启用统计(默认:true)
|
|
1815
|
-
}
|
|
1816
|
-
});
|
|
1817
|
-
|
|
1818
|
-
// 之后创建的 Schema 将使用新的缓存配置
|
|
1819
|
-
const schema = dsl({ name: 'string!' });
|
|
1820
|
-
|
|
1821
|
-
// 也可以在 Validator 创建后动态修改配置(向后兼容)
|
|
1822
|
-
const { getDefaultValidator } = require('schema-dsl');
|
|
1823
|
-
const validator = getDefaultValidator();
|
|
1824
|
-
console.log('当前缓存配置:', validator.cache.options);
|
|
1825
|
-
|
|
1826
|
-
// 动态修改
|
|
1827
|
-
config({
|
|
1828
|
-
cache: { maxSize: 5000 } // 只修改某个参数
|
|
1829
|
-
});
|
|
1830
|
-
```
|
|
1831
|
-
|
|
1832
|
-
**缓存说明**:
|
|
1833
|
-
- Schema 编译结果会被缓存以提高性能
|
|
1834
|
-
- 使用 LRU(最近最少使用)淘汰策略
|
|
1835
|
-
- 支持 TTL(生存时间)自动过期
|
|
1836
|
-
- 可通过 `validator.cache.getStats()` 查看缓存统计信息
|
|
1837
|
-
|
|
1838
|
-
### 6. 插件系统
|
|
1839
|
-
|
|
1840
|
-
```javascript
|
|
1841
|
-
const { PluginManager } = require('schema-dsl');
|
|
466
|
+
```typescript
|
|
467
|
+
import { PluginManager, Validator, dsl } from 'schema-dsl';
|
|
1842
468
|
|
|
1843
469
|
const pluginManager = new PluginManager();
|
|
1844
470
|
|
|
1845
|
-
//
|
|
1846
|
-
pluginManager.register({
|
|
1847
|
-
name: 'custom-validator',
|
|
1848
|
-
version: '1.0.0',
|
|
1849
|
-
|
|
1850
|
-
onBeforeValidate(schema, data) {
|
|
1851
|
-
// 验证前预处理
|
|
1852
|
-
console.log('验证开始');
|
|
1853
|
-
},
|
|
1854
|
-
|
|
1855
|
-
onAfterValidate(result) {
|
|
1856
|
-
// 验证后处理
|
|
1857
|
-
console.log('验证结束:', result.valid);
|
|
1858
|
-
return result;
|
|
1859
|
-
},
|
|
1860
|
-
|
|
1861
|
-
onError(error) {
|
|
1862
|
-
// 错误处理
|
|
1863
|
-
console.error('验证出错:', error);
|
|
1864
|
-
}
|
|
1865
|
-
});
|
|
1866
|
-
|
|
1867
|
-
// 注册自定义格式插件
|
|
471
|
+
// register a custom format plugin (must provide an install function)
|
|
1868
472
|
pluginManager.register({
|
|
1869
|
-
name: '
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
}
|
|
1876
|
-
'mac-address'
|
|
1877
|
-
validate: (
|
|
1878
|
-
message: '必须是有效的 MAC 地址'
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
});
|
|
1882
|
-
|
|
1883
|
-
// 使用自定义格式
|
|
1884
|
-
const schema = dsl({
|
|
1885
|
-
color: 'hex-color!',
|
|
1886
|
-
mac: 'mac-address!'
|
|
1887
|
-
});
|
|
1888
|
-
```
|
|
1889
|
-
|
|
1890
|
-
### 7. 错误处理
|
|
1891
|
-
|
|
1892
|
-
```javascript
|
|
1893
|
-
const { validate, ValidationError } = require('schema-dsl');
|
|
1894
|
-
|
|
1895
|
-
const schema = dsl({
|
|
1896
|
-
email: 'email!',
|
|
1897
|
-
age: 'number:18-120!'
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
|
-
const result = validate(schema, { email: 'invalid', age: 15 });
|
|
1901
|
-
|
|
1902
|
-
if (!result.valid) {
|
|
1903
|
-
console.log(result.errors);
|
|
1904
|
-
/*
|
|
1905
|
-
[
|
|
1906
|
-
{
|
|
1907
|
-
field: 'email',
|
|
1908
|
-
message: '邮箱格式不正确',
|
|
1909
|
-
keyword: 'format',
|
|
1910
|
-
params: { format: 'email' }
|
|
1911
|
-
},
|
|
1912
|
-
{
|
|
1913
|
-
field: 'age',
|
|
1914
|
-
message: '年龄必须大于等于18',
|
|
1915
|
-
keyword: 'minimum',
|
|
1916
|
-
params: { limit: 18 }
|
|
1917
|
-
}
|
|
1918
|
-
]
|
|
1919
|
-
*/
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// 使用 validateAsync + try-catch
|
|
1923
|
-
try {
|
|
1924
|
-
const data = await validateAsync(schema, invalidData);
|
|
1925
|
-
// 验证通过
|
|
1926
|
-
} catch (error) {
|
|
1927
|
-
if (error instanceof ValidationError) {
|
|
1928
|
-
console.log(error.errors); // 错误列表
|
|
1929
|
-
console.log(error.statusCode); // 400
|
|
1930
|
-
console.log(error.toJSON()); // 标准 JSON 格式
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
```
|
|
1934
|
-
|
|
1935
|
-
---
|
|
1936
|
-
|
|
1937
|
-
## ❓ 常见问题 FAQ
|
|
1938
|
-
|
|
1939
|
-
### Q1: 如何判断数据不能为空?(类似 `if(!data)`)
|
|
1940
|
-
|
|
1941
|
-
**方案1:使用必填标记**(推荐)
|
|
1942
|
-
```javascript
|
|
1943
|
-
const schema = dsl({
|
|
1944
|
-
username: 'string!', // 必填,不能为空
|
|
1945
|
-
email: 'email!'
|
|
1946
|
-
});
|
|
1947
|
-
```
|
|
1948
|
-
|
|
1949
|
-
**方案2:使用条件验证 + 抛错**
|
|
1950
|
-
```javascript
|
|
1951
|
-
// 验证失败自动抛错
|
|
1952
|
-
dsl.if(d => !d)
|
|
1953
|
-
.message('数据不能为空')
|
|
1954
|
-
.assert(data);
|
|
1955
|
-
```
|
|
1956
|
-
|
|
1957
|
-
**方案3:异步验证**
|
|
1958
|
-
```javascript
|
|
1959
|
-
// Express/Koa 推荐
|
|
1960
|
-
await dsl.if(d => !d)
|
|
1961
|
-
.message('数据不能为空')
|
|
1962
|
-
.validateAsync(data);
|
|
1963
|
-
```
|
|
1964
|
-
|
|
1965
|
-
---
|
|
1966
|
-
|
|
1967
|
-
### Q2: 如何判断数据是否是对象?(类似 `typeof data === 'object'`)
|
|
1968
|
-
|
|
1969
|
-
**方案1:使用内置 object 类型**(推荐)
|
|
1970
|
-
```javascript
|
|
1971
|
-
const schema = dsl({
|
|
1972
|
-
data: 'object!' // 必须是对象(排除 null 和 array)
|
|
1973
|
-
});
|
|
1974
|
-
|
|
1975
|
-
validate(schema, { data: { name: 'John' } }); // ✅ 通过
|
|
1976
|
-
validate(schema, { data: 'string' }); // ❌ 失败
|
|
1977
|
-
validate(schema, { data: [] }); // ❌ 失败
|
|
1978
|
-
```
|
|
1979
|
-
|
|
1980
|
-
**方案2:条件验证 + 抛错**
|
|
1981
|
-
```javascript
|
|
1982
|
-
dsl.if(d => typeof d !== 'object' || d === null || Array.isArray(d))
|
|
1983
|
-
.message('data 必须是一个对象')
|
|
1984
|
-
.assert(data);
|
|
1985
|
-
```
|
|
1986
|
-
|
|
1987
|
-
**方案3:带结构验证**
|
|
1988
|
-
```javascript
|
|
1989
|
-
const schema = dsl({
|
|
1990
|
-
data: {
|
|
1991
|
-
name: 'string!',
|
|
1992
|
-
age: 'integer!',
|
|
1993
|
-
email: 'email'
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
await validateAsync(schema, input); // 验证对象结构
|
|
1998
|
-
```
|
|
1999
|
-
|
|
2000
|
-
---
|
|
2001
|
-
|
|
2002
|
-
### Q3: 如何验证嵌套对象?
|
|
2003
|
-
|
|
2004
|
-
```javascript
|
|
2005
|
-
const schema = dsl({
|
|
2006
|
-
user: {
|
|
2007
|
-
profile: 'object!', // profile 必须是对象
|
|
2008
|
-
settings: {
|
|
2009
|
-
theme: 'string',
|
|
2010
|
-
notifications: 'object!' // 嵌套对象验证
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
});
|
|
2014
|
-
```
|
|
2015
|
-
|
|
2016
|
-
---
|
|
2017
|
-
|
|
2018
|
-
### Q4: 如何在 Express/Koa 中使用?
|
|
2019
|
-
|
|
2020
|
-
```javascript
|
|
2021
|
-
app.post('/api/user', async (req, res) => {
|
|
2022
|
-
try {
|
|
2023
|
-
// 1. 验证请求体是对象
|
|
2024
|
-
await dsl.if(d => typeof d !== 'object' || d === null)
|
|
2025
|
-
.message('请求体必须是对象')
|
|
2026
|
-
.validateAsync(req.body);
|
|
2027
|
-
|
|
2028
|
-
// 2. 验证字段
|
|
2029
|
-
const schema = dsl({
|
|
2030
|
-
username: 'string:3-32!',
|
|
2031
|
-
email: 'email!',
|
|
2032
|
-
password: 'string:8-!'
|
|
473
|
+
name: 'extra-formats',
|
|
474
|
+
install(core) {
|
|
475
|
+
const validator = core as Validator;
|
|
476
|
+
// register custom formats on the Validator instance via addFormat
|
|
477
|
+
validator.addFormat('hex-color', {
|
|
478
|
+
validate: (v: string) => /^#[0-9A-F]{6}$/i.test(v)
|
|
479
|
+
});
|
|
480
|
+
validator.addFormat('mac-address', {
|
|
481
|
+
validate: (v: string) => /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/i.test(v)
|
|
2033
482
|
});
|
|
2034
|
-
|
|
2035
|
-
const validData = await validateAsync(schema, req.body);
|
|
2036
|
-
|
|
2037
|
-
// 继续处理...
|
|
2038
|
-
res.json({ success: true, data: validData });
|
|
2039
|
-
} catch (error) {
|
|
2040
|
-
res.status(400).json({ error: error.message });
|
|
2041
483
|
}
|
|
2042
484
|
});
|
|
2043
|
-
```
|
|
2044
|
-
|
|
2045
|
-
---
|
|
2046
485
|
|
|
2047
|
-
|
|
486
|
+
// create a Validator and install plugins
|
|
487
|
+
const validator = new Validator();
|
|
488
|
+
pluginManager.install(validator);
|
|
2048
489
|
|
|
2049
|
-
|
|
2050
|
-
const schema = dsl({
|
|
2051
|
-
|
|
2052
|
-
.label('用户名')
|
|
2053
|
-
.messages({
|
|
2054
|
-
minLength: '用户名至少需要 {{#limit}} 个字符',
|
|
2055
|
-
required: '用户名不能为空'
|
|
2056
|
-
}),
|
|
2057
|
-
|
|
2058
|
-
email: dsl('email!')
|
|
2059
|
-
.label('邮箱地址')
|
|
2060
|
-
.messages({
|
|
2061
|
-
format: '请输入有效的邮箱地址',
|
|
2062
|
-
required: '邮箱不能为空'
|
|
2063
|
-
})
|
|
2064
|
-
});
|
|
490
|
+
// use the custom formats in a schema
|
|
491
|
+
const schema = dsl({ color: 'hex-color!', mac: 'mac-address' });
|
|
492
|
+
const result = validator.validate(schema, { color: '#FF5733', mac: '00:1A:2B:3C:4D:5E' });
|
|
2065
493
|
```
|
|
2066
494
|
|
|
2067
495
|
---
|
|
2068
496
|
|
|
2069
|
-
|
|
497
|
+
## 🔧 Core API Reference
|
|
2070
498
|
|
|
2071
|
-
|
|
|
2072
|
-
|
|
2073
|
-
| `
|
|
2074
|
-
| `
|
|
2075
|
-
| `
|
|
2076
|
-
| `
|
|
2077
|
-
| `
|
|
2078
|
-
| `
|
|
2079
|
-
| `if
|
|
2080
|
-
| `
|
|
499
|
+
| API | Purpose | Returns | Docs |
|
|
500
|
+
|-----|---------|---------|------|
|
|
501
|
+
| `dsl(schema)` | Create a schema | Schema object | [DSL Syntax](https://vextjs.github.io/schema-dsl/dsl-syntax) |
|
|
502
|
+
| `validate(schema, data)` | Synchronous validation | `{ valid, errors, data }` | [Validation Guide](https://vextjs.github.io/schema-dsl/validation-guide) |
|
|
503
|
+
| `validateAsync(schema, data)` | Asynchronous validation | Promise (throws on failure) | [Async Validation](https://vextjs.github.io/schema-dsl/validate-async) |
|
|
504
|
+
| `SchemaUtils.pick()` | Select fields | New schema | [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) |
|
|
505
|
+
| `SchemaUtils.omit()` | Exclude fields | New schema | [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) |
|
|
506
|
+
| `SchemaUtils.partial()` | Make all fields optional | New schema | [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils) |
|
|
507
|
+
| `dsl.if(condition)` | Conditional validation | ConditionalBuilder | [Conditional API](https://vextjs.github.io/schema-dsl/conditional-api) |
|
|
508
|
+
| `dsl.match(field, map)` | Branch validation | ConditionalBuilder | [Conditional API](https://vextjs.github.io/schema-dsl/conditional-api) |
|
|
509
|
+
| `I18nError.throw()` | Throw an i18n error | never | [Error Handling](https://vextjs.github.io/schema-dsl/error-handling) |
|
|
510
|
+
| `I18nError.assert()` | Assert then throw | void | [Error Handling](https://vextjs.github.io/schema-dsl/error-handling) |
|
|
2081
511
|
|
|
2082
512
|
---
|
|
2083
513
|
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
**原代码(多个独立验证)**:
|
|
2087
|
-
```javascript
|
|
2088
|
-
dsl.if(d => !d)
|
|
2089
|
-
.message('ACCOUNT_NOT_FOUND')
|
|
2090
|
-
.assert(account);
|
|
2091
|
-
|
|
2092
|
-
dsl.if(d => d.tradable_credits < amount)
|
|
2093
|
-
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
2094
|
-
.assert(account.tradable_credits);
|
|
2095
|
-
```
|
|
2096
|
-
|
|
2097
|
-
**✅ 方案1:使用 .and() 链式合并(v1.1.1 推荐)**
|
|
2098
|
-
```javascript
|
|
2099
|
-
// ✅ 每个条件都有独立的错误消息
|
|
2100
|
-
dsl.if(d => !d)
|
|
2101
|
-
.message('ACCOUNT_NOT_FOUND')
|
|
2102
|
-
.and(d => d.tradable_credits < amount)
|
|
2103
|
-
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
2104
|
-
.assert(account);
|
|
2105
|
-
|
|
2106
|
-
// 工作原理:
|
|
2107
|
-
// - 第一个条件失败 → 返回 'ACCOUNT_NOT_FOUND'
|
|
2108
|
-
// - 第二个条件失败 → 返回 'INSUFFICIENT_TRADABLE_CREDITS'
|
|
2109
|
-
// - 所有条件通过 → 验证成功
|
|
2110
|
-
```
|
|
2111
|
-
|
|
2112
|
-
**✅ 方案2:使用 .elseIf() 分支验证**
|
|
2113
|
-
```javascript
|
|
2114
|
-
// ✅ 按优先级检查,找到第一个失败的
|
|
2115
|
-
dsl.if(d => !d)
|
|
2116
|
-
.message('ACCOUNT_NOT_FOUND')
|
|
2117
|
-
.elseIf(d => d.tradable_credits < amount)
|
|
2118
|
-
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
2119
|
-
.assert(account);
|
|
2120
|
-
```
|
|
2121
|
-
|
|
2122
|
-
**✅ 方案3:保持独立验证**(最清晰)
|
|
2123
|
-
```javascript
|
|
2124
|
-
// ✅ 两个独立的验证器
|
|
2125
|
-
dsl.if(d => !d).message('ACCOUNT_NOT_FOUND').assert(account);
|
|
2126
|
-
dsl.if(d => d.tradable_credits < amount)
|
|
2127
|
-
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
2128
|
-
.assert(account.tradable_credits);
|
|
2129
|
-
```
|
|
2130
|
-
|
|
2131
|
-
**⚠️ 注意事项**:
|
|
2132
|
-
- `.and()` 用于组合多个条件,每个条件可以有**独立的** `.message()` (v1.1.1)
|
|
2133
|
-
- 如果 `.and()` 后不调用 `.message()`,则使用前一个条件的消息
|
|
2134
|
-
- `.elseIf()` 按顺序检查,找到第一个失败的就停止(if-else-if 逻辑)
|
|
2135
|
-
|
|
2136
|
-
**何时使用**:
|
|
2137
|
-
- ✅ 使用 `.and()` - 多个条件,每个有不同错误消息(v1.1.1)
|
|
2138
|
-
- ✅ 使用 `.elseIf()` - 不同分支有不同验证规则
|
|
2139
|
-
- ✅ 独立验证 - 最清晰,最可靠
|
|
2140
|
-
|
|
2141
|
-
**实际应用示例**:
|
|
2142
|
-
```javascript
|
|
2143
|
-
// 账户验证:检查存在性 + 余额 + 状态
|
|
2144
|
-
dsl.if(d => !d)
|
|
2145
|
-
.message('ACCOUNT_NOT_FOUND')
|
|
2146
|
-
.and(d => d.status !== 'active')
|
|
2147
|
-
.message('ACCOUNT_INACTIVE')
|
|
2148
|
-
.and(d => d.tradable_credits < amount)
|
|
2149
|
-
.message('INSUFFICIENT_TRADABLE_CREDITS')
|
|
2150
|
-
.assert(account);
|
|
2151
|
-
|
|
2152
|
-
// 每个失败条件都有清晰的错误消息!
|
|
2153
|
-
```
|
|
2154
|
-
|
|
2155
|
-
📖 更多示例请查看 [完整文档](./docs/INDEX.md)
|
|
2156
|
-
|
|
2157
|
-
---
|
|
2158
|
-
|
|
2159
|
-
### Q8: 如何统一抛出多语言错误?(v1.1.1+)
|
|
2160
|
-
|
|
2161
|
-
**问题**: 业务代码中抛出的错误无法多语言,与 `.message()` 和 `.label()` 不一致
|
|
2162
|
-
|
|
2163
|
-
**✅ 解决方案:使用 `I18nError` 或 `dsl.error`**
|
|
2164
|
-
|
|
2165
|
-
```javascript
|
|
2166
|
-
const { I18nError, dsl } = require('schema-dsl');
|
|
514
|
+
## 📝 TypeScript Usage
|
|
2167
515
|
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
// 中文: "账户不存在"
|
|
2171
|
-
// 英文: "Account not found"
|
|
516
|
+
```typescript
|
|
517
|
+
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
|
|
2172
518
|
|
|
2173
|
-
//
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
519
|
+
// ✅ wrap strings with dsl() in TypeScript for full type inference
|
|
520
|
+
const userSchema = dsl({
|
|
521
|
+
username: dsl('string:3-32!').label('username'),
|
|
522
|
+
email: dsl('email!').label('email'),
|
|
523
|
+
age: dsl('number:18-100').label('age')
|
|
2177
524
|
});
|
|
2178
|
-
// 输出: "余额不足,当前余额50,需要100"
|
|
2179
|
-
|
|
2180
|
-
// 方式3:断言风格(推荐)
|
|
2181
|
-
I18nError.assert(account, 'account.notFound');
|
|
2182
|
-
I18nError.assert(
|
|
2183
|
-
account.balance >= 100,
|
|
2184
|
-
'account.insufficientBalance',
|
|
2185
|
-
{ balance: account.balance, required: 100 }
|
|
2186
|
-
);
|
|
2187
|
-
|
|
2188
|
-
// 方式4:快捷方法
|
|
2189
|
-
dsl.error.throw('user.noPermission');
|
|
2190
|
-
dsl.error.assert(user.role === 'admin', 'user.noPermission');
|
|
2191
|
-
```
|
|
2192
525
|
|
|
2193
|
-
**🆕 对象格式错误配置(v1.1.5)**
|
|
2194
|
-
|
|
2195
|
-
支持统一的数字错误代码,便于前端处理:
|
|
2196
|
-
|
|
2197
|
-
```javascript
|
|
2198
|
-
// 语言包配置(lib/locales/zh-CN.js)
|
|
2199
|
-
module.exports = {
|
|
2200
|
-
// 字符串格式(向后兼容)
|
|
2201
|
-
'user.notFound': '用户不存在',
|
|
2202
|
-
|
|
2203
|
-
// 对象格式(v1.1.5 新增)- 使用数字错误码
|
|
2204
|
-
'account.notFound': {
|
|
2205
|
-
code: 40001, // 数字错误代码
|
|
2206
|
-
message: '账户不存在'
|
|
2207
|
-
},
|
|
2208
|
-
'account.insufficientBalance': {
|
|
2209
|
-
code: 40002,
|
|
2210
|
-
message: '余额不足,当前{{#balance}},需要{{#required}}'
|
|
2211
|
-
},
|
|
2212
|
-
'order.notPaid': {
|
|
2213
|
-
code: 50001,
|
|
2214
|
-
message: '订单未支付'
|
|
2215
|
-
}
|
|
2216
|
-
};
|
|
2217
|
-
|
|
2218
|
-
// lib/locales/en-US.js
|
|
2219
|
-
module.exports = {
|
|
2220
|
-
'account.notFound': {
|
|
2221
|
-
code: 40001, // 相同的数字 code
|
|
2222
|
-
message: 'Account not found'
|
|
2223
|
-
},
|
|
2224
|
-
'account.insufficientBalance': {
|
|
2225
|
-
code: 40002,
|
|
2226
|
-
message: 'Insufficient balance: {{#balance}}, required: {{#required}}'
|
|
2227
|
-
},
|
|
2228
|
-
'order.notPaid': {
|
|
2229
|
-
code: 50001,
|
|
2230
|
-
message: 'Order not paid'
|
|
2231
|
-
}
|
|
2232
|
-
};
|
|
2233
|
-
|
|
2234
|
-
// 使用
|
|
2235
526
|
try {
|
|
2236
|
-
|
|
527
|
+
const validData = await validateAsync(userSchema, payload);
|
|
528
|
+
// validData has full type inference
|
|
2237
529
|
} catch (error) {
|
|
2238
|
-
error
|
|
2239
|
-
|
|
2240
|
-
error.message // '账户不存在'
|
|
2241
|
-
|
|
2242
|
-
// 两种判断方式
|
|
2243
|
-
error.is('account.notFound') // ✅ 使用 originalKey
|
|
2244
|
-
error.is(40001) // ✅ 使用数字 code
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
// 前端统一处理(不受语言影响)
|
|
2248
|
-
try {
|
|
2249
|
-
await api.getAccount(id);
|
|
2250
|
-
} catch (error) {
|
|
2251
|
-
switch (error.code) {
|
|
2252
|
-
case 40001:
|
|
2253
|
-
router.push('/account-not-found');
|
|
2254
|
-
break;
|
|
2255
|
-
case 40002:
|
|
2256
|
-
showTopUpDialog(error.params.balance, error.params.required);
|
|
2257
|
-
break;
|
|
2258
|
-
case 50001:
|
|
2259
|
-
showPaymentDialog();
|
|
2260
|
-
break;
|
|
530
|
+
if (error instanceof ValidationError) {
|
|
531
|
+
error.errors.forEach(e => console.log(`${e.path}: ${e.message}`));
|
|
2261
532
|
}
|
|
2262
533
|
}
|
|
2263
534
|
```
|
|
2264
535
|
|
|
2265
|
-
|
|
2266
|
-
-
|
|
2267
|
-
- ✅ 完全向后兼容,字符串格式自动转换
|
|
2268
|
-
- ✅ `originalKey` 便于调试和日志追踪
|
|
2269
|
-
- ✅ 数字 code 更简洁,易于管理和文档化
|
|
2270
|
-
|
|
2271
|
-
**错误码规范建议**:
|
|
2272
|
-
- `4xxxx` - 客户端错误(账户、权限、参数等)
|
|
2273
|
-
- `5xxxx` - 业务逻辑错误(订单、支付、库存等)
|
|
2274
|
-
- `6xxxx` - 系统错误(数据库、服务不可用等)
|
|
2275
|
-
|
|
2276
|
-
📖 详细说明: [错误处理文档](./docs/error-handling.md#v115-新功能对象格式错误配置)
|
|
536
|
+
> **Note**: In TypeScript projects, wrap strings with `dsl('...')` to get type inference. In JavaScript projects you can pass strings directly.
|
|
537
|
+
> See the [TypeScript Guide](https://vextjs.github.io/schema-dsl/typescript-guide) for details.
|
|
2277
538
|
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
无需修改全局语言设置,每次调用时指定:
|
|
2281
|
-
|
|
2282
|
-
```javascript
|
|
2283
|
-
// 根据请求头动态返回不同语言
|
|
2284
|
-
app.post('/api/account', (req, res, next) => {
|
|
2285
|
-
const locale = req.headers['accept-language'] || 'en-US';
|
|
2286
|
-
const account = getAccount(req.user.id);
|
|
2287
|
-
|
|
2288
|
-
try {
|
|
2289
|
-
// 第5个参数指定语言
|
|
2290
|
-
dsl.error.assert(account, 'account.notFound', {}, 404, locale);
|
|
2291
|
-
dsl.error.assert(
|
|
2292
|
-
account.balance >= 100,
|
|
2293
|
-
'account.insufficientBalance',
|
|
2294
|
-
{ balance: account.balance, required: 100 },
|
|
2295
|
-
400,
|
|
2296
|
-
locale
|
|
2297
|
-
);
|
|
2298
|
-
// 验证通过...
|
|
2299
|
-
} catch (error) {
|
|
2300
|
-
next(error);
|
|
2301
|
-
}
|
|
2302
|
-
});
|
|
539
|
+
---
|
|
2303
540
|
|
|
2304
|
-
|
|
2305
|
-
const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
|
|
2306
|
-
console.log(error1.message); // "账户不存在"
|
|
541
|
+
## 🛠️ Development
|
|
2307
542
|
|
|
2308
|
-
|
|
2309
|
-
|
|
543
|
+
```bash
|
|
544
|
+
npm run build # compile TypeScript
|
|
545
|
+
npm run test # run tests
|
|
546
|
+
npm run typecheck # type check
|
|
2310
547
|
```
|
|
2311
548
|
|
|
2312
|
-
|
|
2313
|
-
```javascript
|
|
2314
|
-
// 错误处理中间件
|
|
2315
|
-
app.use((error, req, res, next) => {
|
|
2316
|
-
if (error instanceof I18nError) {
|
|
2317
|
-
return res.status(error.statusCode).json(error.toJSON());
|
|
2318
|
-
}
|
|
2319
|
-
next(error);
|
|
2320
|
-
});
|
|
549
|
+
Local documentation preview:
|
|
2321
550
|
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
I18nError.assert(account, 'account.notFound');
|
|
2326
|
-
I18nError.assert(
|
|
2327
|
-
account.balance >= req.body.amount,
|
|
2328
|
-
'account.insufficientBalance',
|
|
2329
|
-
{ balance: account.balance, required: req.body.amount }
|
|
2330
|
-
);
|
|
2331
|
-
// ...
|
|
2332
|
-
});
|
|
551
|
+
```bash
|
|
552
|
+
cd website
|
|
553
|
+
npm run dev
|
|
2333
554
|
```
|
|
2334
555
|
|
|
2335
|
-
**内置错误代码**:
|
|
2336
|
-
- 通用: `error.notFound`, `error.forbidden`, `error.unauthorized`
|
|
2337
|
-
- 账户: `account.notFound`, `account.insufficientBalance`
|
|
2338
|
-
- 用户: `user.notFound`, `user.noPermission`
|
|
2339
|
-
- 订单: `order.notPaid`, `order.paymentMissing`
|
|
2340
|
-
|
|
2341
|
-
📖 完整文档请查看 [examples/i18n-error.examples.js](./examples/i18n-error.examples.js)
|
|
2342
|
-
📖 运行时多语言支持请查看 [docs/runtime-locale-support.md](./docs/runtime-locale-support.md)
|
|
2343
|
-
|
|
2344
556
|
---
|
|
2345
557
|
|
|
2346
|
-
## 🤝
|
|
2347
|
-
|
|
2348
|
-
欢迎贡献代码、报告问题或提出建议!
|
|
2349
|
-
|
|
2350
|
-
### 开发环境
|
|
558
|
+
## 🤝 Contributing
|
|
2351
559
|
|
|
2352
560
|
```bash
|
|
2353
|
-
# 克隆仓库
|
|
2354
561
|
git clone https://github.com/vextjs/schema-dsl.git
|
|
2355
562
|
cd schema-dsl
|
|
2356
|
-
|
|
2357
|
-
# 安装依赖
|
|
2358
563
|
npm install
|
|
2359
|
-
|
|
2360
|
-
# 运行测试
|
|
2361
564
|
npm test
|
|
2362
|
-
|
|
2363
|
-
# 代码检查
|
|
2364
|
-
npm run lint
|
|
2365
|
-
|
|
2366
|
-
# 查看测试覆盖率
|
|
2367
|
-
npm run coverage
|
|
2368
565
|
```
|
|
2369
566
|
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
- 🐛 **Bug 修复**: `fix: 修复XXX问题`
|
|
2373
|
-
- ✨ **新功能**: `feat: 添加XXX功能`
|
|
2374
|
-
- 📝 **文档**: `docs: 更新XXX文档`
|
|
2375
|
-
- 🎨 **代码格式**: `style: 格式化代码`
|
|
2376
|
-
- ♻️ **重构**: `refactor: 重构XXX模块`
|
|
2377
|
-
- ✅ **测试**: `test: 添加XXX测试`
|
|
2378
|
-
|
|
2379
|
-
详见 [贡献指南](./CONTRIBUTING.md)
|
|
567
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
|
|
2380
568
|
|
|
2381
569
|
---
|
|
2382
570
|
|
|
2383
|
-
##
|
|
571
|
+
## 🔗 Links
|
|
2384
572
|
|
|
2385
|
-
|
|
573
|
+
### 📖 Core documentation
|
|
574
|
+
- [Quick Start](https://vextjs.github.io/schema-dsl/quick-start) — up and running in 5 minutes
|
|
575
|
+
- [DSL Syntax Guide](https://vextjs.github.io/schema-dsl/dsl-syntax) — complete syntax reference
|
|
576
|
+
- [Validation Guide](https://vextjs.github.io/schema-dsl/validation-guide) — advanced validation techniques
|
|
577
|
+
- [API Reference](https://vextjs.github.io/schema-dsl/api-reference) — complete API docs
|
|
578
|
+
- [TypeScript Guide](https://vextjs.github.io/schema-dsl/typescript-guide) — required reading for TS users
|
|
579
|
+
- [Best Practices](https://vextjs.github.io/schema-dsl/best-practices) — avoid common pitfalls
|
|
580
|
+
- [Troubleshooting](https://vextjs.github.io/schema-dsl/troubleshooting) — diagnosing issues
|
|
2386
581
|
|
|
2387
|
-
|
|
582
|
+
### 🎯 Feature documentation
|
|
583
|
+
- [SchemaUtils](https://vextjs.github.io/schema-dsl/schema-utils)
|
|
584
|
+
- [Conditional Validation API](https://vextjs.github.io/schema-dsl/conditional-api)
|
|
585
|
+
- [Async Validation](https://vextjs.github.io/schema-dsl/validate-async)
|
|
586
|
+
- [Error Handling & i18n](https://vextjs.github.io/schema-dsl/error-handling)
|
|
587
|
+
- [Union Types](https://vextjs.github.io/schema-dsl/union-types)
|
|
588
|
+
- [Enum Types](https://vextjs.github.io/schema-dsl/enum)
|
|
589
|
+
|
|
590
|
+
### 🗄️ Export & integration
|
|
591
|
+
- [Export Guide](https://vextjs.github.io/schema-dsl/export-guide)
|
|
592
|
+
- [MongoDB Exporter](https://vextjs.github.io/schema-dsl/mongodb-exporter)
|
|
593
|
+
- [MySQL Exporter](https://vextjs.github.io/schema-dsl/mysql-exporter)
|
|
594
|
+
- [PostgreSQL Exporter](https://vextjs.github.io/schema-dsl/postgresql-exporter)
|
|
595
|
+
- [Markdown Exporter](https://vextjs.github.io/schema-dsl/markdown-exporter)
|
|
596
|
+
- [⚠️ Export Limitations](https://vextjs.github.io/schema-dsl/export-limitations)
|
|
2388
597
|
|
|
2389
|
-
|
|
598
|
+
### 💻 Examples
|
|
599
|
+
- [quick-start.ts](./examples/docs/quick-start.ts) — basic usage and registration form
|
|
600
|
+
- [validate-async.ts](./examples/docs/validate-async.ts) — async validation and `ValidationError` handling
|
|
601
|
+
- [export-guide.ts](./examples/docs/export-guide.ts) — database export overview
|
|
602
|
+
- [error-handling.ts](./examples/docs/error-handling.ts) — field errors and business error handling
|
|
603
|
+
- [plugin-system.ts](./examples/docs/plugin-system.ts) — plugin system and hooks
|
|
2390
604
|
|
|
2391
|
-
|
|
2392
|
-
-
|
|
605
|
+
### 📝 Changelog & contributing
|
|
606
|
+
- [Changelog](./CHANGELOG.md)
|
|
607
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
608
|
+
- [Security Policy](./SECURITY.md)
|
|
2393
609
|
|
|
2394
610
|
---
|
|
2395
611
|
|
|
2396
|
-
##
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
- [npm 包](https://www.npmjs.com/package/schema-dsl) - 安装和版本历史
|
|
2400
|
-
- [GitHub 仓库](https://github.com/vextjs/schema-dsl) - 源代码和 Star ⭐
|
|
2401
|
-
- [在线体验](https://runkit.com/npm/schema-dsl) - RunKit 演练场
|
|
2402
|
-
- [问题反馈](https://github.com/vextjs/schema-dsl/issues) - Bug 报告和功能请求
|
|
2403
|
-
- [讨论区](https://github.com/vextjs/schema-dsl/discussions) - 社区交流
|
|
2404
|
-
|
|
2405
|
-
### 📖 核心文档
|
|
2406
|
-
- [完整文档索引](./docs/INDEX.md) - 40+ 篇文档导航
|
|
2407
|
-
- [快速开始](./docs/quick-start.md) - 5 分钟入门
|
|
2408
|
-
- [DSL 语法](./docs/dsl-syntax.md) - 语法完整指南(2815 行)
|
|
2409
|
-
- [API 参考](./docs/api-reference.md) - API 完整文档
|
|
2410
|
-
- [TypeScript 指南](./docs/typescript-guide.md) - TS 用户必读
|
|
2411
|
-
- [最佳实践](./docs/best-practices.md) - 避免常见坑
|
|
2412
|
-
- [常见问题](./docs/faq.md) - FAQ 合集
|
|
2413
|
-
- [故障排查](./docs/troubleshooting.md) - 问题诊断
|
|
2414
|
-
|
|
2415
|
-
### 🎯 功能文档
|
|
2416
|
-
- [字符串扩展](./docs/string-extensions.md) - String 扩展方法
|
|
2417
|
-
- [SchemaUtils 工具](./docs/schema-utils.md) - Schema 复用工具
|
|
2418
|
-
- [条件验证 API](./docs/conditional-api.md) - dsl.if/dsl.match
|
|
2419
|
-
- [验证指南](./docs/validation-guide.md) - 高级验证技巧
|
|
2420
|
-
- [类型参考](./docs/type-reference.md) - 所有内置类型
|
|
2421
|
-
- [枚举类型](./docs/enum.md) - 枚举验证详解
|
|
2422
|
-
- [联合类型](./docs/union-types.md) - v1.1.0 新特性
|
|
2423
|
-
- [数字运算符](./docs/number-operators.md) - v1.1.2 新特性
|
|
2424
|
-
- [错误处理](./docs/error-handling.md) - 错误处理策略
|
|
2425
|
-
|
|
2426
|
-
### 🌍 多语言支持
|
|
2427
|
-
- [多语言用户指南](./docs/i18n-user-guide.md) - 完整使用教程
|
|
2428
|
-
- [多语言配置详解](./docs/i18n.md) - 配置说明
|
|
2429
|
-
- [前端集成指南](./docs/frontend-i18n-guide.md) - 前端使用
|
|
2430
|
-
- [添加自定义语言](./docs/add-custom-locale.md) - 扩展新语言
|
|
2431
|
-
- [动态语言配置](./docs/dynamic-locale.md) - 动态切换
|
|
2432
|
-
- [Label vs Description](./docs/label-vs-description.md) - 最佳实践
|
|
2433
|
-
|
|
2434
|
-
### 🗄️ 数据库导出
|
|
2435
|
-
- [导出指南](./docs/export-guide.md) - 完整导出教程
|
|
2436
|
-
- [MongoDB 导出器](./docs/mongodb-exporter.md) - MongoDB Schema 导出
|
|
2437
|
-
- [MySQL 导出器](./docs/mysql-exporter.md) - MySQL DDL 生成
|
|
2438
|
-
- [PostgreSQL 导出器](./docs/postgresql-exporter.md) - PostgreSQL DDL 生成
|
|
2439
|
-
- [Markdown 导出器](./docs/markdown-exporter.md) - API 文档生成
|
|
2440
|
-
- [⚠️ 导出限制说明](./docs/export-limitations.md) - **必读!了解哪些特性无法导出**
|
|
2441
|
-
|
|
2442
|
-
### 🔌 插件和扩展
|
|
2443
|
-
- [插件系统](./docs/plugin-system.md) - 插件开发和使用
|
|
2444
|
-
- [插件类型注册](./docs/plugin-type-registration.md) - 自定义类型
|
|
2445
|
-
- [自定义扩展指南](./docs/custom-extensions-guide.md) - 添加自定义验证
|
|
2446
|
-
|
|
2447
|
-
### 📊 性能和设计
|
|
2448
|
-
- [性能基准测试报告](./docs/performance-benchmark-report.md) - 性能对比数据
|
|
2449
|
-
- [设计理念](./docs/design-philosophy.md) - 架构和权衡
|
|
2450
|
-
- [缓存管理器](./docs/cache-manager.md) - 缓存配置和优化
|
|
2451
|
-
|
|
2452
|
-
### 💻 示例代码
|
|
2453
|
-
- [examples/](./examples/) - 所有示例代码目录
|
|
2454
|
-
- [Express 集成](./examples/express-integration.js) - Express 完整示例
|
|
2455
|
-
- [中间件使用](./examples/middleware-usage.js) - Koa/Fastify 示例
|
|
2456
|
-
- [用户注册](./examples/user-registration/) - 完整注册流程
|
|
2457
|
-
- [密码重置](./examples/password-reset/) - 密码重置流程
|
|
2458
|
-
- [条件验证](./examples/conditional-example.js) - 条件验证示例
|
|
2459
|
-
- [dsl.match 示例](./examples/dsl-match-example.js) - match 用法
|
|
2460
|
-
- [多语言完整示例](./examples/i18n-full-demo.js) - i18n 完整演示
|
|
2461
|
-
- [I18nError 示例](./examples/i18n-error.examples.js) - 多语言错误
|
|
2462
|
-
- [数据库导出](./examples/export-demo.js) - 导出示例
|
|
2463
|
-
- [Markdown 导出](./examples/markdown-export.js) - 文档生成
|
|
2464
|
-
- [插件系统](./examples/plugin-system.examples.js) - 插件示例
|
|
2465
|
-
- [联合类型](./examples/union-type-example.js) - 联合类型示例
|
|
2466
|
-
- [Slug 验证](./examples/slug.examples.js) - URL slug 示例
|
|
2467
|
-
- [字符串扩展](./examples/string-extensions.js) - String 扩展示例
|
|
2468
|
-
- [批量操作](./examples/batch-operations.examples.js) - 批量验证
|
|
2469
|
-
- [简单示例](./examples/simple-example.js) - 快速上手
|
|
2470
|
-
|
|
2471
|
-
### 📝 版本和贡献
|
|
2472
|
-
- [更新日志](./CHANGELOG.md) - 详细版本历史
|
|
2473
|
-
- [贡献指南](./CONTRIBUTING.md) - 如何参与贡献
|
|
2474
|
-
- [状态文档](./STATUS.md) - 项目状态和路线图
|
|
2475
|
-
- [安全策略](./SECURITY.md) - 安全问题报告
|
|
612
|
+
## 📄 License
|
|
613
|
+
|
|
614
|
+
[MIT](./LICENSE)
|
|
2476
615
|
|
|
2477
616
|
---
|
|
2478
617
|
|
|
2479
618
|
<div align="center">
|
|
2480
619
|
|
|
2481
|
-
|
|
620
|
+
If this project is useful to you, please consider giving it a Star ⭐
|
|
2482
621
|
|
|
2483
|
-
Made with ❤️ by schema-dsl team
|
|
622
|
+
Made with ❤️ by the schema-dsl team
|
|
2484
623
|
|
|
2485
624
|
</div>
|
|
2486
625
|
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|