schema-dsl 2.0.0 → 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 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
package/src/core/DslBuilder.ts
CHANGED
|
@@ -1,677 +1,687 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DslBuilder — chainable DSL builder.
|
|
3
|
-
*
|
|
4
|
-
* v2 changes:
|
|
5
|
-
* - Constructor delegates to DslParser.parseString() (fixes DA-01/DA-02/DA-03)
|
|
6
|
-
* - Custom type registration delegates to TypeRegistry (fixes DB-01/DB-02: unifies three type lists)
|
|
7
|
-
* - _customMessages merges instead of overwriting (fixes v1 overwrite bug)
|
|
8
|
-
* - Implements IDslBuilder interface (error/optional/required/enum chain methods)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { JSONSchema } from '../types/schema.js'
|
|
12
|
-
import type { IDslBuilder } from '../types/dsl.js'
|
|
13
|
-
import { DslParser } from '../parser/DslParser.js'
|
|
14
|
-
import { TypeRegistry } from '../parser/TypeRegistry.js'
|
|
15
|
-
import { PATTERNS } from '../config/patterns.js'
|
|
16
|
-
import
|
|
17
|
-
import type {
|
|
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
|
-
private
|
|
48
|
-
private
|
|
49
|
-
private
|
|
50
|
-
private
|
|
51
|
-
private
|
|
52
|
-
private
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
80
|
-
this.
|
|
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
|
-
* @param
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let
|
|
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
|
-
|
|
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
|
-
this.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
this
|
|
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
|
-
this.
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return this.
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
this._baseSchema.
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
this.
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
this._baseSchema.
|
|
609
|
-
return this
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
if (
|
|
638
|
-
schema.
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
1
|
+
/**
|
|
2
|
+
* DslBuilder — chainable DSL builder.
|
|
3
|
+
*
|
|
4
|
+
* v2 changes:
|
|
5
|
+
* - Constructor delegates to DslParser.parseString() (fixes DA-01/DA-02/DA-03)
|
|
6
|
+
* - Custom type registration delegates to TypeRegistry (fixes DB-01/DB-02: unifies three type lists)
|
|
7
|
+
* - _customMessages merges instead of overwriting (fixes v1 overwrite bug)
|
|
8
|
+
* - Implements IDslBuilder interface (error/optional/required/enum chain methods)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
12
|
+
import type { IDslBuilder } from '../types/dsl.js'
|
|
13
|
+
import { DslParser } from '../parser/DslParser.js'
|
|
14
|
+
import { TypeRegistry } from '../parser/TypeRegistry.js'
|
|
15
|
+
import { PATTERNS } from '../config/patterns.js'
|
|
16
|
+
import safeRegex from 'safe-regex'
|
|
17
|
+
import type { Validator as ValidatorInstance } from './Validator.js'
|
|
18
|
+
import type { ValidationResult } from '../types/validate.js'
|
|
19
|
+
|
|
20
|
+
// ==================== Internal Utilities ====================
|
|
21
|
+
|
|
22
|
+
type CustomValidatorFn = (value: unknown) => unknown
|
|
23
|
+
|
|
24
|
+
/** Password strength presets. */
|
|
25
|
+
const PASSWORD_PATTERNS: Record<string, RegExp> = {
|
|
26
|
+
weak: /.{6,}/,
|
|
27
|
+
medium: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
|
|
28
|
+
strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
|
|
29
|
+
veryStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{10,}$/,
|
|
30
|
+
}
|
|
31
|
+
const PASSWORD_MIN_LENGTHS: Record<string, number> = {
|
|
32
|
+
weak: 6, medium: 8, strong: 8, veryStrong: 10,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ==================== DslBuilder ====================
|
|
36
|
+
|
|
37
|
+
export class DslBuilder implements IDslBuilder {
|
|
38
|
+
// Required IDslBuilder field
|
|
39
|
+
readonly _isDslBuilder = true as const
|
|
40
|
+
|
|
41
|
+
/** schema-dsl custom validation keyword set (stripped during toJsonSchema). */
|
|
42
|
+
static readonly _internalKeys: ReadonlySet<string> = TypeRegistry.getInternalKeys()
|
|
43
|
+
|
|
44
|
+
/** Custom type cache (BC with v1 DslBuilder._customTypes). */
|
|
45
|
+
private static readonly _customTypes = new Map<string, JSONSchema | (() => JSONSchema)>()
|
|
46
|
+
|
|
47
|
+
private _baseSchema: JSONSchema
|
|
48
|
+
private _required: boolean
|
|
49
|
+
private _optional: boolean
|
|
50
|
+
private _customMessages: Record<string, string>
|
|
51
|
+
private _label: string | null
|
|
52
|
+
private _description: string | null
|
|
53
|
+
private _customValidators: CustomValidatorFn[]
|
|
54
|
+
private _whenConditions: unknown[]
|
|
55
|
+
|
|
56
|
+
// ==================== Constructor ====================
|
|
57
|
+
|
|
58
|
+
constructor(dslString: string) {
|
|
59
|
+
if (!dslString || typeof dslString !== 'string') {
|
|
60
|
+
throw new Error('[schema-dsl] DSL string is required')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let s = dslString.trim()
|
|
64
|
+
|
|
65
|
+
// array!N-M special syntax (v1 compat) → array:N-M + required=true
|
|
66
|
+
const arrayBangMatch = /^array!([\d-]+)$/.exec(s)
|
|
67
|
+
if (arrayBangMatch) {
|
|
68
|
+
s = `array:${arrayBangMatch[1]}`
|
|
69
|
+
this._required = true
|
|
70
|
+
this._optional = false
|
|
71
|
+
} else {
|
|
72
|
+
this._required = s.endsWith('!')
|
|
73
|
+
this._optional = s.endsWith('?') && !this._required
|
|
74
|
+
if (this._required || this._optional) s = s.slice(0, -1)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this._customMessages = {}
|
|
78
|
+
this._label = null
|
|
79
|
+
this._description = null
|
|
80
|
+
this._customValidators = []
|
|
81
|
+
this._whenConditions = []
|
|
82
|
+
|
|
83
|
+
this._baseSchema = DslBuilder._parseBody(s)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ==================== Internal Parsing ====================
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Parse DSL body (without ! or ?).
|
|
90
|
+
* Delegates to the unified parser so string and builder DSL parsing stay in lockstep.
|
|
91
|
+
*/
|
|
92
|
+
private static _parseBody(dsl: string): JSONSchema {
|
|
93
|
+
return DslParser.parseString(dsl)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ==================== Static Methods (BC with v1) ====================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Register a custom type (delegates to TypeRegistry).
|
|
100
|
+
*/
|
|
101
|
+
static registerType(name: string, schema: JSONSchema | (() => JSONSchema)): void {
|
|
102
|
+
if (!name || typeof name !== 'string') {
|
|
103
|
+
throw new Error('[schema-dsl] Type name must be a non-empty string')
|
|
104
|
+
}
|
|
105
|
+
if (!schema || (typeof schema !== 'object' && typeof schema !== 'function')) {
|
|
106
|
+
throw new Error('[schema-dsl] Schema must be an object or function')
|
|
107
|
+
}
|
|
108
|
+
DslBuilder._customTypes.set(name, schema)
|
|
109
|
+
if (typeof schema === 'function') {
|
|
110
|
+
// Store function as a dynamic type — resolved on each access
|
|
111
|
+
TypeRegistry.registerDynamic(name, schema)
|
|
112
|
+
} else {
|
|
113
|
+
TypeRegistry.register(name, schema)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Check whether a type is registered (built-in or custom). */
|
|
118
|
+
static hasType(type: string): boolean {
|
|
119
|
+
return TypeRegistry.has(type)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Get all registered custom type names. */
|
|
123
|
+
static getCustomTypes(): string[] {
|
|
124
|
+
return Array.from(DslBuilder._customTypes.keys())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Clear all custom types (primarily for testing). */
|
|
128
|
+
static clearCustomTypes(): void {
|
|
129
|
+
TypeRegistry.clearCustomTypes()
|
|
130
|
+
DslBuilder._customTypes.clear()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Validate schema nesting depth.
|
|
135
|
+
* @param schema - JSON Schema to validate
|
|
136
|
+
* @param maxDepth - maximum allowed depth (default 3)
|
|
137
|
+
*/
|
|
138
|
+
static validateNestingDepth(
|
|
139
|
+
schema: JSONSchema,
|
|
140
|
+
maxDepth = 3,
|
|
141
|
+
): { valid: boolean; depth: number; path: string; message: string } {
|
|
142
|
+
let maxFound = 0
|
|
143
|
+
let deepestPath = ''
|
|
144
|
+
|
|
145
|
+
function traverse(obj: JSONSchema, depth: number, path: string, isRoot: boolean): void {
|
|
146
|
+
if (!isRoot && (obj.properties || obj.items)) {
|
|
147
|
+
if (depth > maxFound) {
|
|
148
|
+
maxFound = depth
|
|
149
|
+
deepestPath = path
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (obj.properties) {
|
|
153
|
+
const nextDepth = depth + 1
|
|
154
|
+
for (const key of Object.keys(obj.properties)) {
|
|
155
|
+
traverse(
|
|
156
|
+
(obj.properties as Record<string, JSONSchema>)[key],
|
|
157
|
+
nextDepth,
|
|
158
|
+
`${path}.${key}`.replace(/^\./, ''),
|
|
159
|
+
false,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (obj.items && !Array.isArray(obj.items)) {
|
|
164
|
+
traverse(obj.items as JSONSchema, depth, `${path}[]`, false)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
traverse(schema, 0, '', true)
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
valid: maxFound <= maxDepth,
|
|
172
|
+
depth: maxFound,
|
|
173
|
+
path: deepestPath,
|
|
174
|
+
message:
|
|
175
|
+
maxFound > maxDepth
|
|
176
|
+
? `Nesting depth ${maxFound} exceeds limit ${maxDepth}, path: ${deepestPath}`
|
|
177
|
+
: `Nesting depth ${maxFound} is within the limit`,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ==================== Private Utilities ====================
|
|
182
|
+
|
|
183
|
+
private _assertType(method: string, ...types: string[]): void {
|
|
184
|
+
const t = this._baseSchema.type as string
|
|
185
|
+
if (!types.includes(t)) {
|
|
186
|
+
throw new Error(`[schema-dsl] ${method}() only applies to ${types.join('/')} type`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private _assertStringType(method: string): void {
|
|
191
|
+
this._assertType(method, 'string')
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private _assertNumberType(method: string): void {
|
|
195
|
+
this._assertType(method, 'number', 'integer')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _assertObjectType(method: string): void {
|
|
199
|
+
this._assertType(method, 'object')
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private _assertArrayType(method: string): void {
|
|
203
|
+
this._assertType(method, 'array')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ==================== Common Chain Methods ====================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Set format.
|
|
210
|
+
*/
|
|
211
|
+
format(fmt: string): this {
|
|
212
|
+
this._baseSchema.format = fmt
|
|
213
|
+
return this
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Add regex validation.
|
|
218
|
+
*/
|
|
219
|
+
pattern(regex: RegExp | string, message?: string): this {
|
|
220
|
+
const source = regex instanceof RegExp ? regex.source : regex
|
|
221
|
+
if (!safeRegex(source)) {
|
|
222
|
+
throw new Error(`[schema-dsl] Unsafe regex pattern rejected (potential ReDoS): ${source}`)
|
|
223
|
+
}
|
|
224
|
+
return this._setPattern(source, message)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Internal: set pattern without safe-regex check (used by built-in validators with pre-approved patterns). */
|
|
228
|
+
private _setPattern(source: string, message?: string): this {
|
|
229
|
+
this._baseSchema.pattern = source
|
|
230
|
+
if (message) {
|
|
231
|
+
this._customMessages['string.pattern'] = message
|
|
232
|
+
}
|
|
233
|
+
return this
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Custom error messages (IDslBuilder: error; BC alias: messages).
|
|
238
|
+
*/
|
|
239
|
+
messages(msgs: Record<string, string>): this {
|
|
240
|
+
Object.assign(this._customMessages, msgs)
|
|
241
|
+
return this
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** IDslBuilder.error — alias for messages() */
|
|
245
|
+
error(msgs: Record<string, string>): this {
|
|
246
|
+
return this.messages(msgs)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Set field label (used in error messages).
|
|
251
|
+
*/
|
|
252
|
+
label(text: string): this {
|
|
253
|
+
this._label = text
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Set description.
|
|
259
|
+
*/
|
|
260
|
+
description(text: string): this {
|
|
261
|
+
this._description = text
|
|
262
|
+
return this
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Set default value.
|
|
267
|
+
*/
|
|
268
|
+
default(value: unknown): this {
|
|
269
|
+
this._baseSchema.default = value
|
|
270
|
+
return this
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Set allowed enum values (IDslBuilder).
|
|
275
|
+
*/
|
|
276
|
+
enum(...values: unknown[]): this {
|
|
277
|
+
this._baseSchema.enum = values
|
|
278
|
+
return this
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Mark field as optional.
|
|
283
|
+
*/
|
|
284
|
+
optional(): this {
|
|
285
|
+
this._required = false
|
|
286
|
+
this._optional = true
|
|
287
|
+
return this
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Mark field as required.
|
|
292
|
+
*/
|
|
293
|
+
required(): this {
|
|
294
|
+
this._required = true
|
|
295
|
+
this._optional = false
|
|
296
|
+
return this
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Add a custom validator function.
|
|
301
|
+
*/
|
|
302
|
+
custom(validatorFn: CustomValidatorFn): this {
|
|
303
|
+
if (typeof validatorFn !== 'function') {
|
|
304
|
+
throw new Error('[schema-dsl] Custom validator must be a function')
|
|
305
|
+
}
|
|
306
|
+
this._customValidators.push(validatorFn)
|
|
307
|
+
return this
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ==================== String Chain Methods ====================
|
|
311
|
+
|
|
312
|
+
/** String minimum length. */
|
|
313
|
+
min(n: number): this {
|
|
314
|
+
this._assertStringType('min')
|
|
315
|
+
this._baseSchema.minLength = n
|
|
316
|
+
return this
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** String maximum length. */
|
|
320
|
+
max(n: number): this {
|
|
321
|
+
this._assertStringType('max')
|
|
322
|
+
this._baseSchema.maxLength = n
|
|
323
|
+
return this
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** String exact length (→ exactLength custom keyword). */
|
|
327
|
+
length(n: number): this {
|
|
328
|
+
this._assertStringType('length')
|
|
329
|
+
this._baseSchema.exactLength = n
|
|
330
|
+
return this
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** String: only alphanumeric characters allowed. */
|
|
334
|
+
alphanum(): this {
|
|
335
|
+
this._assertStringType('alphanum')
|
|
336
|
+
this._baseSchema.alphanum = true
|
|
337
|
+
return this
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** String: no leading/trailing whitespace. */
|
|
341
|
+
trim(): this {
|
|
342
|
+
this._assertStringType('trim')
|
|
343
|
+
this._baseSchema.trim = true
|
|
344
|
+
return this
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** String: must be lowercase. */
|
|
348
|
+
lowercase(): this {
|
|
349
|
+
this._assertStringType('lowercase')
|
|
350
|
+
this._baseSchema.lowercase = true
|
|
351
|
+
return this
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** String: must be uppercase. */
|
|
355
|
+
uppercase(): this {
|
|
356
|
+
this._assertStringType('uppercase')
|
|
357
|
+
this._baseSchema.uppercase = true
|
|
358
|
+
return this
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** String: must be a valid JSON string. */
|
|
362
|
+
json(): this {
|
|
363
|
+
this._assertStringType('json')
|
|
364
|
+
this._baseSchema.jsonString = true
|
|
365
|
+
return this
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** String date format validation. */
|
|
369
|
+
dateFormat(fmt: string): this {
|
|
370
|
+
this._assertStringType('dateFormat')
|
|
371
|
+
this._baseSchema.dateFormat = fmt
|
|
372
|
+
return this
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** String: must be after the given date. */
|
|
376
|
+
after(date: string): this {
|
|
377
|
+
this._assertStringType('after')
|
|
378
|
+
this._baseSchema.dateGreater = date
|
|
379
|
+
return this
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** String: must be before the given date. */
|
|
383
|
+
before(date: string): this {
|
|
384
|
+
this._assertStringType('before')
|
|
385
|
+
this._baseSchema.dateLess = date
|
|
386
|
+
return this
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** v1.0.2 alias: dateGreater. */
|
|
390
|
+
dateGreater(date: string): this {
|
|
391
|
+
this._assertStringType('dateGreater')
|
|
392
|
+
this._baseSchema.dateGreater = date
|
|
393
|
+
return this
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** v1.0.2 alias: dateLess. */
|
|
397
|
+
dateLess(date: string): this {
|
|
398
|
+
this._assertStringType('dateLess')
|
|
399
|
+
this._baseSchema.dateLess = date
|
|
400
|
+
return this
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** String slug format validation. */
|
|
404
|
+
slug(): this {
|
|
405
|
+
this._assertStringType('slug')
|
|
406
|
+
this._baseSchema.pattern = '^[a-z0-9]+(?:-[a-z0-9]+)*$'
|
|
407
|
+
const existing = (this._baseSchema._customMessages as Record<string, string> | undefined) || {}
|
|
408
|
+
this._baseSchema._customMessages = { ...existing, pattern: 'pattern.slug' }
|
|
409
|
+
return this
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** String domain validation. */
|
|
413
|
+
domain(): this {
|
|
414
|
+
this._assertStringType('domain')
|
|
415
|
+
const cfg = PATTERNS.common.domain
|
|
416
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** String IP address validation (IPv4 or IPv6). */
|
|
420
|
+
ip(): this {
|
|
421
|
+
this._assertStringType('ip')
|
|
422
|
+
const cfg = PATTERNS.common.ip
|
|
423
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** String Base64 encoding validation. */
|
|
427
|
+
base64(): this {
|
|
428
|
+
this._assertStringType('base64')
|
|
429
|
+
const cfg = PATTERNS.common.base64
|
|
430
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/** String JWT token validation. */
|
|
434
|
+
jwt(): this {
|
|
435
|
+
this._assertStringType('jwt')
|
|
436
|
+
const cfg = PATTERNS.common.jwt
|
|
437
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ==================== Identity / Pattern Chain Methods ====================
|
|
441
|
+
|
|
442
|
+
/** Phone number validation (auto-corrects number → string). */
|
|
443
|
+
phone(country = 'cn'): this {
|
|
444
|
+
// Auto-correct type
|
|
445
|
+
if (this._baseSchema.type === 'number' || this._baseSchema.type === 'integer') {
|
|
446
|
+
this._baseSchema.type = 'string'
|
|
447
|
+
delete (this._baseSchema as Record<string, unknown>)['minimum']
|
|
448
|
+
delete (this._baseSchema as Record<string, unknown>)['maximum']
|
|
449
|
+
}
|
|
450
|
+
const cfg = PATTERNS.phone[country]
|
|
451
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported country: ${country}`)
|
|
452
|
+
if (cfg.min !== undefined && !this._baseSchema.minLength) this._baseSchema.minLength = cfg.min
|
|
453
|
+
if (cfg.max !== undefined && !this._baseSchema.maxLength) this._baseSchema.maxLength = cfg.max
|
|
454
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** phone() alias (BC). */
|
|
458
|
+
phoneNumber(country = 'cn'): this {
|
|
459
|
+
return this.phone(country)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** National ID (idCard) validation. */
|
|
463
|
+
idCard(country = 'cn'): this {
|
|
464
|
+
const lower = country.toLowerCase()
|
|
465
|
+
const cfg = PATTERNS.idCard[lower]
|
|
466
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported country for idCard: ${country}`)
|
|
467
|
+
if (cfg.min !== undefined && !this._baseSchema.minLength) this._baseSchema.minLength = cfg.min
|
|
468
|
+
if (cfg.max !== undefined && !this._baseSchema.maxLength) this._baseSchema.maxLength = cfg.max
|
|
469
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** URL slug validation. */
|
|
473
|
+
slugChain(): this {
|
|
474
|
+
return this._setPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/.source).messages({ pattern: 'pattern.slug' })
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Credit card number validation. */
|
|
478
|
+
creditCard(type = 'visa'): this {
|
|
479
|
+
const cfg = PATTERNS.creditCard[type.toLowerCase()]
|
|
480
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported credit card type: ${type}`)
|
|
481
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Vehicle license plate validation. */
|
|
485
|
+
licensePlate(country = 'cn'): this {
|
|
486
|
+
const cfg = PATTERNS.licensePlate[country.toLowerCase()]
|
|
487
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported country for licensePlate: ${country}`)
|
|
488
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Postal code validation. */
|
|
492
|
+
postalCode(country = 'cn'): this {
|
|
493
|
+
const cfg = PATTERNS.postalCode[country.toLowerCase()]
|
|
494
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported country for postalCode: ${country}`)
|
|
495
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** Passport number validation. */
|
|
499
|
+
passport(country = 'cn'): this {
|
|
500
|
+
const cfg = PATTERNS.passport[country.toLowerCase()]
|
|
501
|
+
if (!cfg) throw new Error(`[schema-dsl] Unsupported country for passport: ${country}`)
|
|
502
|
+
return this._setPattern(cfg.pattern.source).messages({ pattern: cfg.key })
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Username validation.
|
|
507
|
+
* @param preset - 'short'(3-16) | 'medium'(3-32) | 'long'(3-64) | 'N-M' | object
|
|
508
|
+
*/
|
|
509
|
+
username(preset: string | { minLength?: number; maxLength?: number; allowUnderscore?: boolean; allowNumber?: boolean } = 'medium'): this {
|
|
510
|
+
let minLength: number
|
|
511
|
+
let maxLength: number
|
|
512
|
+
let allowUnderscore = true
|
|
513
|
+
let allowNumber = true
|
|
514
|
+
|
|
515
|
+
if (typeof preset === 'string') {
|
|
516
|
+
const rangeMatch = /^(\d+)-(\d+)$/.exec(preset)
|
|
517
|
+
if (rangeMatch) {
|
|
518
|
+
minLength = parseInt(rangeMatch[1], 10)
|
|
519
|
+
maxLength = parseInt(rangeMatch[2], 10)
|
|
520
|
+
} else {
|
|
521
|
+
const presets: Record<string, { min: number; max: number }> = {
|
|
522
|
+
short: { min: 3, max: 16 },
|
|
523
|
+
medium: { min: 3, max: 32 },
|
|
524
|
+
long: { min: 3, max: 64 },
|
|
525
|
+
}
|
|
526
|
+
const p = presets[preset] ?? presets['medium']
|
|
527
|
+
minLength = p.min
|
|
528
|
+
maxLength = p.max
|
|
529
|
+
}
|
|
530
|
+
} else {
|
|
531
|
+
minLength = preset.minLength ?? 3
|
|
532
|
+
maxLength = preset.maxLength ?? 32
|
|
533
|
+
allowUnderscore = preset.allowUnderscore !== false
|
|
534
|
+
allowNumber = preset.allowNumber !== false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = minLength
|
|
538
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = maxLength
|
|
539
|
+
|
|
540
|
+
let pat = '^[a-zA-Z]'
|
|
541
|
+
if (allowUnderscore && allowNumber) {
|
|
542
|
+
pat += '[a-zA-Z0-9_]*$'
|
|
543
|
+
} else if (allowNumber) {
|
|
544
|
+
pat += '[a-zA-Z0-9]*$'
|
|
545
|
+
} else {
|
|
546
|
+
pat += '[a-zA-Z]*$'
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return this._setPattern(pat).messages({ pattern: 'pattern.username' })
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Password strength validation.
|
|
554
|
+
* @param strength - 'weak' | 'medium' | 'strong' | 'veryStrong'
|
|
555
|
+
*/
|
|
556
|
+
password(strength = 'medium'): this {
|
|
557
|
+
const pat = PASSWORD_PATTERNS[strength]
|
|
558
|
+
if (!pat) throw new Error(`[schema-dsl] Invalid password strength: ${strength}`)
|
|
559
|
+
if (!this._baseSchema.minLength) this._baseSchema.minLength = PASSWORD_MIN_LENGTHS[strength]
|
|
560
|
+
if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 64
|
|
561
|
+
return this._setPattern(pat.source).messages({ pattern: `pattern.password.${strength}` })
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ==================== Number Chain Methods ====================
|
|
565
|
+
|
|
566
|
+
/** Number decimal places limit. */
|
|
567
|
+
precision(n: number): this {
|
|
568
|
+
this._assertNumberType('precision')
|
|
569
|
+
this._baseSchema.precision = n
|
|
570
|
+
return this
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** Number multiple-of validation (standard JSON Schema multipleOf). */
|
|
574
|
+
multiple(n: number): this {
|
|
575
|
+
this._assertNumberType('multiple')
|
|
576
|
+
this._baseSchema.multipleOf = n
|
|
577
|
+
return this
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/** Number port validation (1–65535). */
|
|
581
|
+
port(): this {
|
|
582
|
+
this._assertNumberType('port')
|
|
583
|
+
this._baseSchema.port = true
|
|
584
|
+
return this
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ==================== Object Chain Methods ====================
|
|
588
|
+
|
|
589
|
+
/** Object: all defined properties are required. */
|
|
590
|
+
requireAll(): this {
|
|
591
|
+
this._assertObjectType('requireAll')
|
|
592
|
+
this._baseSchema.requiredAll = true
|
|
593
|
+
return this
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** Object strict mode: no additional properties allowed. */
|
|
597
|
+
strict(): this {
|
|
598
|
+
this._assertObjectType('strict')
|
|
599
|
+
this._baseSchema.strictSchema = true
|
|
600
|
+
return this
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ==================== Array Chain Methods ====================
|
|
604
|
+
|
|
605
|
+
/** Array: sparse arrays are not allowed. */
|
|
606
|
+
noSparse(): this {
|
|
607
|
+
this._assertArrayType('noSparse')
|
|
608
|
+
this._baseSchema.noSparse = true
|
|
609
|
+
return this
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Array: must contain the specified element. */
|
|
613
|
+
includesRequired(items: unknown[]): this {
|
|
614
|
+
this._assertArrayType('includesRequired')
|
|
615
|
+
if (!Array.isArray(items)) {
|
|
616
|
+
throw new Error('[schema-dsl] includesRequired() requires an array parameter')
|
|
617
|
+
}
|
|
618
|
+
this._baseSchema.includesRequired = items
|
|
619
|
+
return this
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ==================== Output Methods ====================
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Convert to a schema with schema-dsl internal fields (for use by Validator).
|
|
626
|
+
*/
|
|
627
|
+
toSchema(): JSONSchema {
|
|
628
|
+
const schema: JSONSchema = { ...this._baseSchema }
|
|
629
|
+
|
|
630
|
+
if (this._description) {
|
|
631
|
+
schema.description = this._description
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Merge _customMessages: base type messages + user custom messages (user takes priority)
|
|
635
|
+
const baseCustomMsgs = (schema._customMessages as Record<string, string> | undefined) || {}
|
|
636
|
+
const mergedMsgs = { ...baseCustomMsgs, ...this._customMessages }
|
|
637
|
+
if (Object.keys(mergedMsgs).length > 0) {
|
|
638
|
+
schema._customMessages = mergedMsgs
|
|
639
|
+
} else {
|
|
640
|
+
delete (schema as Record<string, unknown>)['_customMessages']
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (this._label) {
|
|
644
|
+
schema._label = this._label
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (this._customValidators.length > 0) {
|
|
648
|
+
schema._customValidators = this._customValidators as unknown[]
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (this._whenConditions.length > 0) {
|
|
652
|
+
schema._whenConditions = this._whenConditions
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Always output _required (BC with v1: output even when false)
|
|
656
|
+
schema._required = this._required
|
|
657
|
+
|
|
658
|
+
return schema
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Output a clean JSON Schema (strips all schema-dsl internal fields and custom keywords).
|
|
663
|
+
* Can be embedded directly in OpenAPI / standard JSON Schema documents.
|
|
664
|
+
*/
|
|
665
|
+
toJsonSchema(): JSONSchema {
|
|
666
|
+
return TypeRegistry.toJsonSchema(this.toSchema())
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
toString(): string {
|
|
670
|
+
return JSON.stringify(this.toJsonSchema())
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Validate data (BC with v1).
|
|
675
|
+
* @param data - data to validate
|
|
676
|
+
*/
|
|
677
|
+
private _validator: ValidatorInstance | null = null
|
|
678
|
+
|
|
679
|
+
async validate(data: unknown): Promise<ValidationResult<unknown>> {
|
|
680
|
+
if (!this._validator) {
|
|
681
|
+
const { Validator } = await import('./Validator.js')
|
|
682
|
+
this._validator = new Validator()
|
|
683
|
+
}
|
|
684
|
+
const schema = this.toSchema()
|
|
685
|
+
return this._validator.validate(schema, data)
|
|
686
|
+
}
|
|
687
|
+
}
|