schema-dsl 1.0.9 → 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 (44) hide show
  1. package/CHANGELOG.md +205 -2
  2. package/README.md +257 -0
  3. package/docs/FEATURE-INDEX.md +1 -1
  4. package/docs/best-practices.md +3 -3
  5. package/docs/cache-manager.md +1 -1
  6. package/docs/conditional-api.md +1032 -0
  7. package/docs/dsl-syntax.md +1 -1
  8. package/docs/dynamic-locale.md +2 -2
  9. package/docs/error-handling.md +2 -2
  10. package/docs/export-guide.md +2 -2
  11. package/docs/export-limitations.md +3 -3
  12. package/docs/faq.md +6 -6
  13. package/docs/frontend-i18n-guide.md +1 -1
  14. package/docs/mongodb-exporter.md +3 -3
  15. package/docs/multi-type-support.md +12 -2
  16. package/docs/mysql-exporter.md +1 -1
  17. package/docs/plugin-system.md +4 -4
  18. package/docs/postgresql-exporter.md +1 -1
  19. package/docs/quick-start.md +4 -4
  20. package/docs/troubleshooting.md +2 -2
  21. package/docs/type-reference.md +5 -5
  22. package/docs/typescript-guide.md +5 -6
  23. package/docs/union-type-guide.md +147 -0
  24. package/docs/union-types.md +277 -0
  25. package/docs/validate-async.md +1 -1
  26. package/examples/array-dsl-example.js +1 -1
  27. package/examples/conditional-example.js +288 -0
  28. package/examples/conditional-non-object.js +129 -0
  29. package/examples/conditional-validate-example.js +321 -0
  30. package/examples/union-type-example.js +127 -0
  31. package/examples/union-types-example.js +77 -0
  32. package/index.d.ts +332 -7
  33. package/index.js +19 -1
  34. package/lib/adapters/DslAdapter.js +14 -5
  35. package/lib/core/ConditionalBuilder.js +401 -0
  36. package/lib/core/DslBuilder.js +113 -0
  37. package/lib/core/Locale.js +13 -8
  38. package/lib/core/Validator.js +246 -2
  39. package/lib/locales/en-US.js +14 -0
  40. package/lib/locales/es-ES.js +4 -0
  41. package/lib/locales/fr-FR.js +4 -0
  42. package/lib/locales/ja-JP.js +9 -0
  43. package/lib/locales/zh-CN.js +14 -0
  44. package/package.json +3 -1
@@ -135,6 +135,24 @@ class Validator {
135
135
  schema = this.dslSchemaCache.get(schema);
136
136
  }
137
137
 
138
+ // ✅ 处理 ConditionalBuilder 条件链(运行时条件判断)
139
+ if (schema && schema._isConditional) {
140
+ // 顶层 ConditionalBuilder(直接作为 Schema 使用)
141
+ // 此时验证的是整个数据对象
142
+ return this._validateConditional(schema, data, null, data, options);
143
+ }
144
+
145
+ // ✅ 预处理包含 ConditionalBuilder 的 Schema
146
+ if (schema && schema.properties) {
147
+ const hasConditional = Object.values(schema.properties).some(
148
+ fieldSchema => fieldSchema && fieldSchema._isConditional
149
+ );
150
+
151
+ if (hasConditional) {
152
+ return this._validateWithConditionals(schema, data, options);
153
+ }
154
+ }
155
+
138
156
  // 检查是否需要移除额外字段 (clean 模式)
