schema-dsl 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +338 -3
  2. package/README.md +296 -17
  3. package/STATUS.md +74 -3
  4. package/docs/FEATURE-INDEX.md +1 -1
  5. package/docs/add-custom-locale.md +395 -0
  6. package/docs/best-practices.md +3 -3
  7. package/docs/cache-manager.md +1 -1
  8. package/docs/conditional-api.md +1032 -0
  9. package/docs/dsl-syntax.md +1 -1
  10. package/docs/dynamic-locale.md +76 -30
  11. package/docs/error-handling.md +2 -2
  12. package/docs/export-guide.md +2 -2
  13. package/docs/export-limitations.md +3 -3
  14. package/docs/faq.md +6 -6
  15. package/docs/frontend-i18n-guide.md +19 -16
  16. package/docs/i18n-user-guide.md +7 -9
  17. package/docs/i18n.md +65 -2
  18. package/docs/mongodb-exporter.md +3 -3
  19. package/docs/multi-type-support.md +12 -2
  20. package/docs/mysql-exporter.md +1 -1
  21. package/docs/plugin-system.md +4 -4
  22. package/docs/postgresql-exporter.md +1 -1
  23. package/docs/quick-start.md +4 -4
  24. package/docs/troubleshooting.md +2 -2
  25. package/docs/type-reference.md +5 -5
  26. package/docs/typescript-guide.md +5 -6
  27. package/docs/union-type-guide.md +147 -0
  28. package/docs/union-types.md +277 -0
  29. package/docs/validate-async.md +1 -1
  30. package/examples/array-dsl-example.js +1 -1
  31. package/examples/conditional-example.js +288 -0
  32. package/examples/conditional-non-object.js +129 -0
  33. package/examples/conditional-validate-example.js +321 -0
  34. package/examples/union-type-example.js +127 -0
  35. package/examples/union-types-example.js +77 -0
  36. package/index.d.ts +395 -12
  37. package/index.js +31 -4
  38. package/lib/adapters/DslAdapter.js +14 -5
  39. package/lib/core/ConditionalBuilder.js +401 -0
  40. package/lib/core/DslBuilder.js +113 -0
  41. package/lib/core/ErrorFormatter.js +81 -33
  42. package/lib/core/Locale.js +13 -8
  43. package/lib/core/Validator.js +252 -16
  44. package/lib/locales/en-US.js +14 -0
  45. package/lib/locales/es-ES.js +4 -0
  46. package/lib/locales/fr-FR.js +4 -0
  47. package/lib/locales/ja-JP.js +9 -0
  48. package/lib/locales/zh-CN.js +14 -0
  49. package/package.json +5 -2
