schema-dsl 1.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/.eslintignore +10 -0
- package/.eslintrc.json +27 -0
- package/.github/CODE_OF_CONDUCT.md +45 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +57 -0
- package/.github/ISSUE_TEMPLATE/config.yml +11 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +45 -0
- package/.github/ISSUE_TEMPLATE/question.md +31 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +70 -0
- package/.github/SECURITY.md +184 -0
- package/.github/workflows/ci.yml +33 -0
- package/CHANGELOG.md +633 -0
- package/CONTRIBUTING.md +368 -0
- package/LICENSE +21 -0
- package/README.md +1184 -0
- package/STATUS.md +101 -0
- package/docs/FEATURE-INDEX.md +519 -0
- package/docs/INDEX.md +253 -0
- package/docs/api-reference.md +1096 -0
- package/docs/best-practices.md +672 -0
- package/docs/cache-manager.md +336 -0
- package/docs/design-philosophy.md +601 -0
- package/docs/dsl-syntax.md +653 -0
- package/docs/dynamic-locale.md +552 -0
- package/docs/error-handling.md +703 -0
- package/docs/export-guide.md +462 -0
- package/docs/export-limitations.md +551 -0
- package/docs/faq.md +577 -0
- package/docs/frontend-i18n-guide.md +290 -0
- package/docs/i18n-user-guide.md +476 -0
- package/docs/label-vs-description.md +262 -0
- package/docs/markdown-exporter.md +397 -0
- package/docs/mongodb-exporter.md +295 -0
- package/docs/multi-type-support.md +319 -0
- package/docs/mysql-exporter.md +273 -0
- package/docs/plugin-system.md +542 -0
- package/docs/postgresql-exporter.md +304 -0
- package/docs/quick-start.md +761 -0
- package/docs/schema-helper.md +340 -0
- package/docs/schema-utils-chaining.md +143 -0
- package/docs/schema-utils.md +490 -0
- package/docs/string-extensions.md +480 -0
- package/docs/troubleshooting.md +471 -0
- package/docs/type-converter.md +319 -0
- package/docs/type-reference.md +219 -0
- package/docs/validate-async.md +480 -0
- package/docs/validate.md +486 -0
- package/docs/validation-guide.md +484 -0
- package/examples/array-dsl-example.js +227 -0
- package/examples/custom-extension.js +85 -0
- package/examples/dsl-match-example.js +74 -0
- package/examples/dsl-style.js +118 -0
- package/examples/dynamic-locale-configuration.js +348 -0
- package/examples/dynamic-locale-example.js +287 -0
- package/examples/export-demo.js +130 -0
- package/examples/express-integration.js +376 -0
- package/examples/i18n-full-demo.js +310 -0
- package/examples/i18n-memory-safety.examples.js +268 -0
- package/examples/markdown-export.js +71 -0
- package/examples/middleware-usage.js +93 -0
- package/examples/new-features-comparison.js +315 -0
- package/examples/password-reset/README.md +153 -0
- package/examples/password-reset/schema.js +26 -0
- package/examples/password-reset/test.js +101 -0
- package/examples/plugin-system.examples.js +205 -0
- package/examples/schema-utils-chaining.examples.js +250 -0
- package/examples/simple-example.js +122 -0
- package/examples/string-extensions.js +297 -0
- package/examples/user-registration/README.md +156 -0
- package/examples/user-registration/routes.js +92 -0
- package/examples/user-registration/schema.js +150 -0
- package/examples/user-registration/server.js +74 -0
- package/index.d.ts +1999 -0
- package/index.js +282 -0
- package/index.mjs +30 -0
- package/lib/adapters/DslAdapter.js +699 -0
- package/lib/adapters/index.js +20 -0
- package/lib/config/constants.js +286 -0
- package/lib/config/patterns/creditCard.js +9 -0
- package/lib/config/patterns/idCard.js +9 -0
- package/lib/config/patterns/index.js +8 -0
- package/lib/config/patterns/licensePlate.js +4 -0
- package/lib/config/patterns/passport.js +4 -0
- package/lib/config/patterns/phone.js +9 -0
- package/lib/config/patterns/postalCode.js +5 -0
- package/lib/core/CacheManager.js +376 -0
- package/lib/core/DslBuilder.js +740 -0
- package/lib/core/ErrorCodes.js +233 -0
- package/lib/core/ErrorFormatter.js +342 -0
- package/lib/core/JSONSchemaCore.js +347 -0
- package/lib/core/Locale.js +119 -0
- package/lib/core/MessageTemplate.js +89 -0
- package/lib/core/PluginManager.js +448 -0
- package/lib/core/StringExtensions.js +209 -0
- package/lib/core/Validator.js +376 -0
- package/lib/errors/ValidationError.js +191 -0
- package/lib/exporters/MarkdownExporter.js +420 -0
- package/lib/exporters/MongoDBExporter.js +162 -0
- package/lib/exporters/MySQLExporter.js +212 -0
- package/lib/exporters/PostgreSQLExporter.js +289 -0
- package/lib/exporters/index.js +24 -0
- package/lib/locales/en-US.js +65 -0
- package/lib/locales/es-ES.js +66 -0
- package/lib/locales/fr-FR.js +66 -0
- package/lib/locales/index.js +8 -0
- package/lib/locales/ja-JP.js +66 -0
- package/lib/locales/zh-CN.js +93 -0
- package/lib/utils/LRUCache.js +174 -0
- package/lib/utils/SchemaHelper.js +240 -0
- package/lib/utils/SchemaUtils.js +445 -0
- package/lib/utils/TypeConverter.js +245 -0
- package/lib/utils/index.js +13 -0
- package/lib/validators/CustomKeywords.js +203 -0
- package/lib/validators/index.js +11 -0
- package/package.json +70 -0
- package/plugins/custom-format.js +101 -0
- package/plugins/custom-validator.js +200 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL DDL导出器
|
|
3
|
+
*
|
|
4
|
+
* 将JSON Schema转换为MySQL CREATE TABLE语句
|
|
5
|
+
*
|
|
6
|
+
* @module lib/exporters/MySQLExporter
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TypeConverter = require('../utils/TypeConverter');
|
|
11
|
+
const SchemaHelper = require('../utils/SchemaHelper');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* MySQL导出器类
|
|
15
|
+
* @class MySQLExporter
|
|
16
|
+
*/
|
|
17
|
+
class MySQLExporter {
|
|
18
|
+
/**
|
|
19
|
+
* 构造函数
|
|
20
|
+
* @param {Object} options - 配置选项
|
|
21
|
+
* @param {string} options.engine - 存储引擎(默认InnoDB)
|
|
22
|
+
* @param {string} options.charset - 字符集(默认utf8mb4)
|
|
23
|
+
* @param {string} options.collate - 排序规则(默认utf8mb4_unicode_ci)
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = {
|
|
27
|
+
engine: options.engine || 'InnoDB',
|
|
28
|
+
charset: options.charset || 'utf8mb4',
|
|
29
|
+
collate: options.collate || 'utf8mb4_unicode_ci',
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 导出为MySQL CREATE TABLE语句
|
|
36
|
+
* @param {string} tableName - 表名
|
|
37
|
+
* @param {Object} jsonSchema - JSON Schema对象
|
|
38
|
+
* @returns {string} MySQL DDL语句
|
|
39
|
+
*/
|
|
40
|
+
export(tableName, jsonSchema) {
|
|
41
|
+
if (!tableName || typeof tableName !== 'string') {
|
|
42
|
+
throw new Error('Table name is required');
|
|
43
|
+
}
|
|
44
|
+
if (!jsonSchema || jsonSchema.type !== 'object') {
|
|
45
|
+
throw new Error('JSON Schema must be an object type');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const columns = this._convertProperties(jsonSchema);
|
|
49
|
+
const primaryKey = this._detectPrimaryKey(jsonSchema);
|
|
50
|
+
|
|
51
|
+
let ddl = `CREATE TABLE \`${tableName}\` (\n`;
|
|
52
|
+
|
|
53
|
+
// 添加列定义
|
|
54
|
+
const columnDefs = columns.map(col => ` ${col}`).join(',\n');
|
|
55
|
+
ddl += columnDefs;
|
|
56
|
+
|
|
57
|
+
// 添加主键
|
|
58
|
+
if (primaryKey) {
|
|
59
|
+
ddl += `,\n PRIMARY KEY (\`${primaryKey}\`)`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ddl += `\n)`;
|
|
63
|
+
|
|
64
|
+
// 添加表选项
|
|
65
|
+
ddl += ` ENGINE=${this.options.engine}`;
|
|
66
|
+
ddl += ` DEFAULT CHARSET=${this.options.charset}`;
|
|
67
|
+
ddl += ` COLLATE=${this.options.collate};`;
|
|
68
|
+
|
|
69
|
+
return ddl;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 转换properties为列定义
|
|
74
|
+
* @private
|
|
75
|
+
* @param {Object} schema - JSON Schema对象
|
|
76
|
+
* @returns {Array<string>} 列定义数组
|
|
77
|
+
*/
|
|
78
|
+
_convertProperties(schema) {
|
|
79
|
+
if (!schema.properties) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const columns = [];
|
|
84
|
+
const required = schema.required || [];
|
|
85
|
+
|
|
86
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
87
|
+
const columnDef = this._convertColumn(name, propSchema, required.includes(name));
|
|
88
|
+
columns.push(columnDef);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return columns;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 转换单个列
|
|
96
|
+
* @private
|
|
97
|
+
* @param {string} name - 列名
|
|
98
|
+
* @param {Object} schema - 属性Schema
|
|
99
|
+
* @param {boolean} isRequired - 是否必填
|
|
100
|
+
* @returns {string} 列定义字符串
|
|
101
|
+
*/
|
|
102
|
+
_convertColumn(name, schema, isRequired) {
|
|
103
|
+
const constraints = TypeConverter.extractConstraints(schema);
|
|
104
|
+
const mysqlType = TypeConverter.toMySQLType(schema.type, constraints);
|
|
105
|
+
|
|
106
|
+
let def = `\`${name}\` ${mysqlType}`;
|
|
107
|
+
|
|
108
|
+
// NULL约束
|
|
109
|
+
if (isRequired) {
|
|
110
|
+
def += ' NOT NULL';
|
|
111
|
+
} else {
|
|
112
|
+
def += ' NULL';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 默认值
|
|
116
|
+
if (schema.default !== undefined) {
|
|
117
|
+
const defaultValue = this._formatDefaultValue(schema.default, schema.type);
|
|
118
|
+
def += ` DEFAULT ${defaultValue}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 注释
|
|
122
|
+
if (schema.description) {
|
|
123
|
+
def += ` COMMENT '${this._escapeString(schema.description)}'`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return def;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 格式化默认值
|
|
131
|
+
* @private
|
|
132
|
+
* @param {*} value - 默认值
|
|
133
|
+
* @param {string} type - 类型
|
|
134
|
+
* @returns {string} 格式化后的默认值
|
|
135
|
+
*/
|
|
136
|
+
_formatDefaultValue(value, type) {
|
|
137
|
+
if (value === null) {
|
|
138
|
+
return 'NULL';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (type === 'string') {
|
|
142
|
+
return `'${this._escapeString(value)}'`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (type === 'boolean') {
|
|
146
|
+
return value ? '1' : '0';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return value.toString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 转义字符串
|
|
154
|
+
* @private
|
|
155
|
+
* @param {string} str - 字符串
|
|
156
|
+
* @returns {string} 转义后的字符串
|
|
157
|
+
*/
|
|
158
|
+
_escapeString(str) {
|
|
159
|
+
return str.replace(/'/g, "''");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 检测主键
|
|
164
|
+
* @private
|
|
165
|
+
* @param {Object} schema - JSON Schema对象
|
|
166
|
+
* @returns {string|null} 主键列名
|
|
167
|
+
*/
|
|
168
|
+
_detectPrimaryKey(schema) {
|
|
169
|
+
if (!schema.properties) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 查找名为id或_id的字段
|
|
174
|
+
if (schema.properties.id) {
|
|
175
|
+
return 'id';
|
|
176
|
+
}
|
|
177
|
+
if (schema.properties._id) {
|
|
178
|
+
return '_id';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 生成索引DDL
|
|
186
|
+
* @param {string} tableName - 表名
|
|
187
|
+
* @param {string} columnName - 列名
|
|
188
|
+
* @param {Object} options - 索引选项
|
|
189
|
+
* @returns {string} CREATE INDEX语句
|
|
190
|
+
*/
|
|
191
|
+
generateIndex(tableName, columnName, options = {}) {
|
|
192
|
+
const indexName = options.name || `idx_${tableName}_${columnName}`;
|
|
193
|
+
const unique = options.unique ? 'UNIQUE ' : '';
|
|
194
|
+
|
|
195
|
+
return `CREATE ${unique}INDEX \`${indexName}\` ON \`${tableName}\` (\`${columnName}\`);`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 静态方法:快速导出
|
|
200
|
+
* @static
|
|
201
|
+
* @param {string} tableName - 表名
|
|
202
|
+
* @param {Object} jsonSchema - JSON Schema对象
|
|
203
|
+
* @returns {string} MySQL DDL语句
|
|
204
|
+
*/
|
|
205
|
+
static export(tableName, jsonSchema) {
|
|
206
|
+
const exporter = new MySQLExporter();
|
|
207
|
+
return exporter.export(tableName, jsonSchema);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = MySQLExporter;
|
|
212
|
+
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL DDL导出器
|
|
3
|
+
*
|
|
4
|
+
* 将JSON Schema转换为PostgreSQL CREATE TABLE语句
|
|
5
|
+
*
|
|
6
|
+
* @module lib/exporters/PostgreSQLExporter
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TypeConverter = require('../utils/TypeConverter');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* PostgreSQL导出器类
|
|
14
|
+
* @class PostgreSQLExporter
|
|
15
|
+
*/
|
|
16
|
+
class PostgreSQLExporter {
|
|
17
|
+
/**
|
|
18
|
+
* 构造函数
|
|
19
|
+
* @param {Object} options - 配置选项
|
|
20
|
+
* @param {string} options.schema - PostgreSQL schema名称(默认public)
|
|
21
|
+
*/
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.options = {
|
|
24
|
+
schema: options.schema || 'public',
|
|
25
|
+
...options
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 导出为PostgreSQL CREATE TABLE语句
|
|
31
|
+
* @param {string} tableName - 表名
|
|
32
|
+
* @param {Object} jsonSchema - JSON Schema对象
|
|
33
|
+
* @returns {string} PostgreSQL DDL语句
|
|
34
|
+
*/
|
|
35
|
+
export(tableName, jsonSchema) {
|
|
36
|
+
if (!tableName || typeof tableName !== 'string') {
|
|
37
|
+
throw new Error('Table name is required');
|
|
38
|
+
}
|
|
39
|
+
if (!jsonSchema || jsonSchema.type !== 'object') {
|
|
40
|
+
throw new Error('JSON Schema must be an object type');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const fullTableName = `${this.options.schema}.${tableName}`;
|
|
44
|
+
const columns = this._convertProperties(jsonSchema);
|
|
45
|
+
const primaryKey = this._detectPrimaryKey(jsonSchema);
|
|
46
|
+
|
|
47
|
+
let ddl = `CREATE TABLE ${fullTableName} (\n`;
|
|
48
|
+
|
|
49
|
+
// 添加列定义
|
|
50
|
+
const columnDefs = columns.map(col => ` ${col}`).join(',\n');
|
|
51
|
+
ddl += columnDefs;
|
|
52
|
+
|
|
53
|
+
// 添加主键
|
|
54
|
+
if (primaryKey) {
|
|
55
|
+
ddl += `,\n PRIMARY KEY (${primaryKey})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ddl += `\n);`;
|
|
59
|
+
|
|
60
|
+
// 添加表注释
|
|
61
|
+
if (jsonSchema.description) {
|
|
62
|
+
ddl += `\n\nCOMMENT ON TABLE ${fullTableName} IS '${this._escapeString(jsonSchema.description)}';`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 添加列注释
|
|
66
|
+
const commentDdls = this._generateColumnComments(fullTableName, jsonSchema);
|
|
67
|
+
if (commentDdls.length > 0) {
|
|
68
|
+
ddl += '\n\n' + commentDdls.join('\n');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return ddl;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 转换properties为列定义
|
|
76
|
+
* @private
|
|
77
|
+
* @param {Object} schema - JSON Schema对象
|
|
78
|
+
* @returns {Array<string>} 列定义数组
|
|
79
|
+
*/
|
|
80
|
+
_convertProperties(schema) {
|
|
81
|
+
if (!schema.properties) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const columns = [];
|
|
86
|
+
const required = schema.required || [];
|
|
87
|
+
|
|
88
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
89
|
+
const columnDef = this._convertColumn(name, propSchema, required.includes(name));
|
|
90
|
+
columns.push(columnDef);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return columns;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 转换单个列
|
|
98
|
+
* @private
|
|
99
|
+
* @param {string} name - 列名
|
|
100
|
+
* @param {Object} schema - 属性Schema
|
|
101
|
+
* @param {boolean} isRequired - 是否必填
|
|
102
|
+
* @returns {string} 列定义字符串
|
|
103
|
+
*/
|
|
104
|
+
_convertColumn(name, schema, isRequired) {
|
|
105
|
+
const constraints = TypeConverter.extractConstraints(schema);
|
|
106
|
+
const pgType = TypeConverter.toPostgreSQLType(schema.type, constraints);
|
|
107
|
+
|
|
108
|
+
let def = `${name} ${pgType}`;
|
|
109
|
+
|
|
110
|
+
// NULL约束
|
|
111
|
+
if (isRequired) {
|
|
112
|
+
def += ' NOT NULL';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 默认值
|
|
116
|
+
if (schema.default !== undefined) {
|
|
117
|
+
const defaultValue = this._formatDefaultValue(schema.default, schema.type);
|
|
118
|
+
def += ` DEFAULT ${defaultValue}`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// UNIQUE约束
|
|
122
|
+
if (schema.format === 'email' || schema.format === 'uuid') {
|
|
123
|
+
// email和uuid通常是唯一的
|
|
124
|
+
// def += ' UNIQUE'; // 可选
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// CHECK约束
|
|
128
|
+
const checkConstraints = this._generateCheckConstraints(name, schema);
|
|
129
|
+
if (checkConstraints.length > 0) {
|
|
130
|
+
def += ' ' + checkConstraints.join(' ');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return def;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 生成CHECK约束
|
|
138
|
+
* @private
|
|
139
|
+
* @param {string} columnName - 列名
|
|
140
|
+
* @param {Object} schema - 属性Schema
|
|
141
|
+
* @returns {Array<string>} CHECK约束数组
|
|
142
|
+
*/
|
|
143
|
+
_generateCheckConstraints(columnName, schema) {
|
|
144
|
+
const checks = [];
|
|
145
|
+
|
|
146
|
+
// 字符串长度
|
|
147
|
+
if (schema.minLength !== undefined || schema.maxLength !== undefined) {
|
|
148
|
+
if (schema.minLength !== undefined && schema.maxLength !== undefined) {
|
|
149
|
+
checks.push(`CHECK (LENGTH(${columnName}) BETWEEN ${schema.minLength} AND ${schema.maxLength})`);
|
|
150
|
+
} else if (schema.minLength !== undefined) {
|
|
151
|
+
checks.push(`CHECK (LENGTH(${columnName}) >= ${schema.minLength})`);
|
|
152
|
+
} else {
|
|
153
|
+
checks.push(`CHECK (LENGTH(${columnName}) <= ${schema.maxLength})`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 数值范围
|
|
158
|
+
if (schema.minimum !== undefined || schema.maximum !== undefined) {
|
|
159
|
+
if (schema.minimum !== undefined && schema.maximum !== undefined) {
|
|
160
|
+
checks.push(`CHECK (${columnName} BETWEEN ${schema.minimum} AND ${schema.maximum})`);
|
|
161
|
+
} else if (schema.minimum !== undefined) {
|
|
162
|
+
checks.push(`CHECK (${columnName} >= ${schema.minimum})`);
|
|
163
|
+
} else {
|
|
164
|
+
checks.push(`CHECK (${columnName} <= ${schema.maximum})`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 枚举
|
|
169
|
+
if (schema.enum) {
|
|
170
|
+
const values = schema.enum.map(v => `'${this._escapeString(v)}'`).join(', ');
|
|
171
|
+
checks.push(`CHECK (${columnName} IN (${values}))`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return checks;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 格式化默认值
|
|
179
|
+
* @private
|
|
180
|
+
* @param {*} value - 默认值
|
|
181
|
+
* @param {string} type - 类型
|
|
182
|
+
* @returns {string} 格式化后的默认值
|
|
183
|
+
*/
|
|
184
|
+
_formatDefaultValue(value, type) {
|
|
185
|
+
if (value === null) {
|
|
186
|
+
return 'NULL';
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (type === 'string') {
|
|
190
|
+
return `'${this._escapeString(value)}'`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (type === 'boolean') {
|
|
194
|
+
return value ? 'TRUE' : 'FALSE';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (type === 'object' || type === 'array') {
|
|
198
|
+
return `'${JSON.stringify(value)}'::JSONB`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return value.toString();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 转义字符串
|
|
206
|
+
* @private
|
|
207
|
+
* @param {string} str - 字符串
|
|
208
|
+
* @returns {string} 转义后的字符串
|
|
209
|
+
*/
|
|
210
|
+
_escapeString(str) {
|
|
211
|
+
return str.replace(/'/g, "''");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* 检测主键
|
|
216
|
+
* @private
|
|
217
|
+
* @param {Object} schema - JSON Schema对象
|
|
218
|
+
* @returns {string|null} 主键列名
|
|
219
|
+
*/
|
|
220
|
+
_detectPrimaryKey(schema) {
|
|
221
|
+
if (!schema.properties) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 查找名为id或_id的字段
|
|
226
|
+
if (schema.properties.id) {
|
|
227
|
+
return 'id';
|
|
228
|
+
}
|
|
229
|
+
if (schema.properties._id) {
|
|
230
|
+
return '_id';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 生成列注释
|
|
238
|
+
* @private
|
|
239
|
+
* @param {string} tableName - 表名
|
|
240
|
+
* @param {Object} schema - JSON Schema对象
|
|
241
|
+
* @returns {Array<string>} COMMENT语句数组
|
|
242
|
+
*/
|
|
243
|
+
_generateColumnComments(tableName, schema) {
|
|
244
|
+
if (!schema.properties) {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const comments = [];
|
|
249
|
+
|
|
250
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
251
|
+
if (propSchema.description) {
|
|
252
|
+
comments.push(`COMMENT ON COLUMN ${tableName}.${name} IS '${this._escapeString(propSchema.description)}';`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return comments;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 生成索引DDL
|
|
261
|
+
* @param {string} tableName - 表名
|
|
262
|
+
* @param {string} columnName - 列名
|
|
263
|
+
* @param {Object} options - 索引选项
|
|
264
|
+
* @returns {string} CREATE INDEX语句
|
|
265
|
+
*/
|
|
266
|
+
generateIndex(tableName, columnName, options = {}) {
|
|
267
|
+
const fullTableName = `${this.options.schema}.${tableName}`;
|
|
268
|
+
const indexName = options.name || `idx_${tableName}_${columnName}`;
|
|
269
|
+
const unique = options.unique ? 'UNIQUE ' : '';
|
|
270
|
+
const method = options.method || 'btree'; // btree, hash, gin, gist
|
|
271
|
+
|
|
272
|
+
return `CREATE ${unique}INDEX ${indexName} ON ${fullTableName} USING ${method} (${columnName});`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 静态方法:快速导出
|
|
277
|
+
* @static
|
|
278
|
+
* @param {string} tableName - 表名
|
|
279
|
+
* @param {Object} jsonSchema - JSON Schema对象
|
|
280
|
+
* @returns {string} PostgreSQL DDL语句
|
|
281
|
+
*/
|
|
282
|
+
static export(tableName, jsonSchema) {
|
|
283
|
+
const exporter = new PostgreSQLExporter();
|
|
284
|
+
return exporter.export(tableName, jsonSchema);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = PostgreSQLExporter;
|
|
289
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exporters - 导出器统一导出
|
|
3
|
+
* @module lib/exporters
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const MongoDBExporter = require('./MongoDBExporter');
|
|
7
|
+
const MySQLExporter = require('./MySQLExporter');
|
|
8
|
+
const PostgreSQLExporter = require('./PostgreSQLExporter');
|
|
9
|
+
const MarkdownExporter = require('./MarkdownExporter');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
MongoDBExporter,
|
|
13
|
+
MySQLExporter,
|
|
14
|
+
PostgreSQLExporter,
|
|
15
|
+
MarkdownExporter,
|
|
16
|
+
|
|
17
|
+
// 别名
|
|
18
|
+
MongoDB: MongoDBExporter,
|
|
19
|
+
MySQL: MySQLExporter,
|
|
20
|
+
PostgreSQL: PostgreSQLExporter,
|
|
21
|
+
Postgres: PostgreSQLExporter,
|
|
22
|
+
Markdown: MarkdownExporter
|
|
23
|
+
};
|
|
24
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
required: '{{#label}} is required',
|
|
3
|
+
type: '{{#label}} should be {{#expected}}, got {{#actual}}',
|
|
4
|
+
min: '{{#label}} length must be at least {{#limit}}',
|
|
5
|
+
max: '{{#label}} length must be at most {{#limit}}',
|
|
6
|
+
length: '{{#label}} length must be exactly {{#expected}}',
|
|
7
|
+
pattern: '{{#label}} format is invalid',
|
|
8
|
+
enum: '{{#label}} must be one of: {{#allowed}}',
|
|
9
|
+
custom: '{{#label}} validation failed: {{#message}}',
|
|
10
|
+
circular: 'Circular reference detected at {{#label}}',
|
|
11
|
+
'max-depth': 'Maximum recursion depth ({{#depth}}) exceeded at {{#label}}',
|
|
12
|
+
exception: '{{#label}} validation exception: {{#message}}',
|
|
13
|
+
|
|
14
|
+
// Patterns
|
|
15
|
+
'pattern.phone': 'Invalid phone number',
|
|
16
|
+
'pattern.phone.international': 'Invalid international phone number',
|
|
17
|
+
|
|
18
|
+
'pattern.idCard': 'Invalid ID card number',
|
|
19
|
+
|
|
20
|
+
'pattern.creditCard': 'Invalid credit card number',
|
|
21
|
+
'pattern.creditCard.visa': 'Invalid Visa card number',
|
|
22
|
+
'pattern.creditCard.mastercard': 'Invalid Mastercard number',
|
|
23
|
+
'pattern.creditCard.amex': 'Invalid American Express card number',
|
|
24
|
+
'pattern.creditCard.discover': 'Invalid Discover card number',
|
|
25
|
+
'pattern.creditCard.jcb': 'Invalid JCB card number',
|
|
26
|
+
'pattern.creditCard.unionpay': 'Invalid UnionPay card number',
|
|
27
|
+
|
|
28
|
+
'pattern.licensePlate': 'Invalid license plate number',
|
|
29
|
+
|
|
30
|
+
'pattern.postalCode': 'Invalid postal code',
|
|
31
|
+
|
|
32
|
+
'pattern.passport': 'Invalid passport number',
|
|
33
|
+
|
|
34
|
+
// New Types
|
|
35
|
+
'pattern.objectId': 'Invalid ObjectId',
|
|
36
|
+
'pattern.hexColor': 'Invalid Hex Color',
|
|
37
|
+
'pattern.macAddress': 'Invalid MAC Address',
|
|
38
|
+
'pattern.cron': 'Invalid Cron Expression',
|
|
39
|
+
'pattern.slug': 'URL slug can only contain lowercase letters, numbers, and hyphens',
|
|
40
|
+
|
|
41
|
+
// Username & Password
|
|
42
|
+
'pattern.username': 'Username must start with a letter and contain only letters, numbers, and underscores',
|
|
43
|
+
'pattern.password.weak': 'Password must be at least 6 characters',
|
|
44
|
+
'pattern.password.medium': 'Password must be at least 8 characters and contain letters and numbers',
|
|
45
|
+
'pattern.password.strong': 'Password must be at least 8 characters and contain uppercase, lowercase letters and numbers',
|
|
46
|
+
'pattern.password.veryStrong': 'Password must be at least 10 characters and contain uppercase, lowercase letters, numbers and special characters',
|
|
47
|
+
|
|
48
|
+
// Unknown error fallback
|
|
49
|
+
'UNKNOWN_ERROR': 'Unknown validation error',
|
|
50
|
+
|
|
51
|
+
// Custom validation
|
|
52
|
+
'CUSTOM_VALIDATION_FAILED': 'Validation failed',
|
|
53
|
+
'ASYNC_VALIDATION_NOT_SUPPORTED': 'Async validation not supported in synchronous validate()',
|
|
54
|
+
'VALIDATE_MUST_BE_FUNCTION': 'validate must be a function',
|
|
55
|
+
|
|
56
|
+
// Formats
|
|
57
|
+
'format.email': '{{#label}} must be a valid email address',
|
|
58
|
+
'format.url': '{{#label}} must be a valid URL',
|
|
59
|
+
'format.uuid': '{{#label}} must be a valid UUID',
|
|
60
|
+
'format.date': '{{#label}} must be a valid date (YYYY-MM-DD)',
|
|
61
|
+
'format.datetime': '{{#label}} must be a valid date-time (ISO 8601)',
|
|
62
|
+
'format.time': '{{#label}} must be a valid time (HH:mm:ss)',
|
|
63
|
+
'format.ipv4': '{{#label}} must be a valid IPv4 address',
|
|
64
|
+
'format.ipv6': '{{#label}} must be a valid IPv6 address'
|
|
65
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
required: '{{#label}} es obligatorio',
|
|
3
|
+
type: '{{#label}} debe ser de tipo {{#expected}}, pero se obtuvo {{#actual}}',
|
|
4
|
+
min: 'La longitud de {{#label}} debe ser al menos {{#limit}}',
|
|
5
|
+
max: 'La longitud de {{#label}} debe ser como máximo {{#limit}}',
|
|
6
|
+
length: 'La longitud de {{#label}} debe ser exactamente {{#expected}}',
|
|
7
|
+
pattern: 'El formato de {{#label}} no es válido',
|
|
8
|
+
enum: '{{#label}} debe ser uno de: {{#allowed}}',
|
|
9
|
+
custom: 'Validación fallida para {{#label}}: {{#message}}',
|
|
10
|
+
circular: 'Referencia circular detectada en {{#label}}',
|
|
11
|
+
'max-depth': 'Profundidad máxima de recursión ({{#depth}}) excedida en {{#label}}',
|
|
12
|
+
exception: 'Excepción de validación en {{#label}}: {{#message}}',
|
|
13
|
+
|
|
14
|
+
// Patterns
|
|
15
|
+
'pattern.phone': 'Número de teléfono no válido',
|
|
16
|
+
'pattern.phone.international': 'Número de teléfono internacional no válido',
|
|
17
|
+
|
|
18
|
+
'pattern.idCard': 'Número de tarjeta de identificación no válido',
|
|
19
|
+
|
|
20
|
+
'pattern.creditCard': 'Número de tarjeta de crédito no válido',
|
|
21
|
+
'pattern.creditCard.visa': 'Número de tarjeta Visa no válido',
|
|
22
|
+
'pattern.creditCard.mastercard': 'Número de Mastercard no válido',
|
|
23
|
+
'pattern.creditCard.amex': 'Número de tarjeta American Express no válido',
|
|
24
|
+
'pattern.creditCard.discover': 'Número de tarjeta Discover no válido',
|
|
25
|
+
'pattern.creditCard.jcb': 'Número de tarjeta JCB no válido',
|
|
26
|
+
'pattern.creditCard.unionpay': 'Número de tarjeta UnionPay no válido',
|
|
27
|
+
|
|
28
|
+
'pattern.licensePlate': 'Número de matrícula no válido',
|
|
29
|
+
|
|
30
|
+
'pattern.postalCode': 'Código postal no válido',
|
|
31
|
+
|
|
32
|
+
'pattern.passport': 'Número de pasaporte no válido',
|
|
33
|
+
|
|
34
|
+
// New Types
|
|
35
|
+
'pattern.objectId': 'ObjectId no válido',
|
|
36
|
+
'pattern.hexColor': 'Color hexadecimal no válido',
|
|
37
|
+
'pattern.macAddress': 'Dirección MAC no válida',
|
|
38
|
+
'pattern.cron': 'Expresión Cron no válida',
|
|
39
|
+
'pattern.slug': 'El slug de URL solo puede contener letras minúsculas, números y guiones',
|
|
40
|
+
|
|
41
|
+
// Username & Password
|
|
42
|
+
'pattern.username': 'El nombre de usuario debe comenzar con una letra y contener solo letras, números y guiones bajos',
|
|
43
|
+
'pattern.password.weak': 'La contraseña debe tener al menos 6 caracteres',
|
|
44
|
+
'pattern.password.medium': 'La contraseña debe tener al menos 8 caracteres y contener letras y números',
|
|
45
|
+
'pattern.password.strong': 'La contraseña debe tener al menos 8 caracteres y contener letras mayúsculas, minúsculas y números',
|
|
46
|
+
'pattern.password.veryStrong': 'La contraseña debe tener al menos 10 caracteres y contener letras mayúsculas, minúsculas, números y caracteres especiales',
|
|
47
|
+
|
|
48
|
+
// Unknown error fallback
|
|
49
|
+
'UNKNOWN_ERROR': 'Error de validación desconocido',
|
|
50
|
+
|
|
51
|
+
// Custom validation
|
|
52
|
+
'CUSTOM_VALIDATION_FAILED': 'Validación fallida',
|
|
53
|
+
'ASYNC_VALIDATION_NOT_SUPPORTED': 'La validación asíncrona no es compatible en validate() síncrono',
|
|
54
|
+
'VALIDATE_MUST_BE_FUNCTION': 'validate debe ser una función',
|
|
55
|
+
|
|
56
|
+
// Formats
|
|
57
|
+
'format.email': 'Dirección de correo electrónico no válida',
|
|
58
|
+
'format.url': 'URL no válida',
|
|
59
|
+
'format.uuid': 'UUID no válido',
|
|
60
|
+
'format.date': 'Formato de fecha no válido (YYYY-MM-DD)',
|
|
61
|
+
'format.datetime': 'Formato de fecha y hora no válido (ISO 8601)',
|
|
62
|
+
'format.time': 'Formato de hora no válido (HH:mm:ss)',
|
|
63
|
+
'format.ipv4': 'Dirección IPv4 no válida',
|
|
64
|
+
'format.ipv6': 'Dirección IPv6 no válida'
|
|
65
|
+
};
|
|
66
|
+
|