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.
- package/CHANGELOG.md +205 -2
- package/README.md +257 -0
- package/docs/FEATURE-INDEX.md +1 -1
- package/docs/best-practices.md +3 -3
- package/docs/cache-manager.md +1 -1
- package/docs/conditional-api.md +1032 -0
- package/docs/dsl-syntax.md +1 -1
- package/docs/dynamic-locale.md +2 -2
- package/docs/error-handling.md +2 -2
- package/docs/export-guide.md +2 -2
- package/docs/export-limitations.md +3 -3
- package/docs/faq.md +6 -6
- package/docs/frontend-i18n-guide.md +1 -1
- package/docs/mongodb-exporter.md +3 -3
- package/docs/multi-type-support.md +12 -2
- package/docs/mysql-exporter.md +1 -1
- package/docs/plugin-system.md +4 -4
- package/docs/postgresql-exporter.md +1 -1
- package/docs/quick-start.md +4 -4
- package/docs/troubleshooting.md +2 -2
- package/docs/type-reference.md +5 -5
- package/docs/typescript-guide.md +5 -6
- package/docs/union-type-guide.md +147 -0
- package/docs/union-types.md +277 -0
- package/docs/validate-async.md +1 -1
- package/examples/array-dsl-example.js +1 -1
- package/examples/conditional-example.js +288 -0
- package/examples/conditional-non-object.js +129 -0
- package/examples/conditional-validate-example.js +321 -0
- package/examples/union-type-example.js +127 -0
- package/examples/union-types-example.js +77 -0
- package/index.d.ts +332 -7
- package/index.js +19 -1
- package/lib/adapters/DslAdapter.js +14 -5
- package/lib/core/ConditionalBuilder.js +401 -0
- package/lib/core/DslBuilder.js +113 -0
- package/lib/core/Locale.js +13 -8
- package/lib/core/Validator.js +246 -2
- package/lib/locales/en-US.js +14 -0
- package/lib/locales/es-ES.js +4 -0
- package/lib/locales/fr-FR.js +4 -0
- package/lib/locales/ja-JP.js +9 -0
- package/lib/locales/zh-CN.js +14 -0
- package/package.json +3 -1
package/lib/core/Validator.js
CHANGED
|
@@ -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) {
|
package/lib/locales/en-US.js
CHANGED
|
@@ -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
|
|
package/lib/locales/es-ES.js
CHANGED
|
@@ -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',
|
package/lib/locales/fr-FR.js
CHANGED
|
@@ -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',
|
package/lib/locales/ja-JP.js
CHANGED
|
@@ -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
|
|
package/lib/locales/zh-CN.js
CHANGED
|
@@ -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
|
|
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",
|