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,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
+ }