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.
Files changed (145) hide show
  1. package/CHANGELOG.md +130 -113
  2. package/LICENSE +21 -21
  3. package/README.md +628 -628
  4. package/dist/{DslBuilder-DkLaOo9Q.d.ts → DslBuilder-BIgQOAXp.d.ts} +2 -0
  5. package/dist/{DslBuilder-DQDN0ZxZ.d.cts → DslBuilder-CjHTucNQ.d.cts} +2 -0
  6. package/dist/{Validator-hFWKGxir.d.ts → Validator-CllRdrY0.d.ts} +1 -1
  7. package/dist/{Validator-C7GsVQOH.d.cts → Validator-D6okG9tr.d.cts} +1 -1
  8. package/dist/index.cjs +75 -29
  9. package/dist/index.d.cts +10 -4
  10. package/dist/index.d.ts +10 -4
  11. package/dist/index.js +75 -29
  12. package/dist/plugins/custom-format.cjs +33 -17
  13. package/dist/plugins/custom-format.d.cts +1 -1
  14. package/dist/plugins/custom-format.d.ts +1 -1
  15. package/dist/plugins/custom-format.js +33 -17
  16. package/dist/plugins/custom-type-example.cjs +33 -17
  17. package/dist/plugins/custom-type-example.d.cts +1 -1
  18. package/dist/plugins/custom-type-example.d.ts +1 -1
  19. package/dist/plugins/custom-type-example.js +33 -17
  20. package/dist/plugins/custom-validator.cjs +0 -2
  21. package/dist/plugins/custom-validator.d.cts +1 -1
  22. package/dist/plugins/custom-validator.d.ts +1 -1
  23. package/dist/plugins/custom-validator.js +0 -2
  24. package/docs/FEATURE-INDEX.md +553 -553
  25. package/docs/add-custom-locale.md +496 -496
  26. package/docs/add-keyword.md +24 -24
  27. package/docs/api-reference.md +1047 -1047
  28. package/docs/api.md +13 -13
  29. package/docs/best-practices-project-structure.md +417 -417
  30. package/docs/best-practices.md +712 -712
  31. package/docs/cache-manager.md +344 -344
  32. package/docs/compile.md +45 -45
  33. package/docs/conditional-api.md +1307 -1307
  34. package/docs/custom-extensions-guide.md +339 -339
  35. package/docs/design-philosophy.md +606 -606
  36. package/docs/doc-index.md +324 -324
  37. package/docs/dsl-syntax.md +714 -714
  38. package/docs/dynamic-locale.md +608 -608
  39. package/docs/enum.md +482 -482
  40. package/docs/error-handling.md +1975 -1975
  41. package/docs/export-guide.md +501 -501
  42. package/docs/export-limitations.md +567 -567
  43. package/docs/faq.md +596 -596
  44. package/docs/frontend-i18n-guide.md +307 -307
  45. package/docs/i18n-user-guide.md +487 -487
  46. package/docs/i18n.md +476 -476
  47. package/docs/index.md +48 -48
  48. package/docs/json-schema-basics.md +40 -40
  49. package/docs/label-vs-description.md +271 -271
  50. package/docs/markdown-exporter.md +406 -406
  51. package/docs/mongodb-exporter.md +302 -302
  52. package/docs/multi-language.md +26 -26
  53. package/docs/multi-type-support.md +322 -322
  54. package/docs/mysql-exporter.md +280 -280
  55. package/docs/number-operators.md +449 -449
  56. package/docs/optional-marker-guide.md +326 -326
  57. package/docs/performance-guide.md +49 -49
  58. package/docs/plugin-system.md +381 -381
  59. package/docs/plugin-type-registration.md +34 -34
  60. package/docs/postgresql-exporter.md +311 -311
  61. package/docs/public/favicon.svg +4 -4
  62. package/docs/quick-start.md +435 -435
  63. package/docs/runtime-locale-support.md +532 -532
  64. package/docs/schema-helper.md +345 -345
  65. package/docs/schema-utils-advanced-issues.md +23 -23
  66. package/docs/schema-utils-best-practices.md +20 -20
  67. package/docs/schema-utils-chaining.md +150 -150
  68. package/docs/schema-utils.md +524 -524
  69. package/docs/security-checklist.md +20 -20
  70. package/docs/string-extensions.md +488 -488
  71. package/docs/troubleshooting.md +486 -486
  72. package/docs/type-converter.md +310 -310
  73. package/docs/type-reference.md +242 -242
  74. package/docs/typescript-guide.md +584 -584
  75. package/docs/union-type-guide.md +157 -157
  76. package/docs/union-types.md +284 -284
  77. package/docs/validate-async.md +491 -491
  78. package/docs/validate-batch.md +49 -49
  79. package/docs/validate-dsl-object-support.md +578 -578
  80. package/docs/validate.md +506 -506
  81. package/docs/validation-guide.md +502 -502
  82. package/docs/validator.md +39 -39
  83. package/package.json +131 -131
  84. package/plugins/custom-format.cjs +8 -8
  85. package/plugins/custom-type-example.cjs +8 -8
  86. package/plugins/custom-validator.cjs +8 -8
  87. package/src/adapters/DslAdapter.ts +111 -111
  88. package/src/adapters/index.ts +1 -1
  89. package/src/config/constants.ts +83 -83
  90. package/src/config/index.ts +2 -2
  91. package/src/config/patterns.ts +77 -77
  92. package/src/core/CacheManager.ts +169 -159
  93. package/src/core/ConditionalBuilder.ts +382 -382
  94. package/src/core/ConditionalRuntime.ts +27 -27
  95. package/src/core/ConditionalValidator.ts +254 -254
  96. package/src/core/DslBuilder.ts +687 -677
  97. package/src/core/ErrorCodes.ts +38 -38
  98. package/src/core/ErrorFormatter.ts +271 -271
  99. package/src/core/JSONSchemaCore.ts +65 -65
  100. package/src/core/Locale.ts +187 -187
  101. package/src/core/MessageTemplate.ts +42 -42
  102. package/src/core/ObjectDslBuilder.ts +64 -64
  103. package/src/core/PluginManager.ts +326 -326
  104. package/src/core/StringExtensions.ts +140 -140
  105. package/src/core/TemplateEngine.ts +44 -44
  106. package/src/core/Validator.ts +448 -448
  107. package/src/errors/I18nError.ts +159 -159
  108. package/src/errors/ValidationError.ts +105 -105
  109. package/src/exporters/BaseExporter.ts +60 -60
  110. package/src/exporters/MarkdownExporter.ts +305 -305
  111. package/src/exporters/MongoDBExporter.ts +126 -126
  112. package/src/exporters/MySQLExporter.ts +156 -155
  113. package/src/exporters/PostgreSQLExporter.ts +222 -222
  114. package/src/exporters/index.ts +18 -18
  115. package/src/index.ts +651 -633
  116. package/src/locales/en-US.ts +160 -160
  117. package/src/locales/es-ES.ts +160 -160
  118. package/src/locales/fr-FR.ts +160 -160
  119. package/src/locales/index.ts +103 -103
  120. package/src/locales/ja-JP.ts +160 -160
  121. package/src/locales/types.ts +156 -156
  122. package/src/locales/zh-CN.ts +160 -160
  123. package/src/parser/ConstraintParser.ts +101 -101
  124. package/src/parser/DslParser.ts +470 -470
  125. package/src/parser/SchemaCompiler.ts +66 -66
  126. package/src/parser/TypeRegistry.ts +250 -250
  127. package/src/parser/index.ts +6 -6
  128. package/src/plugins/custom-format.ts +124 -126
  129. package/src/plugins/custom-type-example.ts +106 -108
  130. package/src/plugins/custom-validator.ts +138 -140
  131. package/src/types/conditional.ts +28 -28
  132. package/src/types/config.ts +59 -59
  133. package/src/types/dsl.ts +131 -131
  134. package/src/types/error.ts +60 -60
  135. package/src/types/index.ts +17 -17
  136. package/src/types/infer.ts +127 -127
  137. package/src/types/plugin.ts +58 -58
  138. package/src/types/safe-regex.d.ts +9 -9
  139. package/src/types/schema.ts +66 -66
  140. package/src/types/validate.ts +71 -71
  141. package/src/utils/SchemaHelper.ts +196 -196
  142. package/src/utils/SchemaUtils.ts +365 -346
  143. package/src/utils/TypeConverter.ts +215 -215
  144. package/src/utils/index.ts +10 -10
  145. package/src/validators/CustomKeywords.ts +477 -477
