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.
Files changed (145) hide show
  1. package/CHANGELOG.md +130 -113
  2. package/LICENSE +21 -21
  3. package/README.md +628 -628
  4. package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
  5. package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
  6. package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
  7. package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
  8. package/dist/index.cjs +75 -29
  9. package/dist/index.d.cts +10 -4
  10. package/dist/index.d.ts +10 -4
  11. package/dist/index.js +75 -29
  12. package/dist/plugins/custom-format.cjs +33 -17
  13. package/dist/plugins/custom-format.d.cts +1 -1
  14. package/dist/plugins/custom-format.d.ts +1 -1
  15. package/dist/plugins/custom-format.js +33 -17
  16. package/dist/plugins/custom-type-example.cjs +33 -17
  17. package/dist/plugins/custom-type-example.d.cts +1 -1
  18. package/dist/plugins/custom-type-example.d.ts +1 -1
  19. package/dist/plugins/custom-type-example.js +33 -17
  20. package/dist/plugins/custom-validator.cjs +0 -2
  21. package/dist/plugins/custom-validator.d.cts +1 -1
  22. package/dist/plugins/custom-validator.d.ts +1 -1
  23. package/dist/plugins/custom-validator.js +0 -2
  24. package/docs/FEATURE-INDEX.md +553 -553
  25. package/docs/add-custom-locale.md +496 -496
  26. package/docs/add-keyword.md +24 -24
  27. package/docs/api-reference.md +1047 -1047
  28. package/docs/api.md +13 -13
  29. package/docs/best-practices-project-structure.md +417 -417
  30. package/docs/best-practices.md +712 -712
  31. package/docs/cache-manager.md +344 -344
  32. package/docs/compile.md +45 -45
  33. package/docs/conditional-api.md +1307 -1307
  34. package/docs/custom-extensions-guide.md +339 -339
  35. package/docs/design-philosophy.md +606 -606
  36. package/docs/doc-index.md +324 -324
  37. package/docs/dsl-syntax.md +714 -714
  38. package/docs/dynamic-locale.md +608 -608
  39. package/docs/enum.md +482 -482
  40. package/docs/error-handling.md +1975 -1975
  41. package/docs/export-guide.md +501 -501
  42. package/docs/export-limitations.md +567 -567
  43. package/docs/faq.md +596 -596
  44. package/docs/frontend-i18n-guide.md +307 -307
  45. package/docs/i18n-user-guide.md +487 -487
  46. package/docs/i18n.md +476 -476
  47. package/docs/index.md +48 -48
  48. package/docs/json-schema-basics.md +40 -40
  49. package/docs/label-vs-description.md +271 -271
  50. package/docs/markdown-exporter.md +406 -406
  51. package/docs/mongodb-exporter.md +302 -302
  52. package/docs/multi-language.md +26 -26
  53. package/docs/multi-type-support.md +322 -322
  54. package/docs/mysql-exporter.md +280 -280
  55. package/docs/number-operators.md +449 -449
  56. package/docs/optional-marker-guide.md +326 -326
  57. package/docs/performance-guide.md +49 -49
  58. package/docs/plugin-system.md +381 -381
  59. package/docs/plugin-type-registration.md +34 -34
  60. package/docs/postgresql-exporter.md +311 -311
  61. package/docs/public/favicon.svg +4 -4
  62. package/docs/quick-start.md +435 -435
  63. package/docs/runtime-locale-support.md +532 -532
  64. package/docs/schema-helper.md +345 -345
  65. package/docs/schema-utils-advanced-issues.md +23 -23
  66. package/docs/schema-utils-best-practices.md +20 -20
  67. package/docs/schema-utils-chaining.md +150 -150
  68. package/docs/schema-utils.md +524 -524
  69. package/docs/security-checklist.md +20 -20
  70. package/docs/string-extensions.md +488 -488
  71. package/docs/troubleshooting.md +486 -486
  72. package/docs/type-converter.md +310 -310
  73. package/docs/type-reference.md +242 -242
  74. package/docs/typescript-guide.md +584 -584
  75. package/docs/union-type-guide.md +157 -157
  76. package/docs/union-types.md +284 -284
  77. package/docs/validate-async.md +491 -491
  78. package/docs/validate-batch.md +49 -49
  79. package/docs/validate-dsl-object-support.md +578 -578
  80. package/docs/validate.md +506 -506
  81. package/docs/validation-guide.md +502 -502
  82. package/docs/validator.md +39 -39
  83. package/package.json +131 -131
  84. package/plugins/custom-format.cjs +8 -8
  85. package/plugins/custom-type-example.cjs +8 -8
  86. package/plugins/custom-validator.cjs +8 -8
  87. package/src/adapters/DslAdapter.ts +111 -111
  88. package/src/adapters/index.ts +1 -1
  89. package/src/config/constants.ts +83 -83
  90. package/src/config/index.ts +2 -2
  91. package/src/config/patterns.ts +77 -77
  92. package/src/core/CacheManager.ts +169 -159
  93. package/src/core/ConditionalBuilder.ts +382 -382
  94. package/src/core/ConditionalRuntime.ts +27 -27
  95. package/src/core/ConditionalValidator.ts +254 -254
  96. package/src/core/DslBuilder.ts +687 -677
  97. package/src/core/ErrorCodes.ts +38 -38
  98. package/src/core/ErrorFormatter.ts +271 -271
  99. package/src/core/JSONSchemaCore.ts +65 -65
  100. package/src/core/Locale.ts +187 -187
  101. package/src/core/MessageTemplate.ts +42 -42
  102. package/src/core/ObjectDslBuilder.ts +64 -64
  103. package/src/core/PluginManager.ts +326 -326
  104. package/src/core/StringExtensions.ts +140 -140
  105. package/src/core/TemplateEngine.ts +44 -44
  106. package/src/core/Validator.ts +448 -448
  107. package/src/errors/I18nError.ts +159 -159
  108. package/src/errors/ValidationError.ts +105 -105
  109. package/src/exporters/BaseExporter.ts +60 -60
  110. package/src/exporters/MarkdownExporter.ts +305 -305
  111. package/src/exporters/MongoDBExporter.ts +126 -126
  112. package/src/exporters/MySQLExporter.ts +156 -155
  113. package/src/exporters/PostgreSQLExporter.ts +222 -222
  114. package/src/exporters/index.ts +18 -18
  115. package/src/index.ts +651 -633
  116. package/src/locales/en-US.ts +160 -160
  117. package/src/locales/es-ES.ts +160 -160
  118. package/src/locales/fr-FR.ts +160 -160
  119. package/src/locales/index.ts +103 -103
  120. package/src/locales/ja-JP.ts +160 -160
  121. package/src/locales/types.ts +156 -156
  122. package/src/locales/zh-CN.ts +160 -160
  123. package/src/parser/ConstraintParser.ts +101 -101
  124. package/src/parser/DslParser.ts +470 -470
  125. package/src/parser/SchemaCompiler.ts +66 -66
  126. package/src/parser/TypeRegistry.ts +250 -250
  127. package/src/parser/index.ts +6 -6
  128. package/src/plugins/custom-format.ts +124 -126
  129. package/src/plugins/custom-type-example.ts +106 -108
  130. package/src/plugins/custom-validator.ts +138 -140
  131. package/src/types/conditional.ts +28 -28
  132. package/src/types/config.ts +59 -59
  133. package/src/types/dsl.ts +131 -131
  134. package/src/types/error.ts +60 -60
  135. package/src/types/index.ts +17 -17
  136. package/src/types/infer.ts +127 -127
  137. package/src/types/plugin.ts +58 -58
  138. package/src/types/safe-regex.d.ts +9 -9
  139. package/src/types/schema.ts +66 -66
  140. package/src/types/validate.ts +71 -71
  141. package/src/utils/SchemaHelper.ts +196 -196
  142. package/src/utils/SchemaUtils.ts +365 -346
  143. package/src/utils/TypeConverter.ts +215 -215
  144. package/src/utils/index.ts +10 -10
  145. package/src/validators/CustomKeywords.ts +477 -477
