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,326 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
import type { Plugin, HookName, HookFn } from '../types/plugin.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PluginManager — plugin registration and hook execution.
|
|
6
|
+
*
|
|
7
|
+
* Full v1 compatible API:
|
|
8
|
+
* - hooks : public hooks Map; supports any hook name
|
|
9
|
+
* - EventEmitter compat: on / once / off / emit / removeListener / removeAllListeners
|
|
10
|
+
* - unhook(name, fn): remove a specific hook handler
|
|
11
|
+
* - runHook(name, ...args): pass arbitrary args, collect return values
|
|
12
|
+
* - install(core, name?, opts?): supports named + options install for a single plugin
|
|
13
|
+
* - install / uninstall pass through context
|
|
14
|
+
* - has / get / list / clear / size / uninstall (alias)
|
|
15
|
+
*/
|
|
16
|
+
export class PluginManager extends EventEmitter {
|
|
17
|
+
readonly plugins: Map<string, Plugin> = new Map()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Public hooks Map (v1 compat: pluginManager.hooks.get('hookName')).
|
|
21
|
+
* Supports any string name, not limited to built-in HookName values.
|
|
22
|
+
*/
|
|
23
|
+
readonly hooks: Map<string, Array<HookFn>> = new Map()
|
|
24
|
+
|
|
25
|
+
/** v1 compat context (passed to plugin install / uninstall). */
|
|
26
|
+
readonly context: { plugins: Map<string, Plugin>; hooks: Map<string, Array<HookFn>> } = {
|
|
27
|
+
plugins: this.plugins,
|
|
28
|
+
hooks: this.hooks,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Per-plugin hook references (used for automatic cleanup on unregister). */
|
|
32
|
+
private readonly _pluginHooks: Map<string, Map<string, Set<HookFn>>> = new Map()
|
|
33
|
+
|
|
34
|
+
private _installedCore: unknown = undefined
|
|
35
|
+
|
|
36
|
+
/** Built-in hook names (pre-initialized on construction). */
|
|
37
|
+
private static readonly BUILTIN_HOOKS: ReadonlyArray<HookName> = [
|
|
38
|
+
'beforeParse',
|
|
39
|
+
'afterParse',
|
|
40
|
+
'beforeCompile',
|
|
41
|
+
'afterCompile',
|
|
42
|
+
'beforeValidate',
|
|
43
|
+
'afterValidate',
|
|
44
|
+
'onError',
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
/** Legacy v1 lifecycle hook names. */
|
|
48
|
+
private static readonly LEGACY_HOOKS: ReadonlyArray<string> = [
|
|
49
|
+
'onBeforeRegister',
|
|
50
|
+
'onAfterRegister',
|
|
51
|
+
'onBeforeValidate',
|
|
52
|
+
'onAfterValidate',
|
|
53
|
+
'onBeforeExport',
|
|
54
|
+
'onAfterExport',
|
|
55
|
+
'onBeforeCompile',
|
|
56
|
+
'onAfterCompile',
|
|
57
|
+
'onError',
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
constructor() {
|
|
61
|
+
super()
|
|
62
|
+
this.setMaxListeners(0)
|
|
63
|
+
for (const name of [...PluginManager.BUILTIN_HOOKS, ...PluginManager.LEGACY_HOOKS]) {
|
|
64
|
+
this._ensureHook(name)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
69
|
+
// Core API
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Register a plugin.
|
|
74
|
+
*/
|
|
75
|
+
register(plugin: Plugin): this {
|
|
76
|
+
if (!plugin || typeof plugin !== 'object') {
|
|
77
|
+
throw new Error('[schema-dsl] Plugin must be an object')
|
|
78
|
+
}
|
|
79
|
+
if (!plugin.name) {
|
|
80
|
+
throw new Error('[schema-dsl] Plugin must have a valid name')
|
|
81
|
+
}
|
|
82
|
+
if (!plugin.install) {
|
|
83
|
+
throw new Error(`[schema-dsl] Plugin must have an install function`)
|
|
84
|
+
}
|
|
85
|
+
if (this.plugins.has(plugin.name)) {
|
|
86
|
+
throw new Error(`[schema-dsl] Plugin "${plugin.name}" is already registered`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._runHookSync('onBeforeRegister', plugin)
|
|
90
|
+
|
|
91
|
+
this.plugins.set(plugin.name, plugin)
|
|
92
|
+
|
|
93
|
+
// Auto-register hooks declared by the plugin
|
|
94
|
+
if (plugin.hooks) {
|
|
95
|
+
for (const [hookName, fn] of Object.entries(plugin.hooks)) {
|
|
96
|
+
if (fn) {
|
|
97
|
+
this._addPluginHook(plugin.name, hookName, fn)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this._runHookSync('onAfterRegister', plugin)
|
|
103
|
+
this.emit('plugin:registered', plugin)
|
|
104
|
+
return this
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add a hook callback (supports any name, not limited to built-in HookName).
|
|
109
|
+
*/
|
|
110
|
+
hook(name: string, fn: HookFn): this {
|
|
111
|
+
this._ensureHook(name)
|
|
112
|
+
this.hooks.get(name)!.push(fn)
|
|
113
|
+
return this
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a specific hook handler (v1 compat: unhook).
|
|
118
|
+
*/
|
|
119
|
+
unhook(name: string, fn: HookFn): this {
|
|
120
|
+
const list = this.hooks.get(name)
|
|
121
|
+
if (list) {
|
|
122
|
+
const idx = list.indexOf(fn)
|
|
123
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
124
|
+
}
|
|
125
|
+
return this
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Run the specified hook.
|
|
130
|
+
* - Args are passed directly to each handler (v1 compat: runHook(name, arg1, arg2, ...)).
|
|
131
|
+
* - Returns an array of all handler return values.
|
|
132
|
+
* - If a handler throws, emits 'hook:error' and runs onError (does not interrupt remaining handlers).
|
|
133
|
+
*/
|
|
134
|
+
async runHook(name: string, ...args: unknown[]): Promise<unknown[]> {
|
|
135
|
+
const list = this.hooks.get(name)
|
|
136
|
+
if (!list || list.length === 0) return []
|
|
137
|
+
|
|
138
|
+
const results: unknown[] = []
|
|
139
|
+
for (const fn of list) {
|
|
140
|
+
try {
|
|
141
|
+
const result = await fn(...args)
|
|
142
|
+
results.push(result)
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const payload = { hookName: name, handler: fn, error }
|
|
145
|
+
this.emit('hook:error', payload)
|
|
146
|
+
this._runHookSync('onError', error, { hookName: name, handler: fn })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return results
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Install plugins.
|
|
154
|
+
*
|
|
155
|
+
* Supports two call forms (v1 compat):
|
|
156
|
+
* install(core) — install all registered plugins
|
|
157
|
+
* install(core, pluginName, options?) — install the named plugin, merging plugin.options + options
|
|
158
|
+
*/
|
|
159
|
+
install(core: unknown, pluginName?: string, extraOptions?: Record<string, unknown>): this {
|
|
160
|
+
this._installedCore = core
|
|
161
|
+
|
|
162
|
+
if (pluginName !== undefined) {
|
|
163
|
+
// Install the named plugin
|
|
164
|
+
const plugin = this.plugins.get(pluginName)
|
|
165
|
+
if (!plugin) {
|
|
166
|
+
throw new Error(`[schema-dsl] Plugin "${pluginName}" is not registered`)
|
|
167
|
+
}
|
|
168
|
+
this._installPlugin(core, plugin, extraOptions)
|
|
169
|
+
} else {
|
|
170
|
+
// Install all plugins
|
|
171
|
+
for (const plugin of this.plugins.values()) {
|
|
172
|
+
this._installPlugin(core, plugin, extraOptions)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return this
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private _installPlugin(core: unknown, plugin: Plugin, extraOptions?: Record<string, unknown>): void {
|
|
179
|
+
const mergedOptions = { ...(plugin.options ?? {}), ...(extraOptions ?? {}) }
|
|
180
|
+
try {
|
|
181
|
+
plugin.install?.(core, mergedOptions, this.context)
|
|
182
|
+
this.emit('plugin:installed', plugin)
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.emit('plugin:error', { plugin, error })
|
|
185
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
186
|
+
throw new Error(`[schema-dsl] Failed to install plugin "${plugin.name}": ${msg}`)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Unregister a plugin (v2 primary method).
|
|
192
|
+
* - Automatically cleans up all hooks registered by this plugin.
|
|
193
|
+
* - Emits 'plugin:uninstalled' event.
|
|
194
|
+
*/
|
|
195
|
+
unregister(name: string, coreInstance?: unknown): this {
|
|
196
|
+
const plugin = this.plugins.get(name)
|
|
197
|
+
if (!plugin) return this
|
|
198
|
+
|
|
199
|
+
const effectiveCore = coreInstance === undefined ? this._installedCore : coreInstance
|
|
200
|
+
|
|
201
|
+
if (typeof plugin.uninstall === 'function') {
|
|
202
|
+
try {
|
|
203
|
+
plugin.uninstall(effectiveCore, this.context)
|
|
204
|
+
} catch (error) {
|
|
205
|
+
this.emit('plugin:error', { plugin, error })
|
|
206
|
+
const msg = error instanceof Error ? error.message : String(error)
|
|
207
|
+
throw new Error(`[schema-dsl] Failed to uninstall plugin "${name}": ${msg}`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Clean up hooks registered by this plugin
|
|
212
|
+
const pluginHookMap = this._pluginHooks.get(name)
|
|
213
|
+
if (pluginHookMap) {
|
|
214
|
+
for (const [hookName, fns] of pluginHookMap) {
|
|
215
|
+
const list = this.hooks.get(hookName)
|
|
216
|
+
if (list) {
|
|
217
|
+
for (const fn of fns) {
|
|
218
|
+
const idx = list.indexOf(fn)
|
|
219
|
+
if (idx !== -1) list.splice(idx, 1)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this._pluginHooks.delete(name)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.plugins.delete(name)
|
|
227
|
+
|
|
228
|
+
this.emit('plugin:uninstalled', plugin)
|
|
229
|
+
return this
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
233
|
+
// v1 Compat API
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
/** v1 compat: check whether a plugin is registered. */
|
|
237
|
+
has(name: string): boolean {
|
|
238
|
+
return this.plugins.has(name)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** v1 compat: get a single plugin or the full plugins Map. */
|
|
242
|
+
get(name?: string): Plugin | Map<string, Plugin> | undefined {
|
|
243
|
+
if (name === undefined) return this.plugins
|
|
244
|
+
return this.plugins.get(name)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** v1 compat: list all plugin metadata. */
|
|
248
|
+
list(): Array<{ name: string; version?: string; description?: string }> {
|
|
249
|
+
return Array.from(this.plugins.values()).map(p => ({
|
|
250
|
+
name: p.name,
|
|
251
|
+
...(p.version !== undefined ? { version: p.version } : {}),
|
|
252
|
+
...(p.description !== undefined ? { description: p.description } : {}),
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** v1 compat: clear all plugins (including hook cleanup). */
|
|
257
|
+
clear(coreInstance?: unknown): this {
|
|
258
|
+
for (const name of Array.from(this.plugins.keys())) {
|
|
259
|
+
try {
|
|
260
|
+
this.unregister(name, coreInstance)
|
|
261
|
+
} catch {
|
|
262
|
+
// v1 behavior: clear() ignores individual plugin uninstall errors and continues
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
this.plugins.clear()
|
|
266
|
+
this._pluginHooks.clear()
|
|
267
|
+
// Clear all custom hooks (keep built-in pre-initialized entries but empty their handler arrays)
|
|
268
|
+
for (const [, list] of this.hooks) {
|
|
269
|
+
list.length = 0
|
|
270
|
+
}
|
|
271
|
+
this.emit('plugins:cleared')
|
|
272
|
+
return this
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** v1 compat: uninstall a plugin (alias for unregister). */
|
|
276
|
+
uninstall(name: string, coreInstance?: unknown): this {
|
|
277
|
+
return this.unregister(name, coreInstance)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
get pluginCount(): number {
|
|
281
|
+
return this.plugins.size
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** v1 compat: plugin count (alias for pluginCount). */
|
|
285
|
+
get size(): number {
|
|
286
|
+
return this.plugins.size
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
290
|
+
// Private Helpers
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
292
|
+
|
|
293
|
+
private _ensureHook(name: string): void {
|
|
294
|
+
if (!this.hooks.has(name)) {
|
|
295
|
+
this.hooks.set(name, [])
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private _addPluginHook(pluginName: string, hookName: string, fn: HookFn): void {
|
|
300
|
+
this._ensureHook(hookName)
|
|
301
|
+
this.hooks.get(hookName)!.push(fn)
|
|
302
|
+
|
|
303
|
+
// Record the mapping (for cleanup on unregister)
|
|
304
|
+
if (!this._pluginHooks.has(pluginName)) {
|
|
305
|
+
this._pluginHooks.set(pluginName, new Map())
|
|
306
|
+
}
|
|
307
|
+
const pluginMap = this._pluginHooks.get(pluginName)!
|
|
308
|
+
if (!pluginMap.has(hookName)) {
|
|
309
|
+
pluginMap.set(hookName, new Set())
|
|
310
|
+
}
|
|
311
|
+
pluginMap.get(hookName)!.add(fn)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private _runHookSync(name: string, ...args: unknown[]): void {
|
|
315
|
+
const list = this.hooks.get(name)
|
|
316
|
+
if (!list || list.length === 0) return
|
|
317
|
+
|
|
318
|
+
for (const handler of list) {
|
|
319
|
+
try {
|
|
320
|
+
handler(...args)
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.emit('hook:error', { hookName: name, handler, error })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StringExtensions — opt-in String.prototype chainable DSL extensions.
|
|
3
|
+
*
|
|
4
|
+
* v2 fixes:
|
|
5
|
+
* S-01/S-02: array-driven symmetric install/uninstall (v1 uninstall was missing `format` and
|
|
6
|
+
* `phoneNumber`). All method names are now maintained in the EXTENSION_METHODS
|
|
7
|
+
* array so both operations are guaranteed to be in sync.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* import { installStringExtensions } from 'schema-dsl'
|
|
11
|
+
* installStringExtensions(dsl)
|
|
12
|
+
* // Then you can use:
|
|
13
|
+
* 'email!'.label('Email address').messages({ format: 'Invalid format' })
|
|
14
|
+
* 'string:3-32!'.username('medium')
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { DslBuilder } from './DslBuilder.js'
|
|
18
|
+
|
|
19
|
+
// S-01/S-02 fix: all extension method names are managed here so install/uninstall stay symmetric
|
|
20
|
+
const EXTENSION_METHODS = [
|
|
21
|
+
'pattern',
|
|
22
|
+
'label',
|
|
23
|
+
'messages',
|
|
24
|
+
'error',
|
|
25
|
+
'description',
|
|
26
|
+
'format',
|
|
27
|
+
'custom',
|
|
28
|
+
'default',
|
|
29
|
+
'toSchema',
|
|
30
|
+
'toJsonSchema',
|
|
31
|
+
'username',
|
|
32
|
+
'password',
|
|
33
|
+
'phone',
|
|
34
|
+
'phoneNumber',
|
|
35
|
+
'idCard',
|
|
36
|
+
'creditCard',
|
|
37
|
+
'licensePlate',
|
|
38
|
+
'postalCode',
|
|
39
|
+
'passport',
|
|
40
|
+
'slug',
|
|
41
|
+
'domain',
|
|
42
|
+
'ip',
|
|
43
|
+
'base64',
|
|
44
|
+
'jwt',
|
|
45
|
+
'dateGreater',
|
|
46
|
+
'dateLess',
|
|
47
|
+
'after',
|
|
48
|
+
'before',
|
|
49
|
+
'dateFormat',
|
|
50
|
+
'min',
|
|
51
|
+
'max',
|
|
52
|
+
'alphanum',
|
|
53
|
+
'lowercase',
|
|
54
|
+
'uppercase',
|
|
55
|
+
'json',
|
|
56
|
+
'precision',
|
|
57
|
+
'multiple',
|
|
58
|
+
'port',
|
|
59
|
+
'requireAll',
|
|
60
|
+
'strict',
|
|
61
|
+
'noSparse',
|
|
62
|
+
'includesRequired',
|
|
63
|
+
'required',
|
|
64
|
+
'optional',
|
|
65
|
+
'enum',
|
|
66
|
+
'_dslExtensionsInstalled',
|
|
67
|
+
] as const
|
|
68
|
+
|
|
69
|
+
type DslFn = (dslStr: string) => DslBuilder
|
|
70
|
+
|
|
71
|
+
// Track which dslFunction the extensions were installed with
|
|
72
|
+
let _installedDslFn: DslFn | null = null
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Install String.prototype extensions.
|
|
76
|
+
* @param dslFunction - dsl() function (converts a string to a DslBuilder instance)
|
|
77
|
+
*/
|
|
78
|
+
export function installStringExtensions(dslFunction: DslFn): void {
|
|
79
|
+
// Idempotent: skip only if installed with the same dslFunction reference
|
|
80
|
+
if (_installedDslFn === dslFunction) return
|
|
81
|
+
// If installed with a different function, uninstall first then reinstall
|
|
82
|
+
if (_installedDslFn !== null) {
|
|
83
|
+
uninstallStringExtensions()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const proto = String.prototype as unknown as Record<string, unknown>
|
|
87
|
+
|
|
88
|
+
function extend(name: string, fn: (...args: unknown[]) => unknown): void {
|
|
89
|
+
proto[name] = fn
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Proxy all listed methods transparently to the DslBuilder instance
|
|
93
|
+
const delegatedMethods: string[] = [
|
|
94
|
+
'pattern', 'label', 'messages', 'error', 'description', 'format', 'custom',
|
|
95
|
+
'default', 'username', 'password', 'phone', 'phoneNumber', 'idCard', 'creditCard',
|
|
96
|
+
'licensePlate', 'postalCode', 'passport', 'slug', 'domain', 'ip', 'base64', 'jwt',
|
|
97
|
+
'dateGreater', 'dateLess', 'after', 'before', 'dateFormat',
|
|
98
|
+
'min', 'max', 'alphanum', 'lowercase', 'uppercase', 'json',
|
|
99
|
+
'precision', 'multiple', 'port', 'requireAll', 'strict',
|
|
100
|
+
'noSparse', 'includesRequired', 'required', 'optional',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for (const method of delegatedMethods) {
|
|
104
|
+
extend(method, function (this: string, ...args: unknown[]): DslBuilder {
|
|
105
|
+
const builder = dslFunction(String(this))
|
|
106
|
+
return (builder as unknown as Record<string, (...args: unknown[]) => DslBuilder>)[method](...args)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// enum method accepts rest parameters
|
|
111
|
+
extend('enum', function (this: string, ...values: unknown[]): DslBuilder {
|
|
112
|
+
return dslFunction(String(this)).enum(...values)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// toSchema / toJsonSchema return JSONSchema (not DslBuilder)
|
|
116
|
+
extend('toSchema', function (this: string) {
|
|
117
|
+
return dslFunction(String(this)).toSchema()
|
|
118
|
+
})
|
|
119
|
+
extend('toJsonSchema', function (this: string) {
|
|
120
|
+
return dslFunction(String(this)).toJsonSchema()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Installation marker (v1 BC)
|
|
124
|
+
proto['_dslExtensionsInstalled'] = true
|
|
125
|
+
_installedDslFn = dslFunction
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Uninstall String.prototype extensions (useful for tests or clean-up).
|
|
130
|
+
* S-01/S-02 fix: uses EXTENSION_METHODS array to guarantee perfect symmetry with install.
|
|
131
|
+
*/
|
|
132
|
+
export function uninstallStringExtensions(): void {
|
|
133
|
+
if (!(String.prototype as unknown as Record<string, unknown>)['_dslExtensionsInstalled']) return
|
|
134
|
+
|
|
135
|
+
const proto = String.prototype as unknown as Record<string, unknown>
|
|
136
|
+
for (const method of EXTENSION_METHODS) {
|
|
137
|
+
delete proto[method]
|
|
138
|
+
}
|
|
139
|
+
_installedDslFn = null
|
|
140
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified template engine.
|
|
3
|
+
*
|
|
4
|
+
* Merges the two rendering implementations from v1 — MessageTemplate.render() and
|
|
5
|
+
* ErrorFormatter._interpolate() — into a single pipeline.
|
|
6
|
+
* Fixes the CORE-03 template injection vulnerability (single-pass replacement so that
|
|
7
|
+
* substituted values are never expanded a second time).
|
|
8
|
+
*
|
|
9
|
+
* Supports two placeholder formats:
|
|
10
|
+
* {{#key}} ← all v1 locale files use this format (must remain compatible)
|
|
11
|
+
* {key} ← recommended format for v2 message templates
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Render a template string by replacing placeholders with corresponding values from params.
|
|
16
|
+
*
|
|
17
|
+
* @param template - Template string, e.g. "{{#label}} must be at least {{#min}} characters"
|
|
18
|
+
* @param params - Substitution parameter object
|
|
19
|
+
* @returns Rendered string; placeholders with no matching key are kept as-is (aids debugging)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* renderTemplate('{{#label}} is required', { label: 'Email' })
|
|
23
|
+
* // → 'Email is required'
|
|
24
|
+
*
|
|
25
|
+
* renderTemplate('{field} must be {min}~{max}', { field: 'age', min: 18, max: 65 })
|
|
26
|
+
* // → 'age must be 18~65'
|
|
27
|
+
*/
|
|
28
|
+
export function renderTemplate(template: string, params: Record<string, unknown>): string {
|
|
29
|
+
// Single-pass replace — prevents substituted values from being expanded again (CORE-03 fix)
|
|
30
|
+
// Regex matches both formats: {{#key}} and {key}
|
|
31
|
+
return template.replace(/\{\{#([^}]+)\}\}|\{([^}]+)\}/g, (match, k1: string | undefined, k2: string | undefined) => {
|
|
32
|
+
const key = k1 ?? k2
|
|
33
|
+
if (key !== undefined && key in params) {
|
|
34
|
+
const val = params[key]
|
|
35
|
+
if (val === null) return 'null'
|
|
36
|
+
if (val === undefined) return match
|
|
37
|
+
if (Array.isArray(val)) return val.join(', ')
|
|
38
|
+
if (val instanceof RegExp) return val.toString()
|
|
39
|
+
if (val instanceof Date) return val.toISOString()
|
|
40
|
+
return String(val)
|
|
41
|
+
}
|
|
42
|
+
return match // No matching key — keep placeholder as-is
|
|
43
|
+
})
|
|
44
|
+
}
|