@@ -1,159 +1,159 @@
1
- import { renderTemplate } from '../core/TemplateEngine.js'
2
- import { Locale } from '../core/Locale.js'
3
-
4
- // V8/Node.js extension
5
- type ErrorWithCaptureStackTrace = typeof Error & {
6
- captureStackTrace?: (target: object, ctor: unknown) => void
7
- }
8
- const ErrorCtor = Error as ErrorWithCaptureStackTrace
9
-
10
- type ParamsOrLocale = Record<string, unknown> | string | null | undefined
11
-
12
- function normalizeParams(
13
- paramsOrLocale: ParamsOrLocale,
14
- statusCode?: unknown,
15
- locale?: unknown
16
- ): { params: Record<string, unknown>; statusCode: number; locale: string | null } {
17
- let params: Record<string, unknown> = {}
18
- let actualStatusCode = 400
19
- let actualLocale: string | null = null
20
-
21
- if (typeof paramsOrLocale === 'string') {
22
- actualLocale = paramsOrLocale
23
- actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
24
- } else if (paramsOrLocale && typeof paramsOrLocale === 'object' && !Array.isArray(paramsOrLocale)) {
25
- params = paramsOrLocale
26
- actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
27
- actualLocale = typeof locale === 'string' ? locale : null
28
- } else {
29
- actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
30
- actualLocale = typeof locale === 'string' ? locale : null
31
- }
32
-
33
- return { params, statusCode: actualStatusCode, locale: actualLocale }
34
- }
35
-
36
- /**
37
- * Internationalised error class.
38
- * Maintains full v1 API compatibility: create / throw / assert / is / toJSON / toString
39
- */
40
- export class I18nError extends Error {
41
- readonly name = 'I18nError' as const
42
- readonly originalKey: string
43
- readonly code: string | number
44
- readonly params: Record<string, unknown>
45
- readonly statusCode: number
46
- readonly locale: string | null
47
-
48
- constructor(
49
- key: string,
50
- params: Record<string, unknown> = {},
51
- statusCode = 400,
52
- locale: string | null = null,
53
- /** Internal: pre-resolved message template, bypasses Locale lookup (used to decouple init ordering). */
54
- _resolvedTemplate?: string,
55
- _resolvedCode?: string | number
56
- ) {
57
- const targetLocale = locale ?? Locale.getLocale()
58
- const normalizedParams: Record<string, unknown> = (params !== null && params !== undefined) ? params : {}
59
-
60
- // Look up locale message config if not pre-resolved
61
- let template: string
62
- let resolvedCode: string | number
63
- if (_resolvedTemplate !== undefined) {
64
- template = _resolvedTemplate
65
- resolvedCode = _resolvedCode ?? key
66
- } else {
67
- const msgConfig = Locale.getMessageConfig(key, {}, targetLocale)
68
- if (typeof msgConfig === 'object' && msgConfig !== null && 'message' in msgConfig) {
69
- template = (msgConfig as { message: string }).message
70
- resolvedCode = (msgConfig as { code?: string | number }).code ?? key
71
- } else if (typeof msgConfig === 'string') {
72
- template = msgConfig
73
- resolvedCode = key
74
- } else {
75
- template = key
76
- resolvedCode = key
77
- }
78
- }
79
-
80
- const message = renderTemplate(template, normalizedParams)
81
-
82
- super(message)
83
-
84
- this.originalKey = key
85
- this.code = resolvedCode
86
- this.params = normalizedParams
87
- this.statusCode = statusCode
88
- this.locale = targetLocale
89
-
90
- if (ErrorCtor.captureStackTrace) {
91
- ErrorCtor.captureStackTrace(this, I18nError)
92
- }
93
- }
94
-
95
- /** Factory method — create an error instance. */
96
- static create(
97
- code: string,
98
- paramsOrLocale?: ParamsOrLocale,
99
- statusCode?: number,
100
- locale?: string
101
- ): I18nError {
102
- const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
103
- return new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
104
- }
105
-
106
- /** Factory method — create and throw an error. */
107
- static throw(
108
- code: string,
109
- paramsOrLocale?: ParamsOrLocale,
110
- statusCode?: number,
111
- locale?: string
112
- ): never {
113
- const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
114
- throw new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
115
- }
116
-
117
- /** Assert — throw when condition is falsy. */
118
- static assert(
119
- condition: unknown,
120
- code: string,
121
- paramsOrLocale?: ParamsOrLocale,
122
- statusCode?: number,
123
- locale?: string
124
- ): asserts condition {
125
- if (!condition) {
126
- const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
127
- throw new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
128
- }
129
- }
130
-
131
- /** Check whether the error matches the given code or original key. */
132
- is(codeOrKey: string | number): boolean {
133
- return this.code === codeOrKey || this.originalKey === codeOrKey
134
- }
135
-
136
- toJSON(): {
137
- error: string
138
- originalKey: string
139
- code: string | number
140
- message: string
141
- params: Record<string, unknown>
142
- statusCode: number
143
- locale: string | null
144
- } {
145
- return {
146
- error: this.name,
147
- originalKey: this.originalKey,
148
- code: this.code,
149
- message: this.message,
150
- params: this.params,
151
- statusCode: this.statusCode,
152
- locale: this.locale,
153
- }
154
- }
155
-
156
- toString(): string {
157
- return `${this.name} [${this.code}]: ${this.message}`
158
- }
159
- }
1
+ import { renderTemplate } from '../core/TemplateEngine.js'
2
+ import { Locale } from '../core/Locale.js'
3
+
4
+ // V8/Node.js extension
5
+ type ErrorWithCaptureStackTrace = typeof Error & {
6
+ captureStackTrace?: (target: object, ctor: unknown) => void
7
+ }
8
+ const ErrorCtor = Error as ErrorWithCaptureStackTrace
9
+
10
+ type ParamsOrLocale = Record<string, unknown> | string | null | undefined
11
+
12
+ function normalizeParams(
13
+ paramsOrLocale: ParamsOrLocale,
14
+ statusCode?: unknown,
15
+ locale?: unknown
16
+ ): { params: Record<string, unknown>; statusCode: number; locale: string | null } {
17
+ let params: Record<string, unknown> = {}
18
+ let actualStatusCode = 400
19
+ let actualLocale: string | null = null
20
+
21
+ if (typeof paramsOrLocale === 'string') {
22
+ actualLocale = paramsOrLocale
23
+ actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
24
+ } else if (paramsOrLocale && typeof paramsOrLocale === 'object' && !Array.isArray(paramsOrLocale)) {
25
+ params = paramsOrLocale
26
+ actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
27
+ actualLocale = typeof locale === 'string' ? locale : null
28
+ } else {
29
+ actualStatusCode = typeof statusCode === 'number' ? statusCode : 400
30
+ actualLocale = typeof locale === 'string' ? locale : null
31
+ }
32
+
33
+ return { params, statusCode: actualStatusCode, locale: actualLocale }
34
+ }
35
+
36
+ /**
37
+ * Internationalised error class.
38
+ * Maintains full v1 API compatibility: create / throw / assert / is / toJSON / toString
39
+ */
40
+ export class I18nError extends Error {
41
+ readonly name = 'I18nError' as const
42
+ readonly originalKey: string
43
+ readonly code: string | number
44
+ readonly params: Record<string, unknown>
45
+ readonly statusCode: number
46
+ readonly locale: string | null
47
+
48
+ constructor(
49
+ key: string,
50
+ params: Record<string, unknown> = {},
51
+ statusCode = 400,
52
+ locale: string | null = null,
53
+ /** Internal: pre-resolved message template, bypasses Locale lookup (used to decouple init ordering). */
54
+ _resolvedTemplate?: string,
55
+ _resolvedCode?: string | number
56
+ ) {
57
+ const targetLocale = locale ?? Locale.getLocale()
58
+ const normalizedParams: Record<string, unknown> = (params !== null && params !== undefined) ? params : {}
59
+
60
+ // Look up locale message config if not pre-resolved
61
+ let template: string
62
+ let resolvedCode: string | number
63
+ if (_resolvedTemplate !== undefined) {
64
+ template = _resolvedTemplate
65
+ resolvedCode = _resolvedCode ?? key
66
+ } else {
67
+ const msgConfig = Locale.getMessageConfig(key, {}, targetLocale)
68
+ if (typeof msgConfig === 'object' && msgConfig !== null && 'message' in msgConfig) {
69
+ template = (msgConfig as { message: string }).message
70
+ resolvedCode = (msgConfig as { code?: string | number }).code ?? key
71
+ } else if (typeof msgConfig === 'string') {
72
+ template = msgConfig
73
+ resolvedCode = key
74
+ } else {
75
+ template = key
76
+ resolvedCode = key
77
+ }
78
+ }
79
+
80
+ const message = renderTemplate(template, normalizedParams)
81
+
82
+ super(message)
83
+
84
+ this.originalKey = key
85
+ this.code = resolvedCode
86
+ this.params = normalizedParams
87
+ this.statusCode = statusCode
88
+ this.locale = targetLocale
89
+
90
+ if (ErrorCtor.captureStackTrace) {
91
+ ErrorCtor.captureStackTrace(this, I18nError)
92
+ }
93
+ }
94
+
95
+ /** Factory method — create an error instance. */
96
+ static create(
97
+ code: string,
98
+ paramsOrLocale?: ParamsOrLocale,
99
+ statusCode?: number,
100
+ locale?: string
101
+ ): I18nError {
102
+ const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
103
+ return new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
104
+ }
105
+
106
+ /** Factory method — create and throw an error. */
107
+ static throw(
108
+ code: string,
109
+ paramsOrLocale?: ParamsOrLocale,
110
+ statusCode?: number,
111
+ locale?: string
112
+ ): never {
113
+ const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
114
+ throw new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
115
+ }
116
+
117
+ /** Assert — throw when condition is falsy. */
118
+ static assert(
119
+ condition: unknown,
120
+ code: string,
121
+ paramsOrLocale?: ParamsOrLocale,
122
+ statusCode?: number,
123
+ locale?: string
124
+ ): asserts condition {
125
+ if (!condition) {
126
+ const normalized = normalizeParams(paramsOrLocale, statusCode, locale)
127
+ throw new I18nError(code, normalized.params, normalized.statusCode, normalized.locale)
128
+ }
129
+ }
130
+
131
+ /** Check whether the error matches the given code or original key. */
132
+ is(codeOrKey: string | number): boolean {
133
+ return this.code === codeOrKey || this.originalKey === codeOrKey
134
+ }
135
+
136
+ toJSON(): {
137
+ error: string
138
+ originalKey: string
139
+ code: string | number
140
+ message: string
141
+ params: Record<string, unknown>
142
+ statusCode: number
143
+ locale: string | null
144
+ } {
145
+ return {
146
+ error: this.name,
147
+ originalKey: this.originalKey,
148
+ code: this.code,
149
+ message: this.message,
150
+ params: this.params,
151
+ statusCode: this.statusCode,
152
+ locale: this.locale,
153
+ }
154
+ }
155
+
156
+ toString(): string {
157
+ return `${this.name} [${this.code}]: ${this.message}`
158
+ }
159
+ }
@@ -1,105 +1,105 @@
1
- // V8/Node.js extension (not in ES2022 lib; declared explicitly)
2
- type ErrorWithCaptureStackTrace = typeof Error & {
3
- captureStackTrace?: (target: object, ctor: unknown) => void
4
- }
5
- const ErrorCtor = Error as ErrorWithCaptureStackTrace
6
-
7
- import type { ValidationErrorItem } from '../types/validate.js'
8
-
9
- /**
10
- * ValidationError — error class thrown when validateAsync() fails.
11
- * Fixes v1 bug: malformed message format when errors array is empty.
12
- */
13
- export class ValidationError extends Error {
14
- readonly name = 'ValidationError' as const
15
- readonly errors: ValidationErrorItem[]
16
- readonly data: unknown
17
- readonly statusCode: number
18
-
19
- constructor(errors: ValidationErrorItem[], data?: unknown, statusCode = 400) {
20
- // Fix: provide friendly message when errors array is empty (v1 bug)
21
- const messages =
22
- errors.length === 0
23
- ? 'Validation failed'
24
- : errors
25
- .map(e => {
26
- if (e.path) {
27
- const field = e.path.replace(/^\//, '')
28
- return field ? `${field}: ${e.message}` : e.message
29
- }
30
- return e.message
31
- })
32
- .join('; ')
33
-
34
- // v1 compat: use " - " separator when no path, ": " otherwise
35
- // v1 compat: single conditional error uses message string directly (no prefix)
36
- const hasNoPath = errors.every(e => e.path === undefined || e.path === null || e.path === '')
37
- const isSingleConditional = errors.length === 1 && errors[0].keyword === 'conditional' && hasNoPath
38
- if (isSingleConditional) {
39
- super(messages)
40
- } else {
41
- super(hasNoPath ? `Validation failed - ${messages}` : `Validation failed: ${messages}`)
42
- }
43
-
44
- this.errors = errors
45
- this.data = data
46
- this.statusCode = statusCode
47
-
48
- if (ErrorCtor.captureStackTrace) {
49
- ErrorCtor.captureStackTrace(this, ValidationError)
50
- }
51
- }
52
-
53
- toJSON(): {
54
- error: string
55
- message: string
56
- statusCode: number
57
- details: Array<{
58
- field: string | null
59
- message: string
60
- keyword: string
61
- params?: Record<string, unknown>
62
- }>
63
- } {
64
- return {
65
- error: this.name,
66
- message: this.message,
67
- statusCode: this.statusCode,
68
- details: this.errors.map(e => ({
69
- field: e.path ? e.path.replace(/^\//, '') : null,
70
- message: e.message,
71
- keyword: e.keyword,
72
- ...(e.params !== undefined ? { params: e.params } : {}),
73
- })),
74
- }
75
- }
76
-
77
- getFieldError(field: string): ValidationErrorItem | null {
78
- const normalized = field.replace(/^\//, '')
79
- return (
80
- this.errors.find(e => {
81
- if (!e.path) return false
82
- return e.path.replace(/^\//, '') === normalized
83
- }) ?? null
84
- )
85
- }
86
-
87
- getFieldErrors(): Record<string, string> {
88
- const result: Record<string, string> = {}
89
- for (const e of this.errors) {
90
- if (e.path) {
91
- const field = e.path.replace(/^\//, '')
92
- if (field) result[field] = e.message
93
- }
94
- }
95
- return result
96
- }
97
-
98
- hasFieldError(field: string): boolean {
99
- return this.getFieldError(field) !== null
100
- }
101
-
102
- getErrorCount(): number {
103
- return this.errors.length
104
- }
105
- }
1
+ // V8/Node.js extension (not in ES2022 lib; declared explicitly)
2
+ type ErrorWithCaptureStackTrace = typeof Error & {
3
+ captureStackTrace?: (target: object, ctor: unknown) => void
4
+ }
5
+ const ErrorCtor = Error as ErrorWithCaptureStackTrace
6
+
7
+ import type { ValidationErrorItem } from '../types/validate.js'
8
+
9
+ /**
10
+ * ValidationError — error class thrown when validateAsync() fails.
11
+ * Fixes v1 bug: malformed message format when errors array is empty.
12
+ */
13
+ export class ValidationError extends Error {
14
+ readonly name = 'ValidationError' as const
15
+ readonly errors: ValidationErrorItem[]
16
+ readonly data: unknown
17
+ readonly statusCode: number
18
+
19
+ constructor(errors: ValidationErrorItem[], data?: unknown, statusCode = 400) {
20
+ // Fix: provide friendly message when errors array is empty (v1 bug)
21
+ const messages =
22
+ errors.length === 0
23
+ ? 'Validation failed'
24
+ : errors
25
+ .map(e => {
26
+ if (e.path) {
27
+ const field = e.path.replace(/^\//, '')
28
+ return field ? `${field}: ${e.message}` : e.message
29
+ }
30
+ return e.message
31
+ })
32
+ .join('; ')
33
+
34
+ // v1 compat: use " - " separator when no path, ": " otherwise
35
+ // v1 compat: single conditional error uses message string directly (no prefix)
36
+ const hasNoPath = errors.every(e => e.path === undefined || e.path === null || e.path === '')
37
+ const isSingleConditional = errors.length === 1 && errors[0].keyword === 'conditional' && hasNoPath
38
+ if (isSingleConditional) {
39
+ super(messages)
40
+ } else {
41
+ super(hasNoPath ? `Validation failed - ${messages}` : `Validation failed: ${messages}`)
42
+ }
43
+
44
+ this.errors = errors
45
+ this.data = data
46
+ this.statusCode = statusCode
47
+
48
+ if (ErrorCtor.captureStackTrace) {
49
+ ErrorCtor.captureStackTrace(this, ValidationError)
50
+ }
51
+ }
52
+
53
+ toJSON(): {
54
+ error: string
55
+ message: string
56
+ statusCode: number
57
+ details: Array<{
58
+ field: string | null
59
+ message: string
60
+ keyword: string
61
+ params?: Record<string, unknown>
62
+ }>
63
+ } {
64
+ return {
65
+ error: this.name,
66
+ message: this.message,
67
+ statusCode: this.statusCode,
68
+ details: this.errors.map(e => ({
69
+ field: e.path ? e.path.replace(/^\//, '') : null,
70
+ message: e.message,
71
+ keyword: e.keyword,
72
+ ...(e.params !== undefined ? { params: e.params } : {}),
73
+ })),
74
+ }
75
+ }
76
+
77
+ getFieldError(field: string): ValidationErrorItem | null {
78
+ const normalized = field.replace(/^\//, '')
79
+ return (
80
+ this.errors.find(e => {
81
+ if (!e.path) return false
82
+ return e.path.replace(/^\//, '') === normalized
83
+ }) ?? null
84
+ )
85
+ }
86
+
87
+ getFieldErrors(): Record<string, string> {
88
+ const result: Record<string, string> = {}
89
+ for (const e of this.errors) {
90
+ if (e.path) {
91
+ const field = e.path.replace(/^\//, '')
92
+ if (field) result[field] = e.message
93
+ }
94
+ }
95
+ return result
96
+ }
97
+
98
+ hasFieldError(field: string): boolean {
99
+ return this.getFieldError(field) !== null
100
+ }
101
+
102
+ getErrorCount(): number {
103
+ return this.errors.length
104
+ }
105
+ }
@@ -1,60 +1,60 @@
1
- /**
2
- * BaseExporter — Base interface and abstract class for all exporters.
3
- *
4
- * Provides a unified abstract export() method signature; each exporter subclass implements it.
5
- */
6
-
7
- import type { JSONSchema } from '../types/schema.js'
8
-
9
- // ==================== Common options type ====================
10
-
11
- export interface ExporterOptions {
12
- [key: string]: unknown
13
- }
14
-
15
- // ==================== BaseExporter ====================
16
-
17
- export abstract class BaseExporter<TOptions extends ExporterOptions = ExporterOptions> {
18
- protected options: TOptions
19
-
20
- constructor(options: Partial<TOptions> = {}) {
21
- this.options = options as TOptions
22
- }
23
-
24
- /**
25
- * Export a JSON Schema to the target format.
26
- * Each subclass must implement this method.
27
- */
28
- abstract export(...args: unknown[]): unknown
29
-
30
- /**
31
- * Assert that the input JSON Schema is a valid object-type schema.
32
- * @throws Error if invalid.
33
- */
34
- protected _assertObjectSchema(jsonSchema: unknown, label = 'JSON Schema'): asserts jsonSchema is JSONSchema & { type: 'object' } {
35
- if (!jsonSchema || typeof jsonSchema !== 'object') {
36
- throw new Error(`[schema-dsl] ${label} must be an object`)
37
- }
38
- const s = jsonSchema as JSONSchema
39
- if (s.type !== 'object') {
40
- throw new Error(`[schema-dsl] ${label} must be an object type (got "${String(s.type)}")`)
41
- }
42
- }
43
-
44
- /**
45
- * Escape SQL single quotes (generic utility).
46
- */
47
- protected _escapeString(str: string): string {
48
- return str.replace(/'/g, "''")
49
- }
50
-
51
- /**
52
- * Detect the primary key column name in a schema (id / _id preferred).
53
- */
54
- protected _detectPrimaryKey(schema: JSONSchema): string | null {
55
- if (!schema.properties) return null
56
- if (schema.properties['id']) return 'id'
57
- if (schema.properties['_id']) return '_id'
58
- return null
59
- }
60
- }
1
+ /**
2
+ * BaseExporter — Base interface and abstract class for all exporters.
3
+ *
4
+ * Provides a unified abstract export() method signature; each exporter subclass implements it.
5
+ */
6
+
7
+ import type { JSONSchema } from '../types/schema.js'
8
+
9
+ // ==================== Common options type ====================
10
+
11
+ export interface ExporterOptions {
12
+ [key: string]: unknown
13
+ }
14
+
15
+ // ==================== BaseExporter ====================
16
+
17
+ export abstract class BaseExporter<TOptions extends ExporterOptions = ExporterOptions> {
18
+ protected options: TOptions
19
+
20
+ constructor(options: Partial<TOptions> = {}) {
21
+ this.options = options as TOptions
22
+ }
23
+
24
+ /**
25
+ * Export a JSON Schema to the target format.
26
+ * Each subclass must implement this method.
27
+ */
28
+ abstract export(...args: unknown[]): unknown
29
+
30
+ /**
31
+ * Assert that the input JSON Schema is a valid object-type schema.
32
+ * @throws Error if invalid.
33
+ */
34
+ protected _assertObjectSchema(jsonSchema: unknown, label = 'JSON Schema'): asserts jsonSchema is JSONSchema & { type: 'object' } {
35
+ if (!jsonSchema || typeof jsonSchema !== 'object') {
36
+ throw new Error(`[schema-dsl] ${label} must be an object`)
37
+ }
38
+ const s = jsonSchema as JSONSchema
39
+ if (s.type !== 'object') {
40
+ throw new Error(`[schema-dsl] ${label} must be an object type (got "${String(s.type)}")`)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Escape SQL single quotes (generic utility).
46
+ */
47
+ protected _escapeString(str: string): string {
48
+ return str.replace(/'/g, "''")
49
+ }
50
+
51
+ /**
52
+ * Detect the primary key column name in a schema (id / _id preferred).
53
+ */
54
+ protected _detectPrimaryKey(schema: JSONSchema): string | null {
55
+ if (!schema.properties) return null
56
+ if (schema.properties['id']) return 'id'
57
+ if (schema.properties['_id']) return '_id'
58
+ return null
59
+ }
60
+ }