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,66 +1,66 @@
|
|
|
1
|
-
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
-
import type { TypeDefinition } from './TypeRegistry.js'
|
|
3
|
-
|
|
4
|
-
/** Callback type for the afterCompile hook (synchronous only). */
|
|
5
|
-
export type AfterCompileHook = (schema: JSONSchema) => void
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* SchemaCompiler — assembles TypeDefinition + constraints into the final JSONSchema.
|
|
9
|
-
*
|
|
10
|
-
* Responsibilities:
|
|
11
|
-
* 1. Merge baseSchema + constraints (constraints take priority)
|
|
12
|
-
* 2. Inject _label / _customMessages / _required (from TypeDefinition or caller)
|
|
13
|
-
* 3. Fire afterCompile hooks
|
|
14
|
-
*/
|
|
15
|
-
export const SchemaCompiler = {
|
|
16
|
-
/**
|
|
17
|
-
* Compile and return a JSONSchema (including internal keys; stripped by toJsonSchema).
|
|
18
|
-
*/
|
|
19
|
-
compile(
|
|
20
|
-
typeDef: TypeDefinition,
|
|
21
|
-
constraints: Partial<JSONSchema>,
|
|
22
|
-
meta?: {
|
|
23
|
-
label?: string
|
|
24
|
-
required?: boolean
|
|
25
|
-
afterCompileHook?: AfterCompileHook
|
|
26
|
-
}
|
|
27
|
-
): JSONSchema {
|
|
28
|
-
// Merge baseSchema + constraints (constraints fields override baseSchema fields)
|
|
29
|
-
const schema: JSONSchema = {
|
|
30
|
-
...typeDef.baseSchema,
|
|
31
|
-
...constraints,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Inject type-level _customMessages (e.g., phone, idCard types)
|
|
35
|
-
if (typeDef.customMessages) {
|
|
36
|
-
schema['_customMessages'] = {
|
|
37
|
-
...(schema['_customMessages'] as Record<string, string> | undefined),
|
|
38
|
-
...typeDef.customMessages,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Inject meta
|
|
43
|
-
if (meta?.label) schema['_label'] = meta.label
|
|
44
|
-
if (meta?.required) schema['_required'] = true
|
|
45
|
-
|
|
46
|
-
// Fire afterCompile hook (synchronous)
|
|
47
|
-
if (meta?.afterCompileHook) {
|
|
48
|
-
meta.afterCompileHook(schema)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return schema
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Strip internal keys and return a clean JSON Schema (used by toJsonSchema() output).
|
|
56
|
-
*/
|
|
57
|
-
toJsonSchema(schema: JSONSchema, internalKeys: ReadonlySet<string>): JSONSchema {
|
|
58
|
-
const result: JSONSchema = {}
|
|
59
|
-
for (const [k, v] of Object.entries(schema)) {
|
|
60
|
-
if (!internalKeys.has(k)) {
|
|
61
|
-
result[k] = v
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
return result
|
|
65
|
-
},
|
|
66
|
-
}
|
|
1
|
+
import type { JSONSchema } from '../types/schema.js'
|
|
2
|
+
import type { TypeDefinition } from './TypeRegistry.js'
|
|
3
|
+
|
|
4
|
+
/** Callback type for the afterCompile hook (synchronous only). */
|
|
5
|
+
export type AfterCompileHook = (schema: JSONSchema) => void
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SchemaCompiler — assembles TypeDefinition + constraints into the final JSONSchema.
|
|
9
|
+
*
|
|
10
|
+
* Responsibilities:
|
|
11
|
+
* 1. Merge baseSchema + constraints (constraints take priority)
|
|
12
|
+
* 2. Inject _label / _customMessages / _required (from TypeDefinition or caller)
|
|
13
|
+
* 3. Fire afterCompile hooks
|
|
14
|
+
*/
|
|
15
|
+
export const SchemaCompiler = {
|
|
16
|
+
/**
|
|
17
|
+
* Compile and return a JSONSchema (including internal keys; stripped by toJsonSchema).
|
|
18
|
+
*/
|
|
19
|
+
compile(
|
|
20
|
+
typeDef: TypeDefinition,
|
|
21
|
+
constraints: Partial<JSONSchema>,
|
|
22
|
+
meta?: {
|
|
23
|
+
label?: string
|
|
24
|
+
required?: boolean
|
|
25
|
+
afterCompileHook?: AfterCompileHook
|
|
26
|
+
}
|
|
27
|
+
): JSONSchema {
|
|
28
|
+
// Merge baseSchema + constraints (constraints fields override baseSchema fields)
|
|
29
|
+
const schema: JSONSchema = {
|
|
30
|
+
...typeDef.baseSchema,
|
|
31
|
+
...constraints,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Inject type-level _customMessages (e.g., phone, idCard types)
|
|
35
|
+
if (typeDef.customMessages) {
|
|
36
|
+
schema['_customMessages'] = {
|
|
37
|
+
...(schema['_customMessages'] as Record<string, string> | undefined),
|
|
38
|
+
...typeDef.customMessages,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Inject meta
|
|
43
|
+
if (meta?.label) schema['_label'] = meta.label
|
|
44
|
+
if (meta?.required) schema['_required'] = true
|
|
45
|
+
|
|
46
|
+
// Fire afterCompile hook (synchronous)
|
|
47
|
+
if (meta?.afterCompileHook) {
|
|
48
|
+
meta.afterCompileHook(schema)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return schema
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Strip internal keys and return a clean JSON Schema (used by toJsonSchema() output).
|
|
56
|
+
*/
|
|
57
|
+
toJsonSchema(schema: JSONSchema, internalKeys: ReadonlySet<string>): JSONSchema {
|
|
58
|
+
const result: JSONSchema = {}
|
|
59
|
+
for (const [k, v] of Object.entries(schema)) {
|
|
60
|
+
if (!internalKeys.has(k)) {
|
|
61
|
+
result[k] = v
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
},
|
|
66
|
+
}
|
|
@@ -1,250 +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
|
|
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
|
package/src/parser/index.ts
CHANGED
|
@@ -1,6 +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'
|
|
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'
|