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.
- package/CHANGELOG.md +87 -210
- package/README.md +391 -2249
- package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
- package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
- package/dist/Validator-C7GsVQOH.d.cts +192 -0
- package/dist/Validator-hFWKGxir.d.ts +192 -0
- package/dist/index.cjs +6594 -0
- package/dist/index.d.cts +1145 -0
- package/dist/index.d.ts +1145 -0
- package/dist/index.js +6528 -0
- package/dist/plugin-CIKtTMtS.d.cts +246 -0
- package/dist/plugin-CIKtTMtS.d.ts +246 -0
- package/dist/plugins/custom-format.cjs +3802 -0
- package/dist/plugins/custom-format.d.cts +12 -0
- package/dist/plugins/custom-format.d.ts +12 -0
- package/dist/plugins/custom-format.js +3772 -0
- package/dist/plugins/custom-type-example.cjs +3795 -0
- package/dist/plugins/custom-type-example.d.cts +8 -0
- package/dist/plugins/custom-type-example.d.ts +8 -0
- package/dist/plugins/custom-type-example.js +3765 -0
- package/dist/plugins/custom-validator.cjs +146 -0
- package/dist/plugins/custom-validator.d.cts +10 -0
- package/dist/plugins/custom-validator.d.ts +10 -0
- package/dist/plugins/custom-validator.js +121 -0
- package/docs/FEATURE-INDEX.md +102 -68
- package/docs/add-custom-locale.md +48 -35
- package/docs/add-keyword.md +24 -0
- package/docs/api-reference.md +396 -154
- package/docs/api.md +13 -0
- package/docs/best-practices-project-structure.md +19 -10
- package/docs/best-practices.md +93 -53
- package/docs/cache-manager.md +23 -15
- package/docs/compile.md +45 -0
- package/docs/conditional-api.md +40 -11
- package/docs/custom-extensions-guide.md +80 -152
- package/docs/design-philosophy.md +76 -71
- package/docs/doc-index.md +324 -0
- package/docs/dsl-syntax.md +69 -19
- package/docs/dynamic-locale.md +24 -14
- package/docs/enum.md +12 -5
- package/docs/error-handling.md +53 -44
- package/docs/export-guide.md +47 -8
- package/docs/export-limitations.md +27 -11
- package/docs/faq.md +86 -67
- package/docs/frontend-i18n-guide.md +26 -12
- package/docs/i18n-user-guide.md +60 -47
- package/docs/i18n.md +51 -32
- package/docs/index.md +48 -0
- package/docs/json-schema-basics.md +40 -0
- package/docs/label-vs-description.md +12 -3
- package/docs/markdown-exporter.md +15 -6
- package/docs/mongodb-exporter.md +11 -4
- package/docs/multi-language.md +26 -0
- package/docs/multi-type-support.md +26 -33
- package/docs/mysql-exporter.md +9 -2
- package/docs/number-operators.md +12 -5
- package/docs/optional-marker-guide.md +28 -23
- package/docs/performance-guide.md +49 -0
- package/docs/plugin-system.md +205 -366
- package/docs/plugin-type-registration.md +34 -0
- package/docs/postgresql-exporter.md +9 -2
- package/docs/public/favicon.svg +5 -0
- package/docs/quick-start.md +37 -363
- package/docs/runtime-locale-support.md +20 -9
- package/docs/schema-helper.md +10 -5
- package/docs/schema-utils-advanced-issues.md +23 -0
- package/docs/schema-utils-best-practices.md +20 -0
- package/docs/schema-utils-chaining.md +7 -0
- package/docs/schema-utils.md +76 -42
- package/docs/security-checklist.md +20 -0
- package/docs/string-extensions.md +17 -9
- package/docs/troubleshooting.md +36 -21
- package/docs/type-converter.md +41 -50
- package/docs/type-reference.md +38 -15
- package/docs/typescript-guide.md +53 -42
- package/docs/union-type-guide.md +11 -1
- package/docs/union-types.md +10 -3
- package/docs/validate-async.md +36 -25
- package/docs/validate-batch.md +49 -0
- package/docs/validate-dsl-object-support.md +33 -28
- package/docs/validate.md +36 -16
- package/docs/validation-guide.md +25 -7
- package/docs/validator.md +39 -0
- package/package.json +85 -27
- package/plugins/custom-format.cjs +8 -0
- package/plugins/custom-type-example.cjs +8 -0
- package/plugins/custom-validator.cjs +8 -0
- package/src/adapters/DslAdapter.ts +111 -0
- package/src/adapters/index.ts +1 -0
- package/src/config/constants.ts +83 -0
- package/src/config/index.ts +2 -0
- package/src/config/patterns.ts +77 -0
- package/src/core/CacheManager.ts +159 -0
- package/src/core/ConditionalBuilder.ts +382 -0
- package/src/core/ConditionalRuntime.ts +28 -0
- package/src/core/ConditionalValidator.ts +255 -0
- package/src/core/DslBuilder.ts +677 -0
- package/src/core/ErrorCodes.ts +38 -0
- package/src/core/ErrorFormatter.ts +271 -0
- package/src/core/JSONSchemaCore.ts +65 -0
- package/src/core/Locale.ts +187 -0
- package/src/core/MessageTemplate.ts +42 -0
- package/src/core/ObjectDslBuilder.ts +64 -0
- package/src/core/PluginManager.ts +326 -0
- package/src/core/StringExtensions.ts +140 -0
- package/src/core/TemplateEngine.ts +44 -0
- package/src/core/Validator.ts +448 -0
- package/src/errors/I18nError.ts +159 -0
- package/src/errors/ValidationError.ts +105 -0
- package/src/exporters/BaseExporter.ts +60 -0
- package/src/exporters/MarkdownExporter.ts +305 -0
- package/src/exporters/MongoDBExporter.ts +126 -0
- package/src/exporters/MySQLExporter.ts +155 -0
- package/src/exporters/PostgreSQLExporter.ts +222 -0
- package/src/exporters/index.ts +18 -0
- package/src/index.ts +633 -0
- package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
- package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
- package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
- package/src/locales/index.ts +103 -0
- package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
- package/src/locales/types.ts +156 -0
- package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
- package/src/parser/ConstraintParser.ts +101 -0
- package/src/parser/DslParser.ts +470 -0
- package/src/parser/SchemaCompiler.ts +66 -0
- package/src/parser/TypeRegistry.ts +250 -0
- package/src/parser/index.ts +6 -0
- package/src/plugins/custom-format.ts +126 -0
- package/src/plugins/custom-type-example.ts +108 -0
- package/src/plugins/custom-validator.ts +140 -0
- package/src/types/conditional.ts +28 -0
- package/src/types/config.ts +59 -0
- package/src/types/dsl.ts +131 -0
- package/src/types/error.ts +60 -0
- package/src/types/index.ts +17 -0
- package/src/types/infer.ts +128 -0
- package/src/types/plugin.ts +58 -0
- package/src/types/safe-regex.d.ts +9 -0
- package/src/types/schema.ts +66 -0
- package/src/types/validate.ts +71 -0
- package/src/utils/SchemaHelper.ts +196 -0
- package/src/utils/SchemaUtils.ts +346 -0
- package/src/utils/TypeConverter.ts +215 -0
- package/src/utils/index.ts +10 -0
- package/src/validators/CustomKeywords.ts +477 -0
- package/.eslintignore +0 -11
- package/.eslintrc.json +0 -27
- package/CONTRIBUTING.md +0 -368
- package/STATUS.md +0 -491
- package/changelogs/v1.0.0.md +0 -328
- package/changelogs/v1.0.9.md +0 -367
- package/changelogs/v1.1.0.md +0 -389
- package/changelogs/v1.1.1.md +0 -308
- package/changelogs/v1.1.2.md +0 -183
- package/changelogs/v1.1.3.md +0 -161
- package/changelogs/v1.1.4.md +0 -432
- package/changelogs/v1.1.5.md +0 -493
- package/changelogs/v1.1.6.md +0 -211
- package/changelogs/v1.1.8.md +0 -376
- package/changelogs/v1.2.3.md +0 -124
- package/docs/INDEX.md +0 -252
- package/docs/issues-resolved-summary.md +0 -196
- package/docs/performance-benchmark-report.md +0 -179
- package/docs/performance-quick-reference.md +0 -123
- package/docs/user-questions-answered.md +0 -353
- package/docs/validation-rules-v1.0.2.md +0 -1608
- package/examples/README.md +0 -81
- package/examples/array-dsl-example.js +0 -227
- package/examples/conditional-example.js +0 -288
- package/examples/conditional-non-object.js +0 -129
- package/examples/conditional-validate-example.js +0 -321
- package/examples/custom-extension.js +0 -85
- package/examples/dsl-match-example.js +0 -74
- package/examples/dsl-style.js +0 -118
- package/examples/dynamic-locale-configuration.js +0 -348
- package/examples/dynamic-locale-example.js +0 -287
- package/examples/enum.examples.js +0 -324
- package/examples/export-demo.js +0 -130
- package/examples/express-integration.js +0 -376
- package/examples/i18n-error-handling-complete.js +0 -381
- package/examples/i18n-error-handling-quickstart.md +0 -0
- package/examples/i18n-error.examples.js +0 -181
- package/examples/i18n-full-demo.js +0 -301
- package/examples/i18n-memory-safety.examples.js +0 -268
- package/examples/markdown-export.js +0 -71
- package/examples/middleware-usage.js +0 -93
- package/examples/new-features-comparison.js +0 -315
- package/examples/password-reset/README.md +0 -153
- package/examples/password-reset/schema.js +0 -26
- package/examples/password-reset/test.js +0 -101
- package/examples/plugin-system.examples.js +0 -205
- package/examples/schema-utils-chaining.examples.js +0 -250
- package/examples/simple-example.js +0 -122
- package/examples/slug.examples.js +0 -179
- package/examples/string-extensions.js +0 -297
- package/examples/union-type-example.js +0 -127
- package/examples/union-types-example.js +0 -77
- package/examples/user-registration/README.md +0 -156
- package/examples/user-registration/routes.js +0 -92
- package/examples/user-registration/schema.js +0 -150
- package/examples/user-registration/server.js +0 -74
- package/index.d.ts +0 -3540
- package/index.js +0 -457
- package/index.mjs +0 -60
- package/lib/adapters/DslAdapter.js +0 -871
- package/lib/adapters/index.js +0 -20
- package/lib/config/constants.js +0 -286
- package/lib/config/patterns/common.js +0 -47
- package/lib/config/patterns/creditCard.js +0 -9
- package/lib/config/patterns/idCard.js +0 -9
- package/lib/config/patterns/index.js +0 -9
- package/lib/config/patterns/licensePlate.js +0 -4
- package/lib/config/patterns/passport.js +0 -4
- package/lib/config/patterns/phone.js +0 -9
- package/lib/config/patterns/postalCode.js +0 -5
- package/lib/core/CacheManager.js +0 -376
- package/lib/core/ConditionalBuilder.js +0 -503
- package/lib/core/DslBuilder.js +0 -1400
- package/lib/core/ErrorCodes.js +0 -233
- package/lib/core/ErrorFormatter.js +0 -445
- package/lib/core/JSONSchemaCore.js +0 -347
- package/lib/core/Locale.js +0 -130
- package/lib/core/MessageTemplate.js +0 -98
- package/lib/core/PluginManager.js +0 -448
- package/lib/core/StringExtensions.js +0 -240
- package/lib/core/Validator.js +0 -654
- package/lib/errors/I18nError.js +0 -328
- package/lib/errors/ValidationError.js +0 -191
- package/lib/exporters/MarkdownExporter.js +0 -420
- package/lib/exporters/MongoDBExporter.js +0 -162
- package/lib/exporters/MySQLExporter.js +0 -212
- package/lib/exporters/PostgreSQLExporter.js +0 -289
- package/lib/exporters/index.js +0 -24
- package/lib/locales/index.js +0 -8
- package/lib/utils/LRUCache.js +0 -174
- package/lib/utils/SchemaHelper.js +0 -240
- package/lib/utils/SchemaUtils.js +0 -445
- package/lib/utils/TypeConverter.js +0 -245
- package/lib/utils/index.js +0 -13
- package/lib/validators/CustomKeywords.js +0 -616
- 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
|
+
}
|