schema-dsl 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,126 +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
|
-
}
|
|
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
|
+
}
|
|
@@ -1,155 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* MySQLExporter — Export JSON Schema as a MySQL CREATE TABLE DDL statement.
|
|
3
|
-
*
|
|
4
|
-
* v2 fixes:
|
|
5
|
-
* _escapeString uses standard SQL single-quote escaping (`'` → `''`)
|
|
6
|
-
* _convertColumn passes the full schema object to TypeConverter.toMySQLType (includes maxLength etc.)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { JSONSchema } from '../types/schema.js'
|
|
10
|
-
import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
|
|
11
|
-
import { TypeConverter } from '../utils/TypeConverter.js'
|
|
12
|
-
|
|
13
|
-
// ==================== Type definitions ====================
|
|
14
|
-
|
|
15
|
-
export interface MySQLExporterOptions extends ExporterOptions {
|
|
16
|
-
engine: string
|
|
17
|
-
charset: string
|
|
18
|
-
collate: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface GenerateIndexOptions {
|
|
22
|
-
name?: string
|
|
23
|
-
unique?: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ==================== MySQLExporter ====================
|
|
27
|
-
|
|
28
|
-
export class MySQLExporter extends BaseExporter<MySQLExporterOptions> {
|
|
29
|
-
constructor(options: Partial<MySQLExporterOptions> = {}) {
|
|
30
|
-
super({
|
|
31
|
-
engine: 'InnoDB',
|
|
32
|
-
charset: 'utf8mb4',
|
|
33
|
-
collate: 'utf8mb4_unicode_ci',
|
|
34
|
-
...options,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Export as a MySQL CREATE TABLE statement.
|
|
40
|
-
*/
|
|
41
|
-
export(tableName: string, jsonSchema: JSONSchema): string {
|
|
42
|
-
if (!tableName || typeof tableName !== 'string') {
|
|
43
|
-
throw new Error('[schema-dsl] Table name is required')
|
|
44
|
-
}
|
|
45
|
-
this._assertObjectSchema(jsonSchema)
|
|
46
|
-
|
|
47
|
-
const columns = this._convertProperties(jsonSchema)
|
|
48
|
-
const primaryKey = this._detectPrimaryKey(jsonSchema)
|
|
49
|
-
|
|
50
|
-
let ddl = `CREATE TABLE ${this._quoteIdent(tableName)} (\n`
|
|
51
|
-
ddl += columns.map(col => ` ${col}`).join(',\n')
|
|
52
|
-
|
|
53
|
-
if (primaryKey) {
|
|
54
|
-
ddl += `,\n PRIMARY KEY (${this._quoteIdent(primaryKey)})`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
ddl += `\n)`
|
|
58
|
-
ddl += ` ENGINE=${this.options.engine}`
|
|
59
|
-
ddl += ` DEFAULT CHARSET=${this.options.charset}`
|
|
60
|
-
ddl += ` COLLATE=${this.options.collate};`
|
|
61
|
-
|
|
62
|
-
return ddl
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Generate a CREATE INDEX statement.
|
|
67
|
-
*/
|
|
68
|
-
generateIndex(tableName: string, columnName: string, options: GenerateIndexOptions = {}): string {
|
|
69
|
-
const indexName = options.name ?? `idx_${tableName}_${columnName}`
|
|
70
|
-
const unique = options.unique ? 'UNIQUE ' : ''
|
|
71
|
-
return `CREATE ${unique}INDEX ${this._quoteIdent(indexName)} ON ${this._quoteIdent(tableName)} (${this._quoteIdent(columnName)});`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Static quick-export shorthand.
|
|
76
|
-
*/
|
|
77
|
-
static export(tableName: string, jsonSchema: JSONSchema): string {
|
|
78
|
-
return new MySQLExporter().export(tableName, jsonSchema)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ==================== Private methods ====================
|
|
82
|
-
|
|
83
|
-
private _quoteIdent(name: string): string {
|
|
84
|
-
return '`' + name.replace(/`/g, '``') + '`'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private _convertProperties(schema: JSONSchema): string[] {
|
|
88
|
-
if (!schema.properties) return []
|
|
89
|
-
|
|
90
|
-
const required = schema.required ?? []
|
|
91
|
-
const columns: string[] = []
|
|
92
|
-
|
|
93
|
-
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
94
|
-
columns.push(this._convertColumn(name, propSchema, required.includes(name)))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return columns
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private _convertColumn(name: string, schema: JSONSchema, isRequired: boolean): string {
|
|
101
|
-
const { jsonType, sqlType } = this._resolveColumnType(name, schema)
|
|
102
|
-
|
|
103
|
-
let def = `${this._quoteIdent(name)} ${sqlType}`
|
|
104
|
-
|
|
105
|
-
def += isRequired ? ' NOT NULL' : ' NULL'
|
|
106
|
-
|
|
107
|
-
if (schema.default !== undefined) {
|
|
108
|
-
def += ` DEFAULT ${this._formatDefaultValue(schema.default, jsonType)}`
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (schema.description) {
|
|
112
|
-
def += ` COMMENT '${this._escapeString(schema.description)}'`
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return def
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private _resolveColumnType(name: string, schema: JSONSchema): { jsonType: string; sqlType: string } {
|
|
119
|
-
if (schema.type) {
|
|
120
|
-
return {
|
|
121
|
-
jsonType: String(schema.type),
|
|
122
|
-
sqlType: TypeConverter.toMySQLType(schema.type as string | string[], schema),
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const variants = (schema.anyOf ?? schema.oneOf) as JSONSchema[] | undefined
|
|
127
|
-
if (!variants?.length) {
|
|
128
|
-
return {
|
|
129
|
-
jsonType: 'string',
|
|
130
|
-
sqlType: TypeConverter.toMySQLType('string', schema),
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const sqlTypes = new Set(
|
|
135
|
-
variants.map(variant => TypeConverter.toMySQLType((variant.type as string | string[] | undefined) ?? 'string', variant))
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
if (sqlTypes.size !== 1) {
|
|
139
|
-
const unionKind = schema.anyOf ? 'anyOf' : 'oneOf'
|
|
140
|
-
throw new Error(`[schema-dsl] MySQL exporter cannot safely map ${unionKind} for column "${name}" to a single SQL type`)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
jsonType: String(variants[0]?.type ?? 'string'),
|
|
145
|
-
sqlType: [...sqlTypes][0],
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
private _formatDefaultValue(value: unknown, type: string): string {
|
|
150
|
-
if (value === null) return 'NULL'
|
|
151
|
-
if (type === 'string') return `'${this._escapeString(String(value))}'`
|
|
152
|
-
if (type === 'boolean') return value ? '1' : '0'
|
|
153
|
-
return
|
|
154
|
-
|
|
155
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* MySQLExporter — Export JSON Schema as a MySQL CREATE TABLE DDL statement.
|
|
3
|
+
*
|
|
4
|
+
* v2 fixes:
|
|
5
|
+
* _escapeString uses standard SQL single-quote escaping (`'` → `''`)
|
|
6
|
+
* _convertColumn passes the full schema object to TypeConverter.toMySQLType (includes maxLength etc.)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
10
|
+
import { BaseExporter, type ExporterOptions } from './BaseExporter.js'
|
|
11
|
+
import { TypeConverter } from '../utils/TypeConverter.js'
|
|
12
|
+
|
|
13
|
+
// ==================== Type definitions ====================
|
|
14
|
+
|
|
15
|
+
export interface MySQLExporterOptions extends ExporterOptions {
|
|
16
|
+
engine: string
|
|
17
|
+
charset: string
|
|
18
|
+
collate: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface GenerateIndexOptions {
|
|
22
|
+
name?: string
|
|
23
|
+
unique?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ==================== MySQLExporter ====================
|
|
27
|
+
|
|
28
|
+
export class MySQLExporter extends BaseExporter<MySQLExporterOptions> {
|
|
29
|
+
constructor(options: Partial<MySQLExporterOptions> = {}) {
|
|
30
|
+
super({
|
|
31
|
+
engine: 'InnoDB',
|
|
32
|
+
charset: 'utf8mb4',
|
|
33
|
+
collate: 'utf8mb4_unicode_ci',
|
|
34
|
+
...options,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Export as a MySQL CREATE TABLE statement.
|
|
40
|
+
*/
|
|
41
|
+
export(tableName: string, jsonSchema: JSONSchema): string {
|
|
42
|
+
if (!tableName || typeof tableName !== 'string') {
|
|
43
|
+
throw new Error('[schema-dsl] Table name is required')
|
|
44
|
+
}
|
|
45
|
+
this._assertObjectSchema(jsonSchema)
|
|
46
|
+
|
|
47
|
+
const columns = this._convertProperties(jsonSchema)
|
|
48
|
+
const primaryKey = this._detectPrimaryKey(jsonSchema)
|
|
49
|
+
|
|
50
|
+
let ddl = `CREATE TABLE ${this._quoteIdent(tableName)} (\n`
|
|
51
|
+
ddl += columns.map(col => ` ${col}`).join(',\n')
|
|
52
|
+
|
|
53
|
+
if (primaryKey) {
|
|
54
|
+
ddl += `,\n PRIMARY KEY (${this._quoteIdent(primaryKey)})`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ddl += `\n)`
|
|
58
|
+
ddl += ` ENGINE=${this.options.engine}`
|
|
59
|
+
ddl += ` DEFAULT CHARSET=${this.options.charset}`
|
|
60
|
+
ddl += ` COLLATE=${this.options.collate};`
|
|
61
|
+
|
|
62
|
+
return ddl
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate a CREATE INDEX statement.
|
|
67
|
+
*/
|
|
68
|
+
generateIndex(tableName: string, columnName: string, options: GenerateIndexOptions = {}): string {
|
|
69
|
+
const indexName = options.name ?? `idx_${tableName}_${columnName}`
|
|
70
|
+
const unique = options.unique ? 'UNIQUE ' : ''
|
|
71
|
+
return `CREATE ${unique}INDEX ${this._quoteIdent(indexName)} ON ${this._quoteIdent(tableName)} (${this._quoteIdent(columnName)});`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Static quick-export shorthand.
|
|
76
|
+
*/
|
|
77
|
+
static export(tableName: string, jsonSchema: JSONSchema): string {
|
|
78
|
+
return new MySQLExporter().export(tableName, jsonSchema)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ==================== Private methods ====================
|
|
82
|
+
|
|
83
|
+
private _quoteIdent(name: string): string {
|
|
84
|
+
return '`' + name.replace(/`/g, '``') + '`'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private _convertProperties(schema: JSONSchema): string[] {
|
|
88
|
+
if (!schema.properties) return []
|
|
89
|
+
|
|
90
|
+
const required = schema.required ?? []
|
|
91
|
+
const columns: string[] = []
|
|
92
|
+
|
|
93
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
94
|
+
columns.push(this._convertColumn(name, propSchema, required.includes(name)))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return columns
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private _convertColumn(name: string, schema: JSONSchema, isRequired: boolean): string {
|
|
101
|
+
const { jsonType, sqlType } = this._resolveColumnType(name, schema)
|
|
102
|
+
|
|
103
|
+
let def = `${this._quoteIdent(name)} ${sqlType}`
|
|
104
|
+
|
|
105
|
+
def += isRequired ? ' NOT NULL' : ' NULL'
|
|
106
|
+
|
|
107
|
+
if (schema.default !== undefined) {
|
|
108
|
+
def += ` DEFAULT ${this._formatDefaultValue(schema.default, jsonType)}`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (schema.description) {
|
|
112
|
+
def += ` COMMENT '${this._escapeString(schema.description)}'`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return def
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private _resolveColumnType(name: string, schema: JSONSchema): { jsonType: string; sqlType: string } {
|
|
119
|
+
if (schema.type) {
|
|
120
|
+
return {
|
|
121
|
+
jsonType: String(schema.type),
|
|
122
|
+
sqlType: TypeConverter.toMySQLType(schema.type as string | string[], schema),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const variants = (schema.anyOf ?? schema.oneOf) as JSONSchema[] | undefined
|
|
127
|
+
if (!variants?.length) {
|
|
128
|
+
return {
|
|
129
|
+
jsonType: 'string',
|
|
130
|
+
sqlType: TypeConverter.toMySQLType('string', schema),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const sqlTypes = new Set(
|
|
135
|
+
variants.map(variant => TypeConverter.toMySQLType((variant.type as string | string[] | undefined) ?? 'string', variant))
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if (sqlTypes.size !== 1) {
|
|
139
|
+
const unionKind = schema.anyOf ? 'anyOf' : 'oneOf'
|
|
140
|
+
throw new Error(`[schema-dsl] MySQL exporter cannot safely map ${unionKind} for column "${name}" to a single SQL type`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
jsonType: String(variants[0]?.type ?? 'string'),
|
|
145
|
+
sqlType: [...sqlTypes][0],
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private _formatDefaultValue(value: unknown, type: string): string {
|
|
150
|
+
if (value === null) return 'NULL'
|
|
151
|
+
if (type === 'string') return `'${this._escapeString(String(value))}'`
|
|
152
|
+
if (type === 'boolean') return value ? '1' : '0'
|
|
153
|
+
if (typeof value === 'object' && value !== null) return `'${this._escapeString(JSON.stringify(value))}'`
|
|
154
|
+
return String(value)
|
|
155
|
+
}
|
|
156
|
+
}
|