schema-dsl 1.2.5 → 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 (243) hide show
  1. package/CHANGELOG.md +130 -238
  2. package/LICENSE +21 -21
  3. package/README.md +628 -2486
  4. package/dist/DslBuilder-BIgQOAXp.d.ts +343 -0
  5. package/dist/DslBuilder-CjHTucNQ.d.cts +343 -0
  6. package/dist/Validator-CllRdrY0.d.ts +192 -0
  7. package/dist/Validator-D6okG9tr.d.cts +192 -0
  8. package/dist/index.cjs +6640 -0
  9. package/dist/index.d.cts +1151 -0
  10. package/dist/index.d.ts +1151 -0
  11. package/dist/index.js +6574 -0
  12. package/dist/plugin-CIKtTMtS.d.cts +246 -0
  13. package/dist/plugin-CIKtTMtS.d.ts +246 -0
  14. package/dist/plugins/custom-format.cjs +3818 -0
  15. package/dist/plugins/custom-format.d.cts +12 -0
  16. package/dist/plugins/custom-format.d.ts +12 -0
  17. package/dist/plugins/custom-format.js +3788 -0
  18. package/dist/plugins/custom-type-example.cjs +3811 -0
  19. package/dist/plugins/custom-type-example.d.cts +8 -0
  20. package/dist/plugins/custom-type-example.d.ts +8 -0
  21. package/dist/plugins/custom-type-example.js +3781 -0
  22. package/dist/plugins/custom-validator.cjs +144 -0
  23. package/dist/plugins/custom-validator.d.cts +10 -0
  24. package/dist/plugins/custom-validator.d.ts +10 -0
  25. package/dist/plugins/custom-validator.js +119 -0
  26. package/docs/FEATURE-INDEX.md +553 -519
  27. package/docs/add-custom-locale.md +496 -483
  28. package/docs/add-keyword.md +24 -0
  29. package/docs/api-reference.md +1047 -805
  30. package/docs/api.md +13 -0
  31. package/docs/best-practices-project-structure.md +417 -408
  32. package/docs/best-practices.md +712 -672
  33. package/docs/cache-manager.md +344 -336
  34. package/docs/compile.md +45 -0
  35. package/docs/conditional-api.md +1307 -1278
  36. package/docs/custom-extensions-guide.md +339 -411
  37. package/docs/design-philosophy.md +606 -601
  38. package/docs/doc-index.md +324 -0
  39. package/docs/dsl-syntax.md +714 -664
  40. package/docs/dynamic-locale.md +608 -598
  41. package/docs/enum.md +482 -475
  42. package/docs/error-handling.md +1975 -1966
  43. package/docs/export-guide.md +501 -462
  44. package/docs/export-limitations.md +567 -551
  45. package/docs/faq.md +596 -577
  46. package/docs/frontend-i18n-guide.md +307 -293
  47. package/docs/i18n-user-guide.md +487 -474
  48. package/docs/i18n.md +476 -457
  49. package/docs/index.md +48 -0
  50. package/docs/json-schema-basics.md +40 -0
  51. package/docs/label-vs-description.md +271 -262
  52. package/docs/markdown-exporter.md +406 -397
  53. package/docs/mongodb-exporter.md +302 -295
  54. package/docs/multi-language.md +26 -0
  55. package/docs/multi-type-support.md +322 -329
  56. package/docs/mysql-exporter.md +280 -273
  57. package/docs/number-operators.md +449 -442
  58. package/docs/optional-marker-guide.md +326 -321
  59. package/docs/performance-guide.md +49 -0
  60. package/docs/plugin-system.md +381 -542
  61. package/docs/plugin-type-registration.md +34 -0
  62. package/docs/postgresql-exporter.md +311 -304
  63. package/docs/public/favicon.svg +5 -0
  64. package/docs/quick-start.md +435 -761
  65. package/docs/runtime-locale-support.md +532 -521
  66. package/docs/schema-helper.md +345 -340
  67. package/docs/schema-utils-advanced-issues.md +23 -0
  68. package/docs/schema-utils-best-practices.md +20 -0
  69. package/docs/schema-utils-chaining.md +150 -143
  70. package/docs/schema-utils.md +524 -490
  71. package/docs/security-checklist.md +20 -0
  72. package/docs/string-extensions.md +488 -480
  73. package/docs/troubleshooting.md +486 -471
  74. package/docs/type-converter.md +310 -319
  75. package/docs/type-reference.md +242 -219
  76. package/docs/typescript-guide.md +584 -573
  77. package/docs/union-type-guide.md +157 -147
  78. package/docs/union-types.md +284 -277
  79. package/docs/validate-async.md +491 -480
  80. package/docs/validate-batch.md +49 -0
  81. package/docs/validate-dsl-object-support.md +578 -573
  82. package/docs/validate.md +506 -486
  83. package/docs/validation-guide.md +502 -484
  84. package/docs/validator.md +39 -0
  85. package/package.json +131 -73
  86. package/plugins/custom-format.cjs +8 -0
  87. package/plugins/custom-type-example.cjs +8 -0
  88. package/plugins/custom-validator.cjs +8 -0
  89. package/src/adapters/DslAdapter.ts +111 -0
  90. package/src/adapters/index.ts +1 -0
  91. package/src/config/constants.ts +83 -0
  92. package/src/config/index.ts +2 -0
  93. package/src/config/patterns.ts +77 -0
  94. package/src/core/CacheManager.ts +169 -0
  95. package/src/core/ConditionalBuilder.ts +382 -0
  96. package/src/core/ConditionalRuntime.ts +28 -0
  97. package/src/core/ConditionalValidator.ts +255 -0
  98. package/src/core/DslBuilder.ts +687 -0
  99. package/src/core/ErrorCodes.ts +38 -0
  100. package/src/core/ErrorFormatter.ts +271 -0
  101. package/src/core/JSONSchemaCore.ts +65 -0
  102. package/src/core/Locale.ts +187 -0
  103. package/src/core/MessageTemplate.ts +42 -0
  104. package/src/core/ObjectDslBuilder.ts +64 -0
  105. package/src/core/PluginManager.ts +326 -0
  106. package/src/core/StringExtensions.ts +140 -0
  107. package/src/core/TemplateEngine.ts +44 -0
  108. package/src/core/Validator.ts +448 -0
  109. package/src/errors/I18nError.ts +159 -0
  110. package/src/errors/ValidationError.ts +105 -0
  111. package/src/exporters/BaseExporter.ts +60 -0
  112. package/src/exporters/MarkdownExporter.ts +305 -0
  113. package/src/exporters/MongoDBExporter.ts +126 -0
  114. package/src/exporters/MySQLExporter.ts +156 -0
  115. package/src/exporters/PostgreSQLExporter.ts +222 -0
  116. package/src/exporters/index.ts +18 -0
  117. package/src/index.ts +651 -0
  118. package/{lib/locales/en-US.js → src/locales/en-US.ts} +160 -176
  119. package/{lib/locales/es-ES.js → src/locales/es-ES.ts} +160 -113
  120. package/{lib/locales/fr-FR.js → src/locales/fr-FR.ts} +160 -113
  121. package/src/locales/index.ts +103 -0
  122. package/{lib/locales/ja-JP.js → src/locales/ja-JP.ts} +160 -118
  123. package/src/locales/types.ts +156 -0
  124. package/{lib/locales/zh-CN.js → src/locales/zh-CN.ts} +160 -177
  125. package/src/parser/ConstraintParser.ts +101 -0
  126. package/src/parser/DslParser.ts +470 -0
  127. package/src/parser/SchemaCompiler.ts +66 -0
  128. package/src/parser/TypeRegistry.ts +250 -0
  129. package/src/parser/index.ts +6 -0
  130. package/src/plugins/custom-format.ts +124 -0
  131. package/src/plugins/custom-type-example.ts +106 -0
  132. package/src/plugins/custom-validator.ts +138 -0
  133. package/src/types/conditional.ts +28 -0
  134. package/src/types/config.ts +59 -0
  135. package/src/types/dsl.ts +131 -0
  136. package/src/types/error.ts +60 -0
  137. package/src/types/index.ts +17 -0
  138. package/src/types/infer.ts +128 -0
  139. package/src/types/plugin.ts +58 -0
  140. package/src/types/safe-regex.d.ts +9 -0
  141. package/src/types/schema.ts +66 -0
  142. package/src/types/validate.ts +71 -0
  143. package/src/utils/SchemaHelper.ts +196 -0
  144. package/src/utils/SchemaUtils.ts +365 -0
  145. package/src/utils/TypeConverter.ts +215 -0
  146. package/src/utils/index.ts +10 -0
  147. package/src/validators/CustomKeywords.ts +477 -0
  148. package/.eslintignore +0 -11
  149. package/.eslintrc.json +0 -27
  150. package/CONTRIBUTING.md +0 -368
  151. package/STATUS.md +0 -491
  152. package/changelogs/v1.0.0.md +0 -328
  153. package/changelogs/v1.0.9.md +0 -367
  154. package/changelogs/v1.1.0.md +0 -389
  155. package/changelogs/v1.1.1.md +0 -308
  156. package/changelogs/v1.1.2.md +0 -183
  157. package/changelogs/v1.1.3.md +0 -161
  158. package/changelogs/v1.1.4.md +0 -432
  159. package/changelogs/v1.1.5.md +0 -493
  160. package/changelogs/v1.1.6.md +0 -211
  161. package/changelogs/v1.1.8.md +0 -376
  162. package/changelogs/v1.2.3.md +0 -124
  163. package/docs/INDEX.md +0 -252
  164. package/docs/issues-resolved-summary.md +0 -196
  165. package/docs/performance-benchmark-report.md +0 -179
  166. package/docs/performance-quick-reference.md +0 -123
  167. package/docs/user-questions-answered.md +0 -353
  168. package/docs/validation-rules-v1.0.2.md +0 -1608
  169. package/examples/README.md +0 -81
  170. package/examples/array-dsl-example.js +0 -227
  171. package/examples/conditional-example.js +0 -288
  172. package/examples/conditional-non-object.js +0 -129
  173. package/examples/conditional-validate-example.js +0 -321
  174. package/examples/custom-extension.js +0 -85
  175. package/examples/dsl-match-example.js +0 -74
  176. package/examples/dsl-style.js +0 -118
  177. package/examples/dynamic-locale-configuration.js +0 -348
  178. package/examples/dynamic-locale-example.js +0 -287
  179. package/examples/enum.examples.js +0 -324
  180. package/examples/export-demo.js +0 -130
  181. package/examples/express-integration.js +0 -376
  182. package/examples/i18n-error-handling-complete.js +0 -381
  183. package/examples/i18n-error-handling-quickstart.md +0 -0
  184. package/examples/i18n-error.examples.js +0 -181
  185. package/examples/i18n-full-demo.js +0 -301
  186. package/examples/i18n-memory-safety.examples.js +0 -268
  187. package/examples/markdown-export.js +0 -71
  188. package/examples/middleware-usage.js +0 -93
  189. package/examples/new-features-comparison.js +0 -315
  190. package/examples/password-reset/README.md +0 -153
  191. package/examples/password-reset/schema.js +0 -26
  192. package/examples/password-reset/test.js +0 -101
  193. package/examples/plugin-system.examples.js +0 -205
  194. package/examples/schema-utils-chaining.examples.js +0 -250
  195. package/examples/simple-example.js +0 -122
  196. package/examples/slug.examples.js +0 -179
  197. package/examples/string-extensions.js +0 -297
  198. package/examples/union-type-example.js +0 -127
  199. package/examples/union-types-example.js +0 -77
  200. package/examples/user-registration/README.md +0 -156
  201. package/examples/user-registration/routes.js +0 -92
  202. package/examples/user-registration/schema.js +0 -150
  203. package/examples/user-registration/server.js +0 -74
  204. package/index.d.ts +0 -3658
  205. package/index.js +0 -475
  206. package/index.mjs +0 -60
  207. package/lib/adapters/DslAdapter.js +0 -995
  208. package/lib/adapters/index.js +0 -20
  209. package/lib/config/constants.js +0 -286
  210. package/lib/config/patterns/common.js +0 -47
  211. package/lib/config/patterns/creditCard.js +0 -9
  212. package/lib/config/patterns/idCard.js +0 -9
  213. package/lib/config/patterns/index.js +0 -9
  214. package/lib/config/patterns/licensePlate.js +0 -4
  215. package/lib/config/patterns/passport.js +0 -4
  216. package/lib/config/patterns/phone.js +0 -9
  217. package/lib/config/patterns/postalCode.js +0 -5
  218. package/lib/core/CacheManager.js +0 -376
  219. package/lib/core/ConditionalBuilder.js +0 -503
  220. package/lib/core/DslBuilder.js +0 -1589
  221. package/lib/core/ErrorCodes.js +0 -233
  222. package/lib/core/ErrorFormatter.js +0 -445
  223. package/lib/core/JSONSchemaCore.js +0 -347
  224. package/lib/core/Locale.js +0 -130
  225. package/lib/core/MessageTemplate.js +0 -98
  226. package/lib/core/PluginManager.js +0 -448
  227. package/lib/core/StringExtensions.js +0 -240
  228. package/lib/core/Validator.js +0 -654
  229. package/lib/errors/I18nError.js +0 -328
  230. package/lib/errors/ValidationError.js +0 -191
  231. package/lib/exporters/MarkdownExporter.js +0 -420
  232. package/lib/exporters/MongoDBExporter.js +0 -162
  233. package/lib/exporters/MySQLExporter.js +0 -212
  234. package/lib/exporters/PostgreSQLExporter.js +0 -289
  235. package/lib/exporters/index.js +0 -24
  236. package/lib/locales/index.js +0 -8
  237. package/lib/utils/LRUCache.js +0 -174
  238. package/lib/utils/SchemaHelper.js +0 -240
  239. package/lib/utils/SchemaUtils.js +0 -445
  240. package/lib/utils/TypeConverter.js +0 -245
  241. package/lib/utils/index.js +0 -13
  242. package/lib/validators/CustomKeywords.js +0 -616
  243. 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
-