schema-dsl 2.0.0 → 2.0.1
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 +130 -113
- package/LICENSE +21 -21
- package/README.md +628 -628
- package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
- package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
- package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
- package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
- package/dist/index.cjs +75 -29
- package/dist/index.d.cts +10 -4
- package/dist/index.d.ts +10 -4
- package/dist/index.js +75 -29
- package/dist/plugins/custom-format.cjs +33 -17
- package/dist/plugins/custom-format.d.cts +1 -1
- package/dist/plugins/custom-format.d.ts +1 -1
- package/dist/plugins/custom-format.js +33 -17
- package/dist/plugins/custom-type-example.cjs +33 -17
- package/dist/plugins/custom-type-example.d.cts +1 -1
- package/dist/plugins/custom-type-example.d.ts +1 -1
- package/dist/plugins/custom-type-example.js +33 -17
- package/dist/plugins/custom-validator.cjs +0 -2
- package/dist/plugins/custom-validator.d.cts +1 -1
- package/dist/plugins/custom-validator.d.ts +1 -1
- package/dist/plugins/custom-validator.js +0 -2
- package/docs/FEATURE-INDEX.md +553 -553
- package/docs/add-custom-locale.md +496 -496
- package/docs/add-keyword.md +24 -24
- package/docs/api-reference.md +1047 -1047
- package/docs/api.md +13 -13
- package/docs/best-practices-project-structure.md +417 -417
- package/docs/best-practices.md +712 -712
- package/docs/cache-manager.md +344 -344
- package/docs/compile.md +45 -45
- package/docs/conditional-api.md +1307 -1307
- package/docs/custom-extensions-guide.md +339 -339
- package/docs/design-philosophy.md +606 -606
- package/docs/doc-index.md +324 -324
- package/docs/dsl-syntax.md +714 -714
- package/docs/dynamic-locale.md +608 -608
- package/docs/enum.md +482 -482
- package/docs/error-handling.md +1975 -1975
- package/docs/export-guide.md +501 -501
- package/docs/export-limitations.md +567 -567
- package/docs/faq.md +596 -596
- package/docs/frontend-i18n-guide.md +307 -307
- package/docs/i18n-user-guide.md +487 -487
- package/docs/i18n.md +476 -476
- package/docs/index.md +48 -48
- package/docs/json-schema-basics.md +40 -40
- package/docs/label-vs-description.md +271 -271
- package/docs/markdown-exporter.md +406 -406
- package/docs/mongodb-exporter.md +302 -302
- package/docs/multi-language.md +26 -26
- package/docs/multi-type-support.md +322 -322
- package/docs/mysql-exporter.md +280 -280
- package/docs/number-operators.md +449 -449
- package/docs/optional-marker-guide.md +326 -326
- package/docs/performance-guide.md +49 -49
- package/docs/plugin-system.md +381 -381
- package/docs/plugin-type-registration.md +34 -34
- package/docs/postgresql-exporter.md +311 -311
- package/docs/public/favicon.svg +4 -4
- package/docs/quick-start.md +435 -435
- package/docs/runtime-locale-support.md +532 -532
- package/docs/schema-helper.md +345 -345
- package/docs/schema-utils-advanced-issues.md +23 -23
- package/docs/schema-utils-best-practices.md +20 -20
- package/docs/schema-utils-chaining.md +150 -150
- package/docs/schema-utils.md +524 -524
- package/docs/security-checklist.md +20 -20
- package/docs/string-extensions.md +488 -488
- package/docs/troubleshooting.md +486 -486
- package/docs/type-converter.md +310 -310
- package/docs/type-reference.md +242 -242
- package/docs/typescript-guide.md +584 -584
- package/docs/union-type-guide.md +157 -157
- package/docs/union-types.md +284 -284
- package/docs/validate-async.md +491 -491
- package/docs/validate-batch.md +49 -49
- package/docs/validate-dsl-object-support.md +578 -578
- package/docs/validate.md +506 -506
- package/docs/validation-guide.md +502 -502
- package/docs/validator.md +39 -39
- package/package.json +131 -131
- package/plugins/custom-format.cjs +8 -8
- package/plugins/custom-type-example.cjs +8 -8
- package/plugins/custom-validator.cjs +8 -8
- package/src/adapters/DslAdapter.ts +111 -111
- package/src/adapters/index.ts +1 -1
- package/src/config/constants.ts +83 -83
- package/src/config/index.ts +2 -2
- package/src/config/patterns.ts +77 -77
- package/src/core/CacheManager.ts +169 -159
- package/src/core/ConditionalBuilder.ts +382 -382
- package/src/core/ConditionalRuntime.ts +27 -27
- package/src/core/ConditionalValidator.ts +254 -254
- package/src/core/DslBuilder.ts +687 -677
- package/src/core/ErrorCodes.ts +38 -38
- package/src/core/ErrorFormatter.ts +271 -271
- package/src/core/JSONSchemaCore.ts +65 -65
- package/src/core/Locale.ts +187 -187
- package/src/core/MessageTemplate.ts +42 -42
- package/src/core/ObjectDslBuilder.ts +64 -64
- package/src/core/PluginManager.ts +326 -326
- package/src/core/StringExtensions.ts +140 -140
- package/src/core/TemplateEngine.ts +44 -44
- package/src/core/Validator.ts +448 -448
- package/src/errors/I18nError.ts +159 -159
- package/src/errors/ValidationError.ts +105 -105
- package/src/exporters/BaseExporter.ts +60 -60
- package/src/exporters/MarkdownExporter.ts +305 -305
- package/src/exporters/MongoDBExporter.ts +126 -126
- package/src/exporters/MySQLExporter.ts +156 -155
- package/src/exporters/PostgreSQLExporter.ts +222 -222
- package/src/exporters/index.ts +18 -18
- package/src/index.ts +651 -633
- package/src/locales/en-US.ts +160 -160
- package/src/locales/es-ES.ts +160 -160
- package/src/locales/fr-FR.ts +160 -160
- package/src/locales/index.ts +103 -103
- package/src/locales/ja-JP.ts +160 -160
- package/src/locales/types.ts +156 -156
- package/src/locales/zh-CN.ts +160 -160
- package/src/parser/ConstraintParser.ts +101 -101
- package/src/parser/DslParser.ts +470 -470
- package/src/parser/SchemaCompiler.ts +66 -66
- package/src/parser/TypeRegistry.ts +250 -250
- package/src/parser/index.ts +6 -6
- package/src/plugins/custom-format.ts +124 -126
- package/src/plugins/custom-type-example.ts +106 -108
- package/src/plugins/custom-validator.ts +138 -140
- package/src/types/conditional.ts +28 -28
- package/src/types/config.ts +59 -59
- package/src/types/dsl.ts +131 -131
- package/src/types/error.ts +60 -60
- package/src/types/index.ts +17 -17
- package/src/types/infer.ts +127 -127
- package/src/types/plugin.ts +58 -58
- package/src/types/safe-regex.d.ts +9 -9
- package/src/types/schema.ts +66 -66
- package/src/types/validate.ts +71 -71
- package/src/utils/SchemaHelper.ts +196 -196
- package/src/utils/SchemaUtils.ts +365 -346
- package/src/utils/TypeConverter.ts +215 -215
- package/src/utils/index.ts +10 -10
- package/src/validators/CustomKeywords.ts +477 -477
|
@@ -1,28 +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
|
|
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
28
|
}
|
|
@@ -1,255 +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
|
-
}
|
|
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
255
|
}
|