schema-dsl 1.2.4 → 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 -210
  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 -3540
  204. package/index.js +0 -457
  205. package/index.mjs +0 -60
  206. package/lib/adapters/DslAdapter.js +0 -871
  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 -1400
  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,196 @@
1
+ /**
2
+ * SchemaHelper — Schema utility functions.
3
+ *
4
+ * Common helpers for JSON Schema structure manipulation:
5
+ * isValidSchema, generateSchemaId, cloneSchema, flattenSchema,
6
+ * getFieldPaths, extractRequiredFields, compareSchemas, simplifySchema,
7
+ * isValidPropertyName, getSchemaComplexity, summarizeSchema
8
+ */
9
+
10
+ import type { JSONSchema } from '../types/schema.js'
11
+
12
+ export class SchemaHelper {
13
+ /**
14
+ * Check whether the value is a valid JSON Schema (must contain at least one of: type / properties / items / $ref).
15
+ */
16
+ static isValidSchema(schema: unknown): schema is JSONSchema {
17
+ if (!schema || typeof schema !== 'object') return false
18
+ const s = schema as Record<string, unknown>
19
+ return !!(s['type'] || s['properties'] || s['items'] || s['$ref']
20
+ || s['anyOf'] || s['oneOf'] || s['allOf'] || s['enum'])
21
+ }
22
+
23
+ /**
24
+ * Generate a content-hash-based unique ID for a schema.
25
+ */
26
+ static generateSchemaId(schema: JSONSchema): string {
27
+ const str = JSON.stringify(schema)
28
+ let hash = 0xcbf29ce484222325n
29
+ const prime = 0x100000001b3n
30
+ for (let i = 0; i < str.length; i++) {
31
+ hash ^= BigInt(str.charCodeAt(i))
32
+ hash = BigInt.asUintN(64, hash * prime)
33
+ }
34
+ return `schema_${hash.toString(36)}`
35
+ }
36
+
37
+ /**
38
+ * Deep-clone a schema via JSON serialisation (Function/RegExp fields are not preserved).
39
+ */
40
+ static cloneSchema(schema: JSONSchema): JSONSchema {
41
+ return JSON.parse(JSON.stringify(schema)) as JSONSchema
42
+ }
43
+
44
+ /**
45
+ * Flatten a nested schema into dot-separated path form.
46
+ * @param prefix - Property path prefix.
47
+ */
48
+ static flattenSchema(schema: JSONSchema, prefix = ''): Record<string, JSONSchema> {
49
+ const result: Record<string, JSONSchema> = {}
50
+
51
+ if (schema.properties) {
52
+ for (const [key, value] of Object.entries(schema.properties)) {
53
+ const fullKey = prefix ? `${prefix}.${key}` : key
54
+ if (value.type === 'object' && value.properties) {
55
+ Object.assign(result, this.flattenSchema(value, fullKey))
56
+ } else {
57
+ result[fullKey] = value
58
+ }
59
+ }
60
+ }
61
+
62
+ return result
63
+ }
64
+
65
+ /**
66
+ * Get all field paths in a schema (including nested object and array paths).
67
+ */
68
+ static getFieldPaths(schema: JSONSchema): string[] {
69
+ const paths: string[] = []
70
+
71
+ function traverse(obj: JSONSchema, currentPath = ''): void {
72
+ if (obj.properties) {
73
+ for (const [key, value] of Object.entries(obj.properties)) {
74
+ const path = currentPath ? `${currentPath}.${key}` : key
75
+ paths.push(path)
76
+ if (value.type === 'object') {
77
+ traverse(value, path)
78
+ } else if (value.type === 'array' && value.items) {
79
+ traverse(value.items as JSONSchema, `${path}[]`)
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ traverse(schema)
86
+ return paths
87
+ }
88
+
89
+ /**
90
+ * Extract all required fields from a schema (including nested paths).
91
+ */
92
+ static extractRequiredFields(schema: JSONSchema): string[] {
93
+ const required: string[] = []
94
+
95
+ function traverse(obj: JSONSchema, prefix = ''): void {
96
+ if (obj.required && Array.isArray(obj.required)) {
97
+ for (const field of obj.required) {
98
+ required.push(prefix ? `${prefix}.${field}` : field)
99
+ }
100
+ }
101
+ if (obj.properties) {
102
+ for (const [key, value] of Object.entries(obj.properties)) {
103
+ if (value.type === 'object') {
104
+ traverse(value, prefix ? `${prefix}.${key}` : key)
105
+ }
106
+ }
107
+ }
108
+ }
109
+
110
+ traverse(schema)
111
+ return required
112
+ }
113
+
114
+ /**
115
+ * Shallow-compare two schemas for equality (via JSON serialisation).
116
+ */
117
+ static compareSchemas(schema1: JSONSchema, schema2: JSONSchema): boolean {
118
+ return JSON.stringify(schema1) === JSON.stringify(schema2)
119
+ }
120
+
121
+ /**
122
+ * Simplify a schema by removing $schema, empty properties, and empty required arrays.
123
+ */
124
+ static simplifySchema(schema: JSONSchema): JSONSchema {
125
+ const simplified = this.cloneSchema(schema) as Record<string, unknown>
126
+
127
+ delete simplified['$schema']
128
+
129
+ const props = simplified['properties'] as Record<string, unknown> | undefined
130
+ if (props && Object.keys(props).length === 0) {
131
+ delete simplified['properties']
132
+ }
133
+
134
+ const req = simplified['required'] as unknown[] | undefined
135
+ if (req && req.length === 0) {
136
+ delete simplified['required']
137
+ }
138
+
139
+ return simplified as JSONSchema
140
+ }
141
+
142
+ /**
143
+ * Validate that a property name is legal (letters/digits/underscores/hyphens; must start with a letter or underscore).
144
+ */
145
+ static isValidPropertyName(name: string): boolean {
146
+ return /^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(name)
147
+ }
148
+
149
+ /**
150
+ * Get the maximum nesting depth (complexity) of a schema.
151
+ */
152
+ static getSchemaComplexity(schema: JSONSchema): number {
153
+ let maxDepth = 0
154
+
155
+ function traverse(obj: JSONSchema, depth: number): void {
156
+ maxDepth = Math.max(maxDepth, depth)
157
+ if (obj.properties) {
158
+ for (const value of Object.values(obj.properties)) {
159
+ if (value.type === 'object') {
160
+ traverse(value, depth + 1)
161
+ } else if (value.type === 'array' && value.items) {
162
+ traverse(value.items as JSONSchema, depth + 1)
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ traverse(schema, 0)
169
+ return maxDepth
170
+ }
171
+
172
+ /**
173
+ * Generate a summary of schema metadata.
174
+ */
175
+ static summarizeSchema(schema: JSONSchema): {
176
+ type: string
177
+ fieldCount: number
178
+ requiredCount: number
179
+ complexity: number
180
+ hasNested: boolean
181
+ fields: string[]
182
+ } {
183
+ const fields = this.getFieldPaths(schema)
184
+ const requiredFields = this.extractRequiredFields(schema)
185
+ const complexity = this.getSchemaComplexity(schema)
186
+
187
+ return {
188
+ type: (schema.type as string) ?? 'unknown',
189
+ fieldCount: fields.length,
190
+ requiredCount: requiredFields.length,
191
+ complexity,
192
+ hasNested: complexity > 0,
193
+ fields,
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,346 @@
1
+ /**
2
+ * SchemaUtils — Advanced schema operations (reuse, merge, extend, performance monitoring)
3
+ *
4
+ * v2 note: v1's extend/validateBatch used require('../adapters/DslAdapter'),
5
+ * v2 uses dynamic import or accepts pre-compiled schema directly (avoids circular deps)
6
+ */
7
+
8
+ import type { JSONSchema } from '../types/schema.js'
9
+ import { DslAdapter } from '../adapters/DslAdapter.js'
10
+
11
+ // Internal: chainable schema wrapper type
12
+ interface ChainableSchema extends JSONSchema {
13
+ _isChainable: true
14
+ partial(fields?: string[]): ChainableSchema
15
+ pick(fields: string[]): ChainableSchema
16
+ omit(fields: string[]): ChainableSchema
17
+ extend(extensions: Record<string, unknown>): ChainableSchema
18
+ }
19
+
20
+ // ==================== SchemaUtils ====================
21
+
22
+ export class SchemaUtils {
23
+ // ========== Schema Reuse ==========
24
+
25
+ /**
26
+ * Create a reusable schema factory.
27
+ * @example const emailField = SchemaUtils.reusable(() => dsl('email!').label('email'))
28
+ */
29
+ static reusable<T>(factory: () => T): () => T {
30
+ return factory
31
+ }
32
+
33
+ /**
34
+ * Create a schema fragment library.
35
+ * @example const fields = SchemaUtils.createLibrary({ email: () => dsl('email!') })
36
+ */
37
+ static createLibrary<T extends Record<string, () => unknown>>(fragments: T): T {
38
+ return fragments
39
+ }
40
+
41
+ // ========== Schema Reuse & Extension ==========
42
+
43
+ /**
44
+ * Extend a schema (like inheritance) — merges extension fields into the base schema.
45
+ */
46
+ static extend(baseSchema: JSONSchema, extensions: JSONSchema | Record<string, unknown>): ChainableSchema {
47
+ const result: Record<string, unknown> = {
48
+ type: 'object',
49
+ properties: {} as Record<string, unknown>,
50
+ required: [] as string[],
51
+ }
52
+
53
+ // Copy base schema
54
+ if (baseSchema.properties) {
55
+ result['properties'] = this._mergeProperties(
56
+ result['properties'] as Record<string, unknown>,
57
+ baseSchema.properties as Record<string, unknown>,
58
+ )
59
+ }
60
+ if (baseSchema.required) {
61
+ result['required'] = [...baseSchema.required]
62
+ }
63
+
64
+ // Detect flat DSL definition: if no 'properties' key but has string values, treat as DSL
65
+ let extSchema: JSONSchema
66
+ if (!('properties' in extensions) && Object.values(extensions).some(v => typeof v === 'string')) {
67
+ extSchema = DslAdapter.parseObject(extensions as Record<string, string>).toSchema()
68
+ } else {
69
+ extSchema = extensions as JSONSchema
70
+ }
71
+
72
+ // Merge extension schema (deep-merge same-name nested objects instead of replacing)
73
+ if (extSchema.properties) {
74
+ result['properties'] = this._mergeProperties(
75
+ result['properties'] as Record<string, unknown>,
76
+ extSchema.properties as Record<string, unknown>,
77
+ )
78
+ }
79
+ if (extSchema.required) {
80
+ result['required'] = [...new Set([
81
+ ...(result['required'] as string[]),
82
+ ...extSchema.required,
83
+ ])]
84
+ }
85
+
86
+ return this._makeChainable(result as JSONSchema)
87
+ }
88
+
89
+ /**
90
+ * Pick a subset of fields from a schema.
91
+ */
92
+ static pick(schema: JSONSchema, fields: string[]): ChainableSchema {
93
+ const result: Record<string, unknown> = {
94
+ type: 'object',
95
+ properties: {} as Record<string, unknown>,
96
+ required: [] as string[],
97
+ }
98
+
99
+ for (const field of fields) {
100
+ if (schema.properties?.[field]) {
101
+ (result['properties'] as Record<string, unknown>)[field] = this._clone(schema.properties[field] as JSONSchema)
102
+ if (schema.required?.includes(field)) {
103
+ (result['required'] as string[]).push(field)
104
+ }
105
+ }
106
+ }
107
+
108
+ return this._makeChainable(result as JSONSchema)
109
+ }
110
+
111
+ /**
112
+ * Omit fields from a schema.
113
+ */
114
+ static omit(schema: JSONSchema, fields: string[]): ChainableSchema {
115
+ const result = this._clone(schema)
116
+
117
+ for (const field of fields) {
118
+ if (result['properties']) {
119
+ delete (result['properties'] as Record<string, unknown>)[field]
120
+ }
121
+ if (Array.isArray(result['required'])) {
122
+ result['required'] = (result['required'] as string[]).filter((f: string) => f !== field)
123
+ }
124
+ }
125
+
126
+ // Remove empty required array
127
+ if (Array.isArray(result['required']) && (result['required'] as string[]).length === 0) {
128
+ delete result['required']
129
+ }
130
+
131
+ return this._makeChainable(result as JSONSchema)
132
+ }
133
+
134
+ /**
135
+ * Make all fields optional (removes required).
136
+ * @param fields - optional; only process these fields (others remain unchanged)
137
+ */
138
+ static partial(schema: JSONSchema, fields?: string[] | null): ChainableSchema {
139
+ let raw: Record<string, unknown>
140
+
141
+ if (fields) {
142
+ const picked = this.pick(schema, fields)
143
+ raw = this._extractSchema(picked)
144
+ } else {
145
+ raw = this._clone(schema)
146
+ }
147
+
148
+ this._deleteRequired(raw)
149
+
150
+ return this._makeChainable(raw as JSONSchema)
151
+ }
152
+
153
+ // ========== Performance Monitoring ==========
154
+
155
+ /**
156
+ * Wrap a Validator instance with performance monitoring.
157
+ */
158
+ static withPerformance<V extends { validate: (...args: unknown[]) => unknown }>(validator: V): V {
159
+ const originalValidate = validator.validate.bind(validator)
160
+ validator.validate = (...args: unknown[]) => {
161
+ const startTime = Date.now()
162
+ const result = originalValidate(...args) as Record<string, unknown>
163
+ result['performance'] = { duration: Date.now() - startTime, timestamp: new Date().toISOString() }
164
+ return result
165
+ }
166
+ return validator
167
+ }
168
+
169
+ /**
170
+ * Batch validate using a pre-compiled Ajv validate function.
171
+ */
172
+ static validateBatch(
173
+ schema: JSONSchema,
174
+ dataArray: unknown[],
175
+ ajvInstance: { compile: (schema: JSONSchema) => (data: unknown) => boolean } & { errors?: unknown },
176
+ ): {
177
+ results: Array<{ index: number; valid: boolean; errors: unknown; data: unknown }>
178
+ summary: { total: number; valid: number; invalid: number; duration: number; averageTime: number }
179
+ } {
180
+ const startTime = Date.now()
181
+ const compiledValidate = ajvInstance.compile(schema)
182
+
183
+ const results = dataArray.map((data, index) => {
184
+ const valid = compiledValidate(data)
185
+ return {
186
+ index,
187
+ valid,
188
+ errors: valid ? null : (compiledValidate as unknown as { errors: unknown }).errors,
189
+ data: valid ? data : null,
190
+ }
191
+ })
192
+
193
+ const duration = Date.now() - startTime
194
+ return {
195
+ results,
196
+ summary: {
197
+ total: dataArray.length,
198
+ valid: results.filter(r => r.valid).length,
199
+ invalid: results.filter(r => !r.valid).length,
200
+ duration,
201
+ averageTime: dataArray.length > 0 ? duration / dataArray.length : 0,
202
+ },
203
+ }
204
+ }
205
+
206
+ // ========== Schema Export ==========
207
+
208
+ static toMarkdown(schema: JSONSchema, options: { title?: string } = {}): string {
209
+ const { title = 'Schema Documentation' } = options
210
+ let md = `# ${title}\n\n`
211
+
212
+ if (schema.properties) {
213
+ md += '## Fields\n\n'
214
+ md += '| Field | Type | Required | Description |\n'
215
+ md += '|-------|------|----------|-------------|\n'
216
+
217
+ for (const [key, prop] of Object.entries(schema.properties)) {
218
+ const required = schema.required?.includes(key) ? '✅' : '❌'
219
+ const type = (prop.type as string) ?? 'any'
220
+ const p = prop as Record<string, unknown>
221
+ const label = (p['_label'] as string) ?? key
222
+
223
+ md += `| ${key} | ${type} | ${required} | ${label} |\n`
224
+
225
+ const constraints: string[] = []
226
+ if (prop.minLength) constraints.push(`minLength: ${prop.minLength}`)
227
+ if (prop.maxLength) constraints.push(`maxLength: ${prop.maxLength}`)
228
+ if (prop.minimum !== undefined) constraints.push(`minimum: ${prop.minimum}`)
229
+ if (prop.maximum !== undefined) constraints.push(`maximum: ${prop.maximum}`)
230
+ if (prop.pattern) constraints.push(`pattern: \`${prop.pattern}\``)
231
+ if (prop.enum) constraints.push(`enum: ${(prop.enum as unknown[]).join(', ')}`)
232
+
233
+ if (constraints.length > 0) {
234
+ md += `| | | | ${constraints.join('; ')} |\n`
235
+ }
236
+ }
237
+ }
238
+
239
+ return md
240
+ }
241
+
242
+ static clone(schema: JSONSchema): JSONSchema {
243
+ return JSON.parse(JSON.stringify(schema)) as JSONSchema
244
+ }
245
+
246
+ /**
247
+ * toHTML — v1 compat: export schema as HTML document
248
+ */
249
+ static toHTML(schema: JSONSchema, options: { title?: string } = {}): string {
250
+ const { title = 'Schema Documentation' } = options
251
+ let html = `<!DOCTYPE html>\n<html>\n<head><meta charset="utf-8"><title>${title}</title></head>\n<body>\n<h1>${title}</h1>\n`
252
+
253
+ if (schema.properties) {
254
+ html += '<table border="1" cellpadding="4">\n'
255
+ html += '<tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th></tr>\n'
256
+
257
+ for (const [key, prop] of Object.entries(schema.properties)) {
258
+ const required = schema.required?.includes(key) ? '✅' : '❌'
259
+ const type = (prop.type as string) ?? 'any'
260
+ const p = prop as Record<string, unknown>
261
+ const label = (p['_label'] as string) ?? key
262
+
263
+ html += `<tr><td>${key}</td><td>${type}</td><td>${required}</td><td>${label}</td></tr>\n`
264
+ }
265
+
266
+ html += '</table>\n'
267
+ }
268
+
269
+ html += '</body>\n</html>'
270
+ return html
271
+ }
272
+
273
+ // ==================== Private Utilities ====================
274
+
275
+ private static _mergeProperties(
276
+ base: Record<string, unknown>,
277
+ ext: Record<string, unknown>,
278
+ ): Record<string, unknown> {
279
+ const result = { ...base }
280
+ for (const [key, extVal] of Object.entries(ext)) {
281
+ const baseVal = result[key]
282
+ if (
283
+ baseVal && extVal &&
284
+ typeof baseVal === 'object' && typeof extVal === 'object' &&
285
+ !Array.isArray(baseVal) && !Array.isArray(extVal)
286
+ ) {
287
+ result[key] = SchemaUtils._mergeProperties(
288
+ baseVal as Record<string, unknown>,
289
+ extVal as Record<string, unknown>,
290
+ )
291
+ } else {
292
+ result[key] = extVal
293
+ }
294
+ }
295
+ return result
296
+ }
297
+
298
+ private static _deleteRequired(obj: Record<string, unknown>): void {
299
+ delete obj['required']
300
+ const props = obj['properties']
301
+ if (props && typeof props === 'object') {
302
+ for (const prop of Object.values(props as Record<string, unknown>)) {
303
+ if (prop && typeof prop === 'object') {
304
+ this._deleteRequired(prop as Record<string, unknown>)
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ private static _clone(schema: JSONSchema | ChainableSchema): Record<string, unknown> {
311
+ const raw = '_isChainable' in schema ? this._extractSchema(schema as ChainableSchema) : schema
312
+ return JSON.parse(JSON.stringify(raw)) as Record<string, unknown>
313
+ }
314
+
315
+ private static _makeChainable(schema: JSONSchema): ChainableSchema {
316
+ const chainable = Object.assign({}, schema) as Record<string, unknown>
317
+
318
+ Object.defineProperty(chainable, '_isChainable', {
319
+ value: true, enumerable: false, configurable: false,
320
+ })
321
+
322
+ const methods = ['partial', 'pick', 'omit', 'extend'] as const
323
+ for (const method of methods) {
324
+ Object.defineProperty(chainable, method, {
325
+ value: (...args: unknown[]) => {
326
+ const rawSchema = SchemaUtils._extractSchema(chainable as ChainableSchema)
327
+ return (SchemaUtils[method] as (...args: unknown[]) => unknown)(rawSchema, ...args)
328
+ },
329
+ enumerable: false,
330
+ configurable: false,
331
+ })
332
+ }
333
+
334
+ return chainable as ChainableSchema
335
+ }
336
+
337
+ private static _extractSchema(chainable: ChainableSchema | Record<string, unknown>): Record<string, unknown> {
338
+ const schema: Record<string, unknown> = {}
339
+ for (const key of Object.keys(chainable)) {
340
+ if (key !== '_isChainable') {
341
+ schema[key] = chainable[key]
342
+ }
343
+ }
344
+ return schema
345
+ }
346
+ }