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.
Files changed (243) hide show
  1. package/CHANGELOG.md +130 -238
  2. package/LICENSE +21 -21
  3. package/README.md +628 -2486
  4. package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
  5. package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
  6. package/dist/Validator-CllRdrY0.d.ts +192 -0
  7. package/dist/Validator-D6okG9tr.d.cts +192 -0
  8. package/dist/index.cjs +6640 -0
  9. package/dist/index.d.cts +1151 -0
  10. package/dist/index.d.ts +1151 -0
  11. package/dist/index.js +6574 -0
  12. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  13. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  14. package/dist/plugins/custom-format.cjs +3818 -0
  15. package/dist/plugins/custom-format.d.cts +12 -0
  16. package/dist/plugins/custom-format.d.ts +12 -0
  17. package/dist/plugins/custom-format.js +3788 -0
  18. package/dist/plugins/custom-type-example.cjs +3811 -0
  19. package/dist/plugins/custom-type-example.d.cts +8 -0
  20. package/dist/plugins/custom-type-example.d.ts +8 -0
  21. package/dist/plugins/custom-type-example.js +3781 -0
  22. package/dist/plugins/custom-validator.cjs +144 -0
  23. package/dist/plugins/custom-validator.d.cts +10 -0
  24. package/dist/plugins/custom-validator.d.ts +10 -0
  25. package/dist/plugins/custom-validator.js +119 -0
  26. package/docs/FEATURE-INDEX.md +553 -519
  27. package/docs/add-custom-locale.md +496 -483
  28. package/docs/add-keyword.md +24 -0
  29. package/docs/api-reference.md +1047 -805
  30. package/docs/api.md +13 -0
  31. package/docs/best-practices-project-structure.md +417 -408
  32. package/docs/best-practices.md +712 -672
  33. package/docs/cache-manager.md +344 -336
  34. package/docs/compile.md +45 -0
  35. package/docs/conditional-api.md +1307 -1278
  36. package/docs/custom-extensions-guide.md +339 -411
  37. package/docs/design-philosophy.md +606 -601
  38. package/docs/doc-index.md +324 -0
  39. package/docs/dsl-syntax.md +714 -664
  40. package/docs/dynamic-locale.md +608 -598
  41. package/docs/enum.md +482 -475
  42. package/docs/error-handling.md +1975 -1966
  43. package/docs/export-guide.md +501 -462
  44. package/docs/export-limitations.md +567 -551
  45. package/docs/faq.md +596 -577
  46. package/docs/frontend-i18n-guide.md +307 -293
  47. package/docs/i18n-user-guide.md +487 -474
  48. package/docs/i18n.md +476 -457
  49. package/docs/index.md +48 -0
  50. package/docs/json-schema-basics.md +40 -0
  51. package/docs/label-vs-description.md +271 -262
  52. package/docs/markdown-exporter.md +406 -397
  53. package/docs/mongodb-exporter.md +302 -295
  54. package/docs/multi-language.md +26 -0
  55. package/docs/multi-type-support.md +322 -329
  56. package/docs/mysql-exporter.md +280 -273
  57. package/docs/number-operators.md +449 -442
  58. package/docs/optional-marker-guide.md +326 -321
  59. package/docs/performance-guide.md +49 -0
  60. package/docs/plugin-system.md +381 -542
  61. package/docs/plugin-type-registration.md +34 -0
  62. package/docs/postgresql-exporter.md +311 -304
  63. package/docs/public/favicon.svg +5 -0
  64. package/docs/quick-start.md +435 -761
  65. package/docs/runtime-locale-support.md +532 -521
  66. package/docs/schema-helper.md +345 -340
  67. package/docs/schema-utils-advanced-issues.md +23 -0
  68. package/docs/schema-utils-best-practices.md +20 -0
  69. package/docs/schema-utils-chaining.md +150 -143
  70. package/docs/schema-utils.md +524 -490
  71. package/docs/security-checklist.md +20 -0
  72. package/docs/string-extensions.md +488 -480
  73. package/docs/troubleshooting.md +486 -471
  74. package/docs/type-converter.md +310 -319
  75. package/docs/type-reference.md +242 -219
  76. package/docs/typescript-guide.md +584 -573
  77. package/docs/union-type-guide.md +157 -147
  78. package/docs/union-types.md +284 -277
  79. package/docs/validate-async.md +491 -480
  80. package/docs/validate-batch.md +49 -0
  81. package/docs/validate-dsl-object-support.md +578 -573
  82. package/docs/validate.md +506 -486
  83. package/docs/validation-guide.md +502 -484
  84. package/docs/validator.md +39 -0
  85. package/package.json +131 -73
  86. package/plugins/custom-format.cjs +8 -0
  87. package/plugins/custom-type-example.cjs +8 -0
  88. package/plugins/custom-validator.cjs +8 -0
  89. package/src/adapters/DslAdapter.ts +111 -0
  90. package/src/adapters/index.ts +1 -0
  91. package/src/config/constants.ts +83 -0
  92. package/src/config/index.ts +2 -0
  93. package/src/config/patterns.ts +77 -0
  94. package/src/core/CacheManager.ts +169 -0
  95. package/src/core/ConditionalBuilder.ts +382 -0
  96. package/src/core/ConditionalRuntime.ts +28 -0
  97. package/src/core/ConditionalValidator.ts +255 -0
  98. package/src/core/DslBuilder.ts +687 -0
  99. package/src/core/ErrorCodes.ts +38 -0
  100. package/src/core/ErrorFormatter.ts +271 -0
  101. package/src/core/JSONSchemaCore.ts +65 -0
  102. package/src/core/Locale.ts +187 -0
  103. package/src/core/MessageTemplate.ts +42 -0
  104. package/src/core/ObjectDslBuilder.ts +64 -0
  105. package/src/core/PluginManager.ts +326 -0
  106. package/src/core/StringExtensions.ts +140 -0
  107. package/src/core/TemplateEngine.ts +44 -0
  108. package/src/core/Validator.ts +448 -0
  109. package/src/errors/I18nError.ts +159 -0
  110. package/src/errors/ValidationError.ts +105 -0
  111. package/src/exporters/BaseExporter.ts +60 -0
  112. package/src/exporters/MarkdownExporter.ts +305 -0
  113. package/src/exporters/MongoDBExporter.ts +126 -0
  114. package/src/exporters/MySQLExporter.ts +156 -0
  115. package/src/exporters/PostgreSQLExporter.ts +222 -0
  116. package/src/exporters/index.ts +18 -0
  117. package/src/index.ts +651 -0
  118. package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
  119. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
  120. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
  121. package/src/locales/index.ts +103 -0
  122. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
  123. package/src/locales/types.ts +156 -0
  124. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
  125. package/src/parser/ConstraintParser.ts +101 -0
  126. package/src/parser/DslParser.ts +470 -0
  127. package/src/parser/SchemaCompiler.ts +66 -0
  128. package/src/parser/TypeRegistry.ts +250 -0
  129. package/src/parser/index.ts +6 -0
  130. package/src/plugins/custom-format.ts +124 -0
  131. package/src/plugins/custom-type-example.ts +106 -0
  132. package/src/plugins/custom-validator.ts +138 -0
  133. package/src/types/conditional.ts +28 -0
  134. package/src/types/config.ts +59 -0
  135. package/src/types/dsl.ts +131 -0
  136. package/src/types/error.ts +60 -0
  137. package/src/types/index.ts +17 -0
  138. package/src/types/infer.ts +128 -0
  139. package/src/types/plugin.ts +58 -0
  140. package/src/types/safe-regex.d.ts +9 -0
  141. package/src/types/schema.ts +66 -0
  142. package/src/types/validate.ts +71 -0
  143. package/src/utils/SchemaHelper.ts +196 -0
  144. package/src/utils/SchemaUtils.ts +365 -0
  145. package/src/utils/TypeConverter.ts +215 -0
  146. package/src/utils/index.ts +10 -0
  147. package/src/validators/CustomKeywords.ts +477 -0
  148. package/.eslintignore +0 -11
  149. package/.eslintrc.json +0 -27
  150. package/CONTRIBUTING.md +0 -368
  151. package/STATUS.md +0 -491
  152. package/changelogs/v1.0.0.md +0 -328
  153. package/changelogs/v1.0.9.md +0 -367
  154. package/changelogs/v1.1.0.md +0 -389
  155. package/changelogs/v1.1.1.md +0 -308
  156. package/changelogs/v1.1.2.md +0 -183
  157. package/changelogs/v1.1.3.md +0 -161
  158. package/changelogs/v1.1.4.md +0 -432
  159. package/changelogs/v1.1.5.md +0 -493
  160. package/changelogs/v1.1.6.md +0 -211
  161. package/changelogs/v1.1.8.md +0 -376
  162. package/changelogs/v1.2.3.md +0 -124
  163. package/docs/INDEX.md +0 -252
  164. package/docs/issues-resolved-summary.md +0 -196
  165. package/docs/performance-benchmark-report.md +0 -179
  166. package/docs/performance-quick-reference.md +0 -123
  167. package/docs/user-questions-answered.md +0 -353
  168. package/docs/validation-rules-v1.0.2.md +0 -1608
  169. package/examples/README.md +0 -81
  170. package/examples/array-dsl-example.js +0 -227
  171. package/examples/conditional-example.js +0 -288
  172. package/examples/conditional-non-object.js +0 -129
  173. package/examples/conditional-validate-example.js +0 -321
  174. package/examples/custom-extension.js +0 -85
  175. package/examples/dsl-match-example.js +0 -74
  176. package/examples/dsl-style.js +0 -118
  177. package/examples/dynamic-locale-configuration.js +0 -348
  178. package/examples/dynamic-locale-example.js +0 -287
  179. package/examples/enum.examples.js +0 -324
  180. package/examples/export-demo.js +0 -130
  181. package/examples/express-integration.js +0 -376
  182. package/examples/i18n-error-handling-complete.js +0 -381
  183. package/examples/i18n-error-handling-quickstart.md +0 -0
  184. package/examples/i18n-error.examples.js +0 -181
  185. package/examples/i18n-full-demo.js +0 -301
  186. package/examples/i18n-memory-safety.examples.js +0 -268
  187. package/examples/markdown-export.js +0 -71
  188. package/examples/middleware-usage.js +0 -93
  189. package/examples/new-features-comparison.js +0 -315
  190. package/examples/password-reset/README.md +0 -153
  191. package/examples/password-reset/schema.js +0 -26
  192. package/examples/password-reset/test.js +0 -101
  193. package/examples/plugin-system.examples.js +0 -205
  194. package/examples/schema-utils-chaining.examples.js +0 -250
  195. package/examples/simple-example.js +0 -122
  196. package/examples/slug.examples.js +0 -179
  197. package/examples/string-extensions.js +0 -297
  198. package/examples/union-type-example.js +0 -127
  199. package/examples/union-types-example.js +0 -77
  200. package/examples/user-registration/README.md +0 -156
  201. package/examples/user-registration/routes.js +0 -92
  202. package/examples/user-registration/schema.js +0 -150
  203. package/examples/user-registration/server.js +0 -74
  204. package/index.d.ts +0 -3658
  205. package/index.js +0 -475
  206. package/index.mjs +0 -60
  207. package/lib/adapters/DslAdapter.js +0 -995
  208. package/lib/adapters/index.js +0 -20
  209. package/lib/config/constants.js +0 -286
  210. package/lib/config/patterns/common.js +0 -47
  211. package/lib/config/patterns/creditCard.js +0 -9
  212. package/lib/config/patterns/idCard.js +0 -9
  213. package/lib/config/patterns/index.js +0 -9
  214. package/lib/config/patterns/licensePlate.js +0 -4
  215. package/lib/config/patterns/passport.js +0 -4
  216. package/lib/config/patterns/phone.js +0 -9
  217. package/lib/config/patterns/postalCode.js +0 -5
  218. package/lib/core/CacheManager.js +0 -376
  219. package/lib/core/ConditionalBuilder.js +0 -503
  220. package/lib/core/DslBuilder.js +0 -1589
  221. package/lib/core/ErrorCodes.js +0 -233
  222. package/lib/core/ErrorFormatter.js +0 -445
  223. package/lib/core/JSONSchemaCore.js +0 -347
  224. package/lib/core/Locale.js +0 -130
  225. package/lib/core/MessageTemplate.js +0 -98
  226. package/lib/core/PluginManager.js +0 -448
  227. package/lib/core/StringExtensions.js +0 -240
  228. package/lib/core/Validator.js +0 -654
  229. package/lib/errors/I18nError.js +0 -328
  230. package/lib/errors/ValidationError.js +0 -191
  231. package/lib/exporters/MarkdownExporter.js +0 -420
  232. package/lib/exporters/MongoDBExporter.js +0 -162
  233. package/lib/exporters/MySQLExporter.js +0 -212
  234. package/lib/exporters/PostgreSQLExporter.js +0 -289
  235. package/lib/exporters/index.js +0 -24
  236. package/lib/locales/index.js +0 -8
  237. package/lib/utils/LRUCache.js +0 -174
  238. package/lib/utils/SchemaHelper.js +0 -240
  239. package/lib/utils/SchemaUtils.js +0 -445
  240. package/lib/utils/TypeConverter.js +0 -245
  241. package/lib/utils/index.js +0 -13
  242. package/lib/validators/CustomKeywords.js +0 -616
  243. package/lib/validators/index.js +0 -11
@@ -0,0 +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 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
+ }