schema-dsl 1.2.5 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (243) hide show
  1. package/CHANGELOG.md +130 -238
  2. package/LICENSE +21 -21
  3. package/README.md +628 -2486
  4. package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
  5. package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
  6. package/dist/Validator-CllRdrY0.d.ts +192 -0
  7. package/dist/Validator-D6okG9tr.d.cts +192 -0
  8. package/dist/index.cjs +6640 -0
  9. package/dist/index.d.cts +1151 -0
  10. package/dist/index.d.ts +1151 -0
  11. package/dist/index.js +6574 -0
  12. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  13. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  14. package/dist/plugins/custom-format.cjs +3818 -0
  15. package/dist/plugins/custom-format.d.cts +12 -0
  16. package/dist/plugins/custom-format.d.ts +12 -0
  17. package/dist/plugins/custom-format.js +3788 -0
  18. package/dist/plugins/custom-type-example.cjs +3811 -0
  19. package/dist/plugins/custom-type-example.d.cts +8 -0
  20. package/dist/plugins/custom-type-example.d.ts +8 -0
  21. package/dist/plugins/custom-type-example.js +3781 -0
  22. package/dist/plugins/custom-validator.cjs +144 -0
  23. package/dist/plugins/custom-validator.d.cts +10 -0
  24. package/dist/plugins/custom-validator.d.ts +10 -0
  25. package/dist/plugins/custom-validator.js +119 -0
  26. package/docs/FEATURE-INDEX.md +553 -519
  27. package/docs/add-custom-locale.md +496 -483
  28. package/docs/add-keyword.md +24 -0
  29. package/docs/api-reference.md +1047 -805
  30. package/docs/api.md +13 -0
  31. package/docs/best-practices-project-structure.md +417 -408
  32. package/docs/best-practices.md +712 -672
  33. package/docs/cache-manager.md +344 -336
  34. package/docs/compile.md +45 -0
  35. package/docs/conditional-api.md +1307 -1278
  36. package/docs/custom-extensions-guide.md +339 -411
  37. package/docs/design-philosophy.md +606 -601
  38. package/docs/doc-index.md +324 -0
  39. package/docs/dsl-syntax.md +714 -664
  40. package/docs/dynamic-locale.md +608 -598
  41. package/docs/enum.md +482 -475
  42. package/docs/error-handling.md +1975 -1966
  43. package/docs/export-guide.md +501 -462
  44. package/docs/export-limitations.md +567 -551
  45. package/docs/faq.md +596 -577
  46. package/docs/frontend-i18n-guide.md +307 -293
  47. package/docs/i18n-user-guide.md +487 -474
  48. package/docs/i18n.md +476 -457
  49. package/docs/index.md +48 -0
  50. package/docs/json-schema-basics.md +40 -0
  51. package/docs/label-vs-description.md +271 -262
  52. package/docs/markdown-exporter.md +406 -397
  53. package/docs/mongodb-exporter.md +302 -295
  54. package/docs/multi-language.md +26 -0
  55. package/docs/multi-type-support.md +322 -329
  56. package/docs/mysql-exporter.md +280 -273
  57. package/docs/number-operators.md +449 -442
  58. package/docs/optional-marker-guide.md +326 -321
  59. package/docs/performance-guide.md +49 -0
  60. package/docs/plugin-system.md +381 -542
  61. package/docs/plugin-type-registration.md +34 -0
  62. package/docs/postgresql-exporter.md +311 -304
  63. package/docs/public/favicon.svg +5 -0
  64. package/docs/quick-start.md +435 -761
  65. package/docs/runtime-locale-support.md +532 -521
  66. package/docs/schema-helper.md +345 -340
  67. package/docs/schema-utils-advanced-issues.md +23 -0
  68. package/docs/schema-utils-best-practices.md +20 -0
  69. package/docs/schema-utils-chaining.md +150 -143
  70. package/docs/schema-utils.md +524 -490
  71. package/docs/security-checklist.md +20 -0
  72. package/docs/string-extensions.md +488 -480
  73. package/docs/troubleshooting.md +486 -471
  74. package/docs/type-converter.md +310 -319
  75. package/docs/type-reference.md +242 -219
  76. package/docs/typescript-guide.md +584 -573
  77. package/docs/union-type-guide.md +157 -147
  78. package/docs/union-types.md +284 -277
  79. package/docs/validate-async.md +491 -480
  80. package/docs/validate-batch.md +49 -0
  81. package/docs/validate-dsl-object-support.md +578 -573
  82. package/docs/validate.md +506 -486
  83. package/docs/validation-guide.md +502 -484
  84. package/docs/validator.md +39 -0
  85. package/package.json +131 -73
  86. package/plugins/custom-format.cjs +8 -0
  87. package/plugins/custom-type-example.cjs +8 -0
  88. package/plugins/custom-validator.cjs +8 -0
  89. package/src/adapters/DslAdapter.ts +111 -0
  90. package/src/adapters/index.ts +1 -0
  91. package/src/config/constants.ts +83 -0
  92. package/src/config/index.ts +2 -0
  93. package/src/config/patterns.ts +77 -0
  94. package/src/core/CacheManager.ts +169 -0
  95. package/src/core/ConditionalBuilder.ts +382 -0
  96. package/src/core/ConditionalRuntime.ts +28 -0
  97. package/src/core/ConditionalValidator.ts +255 -0
  98. package/src/core/DslBuilder.ts +687 -0
  99. package/src/core/ErrorCodes.ts +38 -0
  100. package/src/core/ErrorFormatter.ts +271 -0
  101. package/src/core/JSONSchemaCore.ts +65 -0
  102. package/src/core/Locale.ts +187 -0
  103. package/src/core/MessageTemplate.ts +42 -0
  104. package/src/core/ObjectDslBuilder.ts +64 -0
  105. package/src/core/PluginManager.ts +326 -0
  106. package/src/core/StringExtensions.ts +140 -0
  107. package/src/core/TemplateEngine.ts +44 -0
  108. package/src/core/Validator.ts +448 -0
  109. package/src/errors/I18nError.ts +159 -0
  110. package/src/errors/ValidationError.ts +105 -0
  111. package/src/exporters/BaseExporter.ts +60 -0
  112. package/src/exporters/MarkdownExporter.ts +305 -0
  113. package/src/exporters/MongoDBExporter.ts +126 -0
  114. package/src/exporters/MySQLExporter.ts +156 -0
  115. package/src/exporters/PostgreSQLExporter.ts +222 -0
  116. package/src/exporters/index.ts +18 -0
  117. package/src/index.ts +651 -0
  118. package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
  119. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
  120. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
  121. package/src/locales/index.ts +103 -0
  122. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
  123. package/src/locales/types.ts +156 -0
  124. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
  125. package/src/parser/ConstraintParser.ts +101 -0
  126. package/src/parser/DslParser.ts +470 -0
  127. package/src/parser/SchemaCompiler.ts +66 -0
  128. package/src/parser/TypeRegistry.ts +250 -0
  129. package/src/parser/index.ts +6 -0
  130. package/src/plugins/custom-format.ts +124 -0
  131. package/src/plugins/custom-type-example.ts +106 -0
  132. package/src/plugins/custom-validator.ts +138 -0
  133. package/src/types/conditional.ts +28 -0
  134. package/src/types/config.ts +59 -0
  135. package/src/types/dsl.ts +131 -0
  136. package/src/types/error.ts +60 -0
  137. package/src/types/index.ts +17 -0
  138. package/src/types/infer.ts +128 -0
  139. package/src/types/plugin.ts +58 -0
  140. package/src/types/safe-regex.d.ts +9 -0
  141. package/src/types/schema.ts +66 -0
  142. package/src/types/validate.ts +71 -0
  143. package/src/utils/SchemaHelper.ts +196 -0
  144. package/src/utils/SchemaUtils.ts +365 -0
  145. package/src/utils/TypeConverter.ts +215 -0
  146. package/src/utils/index.ts +10 -0
  147. package/src/validators/CustomKeywords.ts +477 -0
  148. package/.eslintignore +0 -11
  149. package/.eslintrc.json +0 -27
  150. package/CONTRIBUTING.md +0 -368
  151. package/STATUS.md +0 -491
  152. package/changelogs/v1.0.0.md +0 -328
  153. package/changelogs/v1.0.9.md +0 -367
  154. package/changelogs/v1.1.0.md +0 -389
  155. package/changelogs/v1.1.1.md +0 -308
  156. package/changelogs/v1.1.2.md +0 -183
  157. package/changelogs/v1.1.3.md +0 -161
  158. package/changelogs/v1.1.4.md +0 -432
  159. package/changelogs/v1.1.5.md +0 -493
  160. package/changelogs/v1.1.6.md +0 -211
  161. package/changelogs/v1.1.8.md +0 -376
  162. package/changelogs/v1.2.3.md +0 -124
  163. package/docs/INDEX.md +0 -252
  164. package/docs/issues-resolved-summary.md +0 -196
  165. package/docs/performance-benchmark-report.md +0 -179
  166. package/docs/performance-quick-reference.md +0 -123
  167. package/docs/user-questions-answered.md +0 -353
  168. package/docs/validation-rules-v1.0.2.md +0 -1608
  169. package/examples/README.md +0 -81
  170. package/examples/array-dsl-example.js +0 -227
  171. package/examples/conditional-example.js +0 -288
  172. package/examples/conditional-non-object.js +0 -129
  173. package/examples/conditional-validate-example.js +0 -321
  174. package/examples/custom-extension.js +0 -85
  175. package/examples/dsl-match-example.js +0 -74
  176. package/examples/dsl-style.js +0 -118
  177. package/examples/dynamic-locale-configuration.js +0 -348
  178. package/examples/dynamic-locale-example.js +0 -287
  179. package/examples/enum.examples.js +0 -324
  180. package/examples/export-demo.js +0 -130
  181. package/examples/express-integration.js +0 -376
  182. package/examples/i18n-error-handling-complete.js +0 -381
  183. package/examples/i18n-error-handling-quickstart.md +0 -0
  184. package/examples/i18n-error.examples.js +0 -181
  185. package/examples/i18n-full-demo.js +0 -301
  186. package/examples/i18n-memory-safety.examples.js +0 -268
  187. package/examples/markdown-export.js +0 -71
  188. package/examples/middleware-usage.js +0 -93
  189. package/examples/new-features-comparison.js +0 -315
  190. package/examples/password-reset/README.md +0 -153
  191. package/examples/password-reset/schema.js +0 -26
  192. package/examples/password-reset/test.js +0 -101
  193. package/examples/plugin-system.examples.js +0 -205
  194. package/examples/schema-utils-chaining.examples.js +0 -250
  195. package/examples/simple-example.js +0 -122
  196. package/examples/slug.examples.js +0 -179
  197. package/examples/string-extensions.js +0 -297
  198. package/examples/union-type-example.js +0 -127
  199. package/examples/union-types-example.js +0 -77
  200. package/examples/user-registration/README.md +0 -156
  201. package/examples/user-registration/routes.js +0 -92
  202. package/examples/user-registration/schema.js +0 -150
  203. package/examples/user-registration/server.js +0 -74
  204. package/index.d.ts +0 -3658
  205. package/index.js +0 -475
  206. package/index.mjs +0 -60
  207. package/lib/adapters/DslAdapter.js +0 -995
  208. package/lib/adapters/index.js +0 -20
  209. package/lib/config/constants.js +0 -286
  210. package/lib/config/patterns/common.js +0 -47
  211. package/lib/config/patterns/creditCard.js +0 -9
  212. package/lib/config/patterns/idCard.js +0 -9
  213. package/lib/config/patterns/index.js +0 -9
  214. package/lib/config/patterns/licensePlate.js +0 -4
  215. package/lib/config/patterns/passport.js +0 -4
  216. package/lib/config/patterns/phone.js +0 -9
  217. package/lib/config/patterns/postalCode.js +0 -5
  218. package/lib/core/CacheManager.js +0 -376
  219. package/lib/core/ConditionalBuilder.js +0 -503
  220. package/lib/core/DslBuilder.js +0 -1589
  221. package/lib/core/ErrorCodes.js +0 -233
  222. package/lib/core/ErrorFormatter.js +0 -445
  223. package/lib/core/JSONSchemaCore.js +0 -347
  224. package/lib/core/Locale.js +0 -130
  225. package/lib/core/MessageTemplate.js +0 -98
  226. package/lib/core/PluginManager.js +0 -448
  227. package/lib/core/StringExtensions.js +0 -240
  228. package/lib/core/Validator.js +0 -654
  229. package/lib/errors/I18nError.js +0 -328
  230. package/lib/errors/ValidationError.js +0 -191
  231. package/lib/exporters/MarkdownExporter.js +0 -420
  232. package/lib/exporters/MongoDBExporter.js +0 -162
  233. package/lib/exporters/MySQLExporter.js +0 -212
  234. package/lib/exporters/PostgreSQLExporter.js +0 -289
  235. package/lib/exporters/index.js +0 -24
  236. package/lib/locales/index.js +0 -8
  237. package/lib/utils/LRUCache.js +0 -174
  238. package/lib/utils/SchemaHelper.js +0 -240
  239. package/lib/utils/SchemaUtils.js +0 -445
  240. package/lib/utils/TypeConverter.js +0 -245
  241. package/lib/utils/index.js +0 -13
  242. package/lib/validators/CustomKeywords.js +0 -616
  243. package/lib/validators/index.js +0 -11