139
157
  if (schema && schema._removeAdditional) {
140
158
  // 创建新的 Validator 实例,启用 removeAdditional
@@ -369,9 +387,235 @@ class Validator {
369
387
  }
370
388
 
371
389
  /**
372
- * 静态方法:创建验证器实例
390
+ * 验证包含 ConditionalBuilder 字段的 Schema
391
+ * @private
392
+ * @param {Object} schema - Schema 对象
393
+ * @param {*} data - 待验证的数据
394
+ * @param {Object} options - 验证选项
395
+ * @returns {Object} 验证结果
396
+ */
397
+ _validateWithConditionals(schema, data, options = {}) {
398
+ const errors = [];
399
+
400
+ // 深拷贝 schema,避免修改原始对象
401
+ const cleanSchema = JSON.parse(JSON.stringify(schema));
402
+ const conditionalFields = {};
403
+
404
+ // 提取所有条件字段
405
+ for (const [fieldName, fieldSchema] of Object.entries(schema.properties || {})) {
406
+ if (fieldSchema && fieldSchema._isConditional) {
407
+ conditionalFields[fieldName] = fieldSchema;
408
+ // 从 cleanSchema 中删除条件字段
409
+ delete cleanSchema.properties[fieldName];
410
+ }
411
+ }
412
+
413
+ // 先验证非条件字段
414
+ const baseResult = this._validateInternal(cleanSchema, data, options);
415
+ if (!baseResult.valid) {
416
+ errors.push(...baseResult.errors);
417
+ }
418
+
419
+ // 然后验证每个条件字段
420
+ for (const [fieldName, conditionalSchema] of Object.entries(conditionalFields)) {
421
+ // ✅ 传递字段名和完整数据对象
422
+ const fieldResult = this._validateConditional(
423
+ conditionalSchema,
424
+ data,
425
+ fieldName,
426
+ data[fieldName],
427
+ options
428
+ );
429
+
430
+ if (!fieldResult.valid) {
431
+ // 添加字段路径信息
432
+ fieldResult.errors.forEach(error => {
433
+ if (!error.path || error.path === '') {
434
+ error.path = fieldName;
435
+ }
436
+ errors.push(error);
437
+ });
438
+ }
439
+ }
440
+
441
+ return {
442
+ valid: errors.length === 0,
443
+ errors,
444
+ data
445
+ };
446
+ }
447
+
448
+ /**
449
+ * 验证条件链(ConditionalBuilder)
450
+ * @private
451
+ * @param {Object} conditionalSchema - 条件 Schema 对象
452
+ * @param {*} data - 完整数据对象(用于条件判断)
453
+ * @param {string} fieldName - 字段名
454
+ * @param {*} fieldValue - 字段值(用于验证)
455
+ * @param {Object} options - 验证选项
456
+ * @returns {Object} 验证结果
457
+ */
458
+ _validateConditional(conditionalSchema, data, fieldName, fieldValue, options = {}) {
459
+ const locale = options.locale || Locale.getLocale();
460
+
461
+ try {
462
+ // ✅ 遍历条件链,执行第一个匹配的条件
463
+ // 对于 if/elseIf 链,只执行第一个满足条件的分支
464
+ for (let i = 0; i < conditionalSchema.conditions.length; i++) {
465
+ const cond = conditionalSchema.conditions[i];
466
+
467
+ // 执行组合条件(支持 and/or)
468
+ const matched = conditionalSchema._evaluateCondition(cond, data);
469
+
470
+ if (cond.action === 'throw') {
471
+ // ✅ message 模式:条件为 true 时抛错,条件为 false 时通过
472
+ if (matched) {
473
+ // ✅ 条件满足(true),抛出错误
474
+ // 支持多语言:如果 message 是 key(如 'conditional.underAge'),从语言包获取翻译
475
+ // 传递 locale 参数以支持动态语言切换
476
+ const errorMessage = Locale.getMessage(cond.message, options.messages || {}, locale);
477
+
478
+ return {
479
+ valid: false,
480
+ errors: [{
481
+ message: errorMessage,
482
+ path: '',
483
+ keyword: 'conditional',
484
+ params: { condition: cond.type }
485
+ }],
486
+ data
487
+ };
488
+ }
489
+ // 条件不满足(false),继续验证
490
+ continue;
491
+ }
492
+
493
+ // ✅ then/else 模式:找到第一个满足的条件就执行并返回
494
+ if (matched) {
495
+ // 条件满足,执行 then Schema
496
+ if (cond.then !== undefined && cond.then !== null) {
497
+ return this._executeThenBranch(cond.then, data, fieldValue, fieldName, options);
498
+ }
499
+
500
+ // 条件满足但没有 then,表示验证通过
501
+ return {
502
+ valid: true,
503
+ errors: [],
504
+ data
505
+ };
506
+ }
507
+
508
+ // ✅ 如果是 if/elseIf 条件不满足,继续检查下一个 elseIf
509
+ // 这样就支持了 if...elseIf...elseIf...else 链
510
+ }
511
+
512
+ // ✅ 所有条件都不满足,执行 else
513
+ if (conditionalSchema.else !== undefined) {
514
+ if (conditionalSchema.else === null) {
515
+ // else 为 null,表示跳过验证
516
+ return {
517
+ valid: true,
518
+ errors: [],
519
+ data
520
+ };
521
+ }
522
+
523
+ // 执行 else Schema
524
+ return this._executeThenBranch(conditionalSchema.else, data, fieldValue, fieldName, options);
525
+ }
526
+
527
+ // 没有 else,表示不验证
528
+ return {
529
+ valid: true,
530
+ errors: [],
531
+ data
532
+ };
533
+ } catch (error) {
534
+ return {
535
+ valid: false,
536
+ errors: [{
537
+ message: `Conditional validation error: ${error.message}`,
538
+ path: '',
539
+ keyword: 'conditional'
540
+ }],
541
+ data
542
+ };
543
+ }
544
+ }
545
+
546
+ /**
547
+ * 执行 then 分支(提取公共逻辑)
548
+ * @private
549
+ * @param {*} thenSchema - then 分支的 Schema
550
+ * @param {*} data - 完整数据对象(用于嵌套条件判断)
551
+ * @param {*} fieldValue - 字段值(用于验证)
552
+ * @param {string} fieldName - 字段名
553
+ * @param {Object} options - 验证选项
554
+ */
555
+ _executeThenBranch(thenSchema, data, fieldValue, fieldName, options) {
556
+ const DslBuilder = require('./DslBuilder');
557
+
558
+ // ✅ 如果是 ConditionalBuilder 实例(未调用 toSchema),先转换
559
+ if (thenSchema && typeof thenSchema.toSchema === 'function') {
560
+ thenSchema = thenSchema.toSchema();
561
+ }
562
+
563
+ // ✅ 处理嵌套的 ConditionalBuilder(已转换为 Schema 对象)
564
+ if (thenSchema && thenSchema._isConditional) {
565
+ // 嵌套的条件构建器,递归处理
566
+ // 传递完整数据对象用于条件判断,传递字段值用于验证
567
+ return this._validateConditional(thenSchema, data, fieldName, fieldValue, options);
568
+ }
569
+
570
+ // 如果是字符串,解析为 Schema
571
+ if (typeof thenSchema === 'string') {
572
+ const builder = new DslBuilder(thenSchema);
573
+ thenSchema = builder.toSchema();
574
+ }
575
+
576
+ // ✅ 验证字段值
577
+ return this._validateFieldValue(thenSchema, fieldValue, fieldName, options);
578
+ }
579
+
580
+ /**
581
+ * 验证字段值(处理空字符串和 undefined)
582
+ * @private
583
+ * @param {Object} schema - Schema 对象
584
+ * @param {*} fieldValue - 字段值
585
+ * @param {string} fieldName - 字段名
586
+ * @param {Object} options - 验证选项
587
+ * @returns {Object} 验证结果
588
+ */
589
+ _validateFieldValue(schema, fieldValue, fieldName, options = {}) {
590
+ // ✅ 检查字段是否必填
591
+ const isRequired = schema && schema._required === true;
592
+
593
+ // ✅ 处理 undefined:可选字段缺失时跳过验证
594
+ if (!isRequired && fieldValue === undefined) {
595
+ return {
596
+ valid: true,
597
+ errors: [],
598
+ data: fieldValue
599
+ };
600
+ }
601
+
602
+ // ✅ 处理空字符串:可选字段的空字符串视为未提供
603
+ if (!isRequired && fieldValue === '') {
604
+ return {
605
+ valid: true,
606
+ errors: [],
607
+ data: fieldValue
608
+ };
609
+ }
610
+
611
+ // 正常验证
612
+ return this._validateInternal(schema, fieldValue, options);
613
+ }
614
+
615
+ /**
616
+ * 静态工厂方法
373
617
  * @static
374
- * @param {Object} options - 配置选项
618
+ * @param {Object} options - ajv配置选项
375
619
  * @returns {Validator} Validator实例
376
620
  */
377
621
  static create(options) {
@@ -12,6 +12,11 @@ module.exports = {
12
12
  'max-depth': 'Maximum recursion depth ({{#depth}}) exceeded at {{#label}}',
13
13
  exception: '{{#label}} validation exception: {{#message}}',
14
14
 
15
+ // Conditional (ConditionalBuilder)
16
+ 'conditional.underAge': 'Minors cannot register',
17
+ 'conditional.blocked': 'Account has been blocked',
18
+ 'conditional.notAllowed': 'Registration not allowed',
19
+
15
20
  // Formats
16
21
  'format.email': '{{#label}} must be a valid email address',
17
22
  'format.url': '{{#label}} must be a valid URL',
@@ -115,6 +120,15 @@ module.exports = {
115
120
  'pattern.password.strong': 'Password must be at least 8 characters and contain uppercase, lowercase letters and numbers',
116
121
  'pattern.password.veryStrong': 'Password must be at least 10 characters and contain uppercase, lowercase letters, numbers and special characters',
117
122
 
123
+ // Union Type
124
+ 'pattern.emailOrPhone': 'Must be an email or phone number',
125
+ 'pattern.usernameOrEmail': 'Must be a username or email',
126
+ 'pattern.httpOrHttps': 'Must be a URL starting with http or https',
127
+
128
+ // oneOf (cross-type union) - v1.1.0
129
+ oneOf: '{{#label}} must match one of the following types',
130
+ 'oneOf.invalid': '{{#label}} value does not match any allowed type',
131
+
118
132
  // Unknown error fallback
119
133
  'UNKNOWN_ERROR': 'Unknown validation error',
120
134
 
@@ -99,6 +99,10 @@ module.exports = {
99
99
  // Unknown error fallback
100
100
  'UNKNOWN_ERROR': 'Error de validación desconocido',
101
101
 
102
+ // oneOf (unión entre tipos) - v1.1.0
103
+ oneOf: '{{#label}} debe coincidir con uno de los siguientes tipos',
104
+ 'oneOf.invalid': 'El valor de {{#label}} no coincide con ningún tipo permitido',
105
+
102
106
  // Custom validation
103
107
  'CUSTOM_VALIDATION_FAILED': 'Validación fallida',
104
108
  'ASYNC_VALIDATION_NOT_SUPPORTED': 'La validación asíncrona no es compatible en validate() síncrono',
@@ -99,6 +99,10 @@ module.exports = {
99
99
  // Unknown error fallback
100
100
  'UNKNOWN_ERROR': 'Erreur de validation inconnue',
101
101
 
102
+ // oneOf (union de types) - v1.1.0
103
+ oneOf: '{{#label}} doit correspondre à l\'un des types suivants',
104
+ 'oneOf.invalid': 'La valeur de {{#label}} ne correspond à aucun type autorisé',
105
+
102
106
  // Custom validation
103
107
  'CUSTOM_VALIDATION_FAILED': 'Validation échouée',
104
108
  'ASYNC_VALIDATION_NOT_SUPPORTED': 'La validation asynchrone n\'est pas prise en charge dans validate() synchrone',
@@ -12,6 +12,11 @@ module.exports = {
12
12
  'max-depth': '{{#label}}で最大再帰深度 ({{#depth}}) を超えました',
13
13
  exception: '{{#label}}の検証例外: {{#message}}',
14
14
 
15
+ // Conditional (ConditionalBuilder)
16
+ 'conditional.underAge': '未成年者は登録できません',
17
+ 'conditional.blocked': 'アカウントがブロックされています',
18
+ 'conditional.notAllowed': '登録は許可されていません',
19
+
15
20
  // Formats
16
21
  'format.email': '{{#label}}は有効なメールアドレスである必要があります',
17
22
  'format.url': '{{#label}}は有効なURLである必要があります',
@@ -96,6 +101,10 @@ module.exports = {
96
101
  'pattern.password.strong': 'パスワードは少なくとも8文字で、大文字、小文字、数字を含む必要があります',
97
102
  'pattern.password.veryStrong': 'パスワードは少なくとも10文字で、大文字、小文字、数字、特殊文字を含む必要があります',
98
103
 
104
+ // oneOf (型の結合) - v1.1.0
105
+ oneOf: '{{#label}}は次のいずれかの型に一致する必要があります',
106
+ 'oneOf.invalid': '{{#label}}の値は許可された型のいずれとも一致しません',
107
+
99
108
  // Unknown error fallback
100
109
  'UNKNOWN_ERROR': '不明な検証エラー',
101
110
 
@@ -12,6 +12,11 @@ module.exports = {
12
12
  'max-depth': '超过最大递归深度 ({{#depth}}) at {{#label}}',
13
13
  exception: '{{#label}}验证异常: {{#message}}',
14
14
 
15
+ // Conditional (ConditionalBuilder)
16
+ 'conditional.underAge': '未成年用户不能注册',
17
+ 'conditional.blocked': '账号已被封禁',
18
+ 'conditional.notAllowed': '不允许注册',
19
+
15
20
  // Formats
16
21
  'format.email': '{{#label}}必须是有效的邮箱地址',
17
22
  'format.url': '{{#label}}必须是有效的URL地址',
@@ -115,6 +120,15 @@ module.exports = {
115
120
  'pattern.password.strong': '密码至少8位,需包含大小写字母和数字',
116
121
  'pattern.password.veryStrong': '密码至少10位,需包含大小写字母、数字和特殊字符',
117
122
 
123
+ // Union Type (联合类型)
124
+ 'pattern.emailOrPhone': '必须是邮箱或手机号',
125
+ 'pattern.usernameOrEmail': '必须是用户名或邮箱',
126
+ 'pattern.httpOrHttps': '必须是 http 或 https 开头的 URL',
127
+
128
+ // oneOf (跨类型联合) - v1.1.0 新增
129
+ oneOf: '{{#label}}必须匹配以下类型之一',
130
+ 'oneOf.invalid': '{{#label}}的值不匹配任何允许的类型',
131
+
118
132
  // Unknown error fallback
119
133
  'UNKNOWN_ERROR': '未知的验证错误',
120
134
 
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "schema-dsl",
3
- "version": "1.0.9",
3
+ "version": "1.1.0",
4
4
  "description": "简洁强大的JSON Schema验证库 - DSL语法 + String扩展 + 便捷validate",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
6
7
  "exports": {
7
8
  ".": {
8
9
  "require": "./index.js",
@@ -60,6 +61,7 @@
60
61
  "benchmark": "^2.1.4",
61
62
  "chai": "^4.5.0",
62
63
  "eslint": "^8.57.1",
64
+ "express": "^5.2.1",
63
65
  "joi": "^18.0.2",
64
66
  "mocha": "^10.8.2",
65
67
  "monsqlize": "^1.0.1",