schema-dsl 1.2.5 → 2.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/CHANGELOG.md +87 -212
- package/README.md +391 -2249
- package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
- package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
- package/dist/Validator-C7GsVQOH.d.cts +192 -0
- package/dist/Validator-hFWKGxir.d.ts +192 -0
- package/dist/index.cjs +6594 -0
- package/dist/index.d.cts +1145 -0
- package/dist/index.d.ts +1145 -0
- package/dist/index.js +6528 -0
- package/dist/plugin-CIKtTMtS.d.cts +246 -0
- package/dist/plugin-CIKtTMtS.d.ts +246 -0
- package/dist/plugins/custom-format.cjs +3802 -0
- package/dist/plugins/custom-format.d.cts +12 -0
- package/dist/plugins/custom-format.d.ts +12 -0
- package/dist/plugins/custom-format.js +3772 -0
- package/dist/plugins/custom-type-example.cjs +3795 -0
- package/dist/plugins/custom-type-example.d.cts +8 -0
- package/dist/plugins/custom-type-example.d.ts +8 -0
- package/dist/plugins/custom-type-example.js +3765 -0
- package/dist/plugins/custom-validator.cjs +146 -0
- package/dist/plugins/custom-validator.d.cts +10 -0
- package/dist/plugins/custom-validator.d.ts +10 -0
- package/dist/plugins/custom-validator.js +121 -0
- package/docs/FEATURE-INDEX.md +102 -68
- package/docs/add-custom-locale.md +48 -35
- package/docs/add-keyword.md +24 -0
- package/docs/api-reference.md +396 -154
- package/docs/api.md +13 -0
- package/docs/best-practices-project-structure.md +19 -10
- package/docs/best-practices.md +93 -53
- package/docs/cache-manager.md +23 -15
- package/docs/compile.md +45 -0
- package/docs/conditional-api.md +40 -11
- package/docs/custom-extensions-guide.md +80 -152
- package/docs/design-philosophy.md +76 -71
- package/docs/doc-index.md +324 -0
- package/docs/dsl-syntax.md +69 -19
- package/docs/dynamic-locale.md +24 -14
- package/docs/enum.md +12 -5
- package/docs/error-handling.md +53 -44
- package/docs/export-guide.md +47 -8
- package/docs/export-limitations.md +27 -11
- package/docs/faq.md +86 -67
- package/docs/frontend-i18n-guide.md +26 -12
- package/docs/i18n-user-guide.md +60 -47
- package/docs/i18n.md +51 -32
- package/docs/index.md +48 -0
- package/docs/json-schema-basics.md +40 -0
- package/docs/label-vs-description.md +12 -3
- package/docs/markdown-exporter.md +15 -6
- package/docs/mongodb-exporter.md +11 -4
- package/docs/multi-language.md +26 -0
- package/docs/multi-type-support.md +26 -33
- package/docs/mysql-exporter.md +9 -2
- package/docs/number-operators.md +12 -5
- package/docs/optional-marker-guide.md +28 -23
- package/docs/performance-guide.md +49 -0
- package/docs/plugin-system.md +205 -366
- package/docs/plugin-type-registration.md +34 -0
- package/docs/postgresql-exporter.md +9 -2
- package/docs/public/favicon.svg +5 -0
- package/docs/quick-start.md +37 -363
- package/docs/runtime-locale-support.md +20 -9
- package/docs/schema-helper.md +10 -5
- package/docs/schema-utils-advanced-issues.md +23 -0
- package/docs/schema-utils-best-practices.md +20 -0
- package/docs/schema-utils-chaining.md +7 -0
- package/docs/schema-utils.md +76 -42
- package/docs/security-checklist.md +20 -0
- package/docs/string-extensions.md +17 -9
- package/docs/troubleshooting.md +36 -21
- package/docs/type-converter.md +41 -50
- package/docs/type-reference.md +38 -15
- package/docs/typescript-guide.md +53 -42
- package/docs/union-type-guide.md +11 -1
- package/docs/union-types.md +10 -3
- package/docs/validate-async.md +36 -25
- package/docs/validate-batch.md +49 -0
- package/docs/validate-dsl-object-support.md +33 -28
- package/docs/validate.md +36 -16
- package/docs/validation-guide.md +25 -7
- package/docs/validator.md +39 -0
- package/package.json +85 -27
- package/plugins/custom-format.cjs +8 -0
- package/plugins/custom-type-example.cjs +8 -0
- package/plugins/custom-validator.cjs +8 -0
- package/src/adapters/DslAdapter.ts +111 -0
- package/src/adapters/index.ts +1 -0
- package/src/config/constants.ts +83 -0
- package/src/config/index.ts +2 -0
- package/src/config/patterns.ts +77 -0
- package/src/core/CacheManager.ts +159 -0
- package/src/core/ConditionalBuilder.ts +382 -0
- package/src/core/ConditionalRuntime.ts +28 -0
- package/src/core/ConditionalValidator.ts +255 -0
- package/src/core/DslBuilder.ts +677 -0
- package/src/core/ErrorCodes.ts +38 -0
- package/src/core/ErrorFormatter.ts +271 -0
- package/src/core/JSONSchemaCore.ts +65 -0
- package/src/core/Locale.ts +187 -0
- package/src/core/MessageTemplate.ts +42 -0
- package/src/core/ObjectDslBuilder.ts +64 -0
- package/src/core/PluginManager.ts +326 -0
- package/src/core/StringExtensions.ts +140 -0
- package/src/core/TemplateEngine.ts +44 -0
- package/src/core/Validator.ts +448 -0
- package/src/errors/I18nError.ts +159 -0
- package/src/errors/ValidationError.ts +105 -0
- package/src/exporters/BaseExporter.ts +60 -0
- package/src/exporters/MarkdownExporter.ts +305 -0
- package/src/exporters/MongoDBExporter.ts +126 -0
- package/src/exporters/MySQLExporter.ts +155 -0
- package/src/exporters/PostgreSQLExporter.ts +222 -0
- package/src/exporters/index.ts +18 -0
- package/src/index.ts +633 -0
- package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
- package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
- package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
- package/src/locales/index.ts +103 -0
- package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
- package/src/locales/types.ts +156 -0
- package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
- package/src/parser/ConstraintParser.ts +101 -0
- package/src/parser/DslParser.ts +470 -0
- package/src/parser/SchemaCompiler.ts +66 -0
- package/src/parser/TypeRegistry.ts +250 -0
- package/src/parser/index.ts +6 -0
- package/src/plugins/custom-format.ts +126 -0
- package/src/plugins/custom-type-example.ts +108 -0
- package/src/plugins/custom-validator.ts +140 -0
- package/src/types/conditional.ts +28 -0
- package/src/types/config.ts +59 -0
- package/src/types/dsl.ts +131 -0
- package/src/types/error.ts +60 -0
- package/src/types/index.ts +17 -0
- package/src/types/infer.ts +128 -0
- package/src/types/plugin.ts +58 -0
- package/src/types/safe-regex.d.ts +9 -0
- package/src/types/schema.ts +66 -0
- package/src/types/validate.ts +71 -0
- package/src/utils/SchemaHelper.ts +196 -0
- package/src/utils/SchemaUtils.ts +346 -0
- package/src/utils/TypeConverter.ts +215 -0
- package/src/utils/index.ts +10 -0
- package/src/validators/CustomKeywords.ts +477 -0
- package/.eslintignore +0 -11
- package/.eslintrc.json +0 -27
- package/CONTRIBUTING.md +0 -368
- package/STATUS.md +0 -491
- package/changelogs/v1.0.0.md +0 -328
- package/changelogs/v1.0.9.md +0 -367
- package/changelogs/v1.1.0.md +0 -389
- package/changelogs/v1.1.1.md +0 -308
- package/changelogs/v1.1.2.md +0 -183
- package/changelogs/v1.1.3.md +0 -161
- package/changelogs/v1.1.4.md +0 -432
- package/changelogs/v1.1.5.md +0 -493
- package/changelogs/v1.1.6.md +0 -211
- package/changelogs/v1.1.8.md +0 -376
- package/changelogs/v1.2.3.md +0 -124
- package/docs/INDEX.md +0 -252
- package/docs/issues-resolved-summary.md +0 -196
- package/docs/performance-benchmark-report.md +0 -179
- package/docs/performance-quick-reference.md +0 -123
- package/docs/user-questions-answered.md +0 -353
- package/docs/validation-rules-v1.0.2.md +0 -1608
- package/examples/README.md +0 -81
- package/examples/array-dsl-example.js +0 -227
- package/examples/conditional-example.js +0 -288
- package/examples/conditional-non-object.js +0 -129
- package/examples/conditional-validate-example.js +0 -321
- package/examples/custom-extension.js +0 -85
- package/examples/dsl-match-example.js +0 -74
- package/examples/dsl-style.js +0 -118
- package/examples/dynamic-locale-configuration.js +0 -348
- package/examples/dynamic-locale-example.js +0 -287
- package/examples/enum.examples.js +0 -324
- package/examples/export-demo.js +0 -130
- package/examples/express-integration.js +0 -376
- package/examples/i18n-error-handling-complete.js +0 -381
- package/examples/i18n-error-handling-quickstart.md +0 -0
- package/examples/i18n-error.examples.js +0 -181
- package/examples/i18n-full-demo.js +0 -301
- package/examples/i18n-memory-safety.examples.js +0 -268
- package/examples/markdown-export.js +0 -71
- package/examples/middleware-usage.js +0 -93
- package/examples/new-features-comparison.js +0 -315
- package/examples/password-reset/README.md +0 -153
- package/examples/password-reset/schema.js +0 -26
- package/examples/password-reset/test.js +0 -101
- package/examples/plugin-system.examples.js +0 -205
- package/examples/schema-utils-chaining.examples.js +0 -250
- package/examples/simple-example.js +0 -122
- package/examples/slug.examples.js +0 -179
- package/examples/string-extensions.js +0 -297
- package/examples/union-type-example.js +0 -127
- package/examples/union-types-example.js +0 -77
- package/examples/user-registration/README.md +0 -156
- package/examples/user-registration/routes.js +0 -92
- package/examples/user-registration/schema.js +0 -150
- package/examples/user-registration/server.js +0 -74
- package/index.d.ts +0 -3658
- package/index.js +0 -475
- package/index.mjs +0 -60
- package/lib/adapters/DslAdapter.js +0 -995
- package/lib/adapters/index.js +0 -20
- package/lib/config/constants.js +0 -286
- package/lib/config/patterns/common.js +0 -47
- package/lib/config/patterns/creditCard.js +0 -9
- package/lib/config/patterns/idCard.js +0 -9
- package/lib/config/patterns/index.js +0 -9
- package/lib/config/patterns/licensePlate.js +0 -4
- package/lib/config/patterns/passport.js +0 -4
- package/lib/config/patterns/phone.js +0 -9
- package/lib/config/patterns/postalCode.js +0 -5
- package/lib/core/CacheManager.js +0 -376
- package/lib/core/ConditionalBuilder.js +0 -503
- package/lib/core/DslBuilder.js +0 -1589
- package/lib/core/ErrorCodes.js +0 -233
- package/lib/core/ErrorFormatter.js +0 -445
- package/lib/core/JSONSchemaCore.js +0 -347
- package/lib/core/Locale.js +0 -130
- package/lib/core/MessageTemplate.js +0 -98
- package/lib/core/PluginManager.js +0 -448
- package/lib/core/StringExtensions.js +0 -240
- package/lib/core/Validator.js +0 -654
- package/lib/errors/I18nError.js +0 -328
- package/lib/errors/ValidationError.js +0 -191
- package/lib/exporters/MarkdownExporter.js +0 -420
- package/lib/exporters/MongoDBExporter.js +0 -162
- package/lib/exporters/MySQLExporter.js +0 -212
- package/lib/exporters/PostgreSQLExporter.js +0 -289
- package/lib/exporters/index.js +0 -24
- package/lib/locales/index.js +0 -8
- package/lib/utils/LRUCache.js +0 -174
- package/lib/utils/SchemaHelper.js +0 -240
- package/lib/utils/SchemaUtils.js +0 -445
- package/lib/utils/TypeConverter.js +0 -245
- package/lib/utils/index.js +0 -13
- package/lib/validators/CustomKeywords.js +0 -616
- package/lib/validators/index.js +0 -11
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConditionalBuilder — chainable condition builder.
|
|
3
|
+
*
|
|
4
|
+
* v2 fixes:
|
|
5
|
+
* C-03: assert() throws ValidationError instead of plain Error
|
|
6
|
+
* C-Y01: elseIf semantics correct
|
|
7
|
+
* C-Y02: build() as toSchema() alias (IConditionalBuilder interface compat)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
11
|
+
import type { IConditionalBuilder } from '../types/conditional.js'
|
|
12
|
+
import type { ValidateOptions, ValidationResult } from '../types/validate.js'
|
|
13
|
+
import { ValidationError } from '../errors/ValidationError.js'
|
|
14
|
+
import { Validator } from './Validator.js'
|
|
15
|
+
import { Locale } from './Locale.js'
|
|
16
|
+
import { attachConditionalRuntime } from './ConditionalRuntime.js'
|
|
17
|
+
|
|
18
|
+
const RUNTIME_ONLY_VALIDATE_OPTION_KEYS = new Set(['locale', 'messages', 'format'])
|
|
19
|
+
|
|
20
|
+
// ==================== Internal Data Structures ====================
|
|
21
|
+
|
|
22
|
+
type ConditionFn = (data: unknown) => boolean
|
|
23
|
+
|
|
24
|
+
interface CombinedCondition {
|
|
25
|
+
op: 'root' | 'and' | 'or'
|
|
26
|
+
fn: ConditionFn
|
|
27
|
+
message: string | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ConditionEntry {
|
|
31
|
+
type: 'if' | 'elseIf'
|
|
32
|
+
condition: ConditionFn
|
|
33
|
+
combinedConditions: CombinedCondition[]
|
|
34
|
+
message?: string
|
|
35
|
+
action?: 'throw'
|
|
36
|
+
then?: string | JSONSchema | null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface EvaluateResult {
|
|
40
|
+
result: boolean
|
|
41
|
+
failedMessage: string | null
|
|
42
|
+
requirementFailed?: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ==================== ConditionalBuilder ====================
|
|
46
|
+
|
|
47
|
+
export class ConditionalBuilder implements IConditionalBuilder {
|
|
48
|
+
private _conditions: ConditionEntry[]
|
|
49
|
+
private _elseSchema: string | JSONSchema | null | undefined
|
|
50
|
+
|
|
51
|
+
constructor() {
|
|
52
|
+
this._conditions = []
|
|
53
|
+
this._elseSchema = undefined
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ==================== Condition Chain Methods ====================
|
|
57
|
+
|
|
58
|
+
if(conditionFn: ConditionFn | string): this {
|
|
59
|
+
// v1 compat: accept string field name and convert to function
|
|
60
|
+
if (typeof conditionFn === 'string') {
|
|
61
|
+
const fieldName = conditionFn
|
|
62
|
+
conditionFn = ((data: unknown) => Boolean((data as Record<string, unknown>)[fieldName])) as ConditionFn
|
|
63
|
+
}
|
|
64
|
+
if (typeof conditionFn !== 'function') {
|
|
65
|
+
throw new Error('[schema-dsl] Condition must be a function')
|
|
66
|
+
}
|
|
67
|
+
this._conditions.push({
|
|
68
|
+
type: 'if',
|
|
69
|
+
condition: conditionFn,
|
|
70
|
+
combinedConditions: [{ op: 'root', fn: conditionFn, message: null }],
|
|
71
|
+
})
|
|
72
|
+
return this
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
and(conditionFn: ConditionFn): this {
|
|
76
|
+
if (typeof conditionFn !== 'function') {
|
|
77
|
+
throw new Error('[schema-dsl] Condition must be a function')
|
|
78
|
+
}
|
|
79
|
+
const last = this._conditions[this._conditions.length - 1]
|
|
80
|
+
if (!last) throw new Error('[schema-dsl] .and() must follow .if() or .elseIf()')
|
|
81
|
+
last.combinedConditions.push({ op: 'and', fn: conditionFn, message: null })
|
|
82
|
+
return this
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* require(field) — v1 compat: require the specified field to be truthy.
|
|
87
|
+
* Equivalent to .and(data => Boolean(data[field])).
|
|
88
|
+
* BC-5 fix.
|
|
89
|
+
*/
|
|
90
|
+
require(field: string): this {
|
|
91
|
+
return this.and((data: unknown) => Boolean((data as Record<string, unknown>)[field]))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
or(conditionFn: ConditionFn): this {
|
|
95
|
+
if (typeof conditionFn !== 'function') {
|
|
96
|
+
throw new Error('[schema-dsl] Condition must be a function')
|
|
97
|
+
}
|
|
98
|
+
const last = this._conditions[this._conditions.length - 1]
|
|
99
|
+
if (!last) throw new Error('[schema-dsl] .or() must follow .if() or .elseIf()')
|
|
100
|
+
last.combinedConditions.push({ op: 'or', fn: conditionFn, message: null })
|
|
101
|
+
return this
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
elseIf(conditionFn: ConditionFn): this {
|
|
105
|
+
if (typeof conditionFn !== 'function') {
|
|
106
|
+
throw new Error('[schema-dsl] Condition must be a function')
|
|
107
|
+
}
|
|
108
|
+
if (this._conditions.length === 0) {
|
|
109
|
+
throw new Error('[schema-dsl] .elseIf() must follow .if()')
|
|
110
|
+
}
|
|
111
|
+
this._conditions.push({
|
|
112
|
+
type: 'elseIf',
|
|
113
|
+
condition: conditionFn,
|
|
114
|
+
combinedConditions: [{ op: 'root', fn: conditionFn, message: null }],
|
|
115
|
+
})
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
message(msg: string): this {
|
|
120
|
+
if (typeof msg !== 'string') {
|
|
121
|
+
throw new Error('[schema-dsl] Message must be a string')
|
|
122
|
+
}
|
|
123
|
+
const last = this._conditions[this._conditions.length - 1]
|
|
124
|
+
if (!last) throw new Error('[schema-dsl] .message() must follow .if() or .elseIf()')
|
|
125
|
+
|
|
126
|
+
const lastCombined = last.combinedConditions[last.combinedConditions.length - 1]
|
|
127
|
+
if (lastCombined) {
|
|
128
|
+
lastCombined.message = msg
|
|
129
|
+
}
|
|
130
|
+
last.message = msg
|
|
131
|
+
last.action = 'throw'
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
then(schema: string | JSONSchema | null): this {
|
|
136
|
+
const last = this._conditions[this._conditions.length - 1]
|
|
137
|
+
if (!last) throw new Error('[schema-dsl] .then() must follow .if() or .elseIf()')
|
|
138
|
+
last.then = schema
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
else(schema: string | JSONSchema | null): this {
|
|
143
|
+
this._elseSchema = schema
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==================== Output Methods ====================
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Produce a schema object carrying serialisable conditional metadata plus non-enumerable runtime state.
|
|
151
|
+
*/
|
|
152
|
+
toSchema(): JSONSchema {
|
|
153
|
+
return attachConditionalRuntime({
|
|
154
|
+
_isConditional: true,
|
|
155
|
+
_runtimeOnlyConditional: true,
|
|
156
|
+
else: this._elseSchema,
|
|
157
|
+
} as unknown as JSONSchema, {
|
|
158
|
+
conditions: this._conditions,
|
|
159
|
+
elseSchema: this._elseSchema,
|
|
160
|
+
evaluateCondition: (conditionObj: unknown, data: unknown) =>
|
|
161
|
+
this._evaluateCondition(conditionObj as ConditionEntry, data),
|
|
162
|
+
}) as unknown as JSONSchema
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* build() — alias for toSchema() (IConditionalBuilder interface compat).
|
|
167
|
+
*/
|
|
168
|
+
build(): JSONSchema {
|
|
169
|
+
return this.toSchema()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ==================== Validation Methods ====================
|
|
173
|
+
|
|
174
|
+
private readonly _validatorCache = new Map<string, Validator>()
|
|
175
|
+
private static readonly _VALIDATOR_CACHE_MAX = 20
|
|
176
|
+
|
|
177
|
+
validate(data: unknown, options: Record<string, unknown> = {}): ValidationResult<unknown> {
|
|
178
|
+
const validator = this._getValidator(options)
|
|
179
|
+
return validator.validate(this.toSchema(), data, options)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async validateAsync(data: unknown, options: Record<string, unknown> = {}): Promise<ValidationResult<unknown>> {
|
|
183
|
+
const validator = this._getValidator(options)
|
|
184
|
+
// validator.validateAsync() throws ValidationError on failure and returns data on success.
|
|
185
|
+
// Adapt to the ValidationResult contract expected by ConditionalBuilder callers.
|
|
186
|
+
try {
|
|
187
|
+
const resultData = await validator.validateAsync(this.toSchema(), data, options)
|
|
188
|
+
return { valid: true, data: resultData as unknown, errors: [] }
|
|
189
|
+
} catch (err) {
|
|
190
|
+
if (err instanceof ValidationError) {
|
|
191
|
+
return { valid: false, errors: err.errors, data: undefined }
|
|
192
|
+
}
|
|
193
|
+
throw err
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* assert() — synchronous assertion; throws ValidationError on failure (fixes C-03: v1 threw plain Error).
|
|
199
|
+
* Evaluates conditions synchronously without going through Validator (which is async).
|
|
200
|
+
*/
|
|
201
|
+
assert(data: unknown, options: Record<string, unknown> = {}): unknown {
|
|
202
|
+
const locale = (options.locale as string) ?? null
|
|
203
|
+
for (const cond of this._conditions) {
|
|
204
|
+
const { result: matched, failedMessage } = this._evaluateCondition(cond, data)
|
|
205
|
+
if (matched && cond.action === 'throw') {
|
|
206
|
+
const rawMsg = failedMessage ?? cond.message ?? 'Condition failed'
|
|
207
|
+
const message = Locale.getMessageText(rawMsg, {}, locale)
|
|
208
|
+
throw new ValidationError(
|
|
209
|
+
[{ message, path: '', keyword: 'conditional', params: {} }],
|
|
210
|
+
data,
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return data
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
check(data: unknown): boolean {
|
|
218
|
+
try {
|
|
219
|
+
const conditions = this._conditions
|
|
220
|
+
for (const cond of conditions) {
|
|
221
|
+
const { result: matched } = this._evaluateCondition(cond, data)
|
|
222
|
+
if (matched && cond.action === 'throw') return false
|
|
223
|
+
}
|
|
224
|
+
return true
|
|
225
|
+
} catch {
|
|
226
|
+
return false
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ==================== Static Factory Methods ====================
|
|
231
|
+
|
|
232
|
+
static start(conditionFn: ConditionFn | string): ConditionalBuilder {
|
|
233
|
+
return new ConditionalBuilder().if(conditionFn)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ==================== Internal Evaluation Logic ====================
|
|
237
|
+
|
|
238
|
+
private _evaluateCondition(conditionObj: ConditionEntry, data: unknown): EvaluateResult {
|
|
239
|
+
try {
|
|
240
|
+
const isMessageMode = conditionObj.action === 'throw'
|
|
241
|
+
const hasOrConditions = conditionObj.combinedConditions.some(c => c.op === 'or')
|
|
242
|
+
|
|
243
|
+
// Chain check mode (v1 compat): message mode + root has own message
|
|
244
|
+
// Each condition checked left-to-right, first TRUE = fail with its message
|
|
245
|
+
const rootHasMessage = conditionObj.combinedConditions[0]?.message !== null
|
|
246
|
+
const isChainCheckMode = isMessageMode && rootHasMessage
|
|
247
|
+
|
|
248
|
+
if (isChainCheckMode) {
|
|
249
|
+
for (const combined of conditionObj.combinedConditions) {
|
|
250
|
+
try {
|
|
251
|
+
const conditionResult = combined.fn(data)
|
|
252
|
+
if (conditionResult) {
|
|
253
|
+
return { result: true, failedMessage: combined.message ?? conditionObj.message ?? null }
|
|
254
|
+
}
|
|
255
|
+
} catch {
|
|
256
|
+
// Condition threw — treat as not matched
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { result: false, failedMessage: null }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Message mode with AND only (no OR, shared message, root has no own message):
|
|
263
|
+
// ALL conditions must be true to trigger
|
|
264
|
+
if (isMessageMode && !hasOrConditions && conditionObj.combinedConditions.length > 1) {
|
|
265
|
+
let allTrue = true
|
|
266
|
+
for (const combined of conditionObj.combinedConditions) {
|
|
267
|
+
if (!combined.fn(data)) {
|
|
268
|
+
allTrue = false
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (allTrue) {
|
|
273
|
+
return { result: true, failedMessage: conditionObj.message ?? null }
|
|
274
|
+
}
|
|
275
|
+
return { result: false, failedMessage: null }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Message mode with OR (and possibly AND, shared message): AND/OR boolean with precedence
|
|
279
|
+
if (isMessageMode && hasOrConditions) {
|
|
280
|
+
let andGroupResult = true
|
|
281
|
+
let finalResult = false
|
|
282
|
+
for (const combined of conditionObj.combinedConditions) {
|
|
283
|
+
const conditionResult = combined.fn(data)
|
|
284
|
+
if (combined.op === 'root' || combined.op === 'and') {
|
|
285
|
+
andGroupResult = andGroupResult && conditionResult
|
|
286
|
+
} else if (combined.op === 'or') {
|
|
287
|
+
finalResult = finalResult || andGroupResult
|
|
288
|
+
andGroupResult = conditionResult
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
finalResult = finalResult || andGroupResult
|
|
292
|
+
if (finalResult) {
|
|
293
|
+
return { result: true, failedMessage: conditionObj.message ?? null }
|
|
294
|
+
}
|
|
295
|
+
return { result: false, failedMessage: null }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Message mode without AND/OR (single condition)
|
|
299
|
+
if (isMessageMode) {
|
|
300
|
+
const root = conditionObj.combinedConditions[0]
|
|
301
|
+
if (root && root.fn(data)) {
|
|
302
|
+
return { result: true, failedMessage: root.message ?? conditionObj.message ?? null }
|
|
303
|
+
}
|
|
304
|
+
return { result: false, failedMessage: null }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Non-message (then/else) mode: standard AND/OR boolean evaluation
|
|
308
|
+
let andGroupResult = true
|
|
309
|
+
let finalResult = false
|
|
310
|
+
for (const combined of conditionObj.combinedConditions) {
|
|
311
|
+
const conditionResult = combined.fn(data)
|
|
312
|
+
if (combined.op === 'root' || combined.op === 'and') {
|
|
313
|
+
andGroupResult = andGroupResult && conditionResult
|
|
314
|
+
} else if (combined.op === 'or') {
|
|
315
|
+
finalResult = finalResult || andGroupResult
|
|
316
|
+
andGroupResult = conditionResult
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const result = finalResult || andGroupResult
|
|
320
|
+
return { result, failedMessage: null }
|
|
321
|
+
} catch {
|
|
322
|
+
return { result: false, failedMessage: null }
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private _getValidator(options: Record<string, unknown>): Validator {
|
|
327
|
+
const constructorOptions = this._getConstructorOptions(options)
|
|
328
|
+
const cacheKey = this._getValidatorCacheKey(constructorOptions)
|
|
329
|
+
|
|
330
|
+
let validator = this._validatorCache.get(cacheKey)
|
|
331
|
+
if (!validator) {
|
|
332
|
+
if (this._validatorCache.size >= ConditionalBuilder._VALIDATOR_CACHE_MAX) {
|
|
333
|
+
const firstKey = this._validatorCache.keys().next().value
|
|
334
|
+
if (firstKey !== undefined) this._validatorCache.delete(firstKey)
|
|
335
|
+
}
|
|
336
|
+
validator = new Validator(constructorOptions)
|
|
337
|
+
this._validatorCache.set(cacheKey, validator)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return validator
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private _getConstructorOptions(options: Record<string, unknown>): ValidateOptions {
|
|
344
|
+
const constructorOptions: Record<string, unknown> = {}
|
|
345
|
+
|
|
346
|
+
for (const [key, value] of Object.entries(options)) {
|
|
347
|
+
if (!RUNTIME_ONLY_VALIDATE_OPTION_KEYS.has(key)) {
|
|
348
|
+
constructorOptions[key] = value
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return constructorOptions as ValidateOptions
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private _getValidatorCacheKey(options: ValidateOptions): string {
|
|
356
|
+
return JSON.stringify(this._normalizeOptionValue(options))
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private _normalizeOptionValue(value: unknown): unknown {
|
|
360
|
+
if (Array.isArray(value)) {
|
|
361
|
+
return value.map(item => this._normalizeOptionValue(item))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (value && typeof value === 'object') {
|
|
365
|
+
return Object.fromEntries(
|
|
366
|
+
Object.entries(value as Record<string, unknown>)
|
|
367
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
368
|
+
.map(([key, nestedValue]) => [key, this._normalizeOptionValue(nestedValue)])
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeof value === 'function') {
|
|
373
|
+
return `__fn__:${value.name || 'anonymous'}`
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (typeof value === 'bigint') {
|
|
377
|
+
return `__bigint__:${value.toString()}`
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return value
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
+
|
|
3
|
+
export const CONDITIONAL_RUNTIME_STATE: unique symbol = Symbol('schema-dsl.conditionalRuntimeState')
|
|
4
|
+
|
|
5
|
+
export interface ConditionalRuntimeState {
|
|
6
|
+
conditions: unknown[]
|
|
7
|
+
elseSchema: string | JSONSchema | null | undefined
|
|
8
|
+
evaluateCondition: (conditionObj: unknown, data: unknown) => {
|
|
9
|
+
result: boolean
|
|
10
|
+
failedMessage?: string | null
|
|
11
|
+
requirementFailed?: boolean
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ConditionalRuntimeSchema = JSONSchema & {
|
|
16
|
+
[CONDITIONAL_RUNTIME_STATE]?: ConditionalRuntimeState
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function attachConditionalRuntime(schema: JSONSchema, state: ConditionalRuntimeState): ConditionalRuntimeSchema {
|
|
20
|
+
Object.defineProperty(schema, CONDITIONAL_RUNTIME_STATE, {
|
|
21
|
+
value: state,
|
|
22
|
+
enumerable: false,
|
|
23
|
+
configurable: false,
|
|
24
|
+
writable: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
return schema as ConditionalRuntimeSchema
|
|
28
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
+
import type { ValidateOptions, ValidationErrorItem, ValidationResult } from '../types/validate.js'
|
|
3
|
+
import type { DslDefinition } from '../types/dsl.js'
|
|
4
|
+
import { DslParser } from '../parser/DslParser.js'
|
|
5
|
+
import { Locale } from './Locale.js'
|
|
6
|
+
import { CONDITIONAL_RUNTIME_STATE, type ConditionalRuntimeState } from './ConditionalRuntime.js'
|
|
7
|
+
|
|
8
|
+
const EMPTY_ERRORS: ValidationErrorItem[] = []
|
|
9
|
+
|
|
10
|
+
export type ConditionalInternalSchema = JSONSchema & {
|
|
11
|
+
_required?: boolean
|
|
12
|
+
_label?: string
|
|
13
|
+
_customMessages?: Record<string, string>
|
|
14
|
+
_isConditional?: boolean
|
|
15
|
+
_runtimeOnlyConditional?: boolean
|
|
16
|
+
conditions?: Array<{ action?: string; message?: string; then?: unknown }>
|
|
17
|
+
_evaluateCondition?: (cond: unknown, data: unknown) => { result: boolean; failedMessage?: string; requirementFailed?: boolean }
|
|
18
|
+
else?: unknown
|
|
19
|
+
[CONDITIONAL_RUNTIME_STATE]?: ConditionalRuntimeState
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ConditionalValidatorHooks {
|
|
23
|
+
validateSchema<T>(schema: JSONSchema, data: T, options: ValidateOptions): ValidationResult<T>
|
|
24
|
+
internalError<T>(error: unknown, data: T): ValidationResult<T>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class ConditionalValidator {
|
|
28
|
+
constructor(private readonly hooks: ConditionalValidatorHooks) { }
|
|
29
|
+
|
|
30
|
+
hasAnyConditional(schema: ConditionalInternalSchema): boolean {
|
|
31
|
+
if (!schema.properties) return false
|
|
32
|
+
return Object.values(schema.properties).some((fieldSchema) => {
|
|
33
|
+
const fs = fieldSchema as ConditionalInternalSchema
|
|
34
|
+
if (fs._isConditional) return true
|
|
35
|
+
if (fs.properties) return this.hasAnyConditional(fs)
|
|
36
|
+
return false
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
validateWithConditionals<T>(
|
|
41
|
+
schema: ConditionalInternalSchema,
|
|
42
|
+
data: T,
|
|
43
|
+
options: ValidateOptions,
|
|
44
|
+
rootData?: Record<string, unknown>
|
|
45
|
+
): ValidationResult<T> {
|
|
46
|
+
const errors: ValidationErrorItem[] = []
|
|
47
|
+
const effectiveRoot = rootData ?? (data as Record<string, unknown>)
|
|
48
|
+
const cleanSchema = JSON.parse(JSON.stringify(schema)) as ConditionalInternalSchema
|
|
49
|
+
const conditionalFields: Record<string, ConditionalInternalSchema> = {}
|
|
50
|
+
const nestedObjectFields: Record<string, ConditionalInternalSchema> = {}
|
|
51
|
+
|
|
52
|
+
for (const [fieldName, fieldSchema] of Object.entries(schema.properties ?? {})) {
|
|
53
|
+
const fs = fieldSchema as ConditionalInternalSchema
|
|
54
|
+
if (fs._isConditional) {
|
|
55
|
+
conditionalFields[fieldName] = fs
|
|
56
|
+
delete cleanSchema.properties?.[fieldName]
|
|
57
|
+
|
|
58
|
+
if (cleanSchema.required) {
|
|
59
|
+
cleanSchema.required = cleanSchema.required.filter(r => r !== fieldName)
|
|
60
|
+
}
|
|
61
|
+
} else if (fs.properties && this.hasAnyConditional(fs)) {
|
|
62
|
+
nestedObjectFields[fieldName] = fs
|
|
63
|
+
delete cleanSchema.properties?.[fieldName]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const baseResult = this.hooks.validateSchema(cleanSchema, data, options)
|
|
68
|
+
if (!baseResult.valid) {
|
|
69
|
+
errors.push(...(baseResult.errors ?? []))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [fieldName, conditionalSchema] of Object.entries(conditionalFields)) {
|
|
73
|
+
const dataRecord = data as Record<string, unknown>
|
|
74
|
+
const fieldResult = this.validateConditional(conditionalSchema, effectiveRoot, fieldName, dataRecord[fieldName], options)
|
|
75
|
+
|
|
76
|
+
if (!fieldResult.valid) {
|
|
77
|
+
for (const err of (fieldResult.errors ?? [])) {
|
|
78
|
+
const errPath = (!err.path || err.path === 'value') ? fieldName : err.path
|
|
79
|
+
errors.push({ ...err, path: errPath, field: errPath })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const [fieldName, nestedSchema] of Object.entries(nestedObjectFields)) {
|
|
85
|
+
const dataRecord = data as Record<string, unknown>
|
|
86
|
+
const nestedData = dataRecord[fieldName]
|
|
87
|
+
|
|
88
|
+
if (nestedData === undefined || nestedData === null) {
|
|
89
|
+
const partialSchema = JSON.parse(JSON.stringify(schema)) as ConditionalInternalSchema
|
|
90
|
+
partialSchema.properties = { [fieldName]: nestedSchema }
|
|
91
|
+
partialSchema.required = (schema.required ?? []).filter(r => r === fieldName)
|
|
92
|
+
const partialResult = this.hooks.validateSchema(partialSchema, data, options)
|
|
93
|
+
if (!partialResult.valid) {
|
|
94
|
+
errors.push(...(partialResult.errors ?? []))
|
|
95
|
+
}
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const nestedResult = this.validateWithConditionals(nestedSchema, nestedData, options, effectiveRoot)
|
|
100
|
+
if (!nestedResult.valid) {
|
|
101
|
+
for (const err of (nestedResult.errors ?? [])) {
|
|
102
|
+
const errPath = err.path ? `${fieldName}/${err.path}` : fieldName
|
|
103
|
+
errors.push({ ...err, path: errPath, field: errPath })
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (errors.length === 0) return { valid: true, data, errors: EMPTY_ERRORS }
|
|
109
|
+
return { valid: false, data, errors, errorMessage: errors[0]?.message }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
validateConditional<T>(
|
|
113
|
+
conditionalSchema: ConditionalInternalSchema,
|
|
114
|
+
data: Record<string, unknown>,
|
|
115
|
+
fieldName: string | null,
|
|
116
|
+
fieldValue: T,
|
|
117
|
+
options: ValidateOptions
|
|
118
|
+
): ValidationResult<T> {
|
|
119
|
+
const locale = options.locale ?? Locale.getLocale()
|
|
120
|
+
const runtimeState = conditionalSchema[CONDITIONAL_RUNTIME_STATE]
|
|
121
|
+
const conditions = (runtimeState?.conditions ?? conditionalSchema.conditions ?? []) as Array<{ action?: string; message?: string; then?: unknown; type?: string }>
|
|
122
|
+
|
|
123
|
+
if (conditions.length === 0 && conditionalSchema._runtimeOnlyConditional) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
data: fieldValue,
|
|
127
|
+
errors: [{
|
|
128
|
+
message: '[schema-dsl] Function-based conditional schemas are runtime-only and cannot be restored from JSON serialization.',
|
|
129
|
+
path: '',
|
|
130
|
+
keyword: 'conditional',
|
|
131
|
+
params: {},
|
|
132
|
+
}],
|
|
133
|
+
errorMessage: '[schema-dsl] Function-based conditional schemas are runtime-only and cannot be restored from JSON serialization.',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
for (const cond of conditions) {
|
|
139
|
+
const evaluation = runtimeState?.evaluateCondition(cond, data)
|
|
140
|
+
?? conditionalSchema._evaluateCondition?.(cond, data)
|
|
141
|
+
?? { result: false }
|
|
142
|
+
const matched = evaluation.result
|
|
143
|
+
|
|
144
|
+
if (cond.action === 'throw') {
|
|
145
|
+
if (matched) {
|
|
146
|
+
const errorMsg = evaluation.failedMessage ?? cond.message ?? 'Conditional validation failed'
|
|
147
|
+
const message = Locale.getMessageText(errorMsg, (options.messages ?? {}) as Record<string, string>, locale)
|
|
148
|
+
return {
|
|
149
|
+
valid: false,
|
|
150
|
+
data: fieldValue,
|
|
151
|
+
errors: [{ message, path: '', keyword: 'conditional', params: { condition: (cond as Record<string, unknown>)['type'] } }],
|
|
152
|
+
errorMessage: message,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (matched) {
|
|
159
|
+
const thenSchema = (cond as Record<string, unknown>)['then']
|
|
160
|
+
if (thenSchema !== undefined && thenSchema !== null) {
|
|
161
|
+
return this.executeThenBranch(thenSchema, data, fieldValue, fieldName, options)
|
|
162
|
+
}
|
|
163
|
+
return { valid: true, data: fieldValue, errors: EMPTY_ERRORS }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (evaluation.requirementFailed) {
|
|
167
|
+
const errorMsg = cond.message ?? 'Condition not met'
|
|
168
|
+
const message = Locale.getMessageText(errorMsg, (options.messages ?? {}) as Record<string, string>, locale)
|
|
169
|
+
return {
|
|
170
|
+
valid: false,
|
|
171
|
+
data: fieldValue,
|
|
172
|
+
errors: [{ message, path: '', keyword: 'conditional', params: {} }],
|
|
173
|
+
errorMessage: message,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const elseSchema = runtimeState ? runtimeState.elseSchema : conditionalSchema.else
|
|
179
|
+
if (elseSchema !== undefined) {
|
|
180
|
+
if (elseSchema === null) return { valid: true, data: fieldValue, errors: EMPTY_ERRORS }
|
|
181
|
+
return this.executeThenBranch(elseSchema, data, fieldValue, fieldName, options)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { valid: true, data: fieldValue, errors: EMPTY_ERRORS }
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return this.hooks.internalError(error, fieldValue)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private executeThenBranch<T>(
|
|
191
|
+
thenSchema: unknown,
|
|
192
|
+
data: Record<string, unknown>,
|
|
193
|
+
fieldValue: T,
|
|
194
|
+
fieldName: string | null,
|
|
195
|
+
options: ValidateOptions
|
|
196
|
+
): ValidationResult<T> {
|
|
197
|
+
let resolved = thenSchema
|
|
198
|
+
|
|
199
|
+
if (typeof resolved === 'string') {
|
|
200
|
+
resolved = DslParser.parseString(resolved)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (resolved !== null && typeof resolved === 'object') {
|
|
204
|
+
const obj = resolved as Record<string, unknown>
|
|
205
|
+
if (typeof obj['toSchema'] === 'function') {
|
|
206
|
+
resolved = (obj['toSchema'] as () => JSONSchema)()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const resolvedSchema = resolved as ConditionalInternalSchema
|
|
211
|
+
if (resolvedSchema?._isConditional) {
|
|
212
|
+
return this.validateConditional(resolvedSchema, data, fieldName, fieldValue, options)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (resolved !== null && typeof resolved === 'object' && !Array.isArray(resolved)) {
|
|
216
|
+
const obj = resolved as Record<string, unknown>
|
|
217
|
+
if (obj['type'] === undefined && obj['oneOf'] === undefined && obj['anyOf'] === undefined && obj['allOf'] === undefined) {
|
|
218
|
+
resolved = DslParser.parseObject(resolved as DslDefinition)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return this.validateFieldValue(resolved as JSONSchema, fieldValue, options)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private validateFieldValue<T>(schema: JSONSchema, fieldValue: T, options: ValidateOptions): ValidationResult<T> {
|
|
226
|
+
const internalSchema = schema as ConditionalInternalSchema
|
|
227
|
+
const isRequired = internalSchema._required === true
|
|
228
|
+
|
|
229
|
+
if (!isRequired && (fieldValue === undefined || fieldValue === '')) {
|
|
230
|
+
return { valid: true, data: fieldValue, errors: EMPTY_ERRORS }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (isRequired && fieldValue === undefined) {
|
|
234
|
+
const locale = options.locale ?? Locale.getLocale()
|
|
235
|
+
const label = internalSchema._label ?? ''
|
|
236
|
+
const customMsgs = internalSchema._customMessages ?? {}
|
|
237
|
+
const allMsgs = { ...(options.messages ?? {}), ...customMsgs } as Record<string, string>
|
|
238
|
+
let message: string
|
|
239
|
+
if (allMsgs['required']) {
|
|
240
|
+
message = Locale.getMessageText(allMsgs['required'], allMsgs, locale)
|
|
241
|
+
} else {
|
|
242
|
+
message = Locale.getMessageText('required', allMsgs, locale)
|
|
243
|
+
if (label) message = `${label} ${message}`
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
valid: false,
|
|
247
|
+
data: fieldValue,
|
|
248
|
+
errors: [{ message, path: '', keyword: 'required', params: {} }],
|
|
249
|
+
errorMessage: message,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return this.hooks.validateSchema(schema, fieldValue, options)
|
|
254
|
+
}
|
|
255
|
+
}
|