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,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema工具类 v2.0.1
|
|
3
|
+
*
|
|
4
|
+
* 提供Schema复用、合并、性能监控等功能
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class SchemaUtils {
|
|
8
|
+
// ========== Schema复用 ==========
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 创建可复用的Schema片段
|
|
12
|
+
* @param {Function} factory - Schema工厂函数
|
|
13
|
+
* @returns {Function} 返回工厂函数
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* const emailField = SchemaUtils.reusable(() =>
|
|
17
|
+
* 'email!'.pattern(/custom/).label('邮箱')
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* const schema1 = dsl({ email: emailField() });
|
|
21
|
+
* const schema2 = dsl({ contactEmail: emailField() });
|
|
22
|
+
*/
|
|
23
|
+
static reusable(factory) {
|
|
24
|
+
return factory;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 创建Schema片段库
|
|
29
|
+
* @param {Object} fragments - Schema片段对象
|
|
30
|
+
* @returns {Object} 片段库
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const fields = SchemaUtils.createLibrary({
|
|
34
|
+
* email: () => 'email!'.label('邮箱'),
|
|
35
|
+
* phone: () => 'string:11!'.phoneNumber('cn').label('手机号'),
|
|
36
|
+
* username: () => 'string:3-32!'.username().label('用户名')
|
|
37
|
+
* });
|
|
38
|
+
*
|
|
39
|
+
* const schema = dsl({
|
|
40
|
+
* email: fields.email(),
|
|
41
|
+
* phone: fields.phone()
|
|
42
|
+
* });
|
|
43
|
+
*/
|
|
44
|
+
static createLibrary(fragments) {
|
|
45
|
+
return fragments;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ========== Schema复用和扩展 ==========
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 扩展Schema(类似继承)
|
|
52
|
+
* @param {Object} baseSchema - 基础Schema
|
|
53
|
+
* @param {Object} extensions - 扩展定义
|
|
54
|
+
* @returns {Object} 扩展后的Schema(支持链式调用)
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* const baseUser = dsl({ name: 'string!', email: 'email!' });
|
|
58
|
+
* const admin = SchemaUtils.extend(baseUser, {
|
|
59
|
+
* role: 'admin|superadmin',
|
|
60
|
+
* permissions: 'array<string>'
|
|
61
|
+
* });
|
|
62
|
+
*/
|
|
63
|
+
static extend(baseSchema, extensions) {
|
|
64
|
+
const dsl = require('../adapters/DslAdapter');
|
|
65
|
+
const extensionSchema = typeof extensions === 'function'
|
|
66
|
+
? extensions
|
|
67
|
+
: dsl(extensions);
|
|
68
|
+
|
|
69
|
+
// 合并 properties
|
|
70
|
+
const result = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {},
|
|
73
|
+
required: []
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// 复制基础 schema
|
|
77
|
+
if (baseSchema.properties) {
|
|
78
|
+
Object.assign(result.properties, baseSchema.properties);
|
|
79
|
+
}
|
|
80
|
+
if (baseSchema.required) {
|
|
81
|
+
result.required = [...baseSchema.required];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 添加扩展
|
|
85
|
+
if (extensionSchema.properties) {
|
|
86
|
+
Object.assign(result.properties, extensionSchema.properties);
|
|
87
|
+
}
|
|
88
|
+
if (extensionSchema.required) {
|
|
89
|
+
result.required = [...new Set([...result.required, ...extensionSchema.required])];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this._makeChainable(result);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 挑选Schema的部分字段
|
|
97
|
+
* @param {Object} schema - 原始Schema
|
|
98
|
+
* @param {string[]} fields - 要挑选的字段
|
|
99
|
+
* @returns {Object} 新Schema
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const fullUser = dsl({ name: 'string!', email: 'email!', age: 'number' });
|
|
103
|
+
* const publicUser = SchemaUtils.pick(fullUser, ['name', 'email']);
|
|
104
|
+
*/
|
|
105
|
+
static pick(schema, fields) {
|
|
106
|
+
const result = {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {},
|
|
109
|
+
required: []
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
fields.forEach(field => {
|
|
113
|
+
if (schema.properties && schema.properties[field]) {
|
|
114
|
+
result.properties[field] = schema.properties[field];
|
|
115
|
+
if (schema.required && schema.required.includes(field)) {
|
|
116
|
+
result.required.push(field);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return this._makeChainable(result);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 排除Schema的部分字段
|
|
126
|
+
* @param {Object} schema - 原始Schema
|
|
127
|
+
* @param {string[]} fields - 要排除的字段
|
|
128
|
+
* @returns {Object} 新Schema(支持链式调用)
|
|
129
|
+
*/
|
|
130
|
+
static omit(schema, fields) {
|
|
131
|
+
const result = this._clone(schema);
|
|
132
|
+
|
|
133
|
+
fields.forEach(field => {
|
|
134
|
+
if (result.properties) {
|
|
135
|
+
delete result.properties[field];
|
|
136
|
+
}
|
|
137
|
+
if (result.required) {
|
|
138
|
+
result.required = result.required.filter(f => f !== field);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 清理空数组
|
|
143
|
+
if (result.required && result.required.length === 0) {
|
|
144
|
+
delete result.required;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return this._makeChainable(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ========== v2.1.0 新增:Schema转换方法(支持链式调用) ==========
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 部分验证:移除必填限制
|
|
154
|
+
*
|
|
155
|
+
* @param {Object} schema - 原始Schema
|
|
156
|
+
* @param {string[]} fields - 要验证的字段(可选,默认全部)
|
|
157
|
+
* @returns {Object} 新Schema(支持链式调用)
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* // 所有字段变为可选
|
|
161
|
+
* const partialSchema = SchemaUtils.partial(userSchema);
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // 只验证指定字段
|
|
165
|
+
* const updateSchema = SchemaUtils.partial(userSchema, ['name', 'age']);
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // 链式调用
|
|
169
|
+
* const patchSchema = SchemaUtils
|
|
170
|
+
* .pick(userSchema, ['name', 'age'])
|
|
171
|
+
* .partial();
|
|
172
|
+
*/
|
|
173
|
+
static partial(schema, fields = null) {
|
|
174
|
+
let result;
|
|
175
|
+
|
|
176
|
+
if (fields) {
|
|
177
|
+
// 只保留指定字段 (pick 已经返回 chainable 对象)
|
|
178
|
+
result = this.pick(schema, fields);
|
|
179
|
+
// 提取原始 schema
|
|
180
|
+
if (result._isChainable) {
|
|
181
|
+
result = this._extractSchema(result);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
result = this._clone(schema);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 移除所有 required
|
|
188
|
+
delete result.required;
|
|
189
|
+
|
|
190
|
+
// 递归处理嵌套对象
|
|
191
|
+
if (result.properties) {
|
|
192
|
+
Object.keys(result.properties).forEach(key => {
|
|
193
|
+
const prop = result.properties[key];
|
|
194
|
+
if (prop && prop.type === 'object' && prop.required) {
|
|
195
|
+
delete prop.required;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return this._makeChainable(result);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ========== 性能监控 ==========
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* 创建带性能监控的Validator
|
|
207
|
+
* @param {Validator} validator - Validator实例
|
|
208
|
+
* @returns {Validator} 增强的Validator
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* const validator = SchemaUtils.withPerformance(new Validator());
|
|
212
|
+
* const result = validator.validate(schema, data);
|
|
213
|
+
* console.log(result.performance);
|
|
214
|
+
*/
|
|
215
|
+
static withPerformance(validator) {
|
|
216
|
+
const originalValidate = validator.validate.bind(validator);
|
|
217
|
+
|
|
218
|
+
validator.validate = function(schema, data) {
|
|
219
|
+
const startTime = Date.now();
|
|
220
|
+
const result = originalValidate(schema, data);
|
|
221
|
+
const endTime = Date.now();
|
|
222
|
+
|
|
223
|
+
result.performance = {
|
|
224
|
+
duration: endTime - startTime,
|
|
225
|
+
timestamp: new Date().toISOString()
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
return result;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return validator;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 批量验证优化
|
|
236
|
+
* @param {Object} schema - Schema对象
|
|
237
|
+
* @param {Array} dataArray - 数据数组
|
|
238
|
+
* @param {Validator} validator - Validator实例
|
|
239
|
+
* @returns {Array} 验证结果数组
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* const results = SchemaUtils.validateBatch(schema, users, validator);
|
|
243
|
+
*/
|
|
244
|
+
static validateBatch(schema, dataArray, validator) {
|
|
245
|
+
const startTime = Date.now();
|
|
246
|
+
|
|
247
|
+
// 复用编译后的Schema
|
|
248
|
+
const compiledValidate = validator.getAjv().compile(schema);
|
|
249
|
+
|
|
250
|
+
const results = dataArray.map((data, index) => {
|
|
251
|
+
const valid = compiledValidate(data);
|
|
252
|
+
return {
|
|
253
|
+
index,
|
|
254
|
+
valid,
|
|
255
|
+
errors: valid ? null : compiledValidate.errors,
|
|
256
|
+
data: valid ? data : null
|
|
257
|
+
};
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const endTime = Date.now();
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
results,
|
|
264
|
+
summary: {
|
|
265
|
+
total: dataArray.length,
|
|
266
|
+
valid: results.filter(r => r.valid).length,
|
|
267
|
+
invalid: results.filter(r => !r.valid).length,
|
|
268
|
+
duration: endTime - startTime,
|
|
269
|
+
averageTime: (endTime - startTime) / dataArray.length
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ========== Schema导出 ==========
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 导出Schema为Markdown文档
|
|
278
|
+
* @param {Object} schema - Schema对象
|
|
279
|
+
* @param {Object} options - 选项
|
|
280
|
+
* @returns {string} Markdown文档
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* const markdown = SchemaUtils.toMarkdown(schema, { title: 'User Schema' });
|
|
284
|
+
*/
|
|
285
|
+
static toMarkdown(schema, options = {}) {
|
|
286
|
+
const { title = 'Schema文档', locale = 'zh-CN' } = options;
|
|
287
|
+
|
|
288
|
+
let md = `# ${title}\n\n`;
|
|
289
|
+
|
|
290
|
+
if (schema.properties) {
|
|
291
|
+
md += '## 字段列表\n\n';
|
|
292
|
+
md += '| 字段 | 类型 | 必填 | 说明 |\n';
|
|
293
|
+
md += '|------|------|------|------|\n';
|
|
294
|
+
|
|
295
|
+
Object.keys(schema.properties).forEach(key => {
|
|
296
|
+
const prop = schema.properties[key];
|
|
297
|
+
const required = schema.required?.includes(key) ? '✅' : '❌';
|
|
298
|
+
const type = prop.type || 'any';
|
|
299
|
+
const label = prop._label || key;
|
|
300
|
+
const desc = prop._description || '-';
|
|
301
|
+
|
|
302
|
+
md += `| ${key} | ${type} | ${required} | ${label} |\n`;
|
|
303
|
+
|
|
304
|
+
if (desc !== '-') {
|
|
305
|
+
md += `| | | | *${desc}* |\n`;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 约束信息
|
|
309
|
+
const constraints = [];
|
|
310
|
+
if (prop.minLength) constraints.push(`最小长度: ${prop.minLength}`);
|
|
311
|
+
if (prop.maxLength) constraints.push(`最大长度: ${prop.maxLength}`);
|
|
312
|
+
if (prop.minimum) constraints.push(`最小值: ${prop.minimum}`);
|
|
313
|
+
if (prop.maximum) constraints.push(`最大值: ${prop.maximum}`);
|
|
314
|
+
if (prop.pattern) constraints.push(`格式: \`${prop.pattern}\``);
|
|
315
|
+
if (prop.enum) constraints.push(`可选值: ${prop.enum.join(', ')}`);
|
|
316
|
+
|
|
317
|
+
if (constraints.length > 0) {
|
|
318
|
+
md += `| | | | ${constraints.join('; ')} |\n`;
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return md;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* 导出Schema为HTML文档
|
|
328
|
+
* @param {Object} schema - Schema对象
|
|
329
|
+
* @param {Object} options - 选项
|
|
330
|
+
* @returns {string} HTML文档
|
|
331
|
+
*/
|
|
332
|
+
static toHTML(schema, options = {}) {
|
|
333
|
+
const { title = 'Schema文档' } = options;
|
|
334
|
+
const markdown = this.toMarkdown(schema, options);
|
|
335
|
+
|
|
336
|
+
// 简单的Markdown到HTML转换
|
|
337
|
+
let html = `<!DOCTYPE html>
|
|
338
|
+
<html>
|
|
339
|
+
<head>
|
|
340
|
+
<meta charset="UTF-8">
|
|
341
|
+
<title>${title}</title>
|
|
342
|
+
<style>
|
|
343
|
+
body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
344
|
+
table { width: 100%; border-collapse: collapse; margin: 20px 0; }
|
|
345
|
+
th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
|
|
346
|
+
th { background-color: #f5f5f5; font-weight: bold; }
|
|
347
|
+
code { background-color: #f5f5f5; padding: 2px 6px; border-radius: 3px; }
|
|
348
|
+
</style>
|
|
349
|
+
</head>
|
|
350
|
+
<body>
|
|
351
|
+
`;
|
|
352
|
+
|
|
353
|
+
html += markdown
|
|
354
|
+
.replace(/^# (.+)$/gm, '<h1>$1</h1>')
|
|
355
|
+
.replace(/^## (.+)$/gm, '<h2>$1</h2>')
|
|
356
|
+
.replace(/\`([^`]+)\`/g, '<code>$1</code>');
|
|
357
|
+
|
|
358
|
+
html += '\n</body>\n</html>';
|
|
359
|
+
|
|
360
|
+
return html;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* 克隆Schema
|
|
365
|
+
* @param {Object} schema - 原始Schema
|
|
366
|
+
* @returns {Object} 克隆的Schema
|
|
367
|
+
*/
|
|
368
|
+
static clone(schema) {
|
|
369
|
+
return JSON.parse(JSON.stringify(schema));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 深拷贝Schema
|
|
374
|
+
* @private
|
|
375
|
+
* @param {Object} schema - 原始Schema
|
|
376
|
+
* @returns {Object} 拷贝后的Schema
|
|
377
|
+
*/
|
|
378
|
+
static _clone(schema) {
|
|
379
|
+
// 如果是 chainable 对象,先提取原始 schema
|
|
380
|
+
if (schema && schema._isChainable) {
|
|
381
|
+
schema = this._extractSchema(schema);
|
|
382
|
+
}
|
|
383
|
+
return JSON.parse(JSON.stringify(schema));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* 使 schema 支持链式调用
|
|
388
|
+
* @private
|
|
389
|
+
* @param {Object} schema - Schema对象
|
|
390
|
+
* @returns {Object} 支持链式调用的 Schema
|
|
391
|
+
*/
|
|
392
|
+
static _makeChainable(schema) {
|
|
393
|
+
// 如果已经是 chainable,直接返回
|
|
394
|
+
if (schema && schema._isChainable) {
|
|
395
|
+
return schema;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 复制 schema 的所有属性
|
|
399
|
+
const chainable = Object.assign({}, schema);
|
|
400
|
+
|
|
401
|
+
// 标记为 chainable
|
|
402
|
+
Object.defineProperty(chainable, '_isChainable', {
|
|
403
|
+
value: true,
|
|
404
|
+
enumerable: false,
|
|
405
|
+
configurable: false
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// 添加链式方法(只保留核心4个方法)
|
|
409
|
+
const methods = ['partial', 'omit', 'pick', 'extend'];
|
|
410
|
+
const self = this; // 保存 this 引用
|
|
411
|
+
methods.forEach(method => {
|
|
412
|
+
Object.defineProperty(chainable, method, {
|
|
413
|
+
value: (...args) => {
|
|
414
|
+
// 提取原始 schema(去掉链式方法)
|
|
415
|
+
const rawSchema = self._extractSchema(chainable);
|
|
416
|
+
// 调用 SchemaUtils 的静态方法
|
|
417
|
+
return SchemaUtils[method](rawSchema, ...args);
|
|
418
|
+
},
|
|
419
|
+
enumerable: false,
|
|
420
|
+
configurable: false
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
return chainable;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* 从 chainable 对象中提取原始 schema
|
|
429
|
+
* @private
|
|
430
|
+
* @param {Object} chainable - Chainable对象
|
|
431
|
+
* @returns {Object} 原始 Schema
|
|
432
|
+
*/
|
|
433
|
+
static _extractSchema(chainable) {
|
|
434
|
+
const schema = {};
|
|
435
|
+
for (const key in chainable) {
|
|
436
|
+
if (chainable.hasOwnProperty(key) && key !== '_isChainable') {
|
|
437
|
+
schema[key] = chainable[key];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return schema;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = SchemaUtils;
|
|
445
|
+
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeConverter - 类型转换工具
|
|
3
|
+
*
|
|
4
|
+
* 提供各种类型转换功能,用于适配器和导出器
|
|
5
|
+
*
|
|
6
|
+
* @module lib/utils/TypeConverter
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 类型转换工具类
|
|
12
|
+
* @class TypeConverter
|
|
13
|
+
*/
|
|
14
|
+
class TypeConverter {
|
|
15
|
+
static init() { console.log('TypeConverter loaded'); }
|
|
16
|
+
/**
|
|
17
|
+
* 将简单类型字符串转换为JSON Schema类型
|
|
18
|
+
* @static
|
|
19
|
+
* @param {string} simpleType - 简单类型(string/number/boolean等)
|
|
20
|
+
* @returns {Object} JSON Schema类型对象
|
|
21
|
+
*/
|
|
22
|
+
static toJSONSchemaType(simpleType) {
|
|
23
|
+
const typeMap = {
|
|
24
|
+
'string': { type: 'string' },
|
|
25
|
+
'str': { type: 'string' },
|
|
26
|
+
's': { type: 'string' },
|
|
27
|
+
|
|
28
|
+
'number': { type: 'number' },
|
|
29
|
+
'num': { type: 'number' },
|
|
30
|
+
'n': { type: 'number' },
|
|
31
|
+
|
|
32
|
+
'integer': { type: 'integer' },
|
|
33
|
+
'int': { type: 'integer' },
|
|
34
|
+
'i': { type: 'integer' },
|
|
35
|
+
|
|
36
|
+
'boolean': { type: 'boolean' },
|
|
37
|
+
'bool': { type: 'boolean' },
|
|
38
|
+
'b': { type: 'boolean' },
|
|
39
|
+
|
|
40
|
+
'object': { type: 'object' },
|
|
41
|
+
'obj': { type: 'object' },
|
|
42
|
+
'o': { type: 'object' },
|
|
43
|
+
|
|
44
|
+
'array': { type: 'array' },
|
|
45
|
+
'arr': { type: 'array' },
|
|
46
|
+
'a': { type: 'array' },
|
|
47
|
+
|
|
48
|
+
'null': { type: 'null' },
|
|
49
|
+
'any': {} // any类型不限制
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return typeMap[simpleType] || { type: 'string' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* JSON Schema类型转MongoDB类型
|
|
57
|
+
* @static
|
|
58
|
+
* @param {string} jsonSchemaType - JSON Schema类型
|
|
59
|
+
* @returns {string} MongoDB BSON类型
|
|
60
|
+
*/
|
|
61
|
+
static toMongoDBType(jsonSchemaType) {
|
|
62
|
+
const typeMap = {
|
|
63
|
+
'string': 'string',
|
|
64
|
+
'number': 'double',
|
|
65
|
+
'integer': 'int',
|
|
66
|
+
'boolean': 'bool',
|
|
67
|
+
'object': 'object',
|
|
68
|
+
'array': 'array',
|
|
69
|
+
'null': 'null'
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return typeMap[jsonSchemaType] || 'string';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* JSON Schema类型转MySQL类型
|
|
77
|
+
* @static
|
|
78
|
+
* @param {string} jsonSchemaType - JSON Schema类型
|
|
79
|
+
* @param {Object} constraints - 约束条件(长度、范围等)
|
|
80
|
+
* @returns {string} MySQL数据类型
|
|
81
|
+
*/
|
|
82
|
+
static toMySQLType(jsonSchemaType, constraints = {}) {
|
|
83
|
+
const { maxLength, maximum, format, enum: enumValues } = constraints;
|
|
84
|
+
|
|
85
|
+
if (enumValues && Array.isArray(enumValues) && enumValues.length > 0) {
|
|
86
|
+
return `ENUM(${enumValues.map(v => `'${v}'`).join(', ')})`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (jsonSchemaType) {
|
|
90
|
+
case 'string':
|
|
91
|
+
if (format === 'date-time' || format === 'date') {
|
|
92
|
+
return 'DATETIME';
|
|
93
|
+
}
|
|
94
|
+
if (format === 'email') {
|
|
95
|
+
return 'VARCHAR(255)';
|
|
96
|
+
}
|
|
97
|
+
if (maxLength) {
|
|
98
|
+
return maxLength > 255 ? `TEXT` : `VARCHAR(${maxLength})`;
|
|
99
|
+
}
|
|
100
|
+
return 'VARCHAR(255)';
|
|
101
|
+
|
|
102
|
+
case 'integer':
|
|
103
|
+
if (maximum && maximum <= 127) return 'TINYINT';
|
|
104
|
+
if (maximum && maximum <= 32767) return 'SMALLINT';
|
|
105
|
+
if (maximum && maximum <= 2147483647) return 'INT';
|
|
106
|
+
return 'BIGINT';
|
|
107
|
+
|
|
108
|
+
case 'number':
|
|
109
|
+
return 'DOUBLE';
|
|
110
|
+
|
|
111
|
+
case 'boolean':
|
|
112
|
+
return 'BOOLEAN';
|
|
113
|
+
|
|
114
|
+
case 'object':
|
|
115
|
+
return 'JSON';
|
|
116
|
+
|
|
117
|
+
case 'array':
|
|
118
|
+
return 'JSON';
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
return 'VARCHAR(255)';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* JSON Schema类型转PostgreSQL类型
|
|
127
|
+
* @static
|
|
128
|
+
* @param {string} jsonSchemaType - JSON Schema类型
|
|
129
|
+
* @param {Object} constraints - 约束条件
|
|
130
|
+
* @returns {string} PostgreSQL数据类型
|
|
131
|
+
*/
|
|
132
|
+
static toPostgreSQLType(jsonSchemaType, constraints = {}) {
|
|
133
|
+
const { maxLength, maximum, format } = constraints;
|
|
134
|
+
|
|
135
|
+
switch (jsonSchemaType) {
|
|
136
|
+
case 'string':
|
|
137
|
+
if (format === 'date-time') return 'TIMESTAMP';
|
|
138
|
+
if (format === 'date') return 'DATE';
|
|
139
|
+
if (format === 'email') return 'VARCHAR(255)';
|
|
140
|
+
if (format === 'uuid') return 'UUID';
|
|
141
|
+
if (maxLength) {
|
|
142
|
+
return maxLength > 255 ? `TEXT` : `VARCHAR(${maxLength})`;
|
|
143
|
+
}
|
|
144
|
+
return 'VARCHAR(255)';
|
|
145
|
+
|
|
146
|
+
case 'integer':
|
|
147
|
+
if (maximum && maximum <= 32767) return 'SMALLINT';
|
|
148
|
+
if (maximum && maximum <= 2147483647) return 'INTEGER';
|
|
149
|
+
return 'BIGINT';
|
|
150
|
+
|
|
151
|
+
case 'number':
|
|
152
|
+
return 'DOUBLE PRECISION';
|
|
153
|
+
|
|
154
|
+
case 'boolean':
|
|
155
|
+
return 'BOOLEAN';
|
|
156
|
+
|
|
157
|
+
case 'object':
|
|
158
|
+
return 'JSONB';
|
|
159
|
+
|
|
160
|
+
case 'array':
|
|
161
|
+
return 'JSONB';
|
|
162
|
+
|
|
163
|
+
default:
|
|
164
|
+
return 'VARCHAR(255)';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 规范化属性名(转换为数据库友好的命名)
|
|
170
|
+
* @static
|
|
171
|
+
* @param {string} name - 属性名
|
|
172
|
+
* @param {string} style - 命名风格(snake_case/camelCase)
|
|
173
|
+
* @returns {string} 规范化后的属性名
|
|
174
|
+
*/
|
|
175
|
+
static normalizePropertyName(name, style = 'snake_case') {
|
|
176
|
+
if (style === 'snake_case') {
|
|
177
|
+
// camelCase转snake_case
|
|
178
|
+
return name.replace(/([A-Z])/g, '_$1').toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
return name;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* 格式验证函数转正则表达式
|
|
185
|
+
* @static
|
|
186
|
+
* @param {string} format - 格式名称
|
|
187
|
+
* @returns {string|null} 正则表达式字符串
|
|
188
|
+
*/
|
|
189
|
+
static formatToRegex(format) {
|
|
190
|
+
const formatRegex = {
|
|
191
|
+
'email': '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$',
|
|
192
|
+
'uri': '^https?://[^\\s/$.?#].[^\\s]*$',
|
|
193
|
+
'uuid': '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
194
|
+
'ipv4': '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$',
|
|
195
|
+
'ipv6': '^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})$'
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
return formatRegex[format] || null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 合并JSON Schema对象
|
|
203
|
+
* @static
|
|
204
|
+
* @param {Object} base - 基础Schema
|
|
205
|
+
* @param {Object} override - 覆盖Schema
|
|
206
|
+
* @returns {Object} 合并后的Schema
|
|
207
|
+
*/
|
|
208
|
+
static mergeSchemas(base, override) {
|
|
209
|
+
return {
|
|
210
|
+
...base,
|
|
211
|
+
...override,
|
|
212
|
+
// 特殊处理required和properties
|
|
213
|
+
required: [
|
|
214
|
+
...(base.required || []),
|
|
215
|
+
...(override.required || [])
|
|
216
|
+
],
|
|
217
|
+
properties: {
|
|
218
|
+
...(base.properties || {}),
|
|
219
|
+
...(override.properties || {})
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 提取Schema约束条件
|
|
226
|
+
* @static
|
|
227
|
+
* @param {Object} schema - JSON Schema对象
|
|
228
|
+
* @returns {Object} 约束条件对象
|
|
229
|
+
*/
|
|
230
|
+
static extractConstraints(schema) {
|
|
231
|
+
return {
|
|
232
|
+
minLength: schema.minLength,
|
|
233
|
+
maxLength: schema.maxLength,
|
|
234
|
+
minimum: schema.minimum,
|
|
235
|
+
maximum: schema.maximum,
|
|
236
|
+
pattern: schema.pattern,
|
|
237
|
+
format: schema.format,
|
|
238
|
+
enum: schema.enum,
|
|
239
|
+
default: schema.default
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
module.exports = TypeConverter;
|
|
245
|
+
|