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.
Files changed (242) hide show
  1. package/CHANGELOG.md +87 -210
  2. package/README.md +391 -2249
  3. package/dist/DslBuilder-DQDN0ZxZ.d.cts +341 -0
  4. package/dist/DslBuilder-DkLaOo9Q.d.ts +341 -0
  5. package/dist/Validator-C7GsVQOH.d.cts +192 -0
  6. package/dist/Validator-hFWKGxir.d.ts +192 -0
  7. package/dist/index.cjs +6594 -0
  8. package/dist/index.d.cts +1145 -0
  9. package/dist/index.d.ts +1145 -0
  10. package/dist/index.js +6528 -0
  11. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  12. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  13. package/dist/plugins/custom-format.cjs +3802 -0
  14. package/dist/plugins/custom-format.d.cts +12 -0
  15. package/dist/plugins/custom-format.d.ts +12 -0
  16. package/dist/plugins/custom-format.js +3772 -0
  17. package/dist/plugins/custom-type-example.cjs +3795 -0
  18. package/dist/plugins/custom-type-example.d.cts +8 -0
  19. package/dist/plugins/custom-type-example.d.ts +8 -0
  20. package/dist/plugins/custom-type-example.js +3765 -0
  21. package/dist/plugins/custom-validator.cjs +146 -0
  22. package/dist/plugins/custom-validator.d.cts +10 -0
  23. package/dist/plugins/custom-validator.d.ts +10 -0
  24. package/dist/plugins/custom-validator.js +121 -0
  25. package/docs/FEATURE-INDEX.md +102 -68
  26. package/docs/add-custom-locale.md +48 -35
  27. package/docs/add-keyword.md +24 -0
  28. package/docs/api-reference.md +396 -154
  29. package/docs/api.md +13 -0
  30. package/docs/best-practices-project-structure.md +19 -10
  31. package/docs/best-practices.md +93 -53
  32. package/docs/cache-manager.md +23 -15
  33. package/docs/compile.md +45 -0
  34. package/docs/conditional-api.md +40 -11
  35. package/docs/custom-extensions-guide.md +80 -152
  36. package/docs/design-philosophy.md +76 -71
  37. package/docs/doc-index.md +324 -0
  38. package/docs/dsl-syntax.md +69 -19
  39. package/docs/dynamic-locale.md +24 -14
  40. package/docs/enum.md +12 -5
  41. package/docs/error-handling.md +53 -44
  42. package/docs/export-guide.md +47 -8
  43. package/docs/export-limitations.md +27 -11
  44. package/docs/faq.md +86 -67
  45. package/docs/frontend-i18n-guide.md +26 -12
  46. package/docs/i18n-user-guide.md +60 -47
  47. package/docs/i18n.md +51 -32
  48. package/docs/index.md +48 -0
  49. package/docs/json-schema-basics.md +40 -0
  50. package/docs/label-vs-description.md +12 -3
  51. package/docs/markdown-exporter.md +15 -6
  52. package/docs/mongodb-exporter.md +11 -4
  53. package/docs/multi-language.md +26 -0
  54. package/docs/multi-type-support.md +26 -33
  55. package/docs/mysql-exporter.md +9 -2
  56. package/docs/number-operators.md +12 -5
  57. package/docs/optional-marker-guide.md +28 -23
  58. package/docs/performance-guide.md +49 -0
  59. package/docs/plugin-system.md +205 -366
  60. package/docs/plugin-type-registration.md +34 -0
  61. package/docs/postgresql-exporter.md +9 -2
  62. package/docs/public/favicon.svg +5 -0
  63. package/docs/quick-start.md +37 -363
  64. package/docs/runtime-locale-support.md +20 -9
  65. package/docs/schema-helper.md +10 -5
  66. package/docs/schema-utils-advanced-issues.md +23 -0
  67. package/docs/schema-utils-best-practices.md +20 -0
  68. package/docs/schema-utils-chaining.md +7 -0
  69. package/docs/schema-utils.md +76 -42
  70. package/docs/security-checklist.md +20 -0
  71. package/docs/string-extensions.md +17 -9
  72. package/docs/troubleshooting.md +36 -21
  73. package/docs/type-converter.md +41 -50
  74. package/docs/type-reference.md +38 -15
  75. package/docs/typescript-guide.md +53 -42
  76. package/docs/union-type-guide.md +11 -1
  77. package/docs/union-types.md +10 -3
  78. package/docs/validate-async.md +36 -25
  79. package/docs/validate-batch.md +49 -0
  80. package/docs/validate-dsl-object-support.md +33 -28
  81. package/docs/validate.md +36 -16
  82. package/docs/validation-guide.md +25 -7
  83. package/docs/validator.md +39 -0
  84. package/package.json +85 -27
  85. package/plugins/custom-format.cjs +8 -0
  86. package/plugins/custom-type-example.cjs +8 -0
  87. package/plugins/custom-validator.cjs +8 -0
  88. package/src/adapters/DslAdapter.ts +111 -0
  89. package/src/adapters/index.ts +1 -0
  90. package/src/config/constants.ts +83 -0
  91. package/src/config/index.ts +2 -0
  92. package/src/config/patterns.ts +77 -0
  93. package/src/core/CacheManager.ts +159 -0
  94. package/src/core/ConditionalBuilder.ts +382 -0
  95. package/src/core/ConditionalRuntime.ts +28 -0
  96. package/src/core/ConditionalValidator.ts +255 -0
  97. package/src/core/DslBuilder.ts +677 -0
  98. package/src/core/ErrorCodes.ts +38 -0
  99. package/src/core/ErrorFormatter.ts +271 -0
  100. package/src/core/JSONSchemaCore.ts +65 -0
  101. package/src/core/Locale.ts +187 -0
  102. package/src/core/MessageTemplate.ts +42 -0
  103. package/src/core/ObjectDslBuilder.ts +64 -0
  104. package/src/core/PluginManager.ts +326 -0
  105. package/src/core/StringExtensions.ts +140 -0
  106. package/src/core/TemplateEngine.ts +44 -0
  107. package/src/core/Validator.ts +448 -0
  108. package/src/errors/I18nError.ts +159 -0
  109. package/src/errors/ValidationError.ts +105 -0
  110. package/src/exporters/BaseExporter.ts +60 -0
  111. package/src/exporters/MarkdownExporter.ts +305 -0
  112. package/src/exporters/MongoDBExporter.ts +126 -0
  113. package/src/exporters/MySQLExporter.ts +155 -0
  114. package/src/exporters/PostgreSQLExporter.ts +222 -0
  115. package/src/exporters/index.ts +18 -0
  116. package/src/index.ts +633 -0
  117. package/{lib/locales/en-US.js → src/locales/en-US.ts} +21 -37
  118. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +63 -16
  119. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +74 -27
  120. package/src/locales/index.ts +103 -0
  121. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +59 -17
  122. package/src/locales/types.ts +156 -0
  123. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +21 -38
  124. package/src/parser/ConstraintParser.ts +101 -0
  125. package/src/parser/DslParser.ts +470 -0
  126. package/src/parser/SchemaCompiler.ts +66 -0
  127. package/src/parser/TypeRegistry.ts +250 -0
  128. package/src/parser/index.ts +6 -0
  129. package/src/plugins/custom-format.ts +126 -0
  130. package/src/plugins/custom-type-example.ts +108 -0
  131. package/src/plugins/custom-validator.ts +140 -0
  132. package/src/types/conditional.ts +28 -0
  133. package/src/types/config.ts +59 -0
  134. package/src/types/dsl.ts +131 -0
  135. package/src/types/error.ts +60 -0
  136. package/src/types/index.ts +17 -0
  137. package/src/types/infer.ts +128 -0
  138. package/src/types/plugin.ts +58 -0
  139. package/src/types/safe-regex.d.ts +9 -0
  140. package/src/types/schema.ts +66 -0
  141. package/src/types/validate.ts +71 -0
  142. package/src/utils/SchemaHelper.ts +196 -0
  143. package/src/utils/SchemaUtils.ts +346 -0
  144. package/src/utils/TypeConverter.ts +215 -0
  145. package/src/utils/index.ts +10 -0
  146. package/src/validators/CustomKeywords.ts +477 -0
  147. package/.eslintignore +0 -11
  148. package/.eslintrc.json +0 -27
  149. package/CONTRIBUTING.md +0 -368
  150. package/STATUS.md +0 -491
  151. package/changelogs/v1.0.0.md +0 -328
  152. package/changelogs/v1.0.9.md +0 -367
  153. package/changelogs/v1.1.0.md +0 -389
  154. package/changelogs/v1.1.1.md +0 -308
  155. package/changelogs/v1.1.2.md +0 -183
  156. package/changelogs/v1.1.3.md +0 -161
  157. package/changelogs/v1.1.4.md +0 -432
  158. package/changelogs/v1.1.5.md +0 -493
  159. package/changelogs/v1.1.6.md +0 -211
  160. package/changelogs/v1.1.8.md +0 -376
  161. package/changelogs/v1.2.3.md +0 -124
  162. package/docs/INDEX.md +0 -252
  163. package/docs/issues-resolved-summary.md +0 -196
  164. package/docs/performance-benchmark-report.md +0 -179
  165. package/docs/performance-quick-reference.md +0 -123
  166. package/docs/user-questions-answered.md +0 -353
  167. package/docs/validation-rules-v1.0.2.md +0 -1608
  168. package/examples/README.md +0 -81
  169. package/examples/array-dsl-example.js +0 -227
  170. package/examples/conditional-example.js +0 -288
  171. package/examples/conditional-non-object.js +0 -129
  172. package/examples/conditional-validate-example.js +0 -321
  173. package/examples/custom-extension.js +0 -85
  174. package/examples/dsl-match-example.js +0 -74
  175. package/examples/dsl-style.js +0 -118
  176. package/examples/dynamic-locale-configuration.js +0 -348
  177. package/examples/dynamic-locale-example.js +0 -287
  178. package/examples/enum.examples.js +0 -324
  179. package/examples/export-demo.js +0 -130
  180. package/examples/express-integration.js +0 -376
  181. package/examples/i18n-error-handling-complete.js +0 -381
  182. package/examples/i18n-error-handling-quickstart.md +0 -0
  183. package/examples/i18n-error.examples.js +0 -181
  184. package/examples/i18n-full-demo.js +0 -301
  185. package/examples/i18n-memory-safety.examples.js +0 -268
  186. package/examples/markdown-export.js +0 -71
  187. package/examples/middleware-usage.js +0 -93
  188. package/examples/new-features-comparison.js +0 -315
  189. package/examples/password-reset/README.md +0 -153
  190. package/examples/password-reset/schema.js +0 -26
  191. package/examples/password-reset/test.js +0 -101
  192. package/examples/plugin-system.examples.js +0 -205
  193. package/examples/schema-utils-chaining.examples.js +0 -250
  194. package/examples/simple-example.js +0 -122
  195. package/examples/slug.examples.js +0 -179
  196. package/examples/string-extensions.js +0 -297
  197. package/examples/union-type-example.js +0 -127
  198. package/examples/union-types-example.js +0 -77
  199. package/examples/user-registration/README.md +0 -156
  200. package/examples/user-registration/routes.js +0 -92
  201. package/examples/user-registration/schema.js +0 -150
  202. package/examples/user-registration/server.js +0 -74
  203. package/index.d.ts +0 -3540
  204. package/index.js +0 -457
  205. package/index.mjs +0 -60
  206. package/lib/adapters/DslAdapter.js +0 -871
  207. package/lib/adapters/index.js +0 -20
  208. package/lib/config/constants.js +0 -286
  209. package/lib/config/patterns/common.js +0 -47
  210. package/lib/config/patterns/creditCard.js +0 -9
  211. package/lib/config/patterns/idCard.js +0 -9
  212. package/lib/config/patterns/index.js +0 -9
  213. package/lib/config/patterns/licensePlate.js +0 -4
  214. package/lib/config/patterns/passport.js +0 -4
  215. package/lib/config/patterns/phone.js +0 -9
  216. package/lib/config/patterns/postalCode.js +0 -5
  217. package/lib/core/CacheManager.js +0 -376
  218. package/lib/core/ConditionalBuilder.js +0 -503
  219. package/lib/core/DslBuilder.js +0 -1400
  220. package/lib/core/ErrorCodes.js +0 -233
  221. package/lib/core/ErrorFormatter.js +0 -445
  222. package/lib/core/JSONSchemaCore.js +0 -347
  223. package/lib/core/Locale.js +0 -130
  224. package/lib/core/MessageTemplate.js +0 -98
  225. package/lib/core/PluginManager.js +0 -448
  226. package/lib/core/StringExtensions.js +0 -240
  227. package/lib/core/Validator.js +0 -654
  228. package/lib/errors/I18nError.js +0 -328
  229. package/lib/errors/ValidationError.js +0 -191
  230. package/lib/exporters/MarkdownExporter.js +0 -420
  231. package/lib/exporters/MongoDBExporter.js +0 -162
  232. package/lib/exporters/MySQLExporter.js +0 -212
  233. package/lib/exporters/PostgreSQLExporter.js +0 -289
  234. package/lib/exporters/index.js +0 -24
  235. package/lib/locales/index.js +0 -8
  236. package/lib/utils/LRUCache.js +0 -174
  237. package/lib/utils/SchemaHelper.js +0 -240
  238. package/lib/utils/SchemaUtils.js +0 -445
  239. package/lib/utils/TypeConverter.js +0 -245
  240. package/lib/utils/index.js +0 -13
  241. package/lib/validators/CustomKeywords.js +0 -616
  242. package/lib/validators/index.js +0 -11
