schema-dsl 1.2.5 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -238
- package/LICENSE +21 -21
- package/README.md +628 -2486
- package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
- package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
- package/dist/Validator-CllRdrY0.d.ts +192 -0
- package/dist/Validator-D6okG9tr.d.cts +192 -0
- package/dist/index.cjs +6640 -0
- package/dist/index.d.cts +1151 -0
- package/dist/index.d.ts +1151 -0
- package/dist/index.js +6574 -0
- package/dist/plugin-CIKtTMtS.d.cts +246 -0
- package/dist/plugin-CIKtTMtS.d.ts +246 -0
- package/dist/plugins/custom-format.cjs +3818 -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 +3788 -0
- package/dist/plugins/custom-type-example.cjs +3811 -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 +3781 -0
- package/dist/plugins/custom-validator.cjs +144 -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 +119 -0
- package/docs/FEATURE-INDEX.md +553 -519
- package/docs/add-custom-locale.md +496 -483
- package/docs/add-keyword.md +24 -0
- package/docs/api-reference.md +1047 -805
- package/docs/api.md +13 -0
- package/docs/best-practices-project-structure.md +417 -408
- package/docs/best-practices.md +712 -672
- package/docs/cache-manager.md +344 -336
- package/docs/compile.md +45 -0
- package/docs/conditional-api.md +1307 -1278
- package/docs/custom-extensions-guide.md +339 -411
- package/docs/design-philosophy.md +606 -601
- package/docs/doc-index.md +324 -0
- package/docs/dsl-syntax.md +714 -664
- package/docs/dynamic-locale.md +608 -598
- package/docs/enum.md +482 -475
- package/docs/error-handling.md +1975 -1966
- package/docs/export-guide.md +501 -462
- package/docs/export-limitations.md +567 -551
- package/docs/faq.md +596 -577
- package/docs/frontend-i18n-guide.md +307 -293
- package/docs/i18n-user-guide.md +487 -474
- package/docs/i18n.md +476 -457
- package/docs/index.md +48 -0
- package/docs/json-schema-basics.md +40 -0
- package/docs/label-vs-description.md +271 -262
- package/docs/markdown-exporter.md +406 -397
- package/docs/mongodb-exporter.md +302 -295
- package/docs/multi-language.md +26 -0
- package/docs/multi-type-support.md +322 -329
- package/docs/mysql-exporter.md +280 -273
- package/docs/number-operators.md +449 -442
- package/docs/optional-marker-guide.md +326 -321
- package/docs/performance-guide.md +49 -0
- package/docs/plugin-system.md +381 -542
- package/docs/plugin-type-registration.md +34 -0
- package/docs/postgresql-exporter.md +311 -304
- package/docs/public/favicon.svg +5 -0
- package/docs/quick-start.md +435 -761
- package/docs/runtime-locale-support.md +532 -521
- package/docs/schema-helper.md +345 -340
- package/docs/schema-utils-advanced-issues.md +23 -0
- package/docs/schema-utils-best-practices.md +20 -0
- package/docs/schema-utils-chaining.md +150 -143
- package/docs/schema-utils.md +524 -490
- package/docs/security-checklist.md +20 -0
- package/docs/string-extensions.md +488 -480
- package/docs/troubleshooting.md +486 -471
- package/docs/type-converter.md +310 -319
- package/docs/type-reference.md +242 -219
- package/docs/typescript-guide.md +584 -573
- package/docs/union-type-guide.md +157 -147
- package/docs/union-types.md +284 -277
- package/docs/validate-async.md +491 -480
- package/docs/validate-batch.md +49 -0
- package/docs/validate-dsl-object-support.md +578 -573
- package/docs/validate.md +506 -486
- package/docs/validation-guide.md +502 -484
- package/docs/validator.md +39 -0
- package/package.json +131 -73
- 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 +169 -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 +687 -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 +156 -0
- package/src/exporters/PostgreSQLExporter.ts +222 -0
- package/src/exporters/index.ts +18 -0
- package/src/index.ts +651 -0
- package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
- package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
- package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
- package/src/locales/index.ts +103 -0
- package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
- package/src/locales/types.ts +156 -0
- package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
- 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 +124 -0
- package/src/plugins/custom-type-example.ts +106 -0
- package/src/plugins/custom-validator.ts +138 -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 +365 -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,2486 +1,628 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# 🎯 schema-dsl
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[![
|
|
10
|
-
[![
|
|
11
|
-
[![
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
- [
|
|
59
|
-
- [
|
|
60
|
-
- [
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
- [
|
|
64
|
-
- [
|
|
65
|
-
- [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
###
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
###
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
>
|
|
358
|
-
>
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
//
|
|
378
|
-
|
|
379
|
-
//
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
|
500
|
-
|
|
501
|
-
|
|
|
502
|
-
|
|
|
503
|
-
|
|
|
504
|
-
|
|
|
505
|
-
|
|
|
506
|
-
|
|
|
507
|
-
|
|
|
508
|
-
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
email: 'invalid-email', // 格式错误
|
|
630
|
-
age: 15 // 小于最小值18
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
console.log(result2.valid); // false
|
|
634
|
-
console.log(result2.errors); // 错误列表
|
|
635
|
-
/*
|
|
636
|
-
[
|
|
637
|
-
{ path: 'username', message: 'username must be at least 3 characters' },
|
|
638
|
-
{ path: 'email', message: 'must be a valid email' },
|
|
639
|
-
{ path: 'age', message: 'age must be at least 18' }
|
|
640
|
-
]
|
|
641
|
-
*/
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
### 1.5 TypeScript 用法 ⭐
|
|
645
|
-
|
|
646
|
-
**重要**: TypeScript 中**必须**使用 `dsl()` 包裹字符串以获得类型提示(v1.0.6+ 移除了全局 String 类型扩展以避免类型污染):
|
|
647
|
-
|
|
648
|
-
```typescript
|
|
649
|
-
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
|
|
650
|
-
|
|
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
|
-
const createUserSchema = dsl({
|
|
699
|
-
username: 'string:3-32!',
|
|
700
|
-
email: 'email!',
|
|
701
|
-
password: 'string:8-32!'
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
// 在路由中使用
|
|
705
|
-
app.post('/api/users', async (req, res, next) => {
|
|
706
|
-
try {
|
|
707
|
-
// validateAsync 验证失败时会抛出 ValidationError
|
|
708
|
-
const validData = await validateAsync(createUserSchema, req.body);
|
|
709
|
-
|
|
710
|
-
const user = await db.users.create(validData);
|
|
711
|
-
res.json({ success: true, data: user });
|
|
712
|
-
} catch (error) {
|
|
713
|
-
// ValidationError 会被全局错误处理器捕获
|
|
714
|
-
next(error);
|
|
715
|
-
}
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
// 全局错误处理 - 区分验证错误和其他错误
|
|
719
|
-
app.use((error, req, res, next) => {
|
|
720
|
-
if (error instanceof ValidationError) {
|
|
721
|
-
// 验证错误返回 400
|
|
722
|
-
return res.status(400).json({
|
|
723
|
-
success: false,
|
|
724
|
-
message: 'Validation failed',
|
|
725
|
-
errors: error.errors // 详细的字段错误列表
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// 其他错误继续传递
|
|
730
|
-
next(error);
|
|
731
|
-
});
|
|
732
|
-
```
|
|
733
|
-
|
|
734
|
-
### Schema 复用 - 按场景使用
|
|
735
|
-
|
|
736
|
-
```javascript
|
|
737
|
-
const { dsl, SchemaUtils } = require('schema-dsl');
|
|
738
|
-
|
|
739
|
-
// 完整的用户 Schema
|
|
740
|
-
const fullUserSchema = dsl({
|
|
741
|
-
id: 'string!',
|
|
742
|
-
username: 'string:3-32!',
|
|
743
|
-
email: 'email!',
|
|
744
|
-
password: 'string:8-32!',
|
|
745
|
-
age: 'number:18-120',
|
|
746
|
-
role: 'admin|user|guest',
|
|
747
|
-
createdAt: 'datetime!',
|
|
748
|
-
updatedAt: 'datetime!'
|
|
749
|
-
});
|
|
750
|
-
|
|
751
|
-
// 场景1: 创建用户 - 排除自动生成的字段
|
|
752
|
-
// 使用 omit() 排除不需要的字段
|
|
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
|
-
```
|
|
780
|
-
|
|
781
|
-
### 条件验证 - 一行代码搞定
|
|
782
|
-
|
|
783
|
-
**问题场景**:不同情况需要不同的验证规则
|
|
784
|
-
|
|
785
|
-
```javascript
|
|
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
|
-
}
|
|
795
|
-
|
|
796
|
-
// ✅ 新做法:一行代码搞定
|
|
797
|
-
dsl.if(d => d.age < 18)
|
|
798
|
-
.message('未成年用户不能注册')
|
|
799
|
-
.assert(userData); // 失败自动抛错
|
|
800
|
-
|
|
801
|
-
// 场景2:权限检查 - 快速判断
|
|
802
|
-
// ❌ 传统做法:写 if 判断
|
|
803
|
-
if (user.role !== 'admin' && user.role !== 'moderator') {
|
|
804
|
-
return res.status(403).json({ error: '权限不足' });
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
// ✅ 新做法:一行搞定
|
|
808
|
-
if (!dsl.if(d => d.role === 'admin' || d.role === 'moderator')
|
|
809
|
-
.message('权限不足')
|
|
810
|
-
.check(user)) {
|
|
811
|
-
return res.status(403).json({ error: '权限不足' });
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// 场景3:批量过滤 - 筛选符合条件的数据
|
|
815
|
-
// ❌ 传统做法:写 filter 函数
|
|
816
|
-
const adults = users.filter(u => u.age >= 18);
|
|
817
|
-
|
|
818
|
-
// ✅ 新做法:语义更清晰
|
|
819
|
-
const adults = users.filter(u =>
|
|
820
|
-
!dsl.if(d => d.age < 18).message('未成年').check(u)
|
|
821
|
-
);
|
|
822
|
-
```
|
|
823
|
-
|
|
824
|
-
#### 四种方法,满足不同场景
|
|
825
|
-
|
|
826
|
-
| 方法 | 什么时候用 | 返回什么 | 示例 |
|
|
827
|
-
|------|-----------|---------|------|
|
|
828
|
-
| **`.validate()`** | 需要知道错误详情 | `{ valid, errors, data }` | 表单验证 |
|
|
829
|
-
| **`.validateAsync()`** | async/await 场景 | Promise(失败抛错) | Express 中间件 |
|
|
830
|
-
| **`.assert()`** | 快速失败,不想写 if | 失败直接抛错 | 函数入口检查 |
|
|
831
|
-
| **`.check()`** | 只需要判断真假 | `true/false` | 数据过滤 |
|
|
832
|
-
|
|
833
|
-
#### 实际例子
|
|
834
|
-
|
|
835
|
-
**表单验证 - 需要显示错误**
|
|
836
|
-
|
|
837
|
-
```javascript
|
|
838
|
-
// 使用 .validate() 获取错误详情
|
|
839
|
-
const result = dsl.if(d => d.age < 18)
|
|
840
|
-
.message('未成年用户不能注册')
|
|
841
|
-
.validate(formData);
|
|
842
|
-
|
|
843
|
-
if (!result.valid) {
|
|
844
|
-
showError(result.errors[0].message); // 显示给用户
|
|
845
|
-
}
|
|
846
|
-
```
|
|
847
|
-
|
|
848
|
-
**Express 中间件 - 异步验证**
|
|
849
|
-
|
|
850
|
-
```javascript
|
|
851
|
-
// 使用 .validateAsync() 失败自动抛错
|
|
852
|
-
app.post('/register', async (req, res, next) => {
|
|
853
|
-
try {
|
|
854
|
-
await dsl.if(d => d.age < 18)
|
|
855
|
-
.message('未成年用户不能注册')
|
|
856
|
-
.validateAsync(req.body);
|
|
857
|
-
|
|
858
|
-
// 验证通过,继续处理
|
|
859
|
-
const user = await createUser(req.body);
|
|
860
|
-
res.json(user);
|
|
861
|
-
} catch (error) {
|
|
862
|
-
next(error); // 自动传递给错误处理中间件
|
|
863
|
-
}
|
|
864
|
-
});
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
**函数参数检查 - 快速断言**
|
|
868
|
-
|
|
869
|
-
```javascript
|
|
870
|
-
// 使用 .assert() 不满足直接抛错
|
|
871
|
-
function registerUser(userData) {
|
|
872
|
-
// 入口检查,不满足直接抛错,代码更清晰
|
|
873
|
-
dsl.if(d => d.age < 18).message('未成年不能注册').assert(userData);
|
|
874
|
-
dsl.if(d => !d.email).message('邮箱必填').assert(userData);
|
|
875
|
-
dsl.if(d => !d.phone).message('手机号必填').assert(userData);
|
|
876
|
-
|
|
877
|
-
// 检查通过,继续业务逻辑
|
|
878
|
-
return createUser(userData);
|
|
879
|
-
}
|
|
880
|
-
```
|
|
881
|
-
|
|
882
|
-
**批量数据处理 - 快速过滤**
|
|
883
|
-
|
|
884
|
-
```javascript
|
|
885
|
-
// 使用 .check() 只返回 true/false
|
|
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
|
-
```
|
|
898
|
-
|
|
899
|
-
**复用验证器**
|
|
900
|
-
|
|
901
|
-
```javascript
|
|
902
|
-
// 创建一次,到处使用
|
|
903
|
-
const ageValidator = dsl.if(d => d.age < 18)
|
|
904
|
-
.message('未成年用户不能注册');
|
|
905
|
-
|
|
906
|
-
// 不同场景使用不同方法
|
|
907
|
-
const r1 = ageValidator.validate({ age: 16 }); // 同步,返回详情
|
|
908
|
-
const r2 = await ageValidator.validateAsync(data); // 异步,失败抛错
|
|
909
|
-
const r3 = ageValidator.check({ age: 20 }); // 快速判断
|
|
910
|
-
```
|
|
911
|
-
|
|
912
|
-
#### 💡 选择建议
|
|
913
|
-
|
|
914
|
-
- 🎯 **表单验证**:用 `.validate()` - 需要显示错误给用户
|
|
915
|
-
- 🚀 **API 接口**:用 `.validateAsync()` - 配合 try/catch
|
|
916
|
-
- ⚡ **函数入口**:用 `.assert()` - 快速失败,代码简洁
|
|
917
|
-
- 🔍 **数据过滤**:用 `.check()` - 只需要判断真假
|
|
918
|
-
|
|
919
|
-
**完整文档**: [ConditionalBuilder API](./docs/conditional-api.md)
|
|
920
|
-
|
|
921
|
-
---
|
|
922
|
-
|
|
923
|
-
## � 进阶功能
|
|
924
|
-
|
|
925
|
-
### 批量验证
|
|
926
|
-
|
|
927
|
-
**场景**: 验证 1000 条用户数据,性能提升 50 倍
|
|
928
|
-
|
|
929
|
-
```javascript
|
|
930
|
-
const { dsl, SchemaUtils, Validator } = require('schema-dsl');
|
|
931
|
-
|
|
932
|
-
const userSchema = dsl({
|
|
933
|
-
username: 'string:3-32!',
|
|
934
|
-
email: 'email!',
|
|
935
|
-
age: 'number:18-120'
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
// 批量数据
|
|
939
|
-
const users = [
|
|
940
|
-
{ username: 'user1', email: 'user1@example.com', age: 25 },
|
|
941
|
-
{ username: 'u2', email: 'invalid', age: 15 }, // 两个错误
|
|
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);
|
|
948
|
-
|
|
949
|
-
console.log(result.summary);
|
|
950
|
-
/*
|
|
951
|
-
{
|
|
952
|
-
total: 3,
|
|
953
|
-
valid: 2,
|
|
954
|
-
invalid: 1,
|
|
955
|
-
duration: 5 // 毫秒
|
|
956
|
-
}
|
|
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#嵌套对象验证)
|
|
1049
|
-
|
|
1050
|
-
---
|
|
1051
|
-
|
|
1052
|
-
### 数组高级验证
|
|
1053
|
-
|
|
1054
|
-
**场景**: 验证订单商品列表
|
|
1055
|
-
|
|
1056
|
-
```javascript
|
|
1057
|
-
const { dsl, validate } = require('schema-dsl');
|
|
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');
|
|
1842
|
-
|
|
1843
|
-
const pluginManager = new PluginManager();
|
|
1844
|
-
|
|
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
|
-
// 注册自定义格式插件
|
|
1868
|
-
pluginManager.register({
|
|
1869
|
-
name: 'custom-formats',
|
|
1870
|
-
|
|
1871
|
-
formats: {
|
|
1872
|
-
'hex-color': {
|
|
1873
|
-
validate: (value) => /^#[0-9A-F]{6}$/i.test(value),
|
|
1874
|
-
message: '必须是有效的十六进制颜色代码'
|
|
1875
|
-
},
|
|
1876
|
-
'mac-address': {
|
|
1877
|
-
validate: (value) => /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/i.test(value),
|
|
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-!'
|
|
2033
|
-
});
|
|
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
|
-
}
|
|
2042
|
-
});
|
|
2043
|
-
```
|
|
2044
|
-
|
|
2045
|
-
---
|
|
2046
|
-
|
|
2047
|
-
### Q5: 如何自定义错误消息?
|
|
2048
|
-
|
|
2049
|
-
```javascript
|
|
2050
|
-
const schema = dsl({
|
|
2051
|
-
username: dsl('string:3-32!')
|
|
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
|
-
});
|
|
2065
|
-
```
|
|
2066
|
-
|
|
2067
|
-
---
|
|
2068
|
-
|
|
2069
|
-
### Q6: 类型对照表
|
|
2070
|
-
|
|
2071
|
-
| JavaScript 条件 | schema-dsl 写法 |
|
|
2072
|
-
|----------------|----------------|
|
|
2073
|
-
| `if (!data)` | `'string!'` 或 `.assert(data)` |
|
|
2074
|
-
| `if (typeof data === 'object')` | `'object!'` |
|
|
2075
|
-
| `if (typeof data === 'string')` | `'string!'` |
|
|
2076
|
-
| `if (typeof data === 'number')` | `'number!'` |
|
|
2077
|
-
| `if (Array.isArray(data))` | `'array!'` |
|
|
2078
|
-
| `if (data === null)` | `'null!'` |
|
|
2079
|
-
| `if (data > 0)` | `'number:0-!'` |
|
|
2080
|
-
| `if (data.length >= 3)` | `'string:3-!'` |
|
|
2081
|
-
|
|
2082
|
-
---
|
|
2083
|
-
|
|
2084
|
-
### Q7: 如何合并多个 dsl.if() 验证?
|
|
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');
|
|
2167
|
-
|
|
2168
|
-
// 方式1:直接抛出
|
|
2169
|
-
I18nError.throw('account.notFound');
|
|
2170
|
-
// 中文: "账户不存在"
|
|
2171
|
-
// 英文: "Account not found"
|
|
2172
|
-
|
|
2173
|
-
// 方式2:带参数插值
|
|
2174
|
-
I18nError.throw('account.insufficientBalance', {
|
|
2175
|
-
balance: 50,
|
|
2176
|
-
required: 100
|
|
2177
|
-
});
|
|
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
|
-
|
|
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
|
-
try {
|
|
2236
|
-
dsl.error.throw('account.notFound');
|
|
2237
|
-
} catch (error) {
|
|
2238
|
-
error.code // 40001 (数字代码)
|
|
2239
|
-
error.originalKey // 'account.notFound' (原始key)
|
|
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;
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
```
|
|
2264
|
-
|
|
2265
|
-
**优势**:
|
|
2266
|
-
- ✅ 多语言共享相同的数字 `code`,前端统一处理
|
|
2267
|
-
- ✅ 完全向后兼容,字符串格式自动转换
|
|
2268
|
-
- ✅ `originalKey` 便于调试和日志追踪
|
|
2269
|
-
- ✅ 数字 code 更简洁,易于管理和文档化
|
|
2270
|
-
|
|
2271
|
-
**错误码规范建议**:
|
|
2272
|
-
- `4xxxx` - 客户端错误(账户、权限、参数等)
|
|
2273
|
-
- `5xxxx` - 业务逻辑错误(订单、支付、库存等)
|
|
2274
|
-
- `6xxxx` - 系统错误(数据库、服务不可用等)
|
|
2275
|
-
|
|
2276
|
-
📖 详细说明: [错误处理文档](./docs/error-handling.md#v115-新功能对象格式错误配置)
|
|
2277
|
-
|
|
2278
|
-
**🆕 运行时指定语言(v1.1.0+)**
|
|
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
|
-
});
|
|
2303
|
-
|
|
2304
|
-
// 同一请求中使用不同语言
|
|
2305
|
-
const error1 = dsl.error.create('account.notFound', {}, 404, 'zh-CN');
|
|
2306
|
-
console.log(error1.message); // "账户不存在"
|
|
2307
|
-
|
|
2308
|
-
const error2 = dsl.error.create('account.notFound', {}, 404, 'en-US');
|
|
2309
|
-
console.log(error2.message); // "Account not found"
|
|
2310
|
-
```
|
|
2311
|
-
|
|
2312
|
-
**Express/Koa 集成**:
|
|
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
|
-
});
|
|
2321
|
-
|
|
2322
|
-
// 业务代码中使用
|
|
2323
|
-
app.post('/withdraw', (req, res) => {
|
|
2324
|
-
const account = getAccount(req.user.id);
|
|
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
|
-
});
|
|
2333
|
-
```
|
|
2334
|
-
|
|
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
|
-
---
|
|
2345
|
-
|
|
2346
|
-
## 🤝 贡献指南
|
|
2347
|
-
|
|
2348
|
-
欢迎贡献代码、报告问题或提出建议!
|
|
2349
|
-
|
|
2350
|
-
### 开发环境
|
|
2351
|
-
|
|
2352
|
-
```bash
|
|
2353
|
-
# 克隆仓库
|
|
2354
|
-
git clone https://github.com/vextjs/schema-dsl.git
|
|
2355
|
-
cd schema-dsl
|
|
2356
|
-
|
|
2357
|
-
# 安装依赖
|
|
2358
|
-
npm install
|
|
2359
|
-
|
|
2360
|
-
# 运行测试
|
|
2361
|
-
npm test
|
|
2362
|
-
|
|
2363
|
-
# 代码检查
|
|
2364
|
-
npm run lint
|
|
2365
|
-
|
|
2366
|
-
# 查看测试覆盖率
|
|
2367
|
-
npm run coverage
|
|
2368
|
-
```
|
|
2369
|
-
|
|
2370
|
-
### 提交规范
|
|
2371
|
-
|
|
2372
|
-
- 🐛 **Bug 修复**: `fix: 修复XXX问题`
|
|
2373
|
-
- ✨ **新功能**: `feat: 添加XXX功能`
|
|
2374
|
-
- 📝 **文档**: `docs: 更新XXX文档`
|
|
2375
|
-
- 🎨 **代码格式**: `style: 格式化代码`
|
|
2376
|
-
- ♻️ **重构**: `refactor: 重构XXX模块`
|
|
2377
|
-
- ✅ **测试**: `test: 添加XXX测试`
|
|
2378
|
-
|
|
2379
|
-
详见 [贡献指南](./CONTRIBUTING.md)
|
|
2380
|
-
|
|
2381
|
-
---
|
|
2382
|
-
|
|
2383
|
-
## 📄 开源协议
|
|
2384
|
-
|
|
2385
|
-
[MIT License](./LICENSE)
|
|
2386
|
-
|
|
2387
|
-
---
|
|
2388
|
-
|
|
2389
|
-
## 🙏 致谢
|
|
2390
|
-
|
|
2391
|
-
- 感谢 [ajv](https://github.com/ajv-validator/ajv) 提供强大的验证引擎
|
|
2392
|
-
- 感谢所有贡献者和用户的支持
|
|
2393
|
-
|
|
2394
|
-
---
|
|
2395
|
-
|
|
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) - 安全问题报告
|
|
2476
|
-
|
|
2477
|
-
---
|
|
2478
|
-
|
|
2479
|
-
<div align="center">
|
|
2480
|
-
|
|
2481
|
-
**⭐ 如果这个项目对你有帮助,请给一个 Star!**
|
|
2482
|
-
|
|
2483
|
-
Made with ❤️ by schema-dsl team
|
|
2484
|
-
|
|
2485
|
-
</div>
|
|
2486
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🎯 schema-dsl
|
|
4
|
+
|
|
5
|
+
**Declare field rules with the simplest DSL — let one schema drive validation, derivation, export, and documentation.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/schema-dsl)
|
|
8
|
+
[](https://www.npmjs.com/package/schema-dsl)
|
|
9
|
+
[](https://github.com/vextjs/schema-dsl/actions)
|
|
10
|
+
[](https://www.typescriptlang.org/)
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
|
|
13
|
+
[Quick Start](#-quick-start) · [Documentation](https://vextjs.github.io/schema-dsl) · [Feature Overview](#-feature-overview) · [Examples](./examples)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install schema-dsl
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## ⚡ TL;DR (30-second intro)
|
|
24
|
+
|
|
25
|
+
**What is schema-dsl?**
|
|
26
|
+
|
|
27
|
+
Write field rules like this:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { dsl, validate } from 'schema-dsl';
|
|
31
|
+
|
|
32
|
+
const userSchema = dsl({
|
|
33
|
+
username: 'string:3-32!',
|
|
34
|
+
email: 'email!',
|
|
35
|
+
role: 'admin|user|guest',
|
|
36
|
+
contact: 'types:email|phone'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = validate(userSchema, req.body);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then that **same set of rules** continues to power:
|
|
43
|
+
|
|
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
|
|
50
|
+
|
|
51
|
+
**5-minute tutorial**: [Quick Start](https://vextjs.github.io/schema-dsl/quick-start) | **Full docs**: [Online Documentation](https://vextjs.github.io/schema-dsl)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 🗺️ Documentation
|
|
56
|
+
|
|
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
|
|
61
|
+
|
|
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
|
|
68
|
+
|
|
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
|
|
73
|
+
|
|
74
|
+
**Full docs**: [Online Documentation](https://vextjs.github.io/schema-dsl) · [Feature Index](https://vextjs.github.io/schema-dsl/FEATURE-INDEX)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## ✨ Why schema-dsl?
|
|
79
|
+
|
|
80
|
+
### 🎯 Minimal DSL — 65% less code
|
|
81
|
+
|
|
82
|
+
<table>
|
|
83
|
+
<tr>
|
|
84
|
+
<td width="50%" valign="top">
|
|
85
|
+
|
|
86
|
+
**❌ Traditional approach** — verbose
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Joi — requires 8 lines
|
|
90
|
+
const schema = Joi.object({
|
|
91
|
+
username: Joi.string()
|
|
92
|
+
.min(3).max(32).required(),
|
|
93
|
+
email: Joi.string()
|
|
94
|
+
.email().required(),
|
|
95
|
+
age: Joi.number()
|
|
96
|
+
.min(18).max(120)
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
</td>
|
|
101
|
+
<td width="50%" valign="top">
|
|
102
|
+
|
|
103
|
+
**✅ schema-dsl** — concise and clean
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// just 3 lines
|
|
107
|
+
const schema = dsl({
|
|
108
|
+
username: 'string:3-32!',
|
|
109
|
+
email: 'email!',
|
|
110
|
+
age: 'number:18-120'
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
</table>
|
|
117
|
+
|
|
118
|
+
### 💪 Full-featured
|
|
119
|
+
|
|
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 |
|
|
132
|
+
|
|
133
|
+
### 🎨 One schema, many uses (unique capability)
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { dsl, exporters, SchemaUtils } from 'schema-dsl';
|
|
137
|
+
|
|
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!'
|
|
145
|
+
});
|
|
146
|
+
|
|
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']);
|
|
151
|
+
|
|
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);
|
|
156
|
+
|
|
157
|
+
// 📝 generate field documentation from the same schema
|
|
158
|
+
const markdown = exporters.MarkdownExporter.export(userSchema, { title: 'User Field Reference' });
|
|
159
|
+
```
|
|
160
|
+
|
|
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.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 📦 Installation
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm install schema-dsl
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Runtime requirement**: Node.js >= 18.0.0
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 🚀 Quick Start
|
|
176
|
+
|
|
177
|
+
### 1. Basic validation
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { dsl, validate } from 'schema-dsl';
|
|
181
|
+
|
|
182
|
+
const userSchema = dsl({
|
|
183
|
+
username: 'string:3-32!',
|
|
184
|
+
email: 'email!',
|
|
185
|
+
age: 'number:18-120',
|
|
186
|
+
role: 'admin|user|guest',
|
|
187
|
+
tags: 'array<string>'
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ✅ validation passed
|
|
191
|
+
const result = validate(userSchema, {
|
|
192
|
+
username: 'john_doe',
|
|
193
|
+
email: 'john@example.com',
|
|
194
|
+
age: 25,
|
|
195
|
+
role: 'user',
|
|
196
|
+
tags: ['verified']
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
console.log(result.valid); // true
|
|
200
|
+
console.log(result.data); // validated data
|
|
201
|
+
|
|
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
|
+
// ]
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 2. Async validation + Express integration
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
|
|
215
|
+
|
|
216
|
+
const createUserSchema = dsl({
|
|
217
|
+
username: 'string:3-32!',
|
|
218
|
+
email: 'email!',
|
|
219
|
+
password: 'string:8-32!'
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.post('/api/users', async (req, res, next) => {
|
|
223
|
+
try {
|
|
224
|
+
// throws ValidationError automatically on failure
|
|
225
|
+
const validData = await validateAsync(createUserSchema, req.body);
|
|
226
|
+
const user = await db.users.create(validData);
|
|
227
|
+
res.json({ success: true, data: user });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
next(error);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// global error handler
|
|
234
|
+
app.use((error, req, res, next) => {
|
|
235
|
+
if (error instanceof ValidationError) {
|
|
236
|
+
return res.status(400).json({ success: false, errors: error.errors });
|
|
237
|
+
}
|
|
238
|
+
next(error);
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 3. Schema reuse (create / update / public)
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { dsl, SchemaUtils } from 'schema-dsl';
|
|
246
|
+
|
|
247
|
+
const userSchema = dsl({
|
|
248
|
+
id: 'uuid!',
|
|
249
|
+
username: 'string:3-32!',
|
|
250
|
+
email: 'email!',
|
|
251
|
+
password: 'string:8-64!',
|
|
252
|
+
createdAt: 'string!'
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// create endpoint: remove server-generated fields
|
|
256
|
+
const createSchema = SchemaUtils.omit(userSchema, ['id', 'createdAt']);
|
|
257
|
+
|
|
258
|
+
// update endpoint: pick editable fields, all optional
|
|
259
|
+
const updateSchema = SchemaUtils.partial(
|
|
260
|
+
SchemaUtils.pick(userSchema, ['username', 'email'])
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// public response: hide sensitive fields
|
|
264
|
+
const publicSchema = SchemaUtils.omit(userSchema, ['password']);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 4. Database schema export
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { dsl, exporters } from 'schema-dsl';
|
|
271
|
+
|
|
272
|
+
const productSchema = dsl({
|
|
273
|
+
name: 'string:1-100!',
|
|
274
|
+
price: 'number:>0!',
|
|
275
|
+
stock: 'integer:0-!',
|
|
276
|
+
category: 'string!',
|
|
277
|
+
createdAt: 'datetime!'
|
|
278
|
+
});
|
|
279
|
+
|
|
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
|
+
}
|
|
295
|
+
}
|
|
296
|
+
*/
|
|
297
|
+
|
|
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
|
+
*/
|
|
309
|
+
|
|
310
|
+
// Markdown field documentation
|
|
311
|
+
const markdown = exporters.MarkdownExporter.export(productSchema, { title: 'Product Field Reference' });
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 🗒️ Feature Overview
|
|
317
|
+
|
|
318
|
+
### Common use cases
|
|
319
|
+
|
|
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) |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 📖 DSL Syntax Reference
|
|
335
|
+
|
|
336
|
+
### Basic types
|
|
337
|
+
|
|
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
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Built-in formats
|
|
370
|
+
|
|
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
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Fluent chain API (recommended for TypeScript)
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { dsl } from 'schema-dsl';
|
|
388
|
+
|
|
389
|
+
const schema = dsl({
|
|
390
|
+
username: dsl('string:3-32!')
|
|
391
|
+
.username()
|
|
392
|
+
.label('username')
|
|
393
|
+
.messages({ required: 'Username is required' }),
|
|
394
|
+
|
|
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
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Conditional validation
|
|
404
|
+
|
|
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
|
+
});
|
|
415
|
+
|
|
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
|
+
});
|
|
421
|
+
|
|
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
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## 🌍 Internationalization
|
|
433
|
+
|
|
434
|
+
```typescript
|
|
435
|
+
import { dsl, validate, Locale, I18nError } from 'schema-dsl';
|
|
436
|
+
|
|
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
|
|
440
|
+
|
|
441
|
+
// register a custom locale
|
|
442
|
+
Locale.addLocale('zh-CN', {
|
|
443
|
+
'user.notFound': 'User not found',
|
|
444
|
+
'user.forbidden': { code: 40003, message: 'Access forbidden' },
|
|
445
|
+
});
|
|
446
|
+
|
|
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
|
|
451
|
+
|
|
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
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## 🔌 Plugin System
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
import { PluginManager, Validator, dsl } from 'schema-dsl';
|
|
468
|
+
|
|
469
|
+
const pluginManager = new PluginManager();
|
|
470
|
+
|
|
471
|
+
// register a custom format plugin (must provide an install function)
|
|
472
|
+
pluginManager.register({
|
|
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)
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// create a Validator and install plugins
|
|
487
|
+
const validator = new Validator();
|
|
488
|
+
pluginManager.install(validator);
|
|
489
|
+
|
|
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' });
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## 🔧 Core API Reference
|
|
498
|
+
|
|
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) |
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## 📝 TypeScript Usage
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
import { dsl, validateAsync, ValidationError } from 'schema-dsl';
|
|
518
|
+
|
|
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')
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const validData = await validateAsync(userSchema, payload);
|
|
528
|
+
// validData has full type inference
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (error instanceof ValidationError) {
|
|
531
|
+
error.errors.forEach(e => console.log(`${e.path}: ${e.message}`));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
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.
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 🛠️ Development
|
|
542
|
+
|
|
543
|
+
```bash
|
|
544
|
+
npm run build # compile TypeScript
|
|
545
|
+
npm run test # run tests
|
|
546
|
+
npm run typecheck # type check
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
Local documentation preview:
|
|
550
|
+
|
|
551
|
+
```bash
|
|
552
|
+
cd website
|
|
553
|
+
npm run dev
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## 🤝 Contributing
|
|
559
|
+
|
|
560
|
+
```bash
|
|
561
|
+
git clone https://github.com/vextjs/schema-dsl.git
|
|
562
|
+
cd schema-dsl
|
|
563
|
+
npm install
|
|
564
|
+
npm test
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) for details.
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## 🔗 Links
|
|
572
|
+
|
|
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
|
|
581
|
+
|
|
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)
|
|
597
|
+
|
|
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
|
|
604
|
+
|
|
605
|
+
### 📝 Changelog & contributing
|
|
606
|
+
- [Changelog](./CHANGELOG.md)
|
|
607
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|
|
608
|
+
- [Security Policy](./SECURITY.md)
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## 📄 License
|
|
613
|
+
|
|
614
|
+
[MIT](./LICENSE)
|
|
615
|
+
|
|
616
|
+
---
|
|
617
|
+
|
|
618
|
+
<div align="center">
|
|
619
|
+
|
|
620
|
+
If this project is useful to you, please consider giving it a Star ⭐
|
|
621
|
+
|
|
622
|
+
Made with ❤️ by the schema-dsl team
|
|
623
|
+
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
|