@@ -1,66 +1,66 @@
1
- import type { JSONSchema } from '../types/schema.js'
2
- import type { TypeDefinition } from './TypeRegistry.js'
3
-
4
- /** Callback type for the afterCompile hook (synchronous only). */
5
- export type AfterCompileHook = (schema: JSONSchema) => void
6
-
7
- /**
8
- * SchemaCompiler — assembles TypeDefinition + constraints into the final JSONSchema.
9
- *
10
- * Responsibilities:
11
- * 1. Merge baseSchema + constraints (constraints take priority)
12
- * 2. Inject _label / _customMessages / _required (from TypeDefinition or caller)
13
- * 3. Fire afterCompile hooks
14
- */
15
- export const SchemaCompiler = {
16
- /**
17
- * Compile and return a JSONSchema (including internal keys; stripped by toJsonSchema).
18
- */
19
- compile(
20
- typeDef: TypeDefinition,
21
- constraints: Partial<JSONSchema>,
22
- meta?: {
23
- label?: string
24
- required?: boolean
25
- afterCompileHook?: AfterCompileHook
26
- }
27
- ): JSONSchema {
28
- // Merge baseSchema + constraints (constraints fields override baseSchema fields)
29
- const schema: JSONSchema = {
30
- ...typeDef.baseSchema,
31
- ...constraints,
32
- }
33
-
34
- // Inject type-level _customMessages (e.g., phone, idCard types)
35
- if (typeDef.customMessages) {
36
- schema['_customMessages'] = {
37
- ...(schema['_customMessages'] as Record<string, string> | undefined),
38
- ...typeDef.customMessages,
39
- }
40
- }
41
-
42
- // Inject meta
43
- if (meta?.label) schema['_label'] = meta.label
44
- if (meta?.required) schema['_required'] = true
45
-
46
- // Fire afterCompile hook (synchronous)
47
- if (meta?.afterCompileHook) {
48
- meta.afterCompileHook(schema)
49
- }
50
-
51
- return schema
52
- },
53
-
54
- /**
55
- * Strip internal keys and return a clean JSON Schema (used by toJsonSchema() output).
56
- */
57
- toJsonSchema(schema: JSONSchema, internalKeys: ReadonlySet<string>): JSONSchema {
58
- const result: JSONSchema = {}
59
- for (const [k, v] of Object.entries(schema)) {
60
- if (!internalKeys.has(k)) {
61
- result[k] = v
62
- }
63
- }
64
- return result
65
- },
66
- }
1
+ import type { JSONSchema } from '../types/schema.js'
2
+ import type { TypeDefinition } from './TypeRegistry.js'
3
+
4
+ /** Callback type for the afterCompile hook (synchronous only). */
5
+ export type AfterCompileHook = (schema: JSONSchema) => void
6
+
7
+ /**
8
+ * SchemaCompiler — assembles TypeDefinition + constraints into the final JSONSchema.
9
+ *
10
+ * Responsibilities:
11
+ * 1. Merge baseSchema + constraints (constraints take priority)
12
+ * 2. Inject _label / _customMessages / _required (from TypeDefinition or caller)
13
+ * 3. Fire afterCompile hooks
14
+ */
15
+ export const SchemaCompiler = {
16
+ /**
17
+ * Compile and return a JSONSchema (including internal keys; stripped by toJsonSchema).
18
+ */
19
+ compile(
20
+ typeDef: TypeDefinition,
21
+ constraints: Partial<JSONSchema>,
22
+ meta?: {
23
+ label?: string
24
+ required?: boolean
25
+ afterCompileHook?: AfterCompileHook
26
+ }
27
+ ): JSONSchema {
28
+ // Merge baseSchema + constraints (constraints fields override baseSchema fields)
29
+ const schema: JSONSchema = {
30
+ ...typeDef.baseSchema,
31
+ ...constraints,
32
+ }
33
+
34
+ // Inject type-level _customMessages (e.g., phone, idCard types)
35
+ if (typeDef.customMessages) {
36
+ schema['_customMessages'] = {
37
+ ...(schema['_customMessages'] as Record<string, string> | undefined),
38
+ ...typeDef.customMessages,
39
+ }
40
+ }
41
+
42
+ // Inject meta
43
+ if (meta?.label) schema['_label'] = meta.label
44
+ if (meta?.required) schema['_required'] = true
45
+
46
+ // Fire afterCompile hook (synchronous)
47
+ if (meta?.afterCompileHook) {
48
+ meta.afterCompileHook(schema)
49
+ }
50
+
51
+ return schema
52
+ },
53
+
54
+ /**
55
+ * Strip internal keys and return a clean JSON Schema (used by toJsonSchema() output).
56
+ */
57
+ toJsonSchema(schema: JSONSchema, internalKeys: ReadonlySet<string>): JSONSchema {
58
+ const result: JSONSchema = {}
59
+ for (const [k, v] of Object.entries(schema)) {
60
+ if (!internalKeys.has(k)) {
61
+ result[k] = v
62
+ }
63
+ }
64
+ return result
65
+ },
66
+ }
@@ -1,250 +1,250 @@
1
- import type { JSONSchema } from '../types/schema.js'
2
-
3
- /**
4
- * Type definition (structure of each entry in TypeRegistry)
5
- */
6
- export interface TypeDefinition {
7
- /** Base JSON Schema fragment for this type */
8
- baseSchema: Partial<JSONSchema>
9
- /** Custom messages associated with this type (e.g., error key for phone type) */
10
- customMessages?: Record<string, string>
11
- /** Whether this type uses a pattern (keys are standard JSON Schema fields, not stripped in toJsonSchema) */
12
- isPattern?: boolean
13
- }
14
-
15
- /** Known internal key set (stripped from output during toJsonSchema) */
16
- const INTERNAL_KEYS: ReadonlySet<string> = new Set([
17
- '_label',
18
- '_customMessages',
19
- '_description',
20
- '_required',
21
- '_isConditional',
22
- '_runtimeOnlyConditional',
23
- 'conditions',
24
- '_evaluateCondition',
25
- // Custom AJV keywords (non-standard JSON Schema fields, stripped on output)
26
- 'exactLength',
27
- 'alphanum',
28
- 'lowercase',
29
- 'uppercase',
30
- 'trim',
31
- 'jsonString',
32
- 'port',
33
- 'requiredAll',
34
- 'strictSchema',
35
- 'noSparse',
36
- 'includesRequired',
37
- 'dateFormat',
38
- 'dateGreater',
39
- 'dateLess',
40
- 'precision',
41
- // ⚠️ multipleOf is a standard JSON Schema field and is NOT in this list (fix DB-01)
42
- ])
43
-
44
- /**
45
- * Built-in type registry
46
- * 33 types covering v1 DslAdapter.typeMap + DslBuilder type lists (fixes inconsistencies DB-02, DA-01)
47
- */
48
- const BUILTIN_TYPES: Map<string, TypeDefinition> = new Map([
49
- // --- Primitive types ---
50
- ['string', { baseSchema: { type: 'string' } }],
51
- ['number', { baseSchema: { type: 'number' } }],
52
- ['integer', { baseSchema: { type: 'integer' } }],
53
- ['boolean', { baseSchema: { type: 'boolean' } }],
54
- ['object', { baseSchema: { type: 'object' } }],
55
- ['array', { baseSchema: { type: 'array' } }],
56
- ['null', { baseSchema: { type: 'null' } }],
57
- ['any', { baseSchema: {} }],
58
-
59
- // --- Format types ---
60
- ['email', { baseSchema: { type: 'string', format: 'email' } }],
61
- ['url', { baseSchema: { type: 'string', format: 'uri' } }],
62
- ['uri', { baseSchema: { type: 'string', format: 'uri' } }],
63
- ['uuid', { baseSchema: { type: 'string', format: 'uuid' } }],
64
- ['ipv4', { baseSchema: { type: 'string', format: 'ipv4' } }],
65
- ['ipv6', { baseSchema: { type: 'string', format: 'ipv6' } }],
66
- ['ip', { baseSchema: { anyOf: [{ type: 'string', format: 'ipv4' }, { type: 'string', format: 'ipv6' }] } }],
67
- ['hostname', { baseSchema: { type: 'string', format: 'hostname' } }],
68
- ['date', { baseSchema: { type: 'string', format: 'date' } }],
69
- ['datetime', { baseSchema: { type: 'string', format: 'date-time' } }],
70
- ['time', { baseSchema: { type: 'string', format: 'time' } }],
71
-
72
- // --- Special string types ---
73
- ['binary', { baseSchema: { type: 'string', contentEncoding: 'base64' } }],
74
- ['objectId', { baseSchema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, customMessages: { pattern: 'pattern.objectId' } }],
75
- ['hexColor', { baseSchema: { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' }, customMessages: { pattern: 'pattern.hexColor' } }],
76
- ['macAddress', {
77
- baseSchema: { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' },
78
- customMessages: { pattern: 'pattern.macAddress' },
79
- }],
80
- ['cron', {
81
- baseSchema: {
82
- type: 'string',
83
- pattern:
84
- '^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) ' +
85
- '(\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) ' +
86
- '(\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) ' +
87
- '(\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) ' +
88
- '(\\*|([0-6])|\\*\\/([0-6]))$',
89
- },
90
- customMessages: { pattern: 'pattern.cron' },
91
- }],
92
-
93
- // --- slug (fix DB-02: v1 DslAdapter was missing slug type definition) ---
94
- ['slug', {
95
- baseSchema: { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
96
- customMessages: { pattern: 'pattern.slug' },
97
- }],
98
-
99
- // --- CJK / Chinese ---
100
- ['chineseName', {
101
- baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]{2,10}$' },
102
- customMessages: { pattern: 'chineseName' },
103
- }],
104
- ['chinese', {
105
- baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]+$' },
106
- }],
107
-
108
- // --- Domain-related (handled by CustomKeywords; only baseSchema registered here) ---
109
- ['emailDomain', { baseSchema: { type: 'string', format: 'email' } }],
110
-
111
- // --- v1 extension types (DslBuilder v1.0.2) ---
112
- ['alphanum', { baseSchema: { type: 'string', alphanum: true } }],
113
- ['lower', { baseSchema: { type: 'string', lowercase: true } }],
114
- ['upper', { baseSchema: { type: 'string', uppercase: true } }],
115
- ['json', { baseSchema: { type: 'string', jsonString: true } }],
116
- ['port', { baseSchema: { type: 'integer', port: true } }],
117
- ])
118
-
119
- /**
120
- * Custom type registry (populated via registerType)
121
- */
122
- const CUSTOM_TYPES: Map<string, TypeDefinition> = new Map()
123
- const DYNAMIC_TYPES: Map<string, () => JSONSchema> = new Map()
124
-
125
- let _strictMode = false
126
-
127
- /**
128
- * TypeRegistry — unified type registration and resolution
129
- *
130
- * Replaces the three inconsistent type lists in v1 (fixes DB-01/DB-02/DA-01)
131
- */
132
- export const TypeRegistry = {
133
- /**
134
- * Resolve a type name to its TypeDefinition.
135
- * Built-in types take priority; custom types may override non-primitive built-ins.
136
- */
137
- resolve(typeName: string): TypeDefinition {
138
- // Dynamic types: call factory function each time
139
- const dynamicFn = DYNAMIC_TYPES.get(typeName)
140
- if (dynamicFn) {
141
- return { baseSchema: dynamicFn() as Partial<JSONSchema> }
142
- }
143
-
144
- const custom = CUSTOM_TYPES.get(typeName)
145
- if (custom) return custom
146
-
147
- const builtin = BUILTIN_TYPES.get(typeName)
148
- if (builtin) return builtin
149
-
150
- // Unknown type: throw in strict mode, otherwise warn and fall back to string
151
- if (_strictMode) {
152
- throw new Error(`[schema-dsl] Unknown type "${typeName}"`)
153
- }
154
- console.warn(`[schema-dsl] Unknown type "${typeName}", falling back to string`)
155
- return { baseSchema: { type: 'string' } }
156
- },
157
-
158
- /**
159
- * Register a custom type (delegated from DslBuilder.registerType)
160
- */
161
- register(name: string, def: TypeDefinition | Partial<JSONSchema>): void {
162
- if (!name || typeof name !== 'string') {
163
- throw new Error('[schema-dsl] TypeRegistry.register: name must be a non-empty string')
164
- }
165
- // Accept a raw Partial<JSONSchema> and wrap it automatically
166
- const normalized: TypeDefinition =
167
- 'baseSchema' in def ? (def as TypeDefinition) : { baseSchema: def as Partial<JSONSchema> }
168
- CUSTOM_TYPES.set(name, normalized)
169
- },
170
-
171
- /**
172
- * Register a dynamic type (factory function invoked on every resolve call)
173
- */
174
- registerDynamic(name: string, factory: () => JSONSchema): void {
175
- if (!name || typeof name !== 'string') {
176
- throw new Error('[schema-dsl] TypeRegistry.registerDynamic: name must be a non-empty string')
177
- }
178
- DYNAMIC_TYPES.set(name, factory)
179
- },
180
-
181
- /**
182
- * Unregister a custom type
183
- */
184
- unregister(name: string): void {
185
- CUSTOM_TYPES.delete(name)
186
- DYNAMIC_TYPES.delete(name)
187
- },
188
-
189
- /**
190
- * Clear all custom and dynamic types (primarily for testing; called by DslBuilder.clearCustomTypes).
191
- * Built-in types are unaffected.
192
- */
193
- clearCustomTypes(): void {
194
- CUSTOM_TYPES.clear()
195
- DYNAMIC_TYPES.clear()
196
- },
197
-
198
- /**
199
- * Enable or disable strict mode for type resolution.
200
- * In strict mode, resolving an unknown type throws instead of warning and falling back to string.
201
- */
202
- setStrict(flag: boolean): void {
203
- _strictMode = flag
204
- },
205
-
206
- /**
207
- * Check whether a type is registered (built-in or custom)
208
- */
209
- has(typeName: string): boolean {
210
- return BUILTIN_TYPES.has(typeName) || CUSTOM_TYPES.has(typeName) || DYNAMIC_TYPES.has(typeName)
211
- },
212
-
213
- /**
214
- * Return an iterator over all registered types (built-in + custom; custom overrides same-name built-in)
215
- * BC-4 compat: consumed by the DslAdapter.typeMap getter
216
- */
217
- entries(): IterableIterator<[string, TypeDefinition]> {
218
- const merged: Map<string, TypeDefinition> = new Map([...BUILTIN_TYPES, ...CUSTOM_TYPES])
219
- return merged.entries()
220
- },
221
-
222
- /**
223
- * Return the internal key set (used to strip non-standard fields during toJsonSchema)
224
- */
225
- getInternalKeys(): ReadonlySet<string> {
226
- return INTERNAL_KEYS
227
- },
228
-
229
- /**
230
- * Strip internal keys from a schema and return a clean JSON Schema.
231
- *
232
- * Special case for exactLength: translated to standard minLength + maxLength
233
- * instead of being stripped. This preserves v1 DslBuilder string:N behavior
234
- * (output {minLength:N, maxLength:N}) while keeping AJV's exactLength Unicode
235
- * code-point counting advantage internally (CK-Y04).
236
- */
237
- toJsonSchema(schema: JSONSchema): JSONSchema {
238
- const result: JSONSchema = {}
239
- for (const [k, v] of Object.entries(schema)) {
240
- if (k === 'exactLength' && typeof v === 'number') {
241
- // BC-compat: exactLength (AJV custom keyword) → standard JSON Schema minLength + maxLength
242
- result.minLength = v
243
- result.maxLength = v
244
- } else if (!INTERNAL_KEYS.has(k)) {
245
- result[k] = v
246
- }
247
- }
248
- return result
249
- },
250
- } as const
1
+ import type { JSONSchema } from '../types/schema.js'
2
+
3
+ /**
4
+ * Type definition (structure of each entry in TypeRegistry)
5
+ */
6
+ export interface TypeDefinition {
7
+ /** Base JSON Schema fragment for this type */
8
+ baseSchema: Partial<JSONSchema>
9
+ /** Custom messages associated with this type (e.g., error key for phone type) */
10
+ customMessages?: Record<string, string>
11
+ /** Whether this type uses a pattern (keys are standard JSON Schema fields, not stripped in toJsonSchema) */
12
+ isPattern?: boolean
13
+ }
14
+
15
+ /** Known internal key set (stripped from output during toJsonSchema) */
16
+ const INTERNAL_KEYS: ReadonlySet<string> = new Set([
17
+ '_label',
18
+ '_customMessages',
19
+ '_description',
20
+ '_required',
21
+ '_isConditional',
22
+ '_runtimeOnlyConditional',
23
+ 'conditions',
24
+ '_evaluateCondition',
25
+ // Custom AJV keywords (non-standard JSON Schema fields, stripped on output)
26
+ 'exactLength',
27
+ 'alphanum',
28
+ 'lowercase',
29
+ 'uppercase',
30
+ 'trim',
31
+ 'jsonString',
32
+ 'port',
33
+ 'requiredAll',
34
+ 'strictSchema',
35
+ 'noSparse',
36
+ 'includesRequired',
37
+ 'dateFormat',
38
+ 'dateGreater',
39
+ 'dateLess',
40
+ 'precision',
41
+ // ⚠️ multipleOf is a standard JSON Schema field and is NOT in this list (fix DB-01)
42
+ ])
43
+
44
+ /**
45
+ * Built-in type registry
46
+ * 33 types covering v1 DslAdapter.typeMap + DslBuilder type lists (fixes inconsistencies DB-02, DA-01)
47
+ */
48
+ const BUILTIN_TYPES: Map<string, TypeDefinition> = new Map([
49
+ // --- Primitive types ---
50
+ ['string', { baseSchema: { type: 'string' } }],
51
+ ['number', { baseSchema: { type: 'number' } }],
52
+ ['integer', { baseSchema: { type: 'integer' } }],
53
+ ['boolean', { baseSchema: { type: 'boolean' } }],
54
+ ['object', { baseSchema: { type: 'object' } }],
55
+ ['array', { baseSchema: { type: 'array' } }],
56
+ ['null', { baseSchema: { type: 'null' } }],
57
+ ['any', { baseSchema: {} }],
58
+
59
+ // --- Format types ---
60
+ ['email', { baseSchema: { type: 'string', format: 'email' } }],
61
+ ['url', { baseSchema: { type: 'string', format: 'uri' } }],
62
+ ['uri', { baseSchema: { type: 'string', format: 'uri' } }],
63
+ ['uuid', { baseSchema: { type: 'string', format: 'uuid' } }],
64
+ ['ipv4', { baseSchema: { type: 'string', format: 'ipv4' } }],
65
+ ['ipv6', { baseSchema: { type: 'string', format: 'ipv6' } }],
66
+ ['ip', { baseSchema: { anyOf: [{ type: 'string', format: 'ipv4' }, { type: 'string', format: 'ipv6' }] } }],
67
+ ['hostname', { baseSchema: { type: 'string', format: 'hostname' } }],
68
+ ['date', { baseSchema: { type: 'string', format: 'date' } }],
69
+ ['datetime', { baseSchema: { type: 'string', format: 'date-time' } }],
70
+ ['time', { baseSchema: { type: 'string', format: 'time' } }],
71
+
72
+ // --- Special string types ---
73
+ ['binary', { baseSchema: { type: 'string', contentEncoding: 'base64' } }],
74
+ ['objectId', { baseSchema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, customMessages: { pattern: 'pattern.objectId' } }],
75
+ ['hexColor', { baseSchema: { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' }, customMessages: { pattern: 'pattern.hexColor' } }],
76
+ ['macAddress', {
77
+ baseSchema: { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' },
78
+ customMessages: { pattern: 'pattern.macAddress' },
79
+ }],
80
+ ['cron', {
81
+ baseSchema: {
82
+ type: 'string',
83
+ pattern:
84
+ '^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) ' +
85
+ '(\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) ' +
86
+ '(\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) ' +
87
+ '(\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) ' +
88
+ '(\\*|([0-6])|\\*\\/([0-6]))$',
89
+ },
90
+ customMessages: { pattern: 'pattern.cron' },
91
+ }],
92
+
93
+ // --- slug (fix DB-02: v1 DslAdapter was missing slug type definition) ---
94
+ ['slug', {
95
+ baseSchema: { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
96
+ customMessages: { pattern: 'pattern.slug' },
97
+ }],
98
+
99
+ // --- CJK / Chinese ---
100
+ ['chineseName', {
101
+ baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]{2,10}$' },
102
+ customMessages: { pattern: 'chineseName' },
103
+ }],
104
+ ['chinese', {
105
+ baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]+$' },
106
+ }],
107
+
108
+ // --- Domain-related (handled by CustomKeywords; only baseSchema registered here) ---
109
+ ['emailDomain', { baseSchema: { type: 'string', format: 'email' } }],
110
+
111
+ // --- v1 extension types (DslBuilder v1.0.2) ---
112
+ ['alphanum', { baseSchema: { type: 'string', alphanum: true } }],
113
+ ['lower', { baseSchema: { type: 'string', lowercase: true } }],
114
+ ['upper', { baseSchema: { type: 'string', uppercase: true } }],
115
+ ['json', { baseSchema: { type: 'string', jsonString: true } }],
116
+ ['port', { baseSchema: { type: 'integer', port: true } }],
117
+ ])
118
+
119
+ /**
120
+ * Custom type registry (populated via registerType)
121
+ */
122
+ const CUSTOM_TYPES: Map<string, TypeDefinition> = new Map()
123
+ const DYNAMIC_TYPES: Map<string, () => JSONSchema> = new Map()
124
+
125
+ let _strictMode = false
126
+
127
+ /**
128
+ * TypeRegistry — unified type registration and resolution
129
+ *
130
+ * Replaces the three inconsistent type lists in v1 (fixes DB-01/DB-02/DA-01)
131
+ */
132
+ export const TypeRegistry = {
133
+ /**
134
+ * Resolve a type name to its TypeDefinition.
135
+ * Built-in types take priority; custom types may override non-primitive built-ins.
136
+ */
137
+ resolve(typeName: string): TypeDefinition {
138
+ // Dynamic types: call factory function each time
139
+ const dynamicFn = DYNAMIC_TYPES.get(typeName)
140
+ if (dynamicFn) {
141
+ return { baseSchema: dynamicFn() as Partial<JSONSchema> }
142
+ }
143
+
144
+ const custom = CUSTOM_TYPES.get(typeName)
145
+ if (custom) return custom
146
+
147
+ const builtin = BUILTIN_TYPES.get(typeName)
148
+ if (builtin) return builtin
149
+
150
+ // Unknown type: throw in strict mode, otherwise warn and fall back to string
151
+ if (_strictMode) {
152
+ throw new Error(`[schema-dsl] Unknown type "${typeName}"`)
153
+ }
154
+ console.warn(`[schema-dsl] Unknown type "${typeName}", falling back to string`)
155
+ return { baseSchema: { type: 'string' } }
156
+ },
157
+
158
+ /**
159
+ * Register a custom type (delegated from DslBuilder.registerType)
160
+ */
161
+ register(name: string, def: TypeDefinition | Partial<JSONSchema>): void {
162
+ if (!name || typeof name !== 'string') {
163
+ throw new Error('[schema-dsl] TypeRegistry.register: name must be a non-empty string')
164
+ }
165
+ // Accept a raw Partial<JSONSchema> and wrap it automatically
166
+ const normalized: TypeDefinition =
167
+ 'baseSchema' in def ? (def as TypeDefinition) : { baseSchema: def as Partial<JSONSchema> }
168
+ CUSTOM_TYPES.set(name, normalized)
169
+ },
170
+
171
+ /**
172
+ * Register a dynamic type (factory function invoked on every resolve call)
173
+ */
174
+ registerDynamic(name: string, factory: () => JSONSchema): void {
175
+ if (!name || typeof name !== 'string') {
176
+ throw new Error('[schema-dsl] TypeRegistry.registerDynamic: name must be a non-empty string')
177
+ }
178
+ DYNAMIC_TYPES.set(name, factory)
179
+ },
180
+
181
+ /**
182
+ * Unregister a custom type
183
+ */
184
+ unregister(name: string): void {
185
+ CUSTOM_TYPES.delete(name)
186
+ DYNAMIC_TYPES.delete(name)
187
+ },
188
+
189
+ /**
190
+ * Clear all custom and dynamic types (primarily for testing; called by DslBuilder.clearCustomTypes).
191
+ * Built-in types are unaffected.
192
+ */
193
+ clearCustomTypes(): void {
194
+ CUSTOM_TYPES.clear()
195
+ DYNAMIC_TYPES.clear()
196
+ },
197
+
198
+ /**
199
+ * Enable or disable strict mode for type resolution.
200
+ * In strict mode, resolving an unknown type throws instead of warning and falling back to string.
201
+ */
202
+ setStrict(flag: boolean): void {
203
+ _strictMode = flag
204
+ },
205
+
206
+ /**
207
+ * Check whether a type is registered (built-in or custom)
208
+ */
209
+ has(typeName: string): boolean {
210
+ return BUILTIN_TYPES.has(typeName) || CUSTOM_TYPES.has(typeName) || DYNAMIC_TYPES.has(typeName)
211
+ },
212
+
213
+ /**
214
+ * Return an iterator over all registered types (built-in + custom; custom overrides same-name built-in)
215
+ * BC-4 compat: consumed by the DslAdapter.typeMap getter
216
+ */
217
+ entries(): IterableIterator<[string, TypeDefinition]> {
218
+ const merged: Map<string, TypeDefinition> = new Map([...BUILTIN_TYPES, ...CUSTOM_TYPES])
219
+ return merged.entries()
220
+ },
221
+
222
+ /**
223
+ * Return the internal key set (used to strip non-standard fields during toJsonSchema)
224
+ */
225
+ getInternalKeys(): ReadonlySet<string> {
226
+ return INTERNAL_KEYS
227
+ },
228
+
229
+ /**
230
+ * Strip internal keys from a schema and return a clean JSON Schema.
231
+ *
232
+ * Special case for exactLength: translated to standard minLength + maxLength
233
+ * instead of being stripped. This preserves v1 DslBuilder string:N behavior
234
+ * (output {minLength:N, maxLength:N}) while keeping AJV's exactLength Unicode
235
+ * code-point counting advantage internally (CK-Y04).
236
+ */
237
+ toJsonSchema(schema: JSONSchema): JSONSchema {
238
+ const result: JSONSchema = {}
239
+ for (const [k, v] of Object.entries(schema)) {
240
+ if (k === 'exactLength' && typeof v === 'number') {
241
+ // BC-compat: exactLength (AJV custom keyword) → standard JSON Schema minLength + maxLength
242
+ result.minLength = v
243
+ result.maxLength = v
244
+ } else if (!INTERNAL_KEYS.has(k)) {
245
+ result[k] = v
246
+ }
247
+ }
248
+ return result
249
+ },
250
+ } as const
@@ -1,6 +1,6 @@
1
- export { TypeRegistry } from './TypeRegistry.js'
2
- export type { TypeDefinition } from './TypeRegistry.js'
3
- export { ConstraintParser } from './ConstraintParser.js'
4
- export { SchemaCompiler } from './SchemaCompiler.js'
5
- export type { AfterCompileHook } from './SchemaCompiler.js'
6
- export { DslParser } from './DslParser.js'
1
+ export { TypeRegistry } from './TypeRegistry.js'
2
+ export type { TypeDefinition } from './TypeRegistry.js'
3
+ export { ConstraintParser } from './ConstraintParser.js'
4
+ export { SchemaCompiler } from './SchemaCompiler.js'
5
+ export type { AfterCompileHook } from './SchemaCompiler.js'
6
+ export { DslParser } from './DslParser.js'