@@ -1,233 +0,0 @@
1
- /**
2
- * 错误码定义
3
- *
4
- * 定义所有内置的错误码和默认错误消息
5
- * v2.0.1: 支持简化格式(无类型前缀)
6
- *
7
- * @module lib/core/ErrorCodes
8
- */
9
-
10
- const ERROR_CODES = {
11
- // ========== 字符串类型错误 ==========
12
- // v2.0.1: 简化格式(推荐,用户友好)
13
- 'min': {
14
- code: 'TOO_SHORT',
15
- message: '{{#label}} length must be at least {{#limit}} characters'
16
- },
17
- 'max': {
18
- code: 'TOO_LONG',
19
- message: '{{#label}} length must be at most {{#limit}} characters'
20
- },
21
- // ajv 实际使用的关键字(映射到简化版本)
22
- 'minLength': {
23
- code: 'TOO_SHORT',
24
- message: '{{#label}} length must be at least {{#limit}} characters'
25
- },
26
- 'maxLength': {
27
- code: 'TOO_LONG',
28
- message: '{{#label}} length must be at most {{#limit}} characters'
29
- },
30
- 'email': {
31
- code: 'INVALID_EMAIL',
32
- message: '{{#label}} must be a valid email address'
33
- },
34
- 'url': {
35
- code: 'INVALID_URL',
36
- message: '{{#label}} must be a valid URL'
37
- },
38
- 'pattern': {
39
- code: 'INVALID_PATTERN',
40
- message: '{{#label}} format is invalid'
41
- },
42
- 'format': {
43
- code: 'INVALID_FORMAT',
44
- message: '{{#label}} must match format "{{#format}}"'
45
- },
46
- 'required': {
47
- code: 'REQUIRED',
48
- message: '{{#label}} is required'
49
- },
50
-
51
- // Formats (Standardized)
52
- 'format.email': {
53
- code: 'INVALID_EMAIL',
54
- message: '{{#label}} must be a valid email address'
55
- },
56
- 'format.url': {
57
- code: 'INVALID_URL',
58
- message: '{{#label}} must be a valid URL'
59
- },
60
- 'format.uuid': {
61
- code: 'INVALID_UUID',
62
- message: '{{#label}} must be a valid UUID'
63
- },
64
- 'format.ipv4': {
65
- code: 'INVALID_IPV4',
66
- message: '{{#label}} must be a valid IPv4 address'
67
- },
68
- 'format.ipv6': {
69
- code: 'INVALID_IPV6',
70
- message: '{{#label}} must be a valid IPv6 address'
71
- },
72
- 'string.hostname': {
73
- code: 'STRING_INVALID_HOSTNAME',
74
- message: '{{#label}} must be a valid hostname'
75
- },
76
- 'string.pattern': {
77
- code: 'STRING_PATTERN_MISMATCH',
78
- message: '{{#label}} format is invalid'
79
- },
80
- 'string.enum': {
81
- code: 'STRING_INVALID_ENUM',
82
- message: '{{#label}} must be one of: {{#valids}}'
83
- },
84
-
85
- // ========== 数字类型错误 ==========
86
- 'number.base': {
87
- code: 'NUMBER_INVALID_TYPE',
88
- message: '{{#label}} must be a number'
89
- },
90
- 'number.min': {
91
- code: 'NUMBER_TOO_SMALL',
92
- message: '{{#label}} must be greater than or equal to {{#limit}}'
93
- },
94
- 'number.max': {
95
- code: 'NUMBER_TOO_LARGE',
96
- message: '{{#label}} must be less than or equal to {{#limit}}'
97
- },
98
- 'number.integer': {
99
- code: 'NUMBER_NOT_INTEGER',
100
- message: '{{#label}} must be an integer'
101
- },
102
- 'number.positive': {
103
- code: 'NUMBER_NOT_POSITIVE',
104
- message: '{{#label}} must be a positive number'
105
- },
106
- 'number.negative': {
107
- code: 'NUMBER_NOT_NEGATIVE',
108
- message: '{{#label}} must be a negative number'
109
- },
110
-
111
- // ========== 布尔类型错误 ==========
112
- 'boolean.base': {
113
- code: 'BOOLEAN_INVALID_TYPE',
114
- message: '{{#label}} must be a boolean'
115
- },
116
-
117
- // ========== 对象类型错误 ==========
118
- 'object.base': {
119
- code: 'OBJECT_INVALID_TYPE',
120
- message: '{{#label}} must be an object'
121
- },
122
- 'object.min': {
123
- code: 'OBJECT_TOO_FEW_KEYS',
124
- message: '{{#label}} must have at least {{#limit}} keys'
125
- },
126
- 'object.max': {
127
- code: 'OBJECT_TOO_MANY_KEYS',
128
- message: '{{#label}} must have at most {{#limit}} keys'
129
- },
130
- 'object.unknown': {
131
- code: 'OBJECT_UNKNOWN_KEY',
132
- message: '{{#label}} contains unknown key: {{#key}}'
133
- },
134
-
135
- // ========== 数组类型错误 ==========
136
- 'array.base': {
137
- code: 'ARRAY_INVALID_TYPE',
138
- message: '{{#label}} must be an array'
139
- },
140
- 'array.min': {
141
- code: 'ARRAY_TOO_FEW_ITEMS',
142
- message: '{{#label}} must have at least {{#limit}} items'
143
- },
144
- 'array.max': {
145
- code: 'ARRAY_TOO_MANY_ITEMS',
146
- message: '{{#label}} must have at most {{#limit}} items'
147
- },
148
- 'array.length': {
149
- code: 'ARRAY_INVALID_LENGTH',
150
- message: '{{#label}} must have exactly {{#limit}} items'
151
- },
152
- 'array.unique': {
153
- code: 'ARRAY_NOT_UNIQUE',
154
- message: '{{#label}} must contain unique items'
155
- },
156
-
157
- // ========== 日期类型错误 ==========
158
- 'date.base': {
159
- code: 'DATE_INVALID_TYPE',
160
- message: '{{#label}} must be a valid date'
161
- },
162
- 'date.min': {
163
- code: 'DATE_TOO_EARLY',
164
- message: '{{#label}} must be on or after {{#limit}}'
165
- },
166
- 'date.max': {
167
- code: 'DATE_TOO_LATE',
168
- message: '{{#label}} must be on or before {{#limit}}'
169
- },
170
-
171
- // ========== 通用错误 ==========
172
- 'type': {
173
- code: 'INVALID_TYPE',
174
- message: '{{#label}} must be of type {{#expected}}'
175
- },
176
- 'any.required': {
177
- code: 'FIELD_REQUIRED',
178
- message: '{{#label}} is required'
179
- },
180
- 'any.invalid': {
181
- code: 'FIELD_INVALID',
182
- message: '{{#label}} contains an invalid value'
183
- },
184
- 'any.only': {
185
- code: 'FIELD_NOT_MATCH',
186
- message: '{{#label}} must match {{#valids}}'
187
- },
188
- 'any.unknown': {
189
- code: 'FIELD_UNKNOWN',
190
- message: '{{#key}} is not allowed'
191
- },
192
-
193
- // ========== 自定义验证错误 ==========
194
- 'custom.validation': {
195
- code: 'CUSTOM_VALIDATION_FAILED',
196
- message: 'Validation failed'
197
- }
198
- };
199
-
200
- /**
201
- * 获取错误信息
202
- * @param {string} type - 错误类型
203
- * @returns {Object} 错误信息
204
- */
205
- function getErrorInfo(type) {
206
- const errorInfo = ERROR_CODES[type];
207
- if (!errorInfo) {
208
- return {
209
- code: 'UNKNOWN_ERROR',
210
- message: 'Unknown validation error'
211
- };
212
- }
213
-
214
- return {
215
- code: errorInfo.code,
216
- message: errorInfo.message
217
- };
218
- }
219
-
220
- /**
221
- * 获取所有错误码
222
- * @returns {Object} 所有错误码
223
- */
224
- function getAllErrorCodes() {
225
- return ERROR_CODES;
226
- }
227
-
228
- module.exports = {
229
- ERROR_CODES,
230
- getErrorInfo,
231
- getAllErrorCodes
232
- };
233
-
@@ -1,445 +0,0 @@
1
- // lib/core/ErrorFormatter.js
2
-
3
- const CONSTANTS = require('../config/constants');
4
- const defaultLocales = require('../locales');
5
-
6
- /**
7
- * 错误格式化器
8
- * 将验证错误格式化为友好的消息
9
- *
10
- * @class ErrorFormatter
11
- */
12
- class ErrorFormatter {
13
- constructor(locale = 'zh-CN') {
14
- this.locale = locale;
15
- this.messages = this._loadMessages(locale);
16
- }
17
-
18
- /**
19
- * 加载错误消息模板
20
- * @private
21
- * @param {string} locale - 语言环境
22
- * @returns {Object} 消息模板
23
- */
24
- _loadMessages(locale) {
25
- // 优先使用 Locale 类中注册的语言包
26
- const Locale = require('./Locale');
27
- const registered = Locale.locales[locale];
28
- const defaults = defaultLocales[locale] || defaultLocales['en-US'];
29
-
30
- if (registered) {
31
- return { ...defaults, ...registered };
32
- }
33
-
34
- // 其次使用默认语言包
35
- return defaults;
36
- }
37
-
38
- /**
39
- * 格式化单个错误或错误数组
40
- * @param {Object|Array<Object>} error - 错误对象或错误数组
41
- * @param {string|Object} [localeOrOptions] - 语言代码字符串或选项对象
42
- * @param {string} [localeOrOptions.locale] - 动态指定语言(可选)
43
- * @param {Object} [localeOrOptions.messages] - 自定义错误消息(可选)
44
- * @returns {string|Array<Object>} 格式化后的错误消息或错误对象数组
45
- */
46
- format(error, localeOrOptions) {
47
-
48
- // ✅ 支持两种调用方式:
49
- // 1. format(errors, 'zh-CN') - 旧版兼容
50
- // 2. format(errors, { locale: 'zh-CN', messages: {...} }) - 新版增强
51
- let locale;
52
- let customMessages;
53
-
54
- if (typeof localeOrOptions === 'string') {
55
- // 旧版:直接传入语言代码
56
- locale = localeOrOptions;
57
- } else if (typeof localeOrOptions === 'object' && localeOrOptions !== null) {
58
- // 新版:传入选项对象
59
- locale = localeOrOptions.locale;
60
- customMessages = localeOrOptions.messages;
61
- }
62
-
63
- // 如果是数组,格式化为详细对象数组
64
- if (Array.isArray(error)) {
65
- return this.formatDetailed(error, locale, customMessages);
66
- }
67
-
68
- // 获取当前使用的消息模板
69
- const messages = locale ? this._loadMessages(locale) : this.messages;
70
-
71
- // 合并自定义消息
72
- const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
73
-
74
- // 单个错误对象格式化
75
- const template = finalMessages[error.type] || error.message;
76
- return this._interpolate(template, {
77
- ...error,
78
- path: error.path || 'value',
79
- allowed: Array.isArray(error.allowed) ? error.allowed.join(', ') : error.allowed
80
- });
81
- }
82
-
83
- /**
84
- * 格式化所有错误
85
- * @param {Array<Object>} errors - 错误数组
86
- * @param {string} [locale] - 动态指定语言(可选)
87
- * @returns {Array<string>} 格式化后的错误消息数组
88
- */
89
- formatAll(errors, locale) {
90
- return errors.map(err => this.format(err, locale));
91
- }
92
-
93
- /**
94
- * 格式化为详细对象
95
- * @param {Array<Object>} errors - ajv错误数组
96
- * @param {string} [locale] - 动态指定语言(可选)
97
- * @param {Object} [customMessages] - 自定义错误消息(可选)
98
- * @returns {Array<Object>} 详细错误对象数组
99
- */
100
- formatDetailed(errors, locale, customMessages) {
101
-
102
- if (!Array.isArray(errors)) {
103
- errors = [errors];
104
- }
105
-
106
- // 过滤冗余的包装错误(v1.0.7.1 增强)
107
- // 当存在具体字段错误时,移除通用的包装错误
108
- const hasConcreteErrors = errors.some(err => {
109
- const keyword = err.keyword;
110
- // 包装错误关键字:if, anyOf, oneOf, allOf
111
- return keyword !== 'if' &&
112
- keyword !== 'anyOf' &&
113
- keyword !== 'oneOf' &&
114
- keyword !== 'error';
115
- });
116
-
117
- if (hasConcreteErrors) {
118
- // 过滤掉包装错误(这些是通用的组合验证错误)
119
- errors = errors.filter(err => {
120
- const keyword = err.keyword;
121
- return keyword !== 'if' &&
122
- keyword !== 'anyOf' &&
123
- keyword !== 'oneOf';
124
- });
125
- }
126
-
127
- // 获取当前使用的消息模板
128
- const messages = locale ? this._loadMessages(locale) : this.messages;
129
-
130
- // ✅ 合并参数传入的自定义消息(在整个 map 循环中使用)
131
- const finalMessages = customMessages ? { ...messages, ...customMessages } : messages;
132
-
133
- return errors.map(err => {
134
- // 处理 ajv 错误格式
135
- const keyword = err.keyword || err.type || 'validation';
136
- const instancePath = err.instancePath || err.path || '';
137
-
138
- // ✅ 修复:正确处理 required 错误的字段名
139
- let fieldName;
140
- if (keyword === 'required' && err.params && err.params.missingProperty) {
141
- // required 错误:字段名应该是父路径 + 缺失属性
142
- const parentPath = instancePath.replace(/^\//, '');
143
- const missingProp = err.params.missingProperty;
144
- fieldName = parentPath ? `${parentPath}/${missingProp}` : missingProp;
145
- } else {
146
- // 其他错误:直接使用 instancePath
147
- fieldName = instancePath.replace(/^\//, '') || 'value';
148
- }
149
-
150
- // 获取 Schema 中的自定义信息
151
- let schema = err.parentSchema || err.schema || {};
152
-
153
- // 特殊处理 required 错误
154
- if (keyword === 'required' && err.params && err.params.missingProperty) {
155
- const prop = err.params.missingProperty;
156
- if (schema.properties && schema.properties[prop]) {
157
- schema = schema.properties[prop];
158
- }
159
- }
160
-
161
- let label = schema._label;
162
-
163
- if (label) {
164
- // 如果显式设置了 label,尝试翻译它
165
- if (finalMessages[label]) {
166
- label = finalMessages[label];
167
- }
168
- } else {
169
- // ✅ 修复:对于所有错误类型,label 都应该只显示字段名(不含路径)
170
- let labelKey;
171
- if (keyword === 'required' && err.params && err.params.missingProperty) {
172
- // required 错误:使用 missingProperty(缺失的字段名)
173
- labelKey = err.params.missingProperty;
174
- } else {
175
- // 其他错误:从完整路径中提取最后一级字段名
176
- // 例如: "user/profile/age" -> "age"
177
- // "user/scores/1" -> "1"
178
- const parts = fieldName.split('/');
179
- labelKey = parts[parts.length - 1];
180
- }
181
-
182
- // 如果没有显式设置 label,尝试自动查找翻译 (label.fieldName)
183
- // 将路径分隔符 / 转换为 . (例如 address/city -> address.city)
184
- const autoKey = `label.${labelKey.replace(/\//g, '.')}`;
185
- if (finalMessages[autoKey]) {
186
- label = finalMessages[autoKey];
187
- } else {
188
- // 没找到翻译,回退到字段名
189
- label = labelKey;
190
- }
191
- }
192
-
193
- // Schema 中的自定义消息
194
- const schemaCustomMessages = schema._customMessages || {};
195
-
196
- // ✅ 合并优先级:schemaCustomMessages > finalMessages
197
- // schemaCustomMessages 是字段级的自定义消息,优先级最高
198
- const mergedMessages = { ...finalMessages, ...schemaCustomMessages };
199
-
200
- // 关键字映射 (ajv keyword -> schema-dsl 简写)
201
- // 支持 min/max 作为 minLength/maxLength 的简写
202
- const keywordMap = {
203
- 'minLength': 'min',
204
- 'maxLength': 'max',
205
- 'minimum': 'min',
206
- 'maximum': 'max',
207
- 'minItems': 'min',
208
- 'maxItems': 'max',
209
- 'pattern': 'pattern',
210
- 'format': 'format',
211
- 'required': 'required',
212
- 'enum': 'enum'
213
- };
214
-
215
- const mappedKeyword = keywordMap[keyword] || keyword;
216
- const type = schema.type || 'string';
217
-
218
-
219
- // ✅ 优化:如果 schema 中有自定义消息,优先使用
220
- // 支持三种自定义消息格式:
221
- // 1. 键引用:_customMessages.pattern = "pattern.objectId" → 查找语言包中的 "pattern.objectId"
222
- // 2. 模板字符串:_customMessages.min = "{{#label}}必须大于{{#limit}}" → 直接使用并插值
223
- // 3. 最终消息:_customMessages.pattern = "手机号格式不正确" → 直接使用
224
- let message;
225
-
226
- // 1. 首先检查 schema 是否为该 keyword 定义了自定义消息
227
- let customValue = schemaCustomMessages[keyword] || schemaCustomMessages[mappedKeyword];
228
-
229
- if (customValue) {
230
- // 尝试从 mergedMessages 中查找这个键
231
- const lookupResult = mergedMessages[customValue];
232
-
233
- if (lookupResult) {
234
- // 找到了,说明它是一个键引用
235
- message = lookupResult;
236
- } else {
237
- // 没找到,说明它本身就是模板或最终消息,直接使用
238
- message = customValue;
239
- }
240
- }
241
-
242
- // 2. 如果没有自定义消息,按照通用查找顺序
243
- if (!message) {
244
- message = mergedMessages[`${type}.${keyword}`] ||
245
- mergedMessages[`${type}.${mappedKeyword}`] ||
246
- mergedMessages[mappedKeyword] ||
247
- mergedMessages[keyword] ||
248
- mergedMessages['default'];
249
- }
250
-
251
- // 自动查找 format 类型的消息 (例如 format.email)
252
- if (!message && mappedKeyword === 'format' && err.params && err.params.format) {
253
- let formatName = err.params.format;
254
- // 映射 uri -> url
255
- if (formatName === 'uri') formatName = 'url';
256
-
257
- const formatKey = `format.${formatName}`;
258
-
259
- // 优先查找 mergedMessages 中的 format.email
260
- if (mergedMessages[formatKey]) {
261
- message = mergedMessages[formatKey];
262
- }
263
- }
264
-
265
- if (!message) {
266
- // 使用默认模板
267
- const template = mergedMessages[mappedKeyword] || mergedMessages[keyword] || err.message || 'Validation error';
268
- message = template;
269
- } else {
270
- // 检查 message 是否为 key (包含点号且无空格,或者是已知的 key)
271
- // 如果是 key,尝试从 messages 中查找
272
- if (typeof message === 'string' && (message.includes('.') || mergedMessages[message])) {
273
-
274
- let translated = mergedMessages[message];
275
-
276
- // 尝试回退查找 (例如 pattern.phone.cn -> pattern.phone)
277
- if (!translated && message.includes('.')) {
278
- const parts = message.split('.');
279
- while (parts.length > 1 && !translated) {
280
- parts.pop();
281
- const fallbackKey = parts.join('.');
282
- translated = mergedMessages[fallbackKey];
283
- }
284
- }
285
-
286
- if (translated) {
287
- message = translated;
288
- }
289
- }
290
- }
291
-
292
- // 插值替换
293
- const limit = err.params ? (err.params.limit || err.params.limitLength || err.params.comparison) : undefined;
294
-
295
- const interpolateData = {
296
- ...err.params,
297
- path: label, // 使用 label 替换 path
298
- label: label,
299
- value: err.data,
300
- limit: limit,
301
- // 映射 min/max 以匹配模板
302
- min: limit,
303
- max: limit,
304
- expected: err.params ? err.params.type : undefined,
305
- // ✅ 修复:添加 actual 参数 - 实际接收到的数据类型
306
- actual: err.data === null ? 'null' :
307
- err.data === undefined ? 'undefined' :
308
- Array.isArray(err.data) ? 'array' :
309
- typeof err.data,
310
- // ✅ 修复 enum 错误:将 allowedValues 映射为 valids 和 allowed
311
- valids: err.params && err.params.allowedValues ? err.params.allowedValues.join(', ') : undefined,
312
- allowed: err.params && err.params.allowedValues ? err.params.allowedValues.join(', ') : undefined,
313
- // ✅ 修复 additionalProperties 错误:将 additionalProperty 映射为 key
314
- key: err.params && err.params.additionalProperty ? err.params.additionalProperty : undefined
315
- };
316
-
317
- message = this._interpolate(message, interpolateData);
318
-
319
- return {
320
- path: fieldName,
321
- message: message,
322
- keyword: keyword,
323
- params: err.params
324
- };
325
- });
326
- }
327
-
328
- /**
329
- * 格式化为分组对象
330
- * @param {Array<Object>} errors - 错误数组
331
- * @returns {Object} 按路径分组的错误
332
- */
333
- formatGrouped(errors) {
334
- const grouped = {};
335
-
336
- for (const error of errors) {
337
- const path = error.path || 'value';
338
- if (!grouped[path]) {
339
- grouped[path] = [];
340
- }
341
- grouped[path].push(this.format(error));
342
- }
343
-
344
- return grouped;
345
- }
346
-
347
- /**
348
- * 格式化为纯文本
349
- * @param {Array<Object>} errors - 错误数组
350
- * @param {string} [separator='\n'] - 分隔符
351
- * @returns {string} 纯文本错误消息
352
- */
353
- formatText(errors, separator = '\n') {
354
- return this.formatAll(errors).join(separator);
355
- }
356
-
357
- /**
358
- * 格式化为JSON字符串
359
- * @param {Array<Object>} errors - 错误数组
360
- * @param {boolean} [pretty=false] - 是否美化
361
- * @returns {string} JSON字符串
362
- */
363
- formatJSON(errors, pretty = false) {
364
- const formatted = this.formatDetailed(errors);
365
- return pretty ? JSON.stringify(formatted, null, 2) : JSON.stringify(formatted);
366
- }
367
-
368
- /**
369
- * 格式化为HTML列表
370
- * @param {Array<Object>} errors - 错误数组
371
- * @returns {string} HTML字符串
372
- */
373
- formatHTML(errors) {
374
- const items = this.formatAll(errors)
375
- .map(msg => ` <li>${this._escapeHTML(msg)}</li>`)
376
- .join('\n');
377
-
378
- return `<ul class="validation-errors">\n${items}\n</ul>`;
379
- }
380
-
381
- /**
382
- * 插值替换
383
- * @private
384
- * @param {string} template - 模板字符串
385
- * @param {Object} data - 数据对象
386
- * @returns {string} 替换后的字符串
387
- */
388
- _interpolate(template, data) {
389
- if (!template || typeof template !== 'string') {
390
- return data.message || 'Validation error';
391
- }
392
-
393
-
394
- // 支持 {{#key}} 和 {key} 两种格式
395
- return template.replace(/\{\{?#?(\w+)\}?\}/g, (match, key) => {
396
- return data[key] !== undefined ? data[key] : match;
397
- });
398
- }
399
-
400
- /**
401
- * HTML转义
402
- * @private
403
- * @param {string} text - 文本
404
- * @returns {string} 转义后的文本
405
- */
406
- _escapeHTML(text) {
407
- const map = {
408
- '&': '&amp;',
409
- '<': '&lt;',
410
- '>': '&gt;',
411
- '"': '&quot;',
412
- "'": '&#039;'
413
- };
414
- return text.replace(/[&<>"']/g, char => map[char]);
415
- }
416
-
417
- /**
418
- * 设置语言环境
419
- * @param {string} locale - 语言环境
420
- */
421
- setLocale(locale) {
422
- this.locale = locale;
423
- this.messages = this._loadMessages(locale);
424
- }
425
-
426
- /**
427
- * 添加自定义消息模板
428
- * @param {string} type - 错误类型
429
- * @param {string} template - 消息模板
430
- */
431
- addMessage(type, template) {
432
- this.messages[type] = template;
433
- }
434
-
435
- /**
436
- * 批量添加自定义消息模板
437
- * @param {Object} messages - 消息模板对象
438
- */
439
- addMessages(messages) {
440
- Object.assign(this.messages, messages);
441
- }
442
- }
443
-
444
- module.exports = ErrorFormatter;
445
-