@@ -0,0 +1,105 @@
1
+ // V8/Node.js extension (not in ES2022 lib; declared explicitly)
2
+ type ErrorWithCaptureStackTrace = typeof Error & {
3
+ captureStackTrace?: (target: object, ctor: unknown) => void
4
+ }
5
+ const ErrorCtor = Error as ErrorWithCaptureStackTrace
6
+
7
+ import type { ValidationErrorItem } from '../types/validate.js'
8
+
9
+ /**
10
+ * ValidationError — error class thrown when validateAsync() fails.
11
+ * Fixes v1 bug: malformed message format when errors array is empty.
12
+ */
13
+ export class ValidationError extends Error {
14
+ readonly name = 'ValidationError' as const
15
+ readonly errors: ValidationErrorItem[]
16
+ readonly data: unknown
17
+ readonly statusCode: number
18
+
19
+ constructor(errors: ValidationErrorItem[], data?: unknown, statusCode = 400) {
20
+ // Fix: provide friendly message when errors array is empty (v1 bug)
21
+ const messages =
22
+ errors.length === 0
23
+ ? 'Validation failed'
24
+ : errors
25
+ .map(e => {
26
+ if (e.path) {
27
+ const field = e.path.replace(/^\//, '')
28
+ return field ? `${field}: ${e.message}` : e.message
29
+ }
30
+ return e.message
31
+ })
32
+ .join('; ')
33
+
34
+ // v1 compat: use " - " separator when no path, ": " otherwise
35
+ // v1 compat: single conditional error uses message string directly (no prefix)
36
+ const hasNoPath = errors.every(e => e.path === undefined || e.path === null || e.path === '')
37
+ const isSingleConditional = errors.length === 1 && errors[0].keyword === 'conditional' && hasNoPath
38
+ if (isSingleConditional) {
39
+ super(messages)
40
+ } else {
41
+ super(hasNoPath ? `Validation failed - ${messages}` : `Validation failed: ${messages}`)
42
+ }
43
+
44
+ this.errors = errors
45
+ this.data = data
46
+ this.statusCode = statusCode
47
+
48
+ if (ErrorCtor.captureStackTrace) {
49
+ ErrorCtor.captureStackTrace(this, ValidationError)
50
+ }
51
+ }
52
+
53
+ toJSON(): {
54
+ error: string
55
+ message: string
56
+ statusCode: number
57
+ details: Array<{
58
+ field: string | null
59
+ message: string
60
+ keyword: string
61
+ params?: Record<string, unknown>
62
+ }>
63
+ } {
64
+ return {
65
+ error: this.name,
66
+ message: this.message,
67
+ statusCode: this.statusCode,
68
+ details: this.errors.map(e => ({
69
+ field: e.path ? e.path.replace(/^\//, '') : null,
70
+ message: e.message,
71
+ keyword: e.keyword,
72
+ ...(e.params !== undefined ? { params: e.params } : {}),
73
+ })),
74
+ }
75
+ }
76
+
77
+ getFieldError(field: string): ValidationErrorItem | null {
78
+ const normalized = field.replace(/^\//, '')
79
+ return (
80
+ this.errors.find(e => {
81
+ if (!e.path) return false
82
+ return e.path.replace(/^\//, '') === normalized
83
+ }) ?? null
84
+ )
85
+ }
86
+
87
+ getFieldErrors(): Record<string, string> {
88
+ const result: Record<string, string> = {}
89
+ for (const e of this.errors) {
90
+ if (e.path) {
91
+ const field = e.path.replace(/^\//, '')
92
+ if (field) result[field] = e.message
93
+ }
94
+ }
95
+ return result
96
+ }
97
+
98
+ hasFieldError(field: string): boolean {
99
+ return this.getFieldError(field) !== null
100
+ }
101
+
102
+ getErrorCount(): number {
103
+ return this.errors.length
104
+ }
105
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * BaseExporter — Base interface and abstract class for all exporters.
3
+ *
4
+ * Provides a unified abstract export() method signature; each exporter subclass implements it.
5
+ */
6
+
7
+ import type { JSONSchema } from '../types/schema.js'
8
+
9
+ // ==================== Common options type ====================
10
+
11
+ export interface ExporterOptions {
12
+ [key: string]: unknown
13
+ }
14
+
15
+ // ==================== BaseExporter ====================
16
+
17
+ export abstract class BaseExporter<TOptions extends ExporterOptions = ExporterOptions> {
18
+ protected options: TOptions
19
+
20
+ constructor(options: Partial<TOptions> = {}) {
21
+ this.options = options as TOptions
22
+ }
23
+
24
+ /**
25
+ * Export a JSON Schema to the target format.
26
+ * Each subclass must implement this method.
27
+ */
28
+ abstract export(...args: unknown[]): unknown
29
+
30
+ /**
31
+ * Assert that the input JSON Schema is a valid object-type schema.
32
+ * @throws Error if invalid.
33
+ */
34
+ protected _assertObjectSchema(jsonSchema: unknown, label = 'JSON Schema'): asserts jsonSchema is JSONSchema & { type: 'object' } {
35
+ if (!jsonSchema || typeof jsonSchema !== 'object') {
36
+ throw new Error(`[schema-dsl] ${label} must be an object`)
37
+ }
38
+ const s = jsonSchema as JSONSchema
39
+ if (s.type !== 'object') {
40
+ throw new Error(`[schema-dsl] ${label} must be an object type (got "${String(s.type)}")`)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Escape SQL single quotes (generic utility).
46
+ */
47
+ protected _escapeString(str: string): string {
48
+ return str.replace(/'/g, "''")
49
+ }
50
+
51
+ /**
52
+ * Detect the primary key column name in a schema (id / _id preferred).
53
+ */
54
+ protected _detectPrimaryKey(schema: JSONSchema): string | null {
55
+ if (!schema.properties) return null
56
+ if (schema.properties['id']) return 'id'
57
+ if (schema.properties['_id']) return '_id'
58
+ return null
59
+ }
60
+ }
@@ -0,0 +1,305 @@
1
+ /**
2
+ * MarkdownExporter — Export JSON Schema as human-readable Markdown documentation.
3
+ *
4
+ * v2 fix:
5
+ * EX-01: required check prefers prop._required, then falls back to schema.required?.includes(key)
6
+ * (v1 already had this logic; v2 preserves it with stronger type safety)
7
+ *
8
+ * @version 2.0.0
9
+ */
10
+
11
+ import type { JSONSchema } from '../types/schema.js'
12
+ import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
13
+
14
+ // ==================== Type definitions ====================
15
+
16
+ export interface MarkdownExporterOptions extends ExporterOptions {
17
+ title?: string
18
+ locale?: 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
19
+ includeExample?: boolean
20
+ includeDescription?: boolean
21
+ }
22
+
23
+ type Locale = 'zh-CN' | 'en-US' | 'ja-JP' | 'fr-FR' | 'es-ES'
24
+
25
+ // ==================== MarkdownExporter ====================
26
+
27
+ export class MarkdownExporter extends BaseExporter<MarkdownExporterOptions> {
28
+ constructor(options: Partial<MarkdownExporterOptions> = {}) {
29
+ super({
30
+ title: 'Schema Documentation',
31
+ locale: 'zh-CN',
32
+ includeExample: true,
33
+ includeDescription: true,
34
+ ...options,
35
+ })
36
+ }
37
+
38
+ /**
39
+ * Export as a Markdown document.
40
+ */
41
+ export(schema: JSONSchema, options?: Partial<MarkdownExporterOptions>): string {
42
+ return MarkdownExporter.export(schema, { ...this.options, ...options })
43
+ }
44
+
45
+ /**
46
+ * Static method: export directly without instantiation.
47
+ */
48
+ static export(schema: JSONSchema, options: Partial<MarkdownExporterOptions> = {}): string {
49
+ const {
50
+ title = 'Schema Documentation',
51
+ locale = 'zh-CN',
52
+ includeExample = true,
53
+ includeDescription = true,
54
+ } = options
55
+
56
+ let markdown = `# ${title}\n\n`
57
+
58
+ if (includeDescription && schema.description) {
59
+ markdown += `${schema.description}\n\n`
60
+ }
61
+
62
+ markdown += this._generateFieldsTable(schema, locale)
63
+
64
+ if (includeExample) {
65
+ markdown += '\n' + this._generateExample(schema, locale)
66
+ }
67
+
68
+ markdown += '\n' + this._generateConstraintsSection(schema, locale)
69
+
70
+ return markdown
71
+ }
72
+
73
+ // ==================== Private static methods ====================
74
+
75
+ private static _i18nFields = {
76
+ 'zh-CN': { fields: '字段列表', name: '字段名', type: '类型', required: '必填', constraints: '约束', description: '说明' },
77
+ 'en-US': { fields: 'Fields', name: 'Field', type: 'Type', required: 'Required', constraints: 'Constraints', description: 'Description' },
78
+ 'ja-JP': { fields: 'フィールド一覧', name: 'フィールド名', type: 'タイプ', required: '必須', constraints: '制約', description: '説明' },
79
+ 'fr-FR': { fields: 'Liste des champs', name: 'Champ', type: 'Type', required: 'Obligatoire', constraints: 'Contraintes', description: 'Description' },
80
+ 'es-ES': { fields: 'Lista de campos', name: 'Campo', type: 'Tipo', required: 'Requerido', constraints: 'Restricciones', description: 'Descripción' },
81
+ }
82
+
83
+ private static _i18nTypes: Record<Locale, Record<string, string>> = {
84
+ 'zh-CN': { string: '字符串', number: '数字', integer: '整数', boolean: '布尔值', array: '数组', object: '对象', email: '邮箱', url: '网址', date: '日期', uuid: 'UUID' },
85
+ 'en-US': { string: 'String', number: 'Number', integer: 'Integer', boolean: 'Boolean', array: 'Array', object: 'Object', email: 'Email', url: 'URL', date: 'Date', uuid: 'UUID' },
86
+ 'ja-JP': { string: '文字列', number: '数値', integer: '整数', boolean: 'ブール値', array: '配列', object: 'オブジェクト', email: 'メールアドレス', url: 'URL', date: '日付', uuid: 'UUID' },
87
+ 'fr-FR': { string: 'Chaîne', number: 'Nombre', integer: 'Entier', boolean: 'Booléen', array: 'Tableau', object: 'Objet', email: 'E-mail', url: 'URL', date: 'Date', uuid: 'UUID' },
88
+ 'es-ES': { string: 'Cadena', number: 'Número', integer: 'Entero', boolean: 'Booleano', array: 'Array', object: 'Objeto', email: 'Correo', url: 'URL', date: 'Fecha', uuid: 'UUID' },
89
+ }
90
+
91
+ private static _i18nConstraints: Record<Locale, Record<string, string>> = {
92
+ 'zh-CN': { length: '长度', range: '范围', pattern: '正则', enum: '枚举', items: '元素数' },
93
+ 'en-US': { length: 'Length', range: 'Range', pattern: 'Pattern', enum: 'Enum', items: 'Items' },
94
+ 'ja-JP': { length: '長さ', range: '範囲', pattern: '正規表現', enum: '列挙', items: '要素数' },
95
+ 'fr-FR': { length: 'Longueur', range: 'Plage', pattern: 'Modèle', enum: 'Énumération', items: 'Éléments' },
96
+ 'es-ES': { length: 'Longitud', range: 'Rango', pattern: 'Patrón', enum: 'Enumeración', items: 'Elementos' },
97
+ }
98
+
99
+ private static _i18nRules: Record<Locale, Record<string, string>> = {
100
+ 'zh-CN': { rules: '约束规则', required: '必填字段', optional: '可选字段' },
101
+ 'en-US': { rules: 'Validation Rules', required: 'Required Fields', optional: 'Optional Fields' },
102
+ 'ja-JP': { rules: '検証ルール', required: '必須フィールド', optional: 'オプションフィールド' },
103
+ 'fr-FR': { rules: 'Règles de validation', required: 'Champs obligatoires', optional: 'Champs facultatifs' },
104
+ 'es-ES': { rules: 'Reglas de validación', required: 'Campos requeridos', optional: 'Campos opcionales' },
105
+ }
106
+
107
+ private static _generateFieldsTable(schema: JSONSchema, locale: Locale): string {
108
+ const t = this._i18nFields[locale] ?? this._i18nFields['en-US']
109
+
110
+ let table = `## ${t.fields}\n\n`
111
+ table += `| ${t.name} | ${t.type} | ${t.required} | ${t.constraints} | ${t.description} |\n`
112
+ table += `|--------|------|------|------|------|\n`
113
+
114
+ if (schema.properties) {
115
+ for (const [key, prop] of Object.entries(schema.properties)) {
116
+ const type = this._escapeTableCell(this._formatType(prop, locale))
117
+ // EX-01 fix: prefer _required flag, then fall back to schema.required
118
+ const isRequired = !!(
119
+ (prop as Record<string, unknown>)['_required'] === true ||
120
+ schema.required?.includes(key)
121
+ )
122
+ const required = isRequired ? '✅' : '❌'
123
+ const constraints = this._escapeTableCell(this._formatConstraints(prop, locale))
124
+ const description = this._escapeTableCell(this._getDescription(prop, locale))
125
+ const fieldName = this._escapeTableCell(key)
126
+
127
+ table += `| ${fieldName} | ${type} | ${required} | ${constraints} | ${description} |\n`
128
+ }
129
+ }
130
+
131
+ return table
132
+ }
133
+
134
+ private static _escapeTableCell(value: string): string {
135
+ return value
136
+ .replace(/\|/g, '\\|')
137
+ .replace(/\r\n|\r|\n/g, '<br>')
138
+ }
139
+
140
+ private static _formatType(prop: JSONSchema, locale: Locale): string {
141
+ const t = this._i18nTypes[locale] ?? this._i18nTypes['en-US']
142
+
143
+ if (prop.format) {
144
+ return t[prop.format] ?? prop.format
145
+ }
146
+
147
+ if (prop.type === 'array' && prop.items) {
148
+ const itemType = this._formatType(prop.items as JSONSchema, locale)
149
+ return `${t['array'] ?? 'array'}<${itemType}>`
150
+ }
151
+
152
+ return t[prop.type as string] ?? String(prop.type ?? 'any')
153
+ }
154
+
155
+ private static _formatConstraints(prop: JSONSchema, locale: Locale): string {
156
+ const constraints: string[] = []
157
+ const t = this._i18nConstraints[locale] ?? this._i18nConstraints['en-US']
158
+
159
+ if (prop.minLength !== undefined || prop.maxLength !== undefined) {
160
+ if (prop.minLength !== undefined && prop.maxLength !== undefined) {
161
+ constraints.push(`${t['length']}: ${prop.minLength}-${prop.maxLength}`)
162
+ } else if (prop.minLength !== undefined) {
163
+ constraints.push(`${t['length']}: ≥${prop.minLength}`)
164
+ } else if (prop.maxLength !== undefined) {
165
+ constraints.push(`${t['length']}: ≤${prop.maxLength}`)
166
+ }
167
+ }
168
+
169
+ if (prop.minimum !== undefined || prop.maximum !== undefined) {
170
+ if (prop.minimum !== undefined && prop.maximum !== undefined) {
171
+ constraints.push(`${t['range']}: ${prop.minimum}-${prop.maximum}`)
172
+ } else if (prop.minimum !== undefined) {
173
+ constraints.push(`${t['range']}: ≥${prop.minimum}`)
174
+ } else if (prop.maximum !== undefined) {
175
+ constraints.push(`${t['range']}: ≤${prop.maximum}`)
176
+ }
177
+ }
178
+
179
+ if (prop.minItems !== undefined || prop.maxItems !== undefined) {
180
+ if (prop.minItems !== undefined && prop.maxItems !== undefined) {
181
+ constraints.push(`${t['items']}: ${prop.minItems}-${prop.maxItems}`)
182
+ } else if (prop.minItems !== undefined) {
183
+ constraints.push(`${t['items']}: ≥${prop.minItems}`)
184
+ } else if (prop.maxItems !== undefined) {
185
+ constraints.push(`${t['items']}: ≤${prop.maxItems}`)
186
+ }
187
+ }
188
+
189
+ if (prop.pattern) {
190
+ constraints.push(`${t['pattern']}: \`${prop.pattern}\``)
191
+ }
192
+
193
+ if (prop.enum) {
194
+ const enumStr = (prop.enum as unknown[]).map(v => `\`${String(v)}\``).join(', ')
195
+ constraints.push(`${t['enum']}: ${enumStr}`)
196
+ }
197
+
198
+ return constraints.length > 0 ? constraints.join('<br>') : '-'
199
+ }
200
+
201
+ private static _getDescription(prop: JSONSchema, locale: Locale): string {
202
+ const p = prop as Record<string, unknown>
203
+
204
+ if (p['_labelI18n'] && typeof p['_labelI18n'] === 'object') {
205
+ const i18n = p['_labelI18n'] as Record<string, string>
206
+ if (i18n[locale]) return i18n[locale]
207
+ }
208
+
209
+ if (p['_label']) return p['_label'] as string
210
+ if (prop.description) return prop.description
211
+
212
+ return '-'
213
+ }
214
+
215
+ private static _generateExample(schema: JSONSchema, locale: Locale): string {
216
+ const i18n: Record<Locale, { example: string }> = {
217
+ 'zh-CN': { example: '示例数据' },
218
+ 'en-US': { example: 'Example Data' },
219
+ 'ja-JP': { example: 'サンプルデータ' },
220
+ 'fr-FR': { example: "Données d'exemple" },
221
+ 'es-ES': { example: 'Datos de ejemplo' },
222
+ }
223
+ const t = i18n[locale] ?? i18n['en-US']
224
+
225
+ let example = `## ${t.example}\n\n\`\`\`json\n`
226
+ example += JSON.stringify(this._buildExample(schema), null, 2)
227
+ example += '\n```\n'
228
+ return example
229
+ }
230
+
231
+ private static _buildExample(schema: JSONSchema): unknown {
232
+ if (schema.properties) {
233
+ const obj: Record<string, unknown> = {}
234
+ for (const [key, prop] of Object.entries(schema.properties)) {
235
+ const isRequired = !!(
236
+ (prop as Record<string, unknown>)['_required'] === true ||
237
+ schema.required?.includes(key)
238
+ )
239
+ if (isRequired) {
240
+ obj[key] = this._getExampleValue(prop)
241
+ }
242
+ }
243
+ return obj
244
+ }
245
+ return null
246
+ }
247
+
248
+ private static _getExampleValue(prop: JSONSchema): unknown {
249
+ if (prop.default !== undefined) return prop.default
250
+ if (prop.enum) return (prop.enum as unknown[])[0]
251
+
252
+ switch (prop.type) {
253
+ case 'string':
254
+ if (prop.format === 'email') return 'user@example.com'
255
+ if (prop.format === 'uri' || prop.format === 'url') return 'https://example.com'
256
+ if (prop.format === 'date') return '2025-12-29'
257
+ if (prop.format === 'uuid') return '550e8400-e29b-41d4-a716-446655440000'
258
+ return 'example'
259
+ case 'number':
260
+ case 'integer':
261
+ if (prop.minimum !== undefined) return prop.minimum
262
+ if (prop.maximum !== undefined) return Math.floor(prop.maximum / 2)
263
+ return 0
264
+ case 'boolean':
265
+ return true
266
+ case 'array':
267
+ if (prop.items) return [this._getExampleValue(prop.items as JSONSchema)]
268
+ return []
269
+ case 'object':
270
+ if (prop.properties) return this._buildExample(prop)
271
+ return {}
272
+ default:
273
+ return null
274
+ }
275
+ }
276
+
277
+ private static _generateConstraintsSection(schema: JSONSchema, locale: Locale): string {
278
+ if (!schema.properties) return ''
279
+
280
+ const t = this._i18nRules[locale] ?? this._i18nRules['en-US']
281
+ const requiredFields: string[] = []
282
+ const optionalFields: string[] = []
283
+
284
+ for (const [key, prop] of Object.entries(schema.properties)) {
285
+ const isRequired = !!(
286
+ (prop as Record<string, unknown>)['_required'] === true ||
287
+ schema.required?.includes(key)
288
+ )
289
+ if (isRequired) requiredFields.push(key)
290
+ else optionalFields.push(key)
291
+ }
292
+
293
+ let section = `## ${t['rules']}\n\n`
294
+
295
+ if (requiredFields.length > 0) {
296
+ section += `**${t['required']}**: ${requiredFields.map(f => `\`${f}\``).join(', ')}\n\n`
297
+ }
298
+
299
+ if (optionalFields.length > 0) {
300
+ section += `**${t['optional']}**: ${optionalFields.map(f => `\`${f}\``).join(', ')}\n`
301
+ }
302
+
303
+ return section
304
+ }
305
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * MongoDBExporter — Export JSON Schema as a MongoDB $jsonSchema validation schema.
3
+ */
4
+
5
+ import type { JSONSchema } from '../types/schema.js'
6
+ import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
7
+ import { TypeConverter } from '../utils/TypeConverter.js'
8
+
9
+ // ==================== Type definitions ====================
10
+
11
+ export interface MongoDBExporterOptions extends ExporterOptions {
12
+ /** Whether to use strict mode (validationLevel: 'strict' vs 'moderate'). */
13
+ strict: boolean
14
+ }
15
+
16
+ export interface MongoDBValidationSchema {
17
+ $jsonSchema: Record<string, unknown>
18
+ }
19
+
20
+ export interface MongoDBCreateCommand {
21
+ collectionName: string
22
+ options: {
23
+ validator: MongoDBValidationSchema
24
+ validationLevel: 'strict' | 'moderate'
25
+ validationAction: 'error' | 'warn'
26
+ }
27
+ }
28
+
29
+ // ==================== MongoDBExporter ====================
30
+
31
+ export class MongoDBExporter extends BaseExporter<MongoDBExporterOptions> {
32
+ constructor(options: Partial<MongoDBExporterOptions> = {}) {
33
+ super({ strict: false, ...options })
34
+ }
35
+
36
+ /**
37
+ * Convert a JSON Schema to a MongoDB $jsonSchema validation schema.
38
+ */
39
+ export(jsonSchema: unknown): MongoDBValidationSchema {
40
+ if (!jsonSchema || typeof jsonSchema !== 'object') {
41
+ throw new Error('[schema-dsl] Invalid JSON Schema')
42
+ }
43
+ const mongoSchema = this._convertSchema(jsonSchema as JSONSchema)
44
+ return { $jsonSchema: mongoSchema }
45
+ }
46
+
47
+ /**
48
+ * Generate a db.createCollection() command object.
49
+ */
50
+ generateCreateCommand(collectionName: string, jsonSchema: JSONSchema): MongoDBCreateCommand {
51
+ const validationSchema = this.export(jsonSchema)
52
+ return {
53
+ collectionName,
54
+ options: {
55
+ validator: validationSchema,
56
+ validationLevel: this.options.strict ? 'strict' : 'moderate',
57
+ validationAction: 'error',
58
+ },
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Generate an executable MongoDB command string.
64
+ */
65
+ generateCommand(collectionName: string, jsonSchema: JSONSchema): string {
66
+ const command = this.generateCreateCommand(collectionName, jsonSchema)
67
+ return `db.createCollection("${command.collectionName}", ${JSON.stringify(command.options, null, 2)})`
68
+ }
69
+
70
+ /**
71
+ * Static quick-export shorthand.
72
+ */
73
+ static export(jsonSchema: JSONSchema): MongoDBValidationSchema {
74
+ return new MongoDBExporter().export(jsonSchema)
75
+ }
76
+
77
+ // ==================== Private methods ====================
78
+
79
+ private _convertSchema(schema: JSONSchema): Record<string, unknown> {
80
+ const result: Record<string, unknown> = {}
81
+
82
+ if (schema.type) {
83
+ result['bsonType'] = TypeConverter.toMongoDBType(schema.type as string | string[])
84
+ } else if (schema.anyOf ?? schema.oneOf) {
85
+ const variants = (schema.anyOf ?? schema.oneOf) as JSONSchema[]
86
+ const bsonTypes = [...new Set(variants.map(v => v.type ? TypeConverter.toMongoDBType(v.type as string) : null).filter(Boolean))]
87
+ result['bsonType'] = bsonTypes.length === 1 ? bsonTypes[0] : bsonTypes
88
+ }
89
+
90
+ if (schema.properties) {
91
+ result['properties'] = {}
92
+ for (const [key, value] of Object.entries(schema.properties)) {
93
+ ;(result['properties'] as Record<string, unknown>)[key] = this._convertSchema(value)
94
+ }
95
+ }
96
+
97
+ if (schema.required && Array.isArray(schema.required)) {
98
+ result['required'] = schema.required
99
+ }
100
+
101
+ if (schema.items) {
102
+ result['items'] = this._convertSchema(schema.items as JSONSchema)
103
+ }
104
+
105
+ // String constraints
106
+ if (schema.minLength !== undefined) result['minLength'] = schema.minLength
107
+ if (schema.maxLength !== undefined) result['maxLength'] = schema.maxLength
108
+ if (schema.pattern) result['pattern'] = schema.pattern
109
+
110
+ // Numeric constraints
111
+ if (schema.minimum !== undefined) result['minimum'] = schema.minimum
112
+ if (schema.maximum !== undefined) result['maximum'] = schema.maximum
113
+
114
+ // Array constraints
115
+ if (schema.minItems !== undefined) result['minItems'] = schema.minItems
116
+ if (schema.maxItems !== undefined) result['maxItems'] = schema.maxItems
117
+
118
+ // Enum
119
+ if (schema.enum) result['enum'] = schema.enum
120
+
121
+ // Description
122
+ if (schema.description) result['description'] = schema.description
123
+
124
+ return result
125
+ }
126
+ }