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,250 @@
|
|
|
1
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type definition (structure of each entry in TypeRegistry)
|
|
5
|
+
*/
|
|
6
|
+
export interface TypeDefinition {
|
|
7
|
+
/** Base JSON Schema fragment for this type */
|
|
8
|
+
baseSchema: Partial<JSONSchema>
|
|
9
|
+
/** Custom messages associated with this type (e.g., error key for phone type) */
|
|
10
|
+
customMessages?: Record<string, string>
|
|
11
|
+
/** Whether this type uses a pattern (keys are standard JSON Schema fields, not stripped in toJsonSchema) */
|
|
12
|
+
isPattern?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Known internal key set (stripped from output during toJsonSchema) */
|
|
16
|
+
const INTERNAL_KEYS: ReadonlySet<string> = new Set([
|
|
17
|
+
'_label',
|
|
18
|
+
'_customMessages',
|
|
19
|
+
'_description',
|
|
20
|
+
'_required',
|
|
21
|
+
'_isConditional',
|
|
22
|
+
'_runtimeOnlyConditional',
|
|
23
|
+
'conditions',
|
|
24
|
+
'_evaluateCondition',
|
|
25
|
+
// Custom AJV keywords (non-standard JSON Schema fields, stripped on output)
|
|
26
|
+
'exactLength',
|
|
27
|
+
'alphanum',
|
|
28
|
+
'lowercase',
|
|
29
|
+
'uppercase',
|
|
30
|
+
'trim',
|
|
31
|
+
'jsonString',
|
|
32
|
+
'port',
|
|
33
|
+
'requiredAll',
|
|
34
|
+
'strictSchema',
|
|
35
|
+
'noSparse',
|
|
36
|
+
'includesRequired',
|
|
37
|
+
'dateFormat',
|
|
38
|
+
'dateGreater',
|
|
39
|
+
'dateLess',
|
|
40
|
+
'precision',
|
|
41
|
+
// ⚠️ multipleOf is a standard JSON Schema field and is NOT in this list (fix DB-01)
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Built-in type registry
|
|
46
|
+
* 33 types covering v1 DslAdapter.typeMap + DslBuilder type lists (fixes inconsistencies DB-02, DA-01)
|
|
47
|
+
*/
|
|
48
|
+
const BUILTIN_TYPES: Map<string, TypeDefinition> = new Map([
|
|
49
|
+
// --- Primitive types ---
|
|
50
|
+
['string', { baseSchema: { type: 'string' } }],
|
|
51
|
+
['number', { baseSchema: { type: 'number' } }],
|
|
52
|
+
['integer', { baseSchema: { type: 'integer' } }],
|
|
53
|
+
['boolean', { baseSchema: { type: 'boolean' } }],
|
|
54
|
+
['object', { baseSchema: { type: 'object' } }],
|
|
55
|
+
['array', { baseSchema: { type: 'array' } }],
|
|
56
|
+
['null', { baseSchema: { type: 'null' } }],
|
|
57
|
+
['any', { baseSchema: {} }],
|
|
58
|
+
|
|
59
|
+
// --- Format types ---
|
|
60
|
+
['email', { baseSchema: { type: 'string', format: 'email' } }],
|
|
61
|
+
['url', { baseSchema: { type: 'string', format: 'uri' } }],
|
|
62
|
+
['uri', { baseSchema: { type: 'string', format: 'uri' } }],
|
|
63
|
+
['uuid', { baseSchema: { type: 'string', format: 'uuid' } }],
|
|
64
|
+
['ipv4', { baseSchema: { type: 'string', format: 'ipv4' } }],
|
|
65
|
+
['ipv6', { baseSchema: { type: 'string', format: 'ipv6' } }],
|
|
66
|
+
['ip', { baseSchema: { anyOf: [{ type: 'string', format: 'ipv4' }, { type: 'string', format: 'ipv6' }] } }],
|
|
67
|
+
['hostname', { baseSchema: { type: 'string', format: 'hostname' } }],
|
|
68
|
+
['date', { baseSchema: { type: 'string', format: 'date' } }],
|
|
69
|
+
['datetime', { baseSchema: { type: 'string', format: 'date-time' } }],
|
|
70
|
+
['time', { baseSchema: { type: 'string', format: 'time' } }],
|
|
71
|
+
|
|
72
|
+
// --- Special string types ---
|
|
73
|
+
['binary', { baseSchema: { type: 'string', contentEncoding: 'base64' } }],
|
|
74
|
+
['objectId', { baseSchema: { type: 'string', pattern: '^[0-9a-fA-F]{24}$' }, customMessages: { pattern: 'pattern.objectId' } }],
|
|
75
|
+
['hexColor', { baseSchema: { type: 'string', pattern: '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$' }, customMessages: { pattern: 'pattern.hexColor' } }],
|
|
76
|
+
['macAddress', {
|
|
77
|
+
baseSchema: { type: 'string', pattern: '^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$' },
|
|
78
|
+
customMessages: { pattern: 'pattern.macAddress' },
|
|
79
|
+
}],
|
|
80
|
+
['cron', {
|
|
81
|
+
baseSchema: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
pattern:
|
|
84
|
+
'^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) ' +
|
|
85
|
+
'(\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) ' +
|
|
86
|
+
'(\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) ' +
|
|
87
|
+
'(\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) ' +
|
|
88
|
+
'(\\*|([0-6])|\\*\\/([0-6]))$',
|
|
89
|
+
},
|
|
90
|
+
customMessages: { pattern: 'pattern.cron' },
|
|
91
|
+
}],
|
|
92
|
+
|
|
93
|
+
// --- slug (fix DB-02: v1 DslAdapter was missing slug type definition) ---
|
|
94
|
+
['slug', {
|
|
95
|
+
baseSchema: { type: 'string', pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$' },
|
|
96
|
+
customMessages: { pattern: 'pattern.slug' },
|
|
97
|
+
}],
|
|
98
|
+
|
|
99
|
+
// --- CJK / Chinese ---
|
|
100
|
+
['chineseName', {
|
|
101
|
+
baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]{2,10}$' },
|
|
102
|
+
customMessages: { pattern: 'chineseName' },
|
|
103
|
+
}],
|
|
104
|
+
['chinese', {
|
|
105
|
+
baseSchema: { type: 'string', pattern: '^[\\u4e00-\\u9fa5]+$' },
|
|
106
|
+
}],
|
|
107
|
+
|
|
108
|
+
// --- Domain-related (handled by CustomKeywords; only baseSchema registered here) ---
|
|
109
|
+
['emailDomain', { baseSchema: { type: 'string', format: 'email' } }],
|
|
110
|
+
|
|
111
|
+
// --- v1 extension types (DslBuilder v1.0.2) ---
|
|
112
|
+
['alphanum', { baseSchema: { type: 'string', alphanum: true } }],
|
|
113
|
+
['lower', { baseSchema: { type: 'string', lowercase: true } }],
|
|
114
|
+
['upper', { baseSchema: { type: 'string', uppercase: true } }],
|
|
115
|
+
['json', { baseSchema: { type: 'string', jsonString: true } }],
|
|
116
|
+
['port', { baseSchema: { type: 'integer', port: true } }],
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Custom type registry (populated via registerType)
|
|
121
|
+
*/
|
|
122
|
+
const CUSTOM_TYPES: Map<string, TypeDefinition> = new Map()
|
|
123
|
+
const DYNAMIC_TYPES: Map<string, () => JSONSchema> = new Map()
|
|
124
|
+
|
|
125
|
+
let _strictMode = false
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* TypeRegistry — unified type registration and resolution
|
|
129
|
+
*
|
|
130
|
+
* Replaces the three inconsistent type lists in v1 (fixes DB-01/DB-02/DA-01)
|
|
131
|
+
*/
|
|
132
|
+
export const TypeRegistry = {
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a type name to its TypeDefinition.
|
|
135
|
+
* Built-in types take priority; custom types may override non-primitive built-ins.
|
|
136
|
+
*/
|
|
137
|
+
resolve(typeName: string): TypeDefinition {
|
|
138
|
+
// Dynamic types: call factory function each time
|
|
139
|
+
const dynamicFn = DYNAMIC_TYPES.get(typeName)
|
|
140
|
+
if (dynamicFn) {
|
|
141
|
+
return { baseSchema: dynamicFn() as Partial<JSONSchema> }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const custom = CUSTOM_TYPES.get(typeName)
|
|
145
|
+
if (custom) return custom
|
|
146
|
+
|
|
147
|
+
const builtin = BUILTIN_TYPES.get(typeName)
|
|
148
|
+
if (builtin) return builtin
|
|
149
|
+
|
|
150
|
+
// Unknown type: throw in strict mode, otherwise warn and fall back to string
|
|
151
|
+
if (_strictMode) {
|
|
152
|
+
throw new Error(`[schema-dsl] Unknown type "${typeName}"`)
|
|
153
|
+
}
|
|
154
|
+
console.warn(`[schema-dsl] Unknown type "${typeName}", falling back to string`)
|
|
155
|
+
return { baseSchema: { type: 'string' } }
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register a custom type (delegated from DslBuilder.registerType)
|
|
160
|
+
*/
|
|
161
|
+
register(name: string, def: TypeDefinition | Partial<JSONSchema>): void {
|
|
162
|
+
if (!name || typeof name !== 'string') {
|
|
163
|
+
throw new Error('[schema-dsl] TypeRegistry.register: name must be a non-empty string')
|
|
164
|
+
}
|
|
165
|
+
// Accept a raw Partial<JSONSchema> and wrap it automatically
|
|
166
|
+
const normalized: TypeDefinition =
|
|
167
|
+
'baseSchema' in def ? (def as TypeDefinition) : { baseSchema: def as Partial<JSONSchema> }
|
|
168
|
+
CUSTOM_TYPES.set(name, normalized)
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Register a dynamic type (factory function invoked on every resolve call)
|
|
173
|
+
*/
|
|
174
|
+
registerDynamic(name: string, factory: () => JSONSchema): void {
|
|
175
|
+
if (!name || typeof name !== 'string') {
|
|
176
|
+
throw new Error('[schema-dsl] TypeRegistry.registerDynamic: name must be a non-empty string')
|
|
177
|
+
}
|
|
178
|
+
DYNAMIC_TYPES.set(name, factory)
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unregister a custom type
|
|
183
|
+
*/
|
|
184
|
+
unregister(name: string): void {
|
|
185
|
+
CUSTOM_TYPES.delete(name)
|
|
186
|
+
DYNAMIC_TYPES.delete(name)
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Clear all custom and dynamic types (primarily for testing; called by DslBuilder.clearCustomTypes).
|
|
191
|
+
* Built-in types are unaffected.
|
|
192
|
+
*/
|
|
193
|
+
clearCustomTypes(): void {
|
|
194
|
+
CUSTOM_TYPES.clear()
|
|
195
|
+
DYNAMIC_TYPES.clear()
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Enable or disable strict mode for type resolution.
|
|
200
|
+
* In strict mode, resolving an unknown type throws instead of warning and falling back to string.
|
|
201
|
+
*/
|
|
202
|
+
setStrict(flag: boolean): void {
|
|
203
|
+
_strictMode = flag
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check whether a type is registered (built-in or custom)
|
|
208
|
+
*/
|
|
209
|
+
has(typeName: string): boolean {
|
|
210
|
+
return BUILTIN_TYPES.has(typeName) || CUSTOM_TYPES.has(typeName) || DYNAMIC_TYPES.has(typeName)
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Return an iterator over all registered types (built-in + custom; custom overrides same-name built-in)
|
|
215
|
+
* BC-4 compat: consumed by the DslAdapter.typeMap getter
|
|
216
|
+
*/
|
|
217
|
+
entries(): IterableIterator<[string, TypeDefinition]> {
|
|
218
|
+
const merged: Map<string, TypeDefinition> = new Map([...BUILTIN_TYPES, ...CUSTOM_TYPES])
|
|
219
|
+
return merged.entries()
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Return the internal key set (used to strip non-standard fields during toJsonSchema)
|
|
224
|
+
*/
|
|
225
|
+
getInternalKeys(): ReadonlySet<string> {
|
|
226
|
+
return INTERNAL_KEYS
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Strip internal keys from a schema and return a clean JSON Schema.
|
|
231
|
+
*
|
|
232
|
+
* Special case for exactLength: translated to standard minLength + maxLength
|
|
233
|
+
* instead of being stripped. This preserves v1 DslBuilder string:N behavior
|
|
234
|
+
* (output {minLength:N, maxLength:N}) while keeping AJV's exactLength Unicode
|
|
235
|
+
* code-point counting advantage internally (CK-Y04).
|
|
236
|
+
*/
|
|
237
|
+
toJsonSchema(schema: JSONSchema): JSONSchema {
|
|
238
|
+
const result: JSONSchema = {}
|
|
239
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
240
|
+
if (k === 'exactLength' && typeof v === 'number') {
|
|
241
|
+
// BC-compat: exactLength (AJV custom keyword) → standard JSON Schema minLength + maxLength
|
|
242
|
+
result.minLength = v
|
|
243
|
+
result.maxLength = v
|
|
244
|
+
} else if (!INTERNAL_KEYS.has(k)) {
|
|
245
|
+
result[k] = v
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result
|
|
249
|
+
},
|
|
250
|
+
} as const
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { TypeRegistry } from './TypeRegistry.js'
|
|
2
|
+
export type { TypeDefinition } from './TypeRegistry.js'
|
|
3
|
+
export { ConstraintParser } from './ConstraintParser.js'
|
|
4
|
+
export { SchemaCompiler } from './SchemaCompiler.js'
|
|
5
|
+
export type { AfterCompileHook } from './SchemaCompiler.js'
|
|
6
|
+
export { DslParser } from './DslParser.js'
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Plugin } from '../types/plugin.js'
|
|
2
|
+
import { DslBuilder } from '../core/DslBuilder.js'
|
|
3
|
+
import type { Validator } from '../core/Validator.js'
|
|
4
|
+
|
|
5
|
+
interface FormatConfig {
|
|
6
|
+
pattern?: RegExp
|
|
7
|
+
validate?: (value: string) => boolean
|
|
8
|
+
schema: Record<string, unknown>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FORMATS: Record<string, FormatConfig> = {
|
|
12
|
+
'phone-cn': {
|
|
13
|
+
pattern: /^1[3-9]\d{9}$/,
|
|
14
|
+
schema: { type: 'string', pattern: /^1[3-9]\d{9}$/.source, minLength: 11, maxLength: 11 },
|
|
15
|
+
},
|
|
16
|
+
'postal-code-cn': {
|
|
17
|
+
pattern: /^\d{6}$/,
|
|
18
|
+
schema: { type: 'string', pattern: /^\d{6}$/.source, minLength: 6, maxLength: 6 },
|
|
19
|
+
},
|
|
20
|
+
'ipv4-custom': {
|
|
21
|
+
pattern: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
|
|
22
|
+
schema: { type: 'string', format: 'ipv4' },
|
|
23
|
+
},
|
|
24
|
+
'wechat': {
|
|
25
|
+
pattern: /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/,
|
|
26
|
+
schema: { type: 'string', pattern: /^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/.source, minLength: 6, maxLength: 20 },
|
|
27
|
+
},
|
|
28
|
+
'qq': {
|
|
29
|
+
pattern: /^[1-9][0-9]{4,10}$/,
|
|
30
|
+
schema: { type: 'string', pattern: /^[1-9][0-9]{4,10}$/.source, minLength: 5, maxLength: 11 },
|
|
31
|
+
},
|
|
32
|
+
'bank-card': {
|
|
33
|
+
validate: (value: string) => {
|
|
34
|
+
if (!/^\d{16,19}$/.test(value)) return false
|
|
35
|
+
|
|
36
|
+
let sum = 0
|
|
37
|
+
let shouldDouble = false
|
|
38
|
+
|
|
39
|
+
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
40
|
+
let digit = Number.parseInt(value[i], 10)
|
|
41
|
+
|
|
42
|
+
if (shouldDouble) {
|
|
43
|
+
digit *= 2
|
|
44
|
+
if (digit > 9) digit -= 9
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
sum += digit
|
|
48
|
+
shouldDouble = !shouldDouble
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return sum % 10 === 0
|
|
52
|
+
},
|
|
53
|
+
schema: { type: 'string', minLength: 16, maxLength: 19, pattern: /^\d{16,19}$/.source },
|
|
54
|
+
},
|
|
55
|
+
'license-plate': {
|
|
56
|
+
pattern: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/,
|
|
57
|
+
schema: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
pattern: /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/.source,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
'credit-code': {
|
|
63
|
+
pattern: /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/,
|
|
64
|
+
schema: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
pattern: /^[0-9A-HJ-NPQRTUWXY]{2}\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/.source,
|
|
67
|
+
minLength: 18,
|
|
68
|
+
maxLength: 18,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
'passport-cn': {
|
|
72
|
+
pattern: /^[EG]\d{8}$/,
|
|
73
|
+
schema: { type: 'string', pattern: /^[EG]\d{8}$/.source, minLength: 9, maxLength: 9 },
|
|
74
|
+
},
|
|
75
|
+
'hk-macao-pass': {
|
|
76
|
+
pattern: /^[HM]\d{8,10}$/,
|
|
77
|
+
schema: { type: 'string', pattern: /^[HM]\d{8,10}$/.source, minLength: 9, maxLength: 11 },
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getAjvLike(core: unknown): {
|
|
82
|
+
addFormat: (name: string, definition: RegExp | { validate: RegExp | ((value: string) => boolean) }) => void
|
|
83
|
+
} {
|
|
84
|
+
const coreRecord = core as { getDefaultValidator?: () => Validator }
|
|
85
|
+
const validator = coreRecord.getDefaultValidator?.()
|
|
86
|
+
if (!validator) {
|
|
87
|
+
throw new Error('getDefaultValidator() is not available. Please provide schema-dsl core object.')
|
|
88
|
+
}
|
|
89
|
+
return validator.getAjv() as {
|
|
90
|
+
addFormat: (name: string, definition: RegExp | { validate: RegExp | ((value: string) => boolean) }) => void
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getDslBuilderLike(core: unknown): typeof DslBuilder {
|
|
95
|
+
const coreRecord = core as { DslBuilder?: typeof DslBuilder }
|
|
96
|
+
return coreRecord.DslBuilder ?? DslBuilder
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const customFormatPlugin: Plugin & {
|
|
100
|
+
addCustomFormats: (ajv: { addFormat: (name: string, definition: { validate: RegExp | ((value: string) => boolean) }) => void }, dslBuilder: typeof DslBuilder) => void
|
|
101
|
+
} = {
|
|
102
|
+
name: 'custom-format',
|
|
103
|
+
version: '2.0.0',
|
|
104
|
+
description: 'Custom format validation plugin (with DSL type registration)',
|
|
105
|
+
install(core, _options = {}, _context) {
|
|
106
|
+
const ajv = getAjvLike(core)
|
|
107
|
+
const dslBuilder = getDslBuilderLike(core)
|
|
108
|
+
this.addCustomFormats(ajv, dslBuilder)
|
|
109
|
+
console.log('[Plugin] custom-format v2.0.0 installed (with DSL type registration)')
|
|
110
|
+
},
|
|
111
|
+
uninstall() {
|
|
112
|
+
console.log('[Plugin] custom-format uninstalled')
|
|
113
|
+
},
|
|
114
|
+
addCustomFormats(ajv, dslBuilder) {
|
|
115
|
+
for (const [name, config] of Object.entries(FORMATS)) {
|
|
116
|
+
ajv.addFormat(name, {
|
|
117
|
+
validate: config.validate ?? config.pattern!,
|
|
118
|
+
})
|
|
119
|
+
dslBuilder.registerType(name, config.schema)
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default customFormatPlugin
|
|
125
|
+
|
|
126
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Plugin } from '../types/plugin.js'
|
|
2
|
+
import { DslBuilder } from '../core/DslBuilder.js'
|
|
3
|
+
|
|
4
|
+
export const customTypeExamplePlugin: Plugin & {
|
|
5
|
+
registerCustomTypes: (dslBuilder: typeof DslBuilder) => void
|
|
6
|
+
} = {
|
|
7
|
+
name: 'custom-type-example',
|
|
8
|
+
version: '1.0.0',
|
|
9
|
+
description: 'Custom type registration example plugin',
|
|
10
|
+
install(core, _options = {}, _context) {
|
|
11
|
+
const coreRecord = core as { DslBuilder?: typeof DslBuilder }
|
|
12
|
+
const builder = coreRecord.DslBuilder ?? DslBuilder
|
|
13
|
+
|
|
14
|
+
if (!builder || typeof builder.registerType !== 'function') {
|
|
15
|
+
throw new Error('DslBuilder.registerType is not available. Please upgrade to schema-dsl v1.1.0+')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.registerCustomTypes(builder)
|
|
19
|
+
console.log('[Plugin] custom-type-example installed')
|
|
20
|
+
},
|
|
21
|
+
uninstall() {
|
|
22
|
+
console.log('[Plugin] custom-type-example uninstalled')
|
|
23
|
+
},
|
|
24
|
+
registerCustomTypes(dslBuilder) {
|
|
25
|
+
dslBuilder.registerType('order-id', {
|
|
26
|
+
type: 'string',
|
|
27
|
+
pattern: /^ORD[0-9]{12}$/.source,
|
|
28
|
+
minLength: 15,
|
|
29
|
+
maxLength: 15,
|
|
30
|
+
_customMessages: {
|
|
31
|
+
pattern: 'Invalid order ID format; must be 15 characters starting with ORD',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
dslBuilder.registerType('sku', {
|
|
36
|
+
type: 'string',
|
|
37
|
+
pattern: /^SKU-[A-Z0-9]{6,10}$/.source,
|
|
38
|
+
minLength: 10,
|
|
39
|
+
maxLength: 14,
|
|
40
|
+
_customMessages: {
|
|
41
|
+
pattern: 'Invalid SKU format; must be SKU- followed by 6–10 alphanumeric characters',
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
dslBuilder.registerType('price', {
|
|
46
|
+
type: 'number',
|
|
47
|
+
minimum: 0,
|
|
48
|
+
multipleOf: 0.01,
|
|
49
|
+
_customMessages: {
|
|
50
|
+
minimum: 'Price cannot be negative',
|
|
51
|
+
multipleOf: 'Price can have at most 2 decimal places',
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
dslBuilder.registerType('rating', {
|
|
56
|
+
type: 'integer',
|
|
57
|
+
minimum: 1,
|
|
58
|
+
maximum: 5,
|
|
59
|
+
_customMessages: {
|
|
60
|
+
minimum: 'Rating cannot be lower than 1 star',
|
|
61
|
+
maximum: 'Rating cannot exceed 5 stars',
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
dslBuilder.registerType('color-code', {
|
|
66
|
+
oneOf: [
|
|
67
|
+
{ type: 'string', pattern: /^#[0-9A-Fa-f]{6}$/.source },
|
|
68
|
+
{ type: 'string', pattern: /^#[0-9A-Fa-f]{3}$/.source },
|
|
69
|
+
{ type: 'string', pattern: /^rgb\(\d{1,3},\s*\d{1,3},\s*\d{1,3}\)$/.source },
|
|
70
|
+
{ type: 'string', pattern: /^rgba\(\d{1,3},\s*\d{1,3},\s*\d{1,3},\s*(0|1|0?\.\d+)\)$/.source },
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
dslBuilder.registerType('semver', {
|
|
75
|
+
type: 'string',
|
|
76
|
+
pattern: /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.source,
|
|
77
|
+
_customMessages: {
|
|
78
|
+
pattern: 'Invalid version format; must follow semantic versioning (e.g. 1.0.0, 2.1.3-beta.1)',
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
dslBuilder.registerType('dynamic-age', () => {
|
|
83
|
+
const currentYear = new Date().getFullYear()
|
|
84
|
+
return {
|
|
85
|
+
type: 'integer',
|
|
86
|
+
minimum: 0,
|
|
87
|
+
maximum: currentYear - 1900,
|
|
88
|
+
_customMessages: {
|
|
89
|
+
minimum: 'Age cannot be negative',
|
|
90
|
+
maximum: `Age cannot exceed ${currentYear - 1900} years`,
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
dslBuilder.registerType('phone-intl', {
|
|
96
|
+
type: 'string',
|
|
97
|
+
pattern: /^\+?[1-9]\d{1,14}$/.source,
|
|
98
|
+
minLength: 8,
|
|
99
|
+
maxLength: 15,
|
|
100
|
+
_customMessages: {
|
|
101
|
+
pattern: 'Please enter a valid international phone number (E.164 format)',
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default customTypeExamplePlugin
|
|
108
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { KeywordDefinition } from 'ajv'
|
|
2
|
+
import type { Plugin } from '../types/plugin.js'
|
|
3
|
+
import type { Validator } from '../core/Validator.js'
|
|
4
|
+
|
|
5
|
+
function getValidator(core: unknown): Validator {
|
|
6
|
+
const coreRecord = core as { getDefaultValidator?: () => Validator }
|
|
7
|
+
const validator = coreRecord.getDefaultValidator?.()
|
|
8
|
+
if (!validator) {
|
|
9
|
+
throw new Error('getDefaultValidator() is not available. Please provide schema-dsl core object.')
|
|
10
|
+
}
|
|
11
|
+
return validator
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getPluginBucket(): Record<string, unknown> {
|
|
15
|
+
const globalRecord = globalThis as typeof globalThis & {
|
|
16
|
+
__schemaDsl_plugins?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
if (!globalRecord.__schemaDsl_plugins) {
|
|
19
|
+
globalRecord.__schemaDsl_plugins = {}
|
|
20
|
+
}
|
|
21
|
+
return globalRecord.__schemaDsl_plugins
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const customValidatorPlugin: Plugin & {
|
|
25
|
+
addCustomKeywords: (validator: Validator) => void
|
|
26
|
+
_validateIdCardChecksum: (idCard: string) => boolean
|
|
27
|
+
} = {
|
|
28
|
+
name: 'custom-validator',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
description: 'Custom validator plugin — adds business-specific validation rules',
|
|
31
|
+
install(core, _options = {}, _context) {
|
|
32
|
+
const validator = getValidator(core)
|
|
33
|
+
this.addCustomKeywords(validator)
|
|
34
|
+
getPluginBucket()['custom-validator'] = this
|
|
35
|
+
console.log('[Plugin] custom-validator installed')
|
|
36
|
+
},
|
|
37
|
+
uninstall() {
|
|
38
|
+
delete getPluginBucket()['custom-validator']
|
|
39
|
+
console.log('[Plugin] custom-validator uninstalled')
|
|
40
|
+
},
|
|
41
|
+
addCustomKeywords(validator) {
|
|
42
|
+
const ajv = validator.getAjv() as {
|
|
43
|
+
getKeyword?: (name: string) => unknown
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!ajv.getKeyword?.('unique')) {
|
|
47
|
+
validator.addKeyword('unique', {
|
|
48
|
+
async: true,
|
|
49
|
+
type: 'string',
|
|
50
|
+
validate: async function validateUnique(schema: unknown) {
|
|
51
|
+
if (!schema) return true
|
|
52
|
+
const { table, field } = schema as { table?: string; field?: string }
|
|
53
|
+
const exists = false
|
|
54
|
+
if (exists) {
|
|
55
|
+
;(validateUnique as typeof validateUnique & { errors?: unknown[] }).errors = [{
|
|
56
|
+
keyword: 'unique',
|
|
57
|
+
message: `${field} already exists in ${table}`,
|
|
58
|
+
params: { table, field },
|
|
59
|
+
}]
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
return true
|
|
63
|
+
},
|
|
64
|
+
} as unknown as KeywordDefinition)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!ajv.getKeyword?.('passwordStrength')) {
|
|
68
|
+
validator.addKeyword('passwordStrength', {
|
|
69
|
+
type: 'string',
|
|
70
|
+
validate: function validatePasswordStrength(schema: unknown, data: unknown) {
|
|
71
|
+
if (!schema) return true
|
|
72
|
+
|
|
73
|
+
const strength = String(schema)
|
|
74
|
+
const value = String(data ?? '')
|
|
75
|
+
const rules: Record<string, RegExp> = {
|
|
76
|
+
weak: /^.{6,}$/,
|
|
77
|
+
medium: /^(?=.*[a-z])(?=.*[A-Z]).{8,}$/,
|
|
78
|
+
strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{10,}$/,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const pattern = rules[strength]
|
|
82
|
+
if (!pattern) return true
|
|
83
|
+
|
|
84
|
+
if (!pattern.test(value)) {
|
|
85
|
+
;(validatePasswordStrength as typeof validatePasswordStrength & { errors?: unknown[] }).errors = [{
|
|
86
|
+
keyword: 'passwordStrength',
|
|
87
|
+
message: `Password does not meet ${strength} strength requirements`,
|
|
88
|
+
params: { strength },
|
|
89
|
+
}]
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
},
|
|
95
|
+
} as unknown as KeywordDefinition)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!ajv.getKeyword?.('idCard')) {
|
|
99
|
+
validator.addKeyword('idCard', {
|
|
100
|
+
type: 'string',
|
|
101
|
+
validate: (schema: unknown, data: unknown) => {
|
|
102
|
+
if (!schema) return true
|
|
103
|
+
|
|
104
|
+
const value = String(data ?? '')
|
|
105
|
+
const pattern = /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
|
|
106
|
+
|
|
107
|
+
if (!pattern.test(value)) {
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return customValidatorPlugin._validateIdCardChecksum(value)
|
|
112
|
+
},
|
|
113
|
+
} as unknown as KeywordDefinition)
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
_validateIdCardChecksum(idCard: string) {
|
|
117
|
+
const weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
118
|
+
const checksums = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
119
|
+
|
|
120
|
+
let sum = 0
|
|
121
|
+
for (let i = 0; i < 17; i += 1) {
|
|
122
|
+
sum += Number.parseInt(idCard[i], 10) * weights[i]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const checksum = checksums[sum % 11]
|
|
126
|
+
return idCard[17].toUpperCase() === checksum
|
|
127
|
+
},
|
|
128
|
+
hooks: {
|
|
129
|
+
onBeforeValidate() {},
|
|
130
|
+
onAfterValidate() {},
|
|
131
|
+
onError(error) {
|
|
132
|
+
console.error('[custom-validator] Error:', (error as Error).message)
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export default customValidatorPlugin
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IConditionalBuilder interface (shape definition).
|
|
3
|
+
* Implementation class is in src/core/ConditionalBuilder.ts (Phase 8).
|
|
4
|
+
*/
|
|
5
|
+
import type { JSONSchema } from './schema.js'
|
|
6
|
+
import type { ValidationResult } from './validate.js'
|
|
7
|
+
|
|
8
|
+
export interface IConditionalBuilder {
|
|
9
|
+
and(condition: (data: unknown) => boolean): this
|
|
10
|
+
or(condition: (data: unknown) => boolean): this
|
|
11
|
+
/** then/else accept string DSL, JSONSchema, or null (no-op). */
|
|
12
|
+
then(schema: string | JSONSchema | null): this
|
|
13
|
+
else(schema: string | JSONSchema | null): this
|
|
14
|
+
elseIf(condition: (data: unknown) => boolean): this
|
|
15
|
+
message(msg: string): this
|
|
16
|
+
/** Serialise to a JSON Schema object (internal conditional marker). */
|
|
17
|
+
toSchema(): JSONSchema
|
|
18
|
+
/** Alias for toSchema(). */
|
|
19
|
+
build(): JSONSchema
|
|
20
|
+
/** Synchronous assertion; throws ValidationError on failure. */
|
|
21
|
+
assert(data: unknown, options?: Record<string, unknown>): unknown
|
|
22
|
+
/** Synchronous validation; returns ValidationResult. */
|
|
23
|
+
validate(data: unknown, options?: Record<string, unknown>): ValidationResult<unknown>
|
|
24
|
+
/** Async validation; returns Promise<ValidationResult>. */
|
|
25
|
+
validateAsync(data: unknown, options?: Record<string, unknown>): Promise<ValidationResult<unknown>>
|
|
26
|
+
/** Quick boolean check (no error details). */
|
|
27
|
+
check(data: unknown): boolean
|
|
28
|
+
}
|