@@ -0,0 +1,401 @@
1
+ /**
2
+ * ConditionalBuilder - 链式条件构建器
3
+ *
4
+ * 提供流畅的条件判断 API,类似 JavaScript if-else 语句
5
+ *
6
+ * @module lib/core/ConditionalBuilder
7
+ * @version 1.0.0
8
+ *
9
+ * @example
10
+ * // 简单条件 + 错误消息
11
+ * dsl.if((data) => data.age >= 18)
12
+ * .message('未成年用户不能注册')
13
+ *
14
+ * @example
15
+ * // 多条件 and
16
+ * dsl.if((data) => data.age >= 18)
17
+ * .and((data) => data.userType === 'admin')
18
+ * .then('email!')
19
+ *
20
+ * @example
21
+ * // 多条件 or
22
+ * dsl.if((data) => data.age < 18)
23
+ * .or((data) => data.isBlocked)
24
+ * .message('不允许注册')
25
+ *
26
+ * @example
27
+ * // elseIf 和 else
28
+ * dsl.if((data) => data.userType === 'admin')
29
+ * .then('email!')
30
+ * .elseIf((data) => data.userType === 'vip')
31
+ * .then('email')
32
+ * .else(null)
33
+ */
34
+
35
+ class ConditionalBuilder {
36
+ /**
37
+ * 创建条件构建器实例
38
+ * @private - 不直接调用,使用 dsl.if() 入口
39
+ */
40
+ constructor() {
41
+ this._conditions = [];
42
+ this._elseSchema = undefined;
43
+ this._isConditional = true;
44
+ }
45
+
46
+ /**
47
+ * 开始条件判断
48
+ * @param {Function} conditionFn - 条件函数,接收完整数据对象
49
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
50
+ *
51
+ * @example
52
+ * dsl.if((data) => data.age >= 18)
53
+ */
54
+ if(conditionFn) {
55
+ if (typeof conditionFn !== 'function') {
56
+ throw new Error('Condition must be a function');
57
+ }
58
+
59
+ this._conditions.push({
60
+ type: 'if',
61
+ condition: conditionFn,
62
+ combinedConditions: [{ op: 'root', fn: conditionFn }]
63
+ });
64
+
65
+ return this;
66
+ }
67
+
68
+ /**
69
+ * 添加 AND 条件(与前一个条件组合)
70
+ * @param {Function} conditionFn - 条件函数
71
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
72
+ *
73
+ * @example
74
+ * dsl.if((data) => data.age >= 18)
75
+ * .and((data) => data.userType === 'admin')
76
+ * .then('email!')
77
+ */
78
+ and(conditionFn) {
79
+ if (typeof conditionFn !== 'function') {
80
+ throw new Error('Condition must be a function');
81
+ }
82
+
83
+ const last = this._conditions[this._conditions.length - 1];
84
+ if (!last) {
85
+ throw new Error('.and() must follow .if() or .elseIf()');
86
+ }
87
+
88
+ last.combinedConditions.push({ op: 'and', fn: conditionFn });
89
+ return this;
90
+ }
91
+
92
+ /**
93
+ * 添加 OR 条件(与前一个条件组合)
94
+ * @param {Function} conditionFn - 条件函数
95
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
96
+ *
97
+ * @example
98
+ * dsl.if((data) => data.age < 18)
99
+ * .or((data) => data.isBlocked)
100
+ * .message('不允许注册')
101
+ */
102
+ or(conditionFn) {
103
+ if (typeof conditionFn !== 'function') {
104
+ throw new Error('Condition must be a function');
105
+ }
106
+
107
+ const last = this._conditions[this._conditions.length - 1];
108
+ if (!last) {
109
+ throw new Error('.or() must follow .if() or .elseIf()');
110
+ }
111
+
112
+ last.combinedConditions.push({ op: 'or', fn: conditionFn });
113
+ return this;
114
+ }
115
+
116
+ /**
117
+ * 添加 else-if 分支
118
+ * @param {Function} conditionFn - 条件函数
119
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
120
+ *
121
+ * @example
122
+ * dsl.if((data) => data.userType === 'admin')
123
+ * .then('email!')
124
+ * .elseIf((data) => data.userType === 'vip')
125
+ * .then('email')
126
+ */
127
+ elseIf(conditionFn) {
128
+ if (typeof conditionFn !== 'function') {
129
+ throw new Error('Condition must be a function');
130
+ }
131
+
132
+ if (this._conditions.length === 0) {
133
+ throw new Error('.elseIf() must follow .if()');
134
+ }
135
+
136
+ this._conditions.push({
137
+ type: 'elseIf',
138
+ condition: conditionFn,
139
+ combinedConditions: [{ op: 'root', fn: conditionFn }]
140
+ });
141
+
142
+ return this;
143
+ }
144
+
145
+ /**
146
+ * 设置错误消息(支持多语言 key)
147
+ t * 条件为 true 时自动抛出此错误,条件为 false 时通过验证
148
+ *
149
+ * @param {string} msg - 错误消息或多语言 key
150
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
151
+ *
152
+ * @example
153
+ * // 如果是未成年人,抛出错误
154
+ * dsl.if((data) => data.age < 18)
155
+ * .message('未成年用户不能注册')
156
+ */
157
+ message(msg) {
158
+ if (typeof msg !== 'string') {
159
+ throw new Error('Message must be a string');
160
+ }
161
+
162
+ const last = this._conditions[this._conditions.length - 1];
163
+ if (!last) {
164
+ throw new Error('.message() must follow .if() or .elseIf()');
165
+ }
166
+
167
+ last.message = msg;
168
+ last.action = 'throw'; // 有 message 就自动 throw
169
+ return this;
170
+ }
171
+
172
+ /**
173
+ * 设置满足条件时的 Schema
174
+ * @param {string|Object} schema - DSL 字符串或 Schema 对象
175
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
176
+ *
177
+ * @example
178
+ * dsl.if((data) => data.userType === 'admin')
179
+ * .then('email!')
180
+ */
181
+ then(schema) {
182
+ const last = this._conditions[this._conditions.length - 1];
183
+ if (!last) {
184
+ throw new Error('.then() must follow .if() or .elseIf()');
185
+ }
186
+
187
+ last.then = schema;
188
+ return this;
189
+ }
190
+
191
+ /**
192
+ * 设置默认 Schema(所有条件都不满足时)
193
+ * 可选:不写 else 就是不验证
194
+ *
195
+ * @param {string|Object|null} schema - DSL 字符串、Schema 对象或 null
196
+ * @returns {ConditionalBuilder} 当前实例(支持链式调用)
197
+ *
198
+ * @example
199
+ * // else 可选
200
+ * dsl.if((data) => data.userType === 'admin')
201
+ * .then('email!') // 不写 else
202
+ *
203
+ * @example
204
+ * // 显式指定 else
205
+ * dsl.if((data) => data.userType === 'admin')
206
+ * .then('email!')
207
+ * .else('email')
208
+ */
209
+ else(schema) {
210
+ this._elseSchema = schema;
211
+ return this;
212
+ }
213
+
214
+ /**
215
+ * 执行组合条件(内部方法)
216
+ * @private
217
+ * @param {Object} conditionObj - 条件对象
218
+ * @param {*} data - 待验证数据对象
219
+ * @returns {boolean} 条件结果
220
+ */
221
+ _evaluateCondition(conditionObj, data) {
222
+ try {
223
+ let result = false;
224
+
225
+ for (let i = 0; i < conditionObj.combinedConditions.length; i++) {
226
+ const combined = conditionObj.combinedConditions[i];
227
+
228
+ if (combined.op === 'root') {
229
+ // 第一个条件
230
+ result = combined.fn(data);
231
+ } else if (combined.op === 'and') {
232
+ // AND 组合
233
+ result = result && combined.fn(data);
234
+ } else if (combined.op === 'or') {
235
+ // OR 组合
236
+ result = result || combined.fn(data);
237
+ }
238
+ }
239
+
240
+ return result;
241
+ } catch (error) {
242
+ // 条件函数执行出错,视为不满足
243
+ return false;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * 转换为 Schema 对象(内部方法)
249
+ * @private
250
+ * @returns {Object} Schema 对象
251
+ */
252
+ toSchema() {
253
+ return {
254
+ _isConditional: true,
255
+ conditions: this._conditions,
256
+ else: this._elseSchema,
257
+ // 保存 _evaluateCondition 方法供 Validator 使用
258
+ _evaluateCondition: this._evaluateCondition.bind(this)
259
+ };
260
+ }
261
+
262
+ /**
263
+ * 快捷验证方法 - 返回完整验证结果
264
+ * @param {*} data - 待验证的数据(任意类型)
265
+ * @param {Object} options - 验证选项(可选)
266
+ * @returns {Object} 验证结果 { valid, errors, data }
267
+ *
268
+ * @example
269
+ * // 一行代码验证
270
+ * const result = dsl.if(d => d.age < 18)
271
+ * .message('未成年')
272
+ * .validate({ age: 16 });
273
+ *
274
+ * @example
275
+ * // 复用验证器
276
+ * const validator = dsl.if(d => d.age < 18).message('未成年');
277
+ * const r1 = validator.validate({ age: 16 });
278
+ * const r2 = validator.validate({ age: 20 });
279
+ *
280
+ * @example
281
+ * // 非对象类型
282
+ * const result = dsl.if(d => d.includes('@'))
283
+ * .then('email!')
284
+ * .validate('test@example.com');
285
+ */
286
+ validate(data, options = {}) {
287
+ const Validator = require('./Validator');
288
+ const validator = new Validator(options);
289
+ return validator.validate(this.toSchema(), data, options);
290
+ }
291
+
292
+ /**
293
+ * 异步验证方法 - 失败自动抛出异常
294
+ * @param {*} data - 待验证的数据
295
+ * @param {Object} options - 验证选项(可选)
296
+ * @returns {Promise<*>} 验证通过返回数据,失败抛出异常
297
+ * @throws {ValidationError} 验证失败抛出异常
298
+ *
299
+ * @example
300
+ * // 异步验证,失败自动抛错
301
+ * try {
302
+ * const data = await dsl.if(d => d.age < 18)
303
+ * .message('未成年')
304
+ * .validateAsync({ age: 16 });
305
+ * } catch (error) {
306
+ * console.log(error.message); // "未成年"
307
+ * }
308
+ *
309
+ * @example
310
+ * // Express 中间件
311
+ * app.post('/register', async (req, res, next) => {
312
+ * try {
313
+ * await dsl.if(d => d.age < 18)
314
+ * .message('未成年用户不能注册')
315
+ * .validateAsync(req.body);
316
+ * // 验证通过,继续处理...
317
+ * } catch (error) {
318
+ * next(error);
319
+ * }
320
+ * });
321
+ */
322
+ async validateAsync(data, options = {}) {
323
+ const Validator = require('./Validator');
324
+ const validator = new Validator(options);
325
+ return validator.validateAsync(this.toSchema(), data, options);
326
+ }
327
+
328
+ /**
329
+ * 断言方法 - 同步验证,失败直接抛错
330
+ * @param {*} data - 待验证的数据
331
+ * @param {Object} options - 验证选项(可选)
332
+ * @returns {*} 验证通过返回数据
333
+ * @throws {Error} 验证失败抛出错误
334
+ *
335
+ * @example
336
+ * // 断言验证,失败直接抛错
337
+ * try {
338
+ * dsl.if(d => d.age < 18)
339
+ * .message('未成年')
340
+ * .assert({ age: 16 });
341
+ * } catch (error) {
342
+ * console.log(error.message); // "未成年"
343
+ * }
344
+ *
345
+ * @example
346
+ * // 函数中快速断言
347
+ * function registerUser(userData) {
348
+ * dsl.if(d => d.age < 18)
349
+ * .message('未成年用户不能注册')
350
+ * .assert(userData);
351
+ *
352
+ * // 验证通过,继续处理...
353
+ * return createUser(userData);
354
+ * }
355
+ */
356
+ assert(data, options = {}) {
357
+ const result = this.validate(data, options);
358
+ if (!result.valid) {
359
+ const error = new Error(result.errors[0].message);
360
+ error.errors = result.errors;
361
+ error.name = 'ValidationError';
362
+ throw error;
363
+ }
364
+ return data;
365
+ }
366
+
367
+ /**
368
+ * 快捷检查方法 - 只返回 boolean
369
+ * @param {*} data - 待验证的数据
370
+ * @returns {boolean} 验证是否通过
371
+ *
372
+ * @example
373
+ * // 快速判断
374
+ * const isValid = dsl.if(d => d.age < 18)
375
+ * .message('未成年')
376
+ * .check({ age: 16 });
377
+ * // => false
378
+ *
379
+ * @example
380
+ * // 断言场景
381
+ * if (!validator.check(userData)) {
382
+ * console.log('验证失败');
383
+ * }
384
+ */
385
+ check(data) {
386
+ return this.validate(data).valid;
387
+ }
388
+
389
+ /**
390
+ * 静态工厂方法 - dsl.if() 入口
391
+ * @static
392
+ * @param {Function} conditionFn - 条件函数
393
+ * @returns {ConditionalBuilder} 新的构建器实例
394
+ */
395
+ static start(conditionFn) {
396
+ return new ConditionalBuilder().if(conditionFn);
397
+ }
398
+ }
399
+
400
+ module.exports = ConditionalBuilder;
401
+
@@ -23,6 +23,87 @@ const Locale = require('./Locale');
23
23
  const patterns = require('../config/patterns');
24
24
 
25
25
  class DslBuilder {
26
+ /**
27
+ * 静态属性:存储用户自定义类型(插件注册)
28
+ * @private
29
+ * @type {Map<string, Object|Function>}
30
+ */
31
+ static _customTypes = new Map();
32
+
33
+ /**
34
+ * 注册自定义类型(供插件使用)
35
+ * @param {string} name - 类型名称
36
+ * @param {Object|Function} schema - JSON Schema对象 或 生成函数
37
+ * @throws {Error} 类型名称无效时抛出错误
38
+ *
39
+ * @example
40
+ * // 插件中注册自定义类型
41
+ * DslBuilder.registerType('phone-cn', {
42
+ * type: 'string',
43
+ * pattern: '^1[3-9]\\d{9}$'
44
+ * });
45
+ *
46
+ * // 在DSL中使用
47
+ * dsl('phone-cn!') // ✅ 可用
48
+ * dsl('types:string|phone-cn') // ✅ 可用
49
+ */
50
+ static registerType(name, schema) {
51
+ if (!name || typeof name !== 'string') {
52
+ throw new Error('Type name must be a non-empty string');
53
+ }
54
+
55
+ if (!schema || (typeof schema !== 'object' && typeof schema !== 'function')) {
56
+ throw new Error('Schema must be an object or function');
57
+ }
58
+
59
+ this._customTypes.set(name, schema);
60
+ }
61
+
62
+ /**
63
+ * 检查类型是否已注册(内置或自定义)
64
+ * @param {string} type - 类型名称
65
+ * @returns {boolean}
66
+ *
67
+ * @example
68
+ * DslBuilder.hasType('string') // true (内置)
69
+ * DslBuilder.hasType('phone-cn') // false (未注册)
70
+ *
71
+ * DslBuilder.registerType('phone-cn', { ... });
72
+ * DslBuilder.hasType('phone-cn') // true (已注册)
73
+ */
74
+ static hasType(type) {
75
+ // 检查自定义类型
76
+ if (this._customTypes.has(type)) {
77
+ return true;
78
+ }
79
+
80
+ // 检查内置类型
81
+ const builtInTypes = [
82
+ 'string', 'number', 'integer', 'boolean', 'object', 'array', 'null',
83
+ 'email', 'url', 'uuid', 'date', 'datetime', 'time',
84
+ 'ipv4', 'ipv6', 'binary', 'objectId', 'hexColor', 'macAddress',
85
+ 'cron', 'slug', 'alphanum', 'lower', 'upper', 'json', 'port',
86
+ 'phone', 'idCard', 'creditCard', 'licensePlate', 'postalCode', 'passport', 'any'
87
+ ];
88
+
89
+ return builtInTypes.includes(type);
90
+ }
91
+
92
+ /**
93
+ * 获取所有已注册的自定义类型
94
+ * @returns {Array<string>}
95
+ */
96
+ static getCustomTypes() {
97
+ return Array.from(this._customTypes.keys());
98
+ }
99
+
100
+ /**
101
+ * 清除所有自定义类型(主要用于测试)
102
+ */
103
+ static clearCustomTypes() {
104
+ this._customTypes.clear();
105
+ }
106
+
26
107
  /**
27
108
  * 创建 DslBuilder 实例
28
109
  * @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
@@ -63,6 +144,26 @@ class DslBuilder {
63
144
  * @returns {Object} JSON Schema对象
64
145
  */
65
146
  _parseSimple(dsl) {
147
+ // 🔴 处理跨类型联合:types:type1|type2|type3
148
+ if (dsl.startsWith('types:')) {
149
+ const typesStr = dsl.substring(6); // 去掉 'types:' 前缀
150
+ const types = typesStr.split('|').map(t => t.trim()).filter(t => t);
151
+
152
+ if (types.length === 0) {
153
+ throw new Error('types: requires at least one type');
154
+ }
155
+
156
+ if (types.length === 1) {
157
+ // 只有一个类型,直接解析(避免不必要的oneOf)
158
+ return this._parseSimple(types[0]);
159
+ }
160
+
161
+ // 多个类型,生成oneOf结构
162
+ return {
163
+ oneOf: types.map(type => this._parseSimple(type))
164
+ };
165
+ }
166
+
66
167
  // 处理数组类型:array:1-10 或 array<string>
67
168
  if (dsl.startsWith('array')) {
68
169
  const schema = { type: 'array' };
@@ -233,6 +334,18 @@ class DslBuilder {
233
334
  * @private
234
335
  */
235
336
  _getBaseType(type) {
337
+ // 🔴 优先查询自定义类型(插件注册的)
338
+ if (DslBuilder._customTypes.has(type)) {
339
+ const customSchema = DslBuilder._customTypes.get(type);
340
+ // 如果是函数,调用它生成Schema
341
+ if (typeof customSchema === 'function') {
342
+ return customSchema();
343
+ }
344
+ // 否则返回Schema对象的深拷贝(避免污染)
345
+ return JSON.parse(JSON.stringify(customSchema));
346
+ }
347
+
348
+ // 🔴 查询内置类型
236
349
  const typeMap = {
237
350
  'string': { type: 'string' },
238
351
  'number': { type: 'number' },