schema-dsl 1.2.5 → 2.0.0

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