schema-dsl 1.2.4 → 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 -210
- 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 -3540
- package/index.js +0 -457
- package/index.mjs +0 -60
- package/lib/adapters/DslAdapter.js +0 -871
- 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 -1400
- 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,271 @@
|
|
|
1
|
+
import type { ValidationErrorItem } from '../types/validate.js'
|
|
2
|
+
import type { ErrorMessages } from '../types/error.js'
|
|
3
|
+
import { renderTemplate } from './TemplateEngine.js'
|
|
4
|
+
import { KEYWORD_MAP } from './ErrorCodes.js'
|
|
5
|
+
import { getMessages } from '../locales/index.js'
|
|
6
|
+
import type { LocaleMessage } from '../locales/types.js'
|
|
7
|
+
import { DEFAULT_LOCALE } from './Locale.js'
|
|
8
|
+
|
|
9
|
+
type AjvRawError = {
|
|
10
|
+
keyword: string
|
|
11
|
+
instancePath: string
|
|
12
|
+
schemaPath?: string
|
|
13
|
+
params: Record<string, unknown>
|
|
14
|
+
message?: string
|
|
15
|
+
data?: unknown
|
|
16
|
+
parentSchema?: Record<string, unknown>
|
|
17
|
+
schema?: unknown
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Error formatter.
|
|
22
|
+
* Delegates template interpolation to TemplateEngine.renderTemplate() (fix CORE-03).
|
|
23
|
+
* Maintains full v1 API compatibility.
|
|
24
|
+
*/
|
|
25
|
+
export class ErrorFormatter {
|
|
26
|
+
private messages: ErrorMessages
|
|
27
|
+
private _locale: string
|
|
28
|
+
private readonly _constructorCustomMessages: ErrorMessages
|
|
29
|
+
|
|
30
|
+
constructor(locale = DEFAULT_LOCALE, messages: ErrorMessages | Record<string, LocaleMessage | string | undefined> = {}) {
|
|
31
|
+
this._locale = locale
|
|
32
|
+
// Load locale messages as defaults; constructor-level custom messages override them
|
|
33
|
+
const rawLocaleMessages = getMessages(locale)
|
|
34
|
+
const localeMessages: ErrorMessages = Object.fromEntries(
|
|
35
|
+
Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
|
|
36
|
+
k,
|
|
37
|
+
typeof v === 'string' ? v : (v as { message: string }).message,
|
|
38
|
+
])
|
|
39
|
+
)
|
|
40
|
+
// Normalise caller-supplied messages: LocaleMessage objects → plain string
|
|
41
|
+
const normMessages: ErrorMessages = Object.fromEntries(
|
|
42
|
+
Object.entries(messages).map(([k, v]) => [
|
|
43
|
+
k,
|
|
44
|
+
v == null ? undefined : typeof v === 'string' ? v : (v as { message: string }).message,
|
|
45
|
+
])
|
|
46
|
+
)
|
|
47
|
+
this._constructorCustomMessages = normMessages
|
|
48
|
+
this.messages = { ...localeMessages, ...normMessages }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
get locale(): string {
|
|
52
|
+
return this._locale
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format a single error object → message string (v1 API).
|
|
57
|
+
*/
|
|
58
|
+
format(error: AjvRawError | Record<string, unknown>, locale?: string): string {
|
|
59
|
+
// If locale differs, reload messages for that locale
|
|
60
|
+
let msgs = this.messages
|
|
61
|
+
if (locale && locale !== this._locale) {
|
|
62
|
+
const rawLocaleMessages = getMessages(locale)
|
|
63
|
+
const localeMessages: ErrorMessages = Object.fromEntries(
|
|
64
|
+
Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
|
|
65
|
+
k,
|
|
66
|
+
typeof v === 'string' ? v : (v as { message: string }).message,
|
|
67
|
+
])
|
|
68
|
+
)
|
|
69
|
+
// Preserve constructor-level custom messages across locale switches (R-01 fix)
|
|
70
|
+
msgs = { ...localeMessages, ...this._constructorCustomMessages }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Convert simple { type, path } format to AJV-like error
|
|
74
|
+
const raw = error as Record<string, unknown>
|
|
75
|
+
const ajvError = {
|
|
76
|
+
keyword: (raw['keyword'] as string) ?? (raw['type'] as string) ?? 'validation',
|
|
77
|
+
instancePath: (raw['instancePath'] as string) ?? ('/' + (raw['path'] ?? '')),
|
|
78
|
+
params: (raw['params'] as Record<string, unknown>) ?? {},
|
|
79
|
+
parentSchema: raw['parentSchema'] as Record<string, unknown> | undefined,
|
|
80
|
+
} as AjvRawError
|
|
81
|
+
const item = this._formatOne(ajvError, msgs, locale)
|
|
82
|
+
return item.message
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format an AJV raw error array → ValidationErrorItem[].
|
|
87
|
+
*
|
|
88
|
+
* @param alreadyMerged - when true, customMessages is already a fully merged locale+custom result;
|
|
89
|
+
* skip `{ ...this.messages, ...customMessages }` spread (avoids 100+ key cold-spread overhead).
|
|
90
|
+
*/
|
|
91
|
+
formatDetailed(
|
|
92
|
+
errors: AjvRawError[],
|
|
93
|
+
locale?: string,
|
|
94
|
+
customMessages?: ErrorMessages,
|
|
95
|
+
alreadyMerged = false
|
|
96
|
+
): ValidationErrorItem[] {
|
|
97
|
+
const msgs = customMessages
|
|
98
|
+
? (alreadyMerged ? customMessages : { ...this.messages, ...customMessages })
|
|
99
|
+
: this.messages
|
|
100
|
+
|
|
101
|
+
// Filter wrapper errors (if/anyOf/oneOf) when concrete field errors are present
|
|
102
|
+
const hasConcreteErrors = errors.some(
|
|
103
|
+
e => e.keyword !== 'if' && e.keyword !== 'anyOf' && e.keyword !== 'oneOf' && e.keyword !== 'error'
|
|
104
|
+
)
|
|
105
|
+
const filtered = hasConcreteErrors
|
|
106
|
+
? errors.filter(e => e.keyword !== 'if' && e.keyword !== 'anyOf' && e.keyword !== 'oneOf')
|
|
107
|
+
: errors
|
|
108
|
+
|
|
109
|
+
return filtered.map(err => this._formatOne(err, msgs, locale))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Format a single error entry into a ValidationErrorItem.
|
|
114
|
+
*/
|
|
115
|
+
private _formatOne(
|
|
116
|
+
err: AjvRawError,
|
|
117
|
+
messages: ErrorMessages,
|
|
118
|
+
_locale?: string
|
|
119
|
+
): ValidationErrorItem {
|
|
120
|
+
const keyword = err.keyword ?? 'validation'
|
|
121
|
+
const instancePath = err.instancePath ?? ''
|
|
122
|
+
const params = err.params ?? {} as Record<string, unknown>
|
|
123
|
+
|
|
124
|
+
// Field path calculation (required errors get special handling)
|
|
125
|
+
let fieldName: string
|
|
126
|
+
if (keyword === 'required' && params['missingProperty']) {
|
|
127
|
+
const parentPath = instancePath.replace(/^\//, '')
|
|
128
|
+
const missing = String(params['missingProperty'])
|
|
129
|
+
fieldName = parentPath ? `${parentPath}/${missing}` : missing
|
|
130
|
+
} else {
|
|
131
|
+
fieldName = instancePath.replace(/^\//, '') || 'value'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Label resolution
|
|
135
|
+
const schema = (err.parentSchema ?? {}) as Record<string, unknown>
|
|
136
|
+
let label: string | undefined
|
|
137
|
+
|
|
138
|
+
// For required errors, get label from the specific property schema
|
|
139
|
+
if (keyword === 'required' && params['missingProperty']) {
|
|
140
|
+
const missingProp = String(params['missingProperty'])
|
|
141
|
+
const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined
|
|
142
|
+
if (properties && properties[missingProp]) {
|
|
143
|
+
label = properties[missingProp]['_label'] as string | undefined
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Fallback to parent schema label
|
|
148
|
+
if (!label) {
|
|
149
|
+
label = schema['_label'] as string | undefined
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If _label is set, try to translate it as a locale key reference
|
|
153
|
+
if (label) {
|
|
154
|
+
label = (messages[label] != null ? String(messages[label]) : undefined) ?? label
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!label) {
|
|
158
|
+
let labelKey: string
|
|
159
|
+
if (keyword === 'required' && params['missingProperty']) {
|
|
160
|
+
labelKey = String(params['missingProperty'])
|
|
161
|
+
} else {
|
|
162
|
+
const parts = fieldName.split('/')
|
|
163
|
+
labelKey = parts[parts.length - 1] ?? fieldName
|
|
164
|
+
}
|
|
165
|
+
const autoKey = `label.${labelKey.replace(/\//g, '.')}`
|
|
166
|
+
label = messages[autoKey] ?? labelKey
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Schema-level custom messages
|
|
170
|
+
let schemaCustomMessages = (schema['_customMessages'] ?? {}) as ErrorMessages
|
|
171
|
+
|
|
172
|
+
// For required errors, also check field-level custom messages
|
|
173
|
+
if (keyword === 'required' && params['missingProperty']) {
|
|
174
|
+
const missingProp = String(params['missingProperty'])
|
|
175
|
+
const properties = schema['properties'] as Record<string, Record<string, unknown>> | undefined
|
|
176
|
+
if (properties && properties[missingProp] && properties[missingProp]['_customMessages']) {
|
|
177
|
+
schemaCustomMessages = { ...schemaCustomMessages, ...(properties[missingProp]['_customMessages'] as ErrorMessages) }
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Performance: reuse `messages` directly when schemaCustomMessages is empty (99 % of calls)
|
|
182
|
+
const hasCustomMessages = Object.keys(schemaCustomMessages).length > 0
|
|
183
|
+
const mergedMessages = hasCustomMessages ? { ...messages, ...schemaCustomMessages } : messages
|
|
184
|
+
const mappedKeyword = KEYWORD_MAP[keyword] ?? keyword
|
|
185
|
+
const schemaType = typeof schema['type'] === 'string' ? schema['type'] : 'string'
|
|
186
|
+
|
|
187
|
+
// Message lookup order: schema custom > type+keyword > keyword > fallback
|
|
188
|
+
let message: string | undefined = hasCustomMessages
|
|
189
|
+
? (schemaCustomMessages[keyword] ?? schemaCustomMessages[mappedKeyword])
|
|
190
|
+
: undefined
|
|
191
|
+
|
|
192
|
+
if (message) {
|
|
193
|
+
// May be a key reference — try to resolve from mergedMessages
|
|
194
|
+
message = mergedMessages[message] ?? message
|
|
195
|
+
} else {
|
|
196
|
+
// Special handling for format.email etc.
|
|
197
|
+
if (mappedKeyword === 'format' && params['format']) {
|
|
198
|
+
let fmt = String(params['format'])
|
|
199
|
+
if (fmt === 'uri') fmt = 'url'
|
|
200
|
+
message = mergedMessages[`format.${fmt}`]
|
|
201
|
+
}
|
|
202
|
+
message ??=
|
|
203
|
+
mergedMessages[`${schemaType}.${keyword}`] ??
|
|
204
|
+
mergedMessages[`${schemaType}.${mappedKeyword}`] ??
|
|
205
|
+
mergedMessages[mappedKeyword] ??
|
|
206
|
+
mergedMessages[keyword] ??
|
|
207
|
+
mergedMessages['default'] ??
|
|
208
|
+
err.message ??
|
|
209
|
+
'Validation error'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Interpolation params: spread AJV params first, then override fixed keys
|
|
213
|
+
const limit = params['limit'] ?? params['limitLength'] ?? params['comparison'] ?? ''
|
|
214
|
+
const allowedVals = Array.isArray(params['allowedValues'])
|
|
215
|
+
? (params['allowedValues'] as unknown[]).join(', ')
|
|
216
|
+
: undefined
|
|
217
|
+
const interpolateData: Record<string, unknown> = {
|
|
218
|
+
...params,
|
|
219
|
+
path: label,
|
|
220
|
+
label,
|
|
221
|
+
value: err.data !== undefined ? err.data : '',
|
|
222
|
+
limit,
|
|
223
|
+
min: limit,
|
|
224
|
+
max: limit,
|
|
225
|
+
expected: params['type'],
|
|
226
|
+
actual:
|
|
227
|
+
err.data === null
|
|
228
|
+
? 'null'
|
|
229
|
+
: err.data === undefined
|
|
230
|
+
? 'undefined'
|
|
231
|
+
: Array.isArray(err.data)
|
|
232
|
+
? 'array'
|
|
233
|
+
: typeof err.data,
|
|
234
|
+
valids: allowedVals,
|
|
235
|
+
allowed: allowedVals,
|
|
236
|
+
key: params['additionalProperty'],
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const rendered = renderTemplate(message, interpolateData)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
path: fieldName,
|
|
243
|
+
message: rendered,
|
|
244
|
+
keyword,
|
|
245
|
+
params,
|
|
246
|
+
field: fieldName,
|
|
247
|
+
type: keyword,
|
|
248
|
+
expected: params['type'] !== undefined ? String(params['type']) : undefined,
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
setLocale(locale: string): void {
|
|
253
|
+
this._locale = locale
|
|
254
|
+
const rawLocaleMessages = getMessages(locale)
|
|
255
|
+
const localeMessages: ErrorMessages = Object.fromEntries(
|
|
256
|
+
Object.entries(rawLocaleMessages).map(([k, v]: [string, LocaleMessage]) => [
|
|
257
|
+
k,
|
|
258
|
+
typeof v === 'string' ? v : (v as { message: string }).message,
|
|
259
|
+
])
|
|
260
|
+
)
|
|
261
|
+
this.messages = { ...localeMessages, ...this._constructorCustomMessages }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
addMessage(type: string, template: string): void {
|
|
265
|
+
this.messages[type] = template
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
addMessages(messages: ErrorMessages): void {
|
|
269
|
+
Object.assign(this.messages, messages)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
+
import { Validator } from './Validator.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JSONSchemaCore — v1 compatibility facade.
|
|
6
|
+
*
|
|
7
|
+
* The v2 internals have been split into DslParser / SchemaCompiler / Validator; this class
|
|
8
|
+
* restores the commonly-used chainable entry points from the v1 public API so that users
|
|
9
|
+
* who import from the main entry point do not encounter errors.
|
|
10
|
+
*/
|
|
11
|
+
export class JSONSchemaCore {
|
|
12
|
+
schema: JSONSchema
|
|
13
|
+
|
|
14
|
+
constructor(schema: JSONSchema = {}) {
|
|
15
|
+
this.schema = { ...schema }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type(typeName: string): this {
|
|
19
|
+
this.schema.type = typeName
|
|
20
|
+
return this
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
property(name: string, schema: JSONSchema): this {
|
|
24
|
+
if (!this.schema.properties) this.schema.properties = {}
|
|
25
|
+
this.schema.properties[name] = schema
|
|
26
|
+
return this
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
properties(properties: Record<string, JSONSchema>): this {
|
|
30
|
+
this.schema.properties = { ...(this.schema.properties ?? {}), ...properties }
|
|
31
|
+
return this
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
required(fields: string[] | string): this {
|
|
35
|
+
this.schema.required = Array.isArray(fields) ? fields : [fields]
|
|
36
|
+
return this
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
format(formatName: string): this {
|
|
40
|
+
this.schema.format = formatName
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
pattern(pattern: RegExp | string): this {
|
|
45
|
+
this.schema.pattern = pattern instanceof RegExp ? pattern.source : pattern
|
|
46
|
+
return this
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
items(schema: JSONSchema): this {
|
|
50
|
+
this.schema.items = schema
|
|
51
|
+
return this
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
toSchema(): JSONSchema {
|
|
55
|
+
return this.schema
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getSchema(): JSONSchema {
|
|
59
|
+
return this.toSchema()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
validate(data: unknown): ReturnType<Validator['validate']> {
|
|
63
|
+
return new Validator().validate(this.schema, data)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { LocaleKey, LocaleMessage } from '../locales/types.js'
|
|
2
|
+
import { getMessage, getMessages, isSupportedLocale, getSupportedLocales } from '../locales/index.js'
|
|
3
|
+
|
|
4
|
+
export interface LocaleResolvedMessage {
|
|
5
|
+
code: string | number
|
|
6
|
+
message: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Locale — global locale manager (static class).
|
|
11
|
+
*
|
|
12
|
+
* v1 compatibility semantics:
|
|
13
|
+
* - getMessage() → returns { code, message } for every resolved message
|
|
14
|
+
* - getMessageText() → always returns final message text (used internally by v2)
|
|
15
|
+
* - getMessageConfig() → returns raw LocaleMessage (may contain code object; used by I18nError)
|
|
16
|
+
*/
|
|
17
|
+
export const DEFAULT_LOCALE = 'en-US'
|
|
18
|
+
|
|
19
|
+
export class Locale {
|
|
20
|
+
private static _currentLocale: string = DEFAULT_LOCALE
|
|
21
|
+
private static _customMessages: Record<string, LocaleMessage> = {}
|
|
22
|
+
|
|
23
|
+
/** v1 compat: expose custom messages */
|
|
24
|
+
static get customMessages(): Record<string, LocaleMessage> {
|
|
25
|
+
return this._customMessages
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** v1 compat: expose all locales as { locale: messages } map */
|
|
29
|
+
static get locales(): Record<string, Record<string, LocaleMessage>> {
|
|
30
|
+
const result: Record<string, Record<string, LocaleMessage>> = {}
|
|
31
|
+
// Built-in locales
|
|
32
|
+
for (const locale of getSupportedLocales()) {
|
|
33
|
+
result[locale] = getMessages(locale) as Record<string, LocaleMessage>
|
|
34
|
+
}
|
|
35
|
+
// Custom locales added via addLocale
|
|
36
|
+
for (const key of Object.keys(this._customMessages)) {
|
|
37
|
+
if (key.includes(':')) {
|
|
38
|
+
const colonIdx = key.indexOf(':')
|
|
39
|
+
const locale = key.substring(0, colonIdx)
|
|
40
|
+
const msgKey = key.substring(colonIdx + 1)
|
|
41
|
+
if (!result[locale]) result[locale] = {}
|
|
42
|
+
result[locale][msgKey] = this._customMessages[key]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Locale Switching ─────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
static setLocale(locale: string): void {
|
|
51
|
+
this._currentLocale = locale
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static getLocale(): string {
|
|
55
|
+
return this._currentLocale
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Custom Messages (global override) ───────────────────────────────────
|
|
59
|
+
|
|
60
|
+
static setMessages(messages: Record<string, LocaleMessage>): void {
|
|
61
|
+
this._customMessages = { ...this._customMessages, ...messages }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static addLocale(locale: string, messages: Record<string, LocaleMessage>): void {
|
|
65
|
+
// Dynamically add a locale pack at runtime (merged into existing entries).
|
|
66
|
+
// Records into customMessages and takes priority during lookup.
|
|
67
|
+
for (const [k, v] of Object.entries(messages)) {
|
|
68
|
+
this._customMessages[`${locale}:${k}`] = v
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static getAvailableLocales(): string[] {
|
|
73
|
+
return getSupportedLocales()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static isSupportedLocale(locale: string): boolean {
|
|
77
|
+
return isSupportedLocale(locale)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Core Query Methods ───────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a resolved message (v1 compat: returns { code, message } on hit).
|
|
84
|
+
*
|
|
85
|
+
* Priority: custom messages > locale pack > key itself.
|
|
86
|
+
*/
|
|
87
|
+
static getMessage(
|
|
88
|
+
type: string,
|
|
89
|
+
customMessages: Record<string, LocaleMessage> = {},
|
|
90
|
+
locale: string | null = null
|
|
91
|
+
): LocaleResolvedMessage | string {
|
|
92
|
+
const resolved = this._resolveMessage(type, customMessages, locale)
|
|
93
|
+
if (!resolved) return type
|
|
94
|
+
return this._normalizeResolvedMessage(type, resolved)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the final message text (used internally by v2 to avoid "[object Object]" in message field).
|
|
99
|
+
*/
|
|
100
|
+
static getMessageText(
|
|
101
|
+
type: string,
|
|
102
|
+
customMessages: Record<string, LocaleMessage> = {},
|
|
103
|
+
locale: string | null = null
|
|
104
|
+
): string {
|
|
105
|
+
const resolved = this.getMessage(type, customMessages, locale)
|
|
106
|
+
return typeof resolved === 'string' ? resolved : resolved.message
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get raw message config (used by I18nError; may include a numeric code).
|
|
111
|
+
*/
|
|
112
|
+
static getMessageConfig(
|
|
113
|
+
type: string,
|
|
114
|
+
customMessages: Record<string, LocaleMessage> = {},
|
|
115
|
+
locale: string | null = null
|
|
116
|
+
): LocaleMessage {
|
|
117
|
+
return this._resolveMessage(type, customMessages, locale) ?? { code: type, message: type }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get the full message table for the given locale (built-in + custom).
|
|
122
|
+
*/
|
|
123
|
+
static getMessages(locale?: string): Record<string, LocaleMessage> {
|
|
124
|
+
const targetLocale = locale ?? this._currentLocale
|
|
125
|
+
const builtinMessages = getMessages(targetLocale) as Record<string, LocaleMessage>
|
|
126
|
+
// Merge custom messages added via addLocale/setMessages for this locale
|
|
127
|
+
const customForLocale: Record<string, LocaleMessage> = {}
|
|
128
|
+
for (const [k, v] of Object.entries(this._customMessages)) {
|
|
129
|
+
if (k.startsWith(`${targetLocale}:`)) {
|
|
130
|
+
customForLocale[k.slice(targetLocale.length + 1)] = v
|
|
131
|
+
} else if (!k.includes(':')) {
|
|
132
|
+
// Global custom messages (no locale prefix)
|
|
133
|
+
customForLocale[k] = v
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { ...builtinMessages, ...customForLocale }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Reset to defaults (for testing).
|
|
141
|
+
*/
|
|
142
|
+
static reset(): void {
|
|
143
|
+
this._currentLocale = DEFAULT_LOCALE
|
|
144
|
+
this._customMessages = {}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Private Helpers ──────────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
private static _normalizeResolvedMessage(type: string, msg: LocaleMessage): LocaleResolvedMessage {
|
|
150
|
+
if (typeof msg === 'string') {
|
|
151
|
+
return { code: type, message: msg }
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
code: msg.code ?? type,
|
|
155
|
+
message: msg.message,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private static _resolveMessage(
|
|
160
|
+
type: string,
|
|
161
|
+
customMessages: Record<string, LocaleMessage>,
|
|
162
|
+
locale: string | null
|
|
163
|
+
): LocaleMessage | null {
|
|
164
|
+
const targetLocale = locale ?? this._currentLocale
|
|
165
|
+
|
|
166
|
+
const callerMsg = customMessages[type]
|
|
167
|
+
if (callerMsg !== undefined) return callerMsg
|
|
168
|
+
|
|
169
|
+
const globalMsg = this._customMessages[type]
|
|
170
|
+
if (globalMsg !== undefined) return globalMsg
|
|
171
|
+
|
|
172
|
+
const globalLocaleMsg = this._customMessages[`${targetLocale}:${type}`]
|
|
173
|
+
if (globalLocaleMsg !== undefined) return globalLocaleMsg
|
|
174
|
+
|
|
175
|
+
if (this._isLocaleKey(type)) {
|
|
176
|
+
return getMessage(type as LocaleKey, targetLocale)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private static _isLocaleKey(key: string): boolean {
|
|
183
|
+
// All predefined locale keys are defined in language pack files
|
|
184
|
+
const msgs = getMessages()
|
|
185
|
+
return key in msgs
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { renderTemplate } from './TemplateEngine.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MessageTemplate — wraps a template string for rendering.
|
|
5
|
+
* Delegates to TemplateEngine.renderTemplate() (fix CORE-03).
|
|
6
|
+
* Maintains full v1 API compatibility (constructor + render + static render + static renderBatch).
|
|
7
|
+
*/
|
|
8
|
+
export class MessageTemplate {
|
|
9
|
+
private readonly template: string
|
|
10
|
+
|
|
11
|
+
constructor(template: string) {
|
|
12
|
+
this.template = template
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render the template with the given context.
|
|
17
|
+
*/
|
|
18
|
+
render(context: Record<string, unknown> = {}): string {
|
|
19
|
+
return renderTemplate(this.template, context)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Statically render a template string with the given context.
|
|
24
|
+
*/
|
|
25
|
+
static render(template: string, context: Record<string, unknown> = {}): string {
|
|
26
|
+
return renderTemplate(template, context)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Statically render multiple templates in batch.
|
|
31
|
+
*/
|
|
32
|
+
static renderBatch(
|
|
33
|
+
templates: Record<string, string>,
|
|
34
|
+
context: Record<string, unknown> = {}
|
|
35
|
+
): Record<string, string> {
|
|
36
|
+
const result: Record<string, string> = {}
|
|
37
|
+
for (const [key, tmpl] of Object.entries(templates)) {
|
|
38
|
+
result[key] = renderTemplate(tmpl, context)
|
|
39
|
+
}
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectDslBuilder — v1 compat: dsl(object) returns a chainable builder.
|
|
3
|
+
*
|
|
4
|
+
* BC-2 fix: in v1, the schema returned by dsl({...}) / parseObject() supported .strict() /
|
|
5
|
+
* .requireAll() and other chain decorators, and implemented the toSchema() duck-type interface
|
|
6
|
+
* (Validator passes through internal schema). After v2 refactor, parseObject() returned plain
|
|
7
|
+
* JSONSchema, losing all chain API. This class wraps the DslParser.parseObject() result and
|
|
8
|
+
* exposes v1-equivalent chain methods:
|
|
9
|
+
* - toSchema() — return internal JSONSchema (Validator duck-type entry point)
|
|
10
|
+
* - toJsonSchema() — return clean JSON Schema (internal schema-dsl keywords stripped)
|
|
11
|
+
* - strict() — disallow extra properties
|
|
12
|
+
* - requireAll() — require all defined properties to be present
|
|
13
|
+
* - toString() — serialize to JSON string
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
17
|
+
import { TypeRegistry } from '../parser/TypeRegistry.js'
|
|
18
|
+
|
|
19
|
+
export class ObjectDslBuilder {
|
|
20
|
+
readonly _isDslBuilder = true as const
|
|
21
|
+
readonly _isObjectDsl = true as const
|
|
22
|
+
|
|
23
|
+
private _schema: JSONSchema
|
|
24
|
+
|
|
25
|
+
constructor(schema: JSONSchema) {
|
|
26
|
+
this._schema = schema
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ==================== Output Methods ====================
|
|
30
|
+
|
|
31
|
+
/** Return internal JSONSchema (Validator.validate() duck-type entry point). */
|
|
32
|
+
toSchema(): JSONSchema {
|
|
33
|
+
return this._schema
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Return clean JSON Schema (internal schema-dsl keywords stripped; safe for serialization or external tools). */
|
|
37
|
+
toJsonSchema(): JSONSchema {
|
|
38
|
+
return TypeRegistry.toJsonSchema(this._schema)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
toString(): string {
|
|
42
|
+
return JSON.stringify(this.toJsonSchema())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ==================== Chain Decorator Methods ====================
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* strict() — disallow extra properties (v1 compat).
|
|
49
|
+
* Equivalent to setting strictSchema: true on the compiled schema.
|
|
50
|
+
*/
|
|
51
|
+
strict(): this {
|
|
52
|
+
; (this._schema as Record<string, unknown>)['strictSchema'] = true
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* requireAll() — require all defined properties to be present (v1 compat).
|
|
58
|
+
* Equivalent to setting requiredAll: true on the compiled schema.
|
|
59
|
+
*/
|
|
60
|
+
requireAll(): this {
|
|
61
|
+
; (this._schema as Record<string, unknown>)['requiredAll'] = true
|
|
62
|
+
return this
|
|
63
|
+
}
|
|
64
|
+
}
|