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,1589 +0,0 @@
1
- /**
2
- * DSL Builder - 统一的Schema构建器
3
- *
4
- * 支持链式调用扩展DSL功能
5
- *
6
- * @module lib/core/DslBuilder
7
- * @version 2.0.0
8
- *
9
- * @example
10
- * // 简单使用
11
- * const schema = dsl('email!');
12
- *
13
- * // 链式扩展
14
- * const schema = dsl('email!')
15
- * .pattern(/custom/)
16
- * .messages({ 'string.pattern': '格式不正确' })
17
- * .label('邮箱地址');
18
- */
19
-
20
- const ErrorCodes = require("./ErrorCodes");
21
- const MessageTemplate = require("./MessageTemplate");
22
- const Locale = require("./Locale");
23
- const patterns = require("../config/patterns");
24
-
25
- class DslBuilder {
26
- /**
27
- * schema-dsl 自定义验证关键字集合(非 JSON Schema 标准字段)
28
- *
29
- * toJsonSchema() 使用此集合过滤非标准字段,确保输出纯净的 JSON Schema。
30
- * @private
31
- * @type {Set<string>}
32
- */
33
- static _internalKeys = new Set([
34
- "exactLength",
35
- "alphanum",
36
- "lowercase",
37
- "uppercase",
38
- "trim",
39
- "jsonString",
40
- "port",
41
- "requiredAll",
42
- "strictSchema",
43
- "noSparse",
44
- "includesRequired",
45
- "dateFormat",
46
- "dateGreater",
47
- "dateLess",
48
- "precision",
49
- "multipleOf",
50
- ]);
51
-
52
- /**
53
- * 静态属性:存储用户自定义类型(插件注册)
54
- * @private
55
- * @type {Map<string, Object|Function>}
56
- */
57
- static _customTypes = new Map();
58
-
59
- /**
60
- * 注册自定义类型(供插件使用)
61
- * @param {string} name - 类型名称
62
- * @param {Object|Function} schema - JSON Schema对象 或 生成函数
63
- * @throws {Error} 类型名称无效时抛出错误
64
- *
65
- * @example
66
- * // 插件中注册自定义类型
67
- * DslBuilder.registerType('phone-cn', {
68
- * type: 'string',
69
- * pattern: '^1[3-9]\\d{9}$'
70
- * });
71
- *
72
- * // 在DSL中使用
73
- * dsl('phone-cn!') // ✅ 可用
74
- * dsl('types:string|phone-cn') // ✅ 可用
75
- */
76
- static registerType(name, schema) {
77
- if (!name || typeof name !== "string") {
78
- throw new Error("Type name must be a non-empty string");
79
- }
80
-
81
- if (
82
- !schema ||
83
- (typeof schema !== "object" && typeof schema !== "function")
84
- ) {
85
- throw new Error("Schema must be an object or function");
86
- }
87
-
88
- this._customTypes.set(name, schema);
89
- }
90
-
91
- /**
92
- * 检查类型是否已注册(内置或自定义)
93
- * @param {string} type - 类型名称
94
- * @returns {boolean}
95
- *
96
- * @example
97
- * DslBuilder.hasType('string') // true (内置)
98
- * DslBuilder.hasType('phone-cn') // false (未注册)
99
- *
100
- * DslBuilder.registerType('phone-cn', { ... });
101
- * DslBuilder.hasType('phone-cn') // true (已注册)
102
- */
103
- static hasType(type) {
104
- // 检查自定义类型
105
- if (this._customTypes.has(type)) {
106
- return true;
107
- }
108
-
109
- // 检查内置类型
110
- const builtInTypes = [
111
- "string",
112
- "number",
113
- "integer",
114
- "boolean",
115
- "object",
116
- "array",
117
- "null",
118
- "email",
119
- "url",
120
- "uuid",
121
- "date",
122
- "datetime",
123
- "time",
124
- "ipv4",
125
- "ipv6",
126
- "binary",
127
- "objectId",
128
- "hexColor",
129
- "macAddress",
130
- "cron",
131
- "slug",
132
- "alphanum",
133
- "lower",
134
- "upper",
135
- "json",
136
- "port",
137
- "phone",
138
- "idCard",
139
- "creditCard",
140
- "licensePlate",
141
- "postalCode",
142
- "passport",
143
- "any",
144
- ];
145
-
146
- return builtInTypes.includes(type);
147
- }
148
-
149
- /**
150
- * 获取所有已注册的自定义类型
151
- * @returns {Array<string>}
152
- */
153
- static getCustomTypes() {
154
- return Array.from(this._customTypes.keys());
155
- }
156
-
157
- /**
158
- * 清除所有自定义类型(主要用于测试)
159
- */
160
- static clearCustomTypes() {
161
- this._customTypes.clear();
162
- }
163
-
164
- /**
165
- * 创建 DslBuilder 实例
166
- * @param {string} dslString - DSL字符串,如 'string:3-32!' 或 'email!'
167
- */
168
- constructor(dslString) {
169
- if (!dslString || typeof dslString !== "string") {
170
- throw new Error("DSL string is required");
171
- }
172
-
173
- // 解析DSL字符串
174
- const trimmed = dslString.trim();
175
-
176
- // 特殊处理:array!数字 → array:数字 + 必填
177
- // 例如:array!1-10 → array:1-10!
178
- let processedDsl = trimmed;
179
- if (/^array![\d-]/.test(trimmed)) {
180
- processedDsl = trimmed.replace(/^array!/, "array:") + "!";
181
- }
182
-
183
- // 🔴 处理必填标记 ! 和可选标记 ?
184
- // 优先级:! > ?(如果同时存在,! 优先)
185
- this._required = processedDsl.endsWith("!");
186
- this._optional = processedDsl.endsWith("?") && !this._required;
187
-
188
- let dslWithoutMarker = processedDsl;
189
- if (this._required) {
190
- dslWithoutMarker = processedDsl.slice(0, -1);
191
- } else if (this._optional) {
192
- dslWithoutMarker = processedDsl.slice(0, -1);
193
- }
194
-
195
- // 简单解析为基础Schema(避免循环依赖)
196
- this._baseSchema = this._parseSimple(dslWithoutMarker);
197
-
198
- // 扩展属性
199
- this._customMessages = {};
200
- this._label = null;
201
- this._customValidators = [];
202
- this._description = null;
203
- this._whenConditions = [];
204
- }
205
-
206
- /**
207
- * 简单解析DSL字符串(避免循环依赖)
208
- * @private
209
- * @param {string} dsl - DSL字符串(不含!)
210
- * @returns {Object} JSON Schema对象
211
- */
212
- _parseSimple(dsl) {
213
- // 🔴 处理跨类型联合:types:type1|type2|type3
214
- if (dsl.startsWith("types:")) {
215
- const typesStr = dsl.substring(6); // 去掉 'types:' 前缀
216
- const types = typesStr
217
- .split("|")
218
- .map((t) => t.trim())
219
- .filter((t) => t);
220
-
221
- if (types.length === 0) {
222
- throw new Error("types: requires at least one type");
223
- }
224
-
225
- if (types.length === 1) {
226
- // 只有一个类型,直接解析(避免不必要的oneOf)
227
- return this._parseSimple(types[0]);
228
- }
229
-
230
- // 多个类型,生成oneOf结构
231
- return {
232
- oneOf: types.map((type) => this._parseSimple(type)),
233
- };
234
- }
235
-
236
- // 处理数组类型:array:1-10 或 array<string>
237
- if (dsl.startsWith("array")) {
238
- const schema = { type: "array" };
239
-
240
- // 匹配:array:min-max<itemType> 或 array:constraint<itemType> 或 array<itemType>
241
- const arrayMatch = dsl.match(/^array(?::([^<]+?))?(?:<(.+)>)?$/);
242
-
243
- if (arrayMatch) {
244
- const [, constraint, itemType] = arrayMatch;
245
-
246
- // 解析约束
247
- if (constraint) {
248
- const trimmedConstraint = constraint.trim();
249
-
250
- if (trimmedConstraint.includes("-")) {
251
- // 范围约束: min-max, min-, -max
252
- const [min, max] = trimmedConstraint
253
- .split("-")
254
- .map((v) => v.trim());
255
- if (min) schema.minItems = parseInt(min, 10);
256
- if (max) schema.maxItems = parseInt(max, 10);
257
- } else {
258
- // 单个值 = 最大值
259
- schema.maxItems = parseInt(trimmedConstraint, 10);
260
- }
261
- }
262
-
263
- // 解析元素类型
264
- if (itemType) {
265
- schema.items = this._parseSimple(itemType.trim());
266
- }
267
-
268
- return schema;
269
- }
270
- }
271
-
272
- // 处理 enum: 前缀的枚举(支持逗号分隔和管道分隔)
273
- // 例如: enum:a,b,c / enum:admin,user,guest / enum:number:1,2,3 / enum:a|b|c
274
- if (dsl.startsWith("enum:")) {
275
- const enumBody = dsl.slice(5); // 'a,b,c' 或 'number:1,2,3' 或 'a|b|c'
276
-
277
- // 检查是否有类型前缀:enum:number:1,2,3
278
- const colonIdx = enumBody.indexOf(":");
279
- let enumType = "string";
280
- let enumValues;
281
-
282
- if (colonIdx !== -1) {
283
- // enum:type:values
284
- enumType = enumBody.slice(0, colonIdx);
285
- enumValues = enumBody.slice(colonIdx + 1);
286
- } else {
287
- // enum:values (默认 string)
288
- enumValues = enumBody;
289
- }
290
-
291
- // 统一分隔符:逗号 → 管道(_parseEnum 使用管道分隔)
292
- const normalized = enumValues.includes("|")
293
- ? enumValues
294
- : enumValues.replace(/,/g, "|");
295
- return this._parseEnum(enumType, normalized);
296
- }
297
-
298
- // 处理简写枚举(管道分隔,无 enum: 前缀)
299
- // 例如: admin|user|guest / 1|2|3
300
- if (dsl.includes("|")) {
301
- let enumType = "string"; // 默认字符串
302
- let enumValues = dsl;
303
-
304
- if (dsl.includes(":") && !this._isKnownType(dsl.split(":")[0])) {
305
- // 如果有冒号但不是已知类型(如 string:3-32),不作为枚举
306
- // 让后续逻辑处理
307
- } else {
308
- // 简写形式:value1|value2
309
- // 自动识别类型
310
- enumType = this._detectEnumType(enumValues);
311
- }
312
-
313
- // 如果是枚举,解析值
314
- if (enumValues.includes("|")) {
315
- return this._parseEnum(enumType, enumValues);
316
- }
317
- }
318
-
319
- // 处理类型:约束格式
320
- const colonIndex = dsl.indexOf(":");
321
- let type, constraint;
322
-
323
- if (colonIndex === -1) {
324
- type = dsl;
325
- constraint = "";
326
- } else {
327
- type = dsl.substring(0, colonIndex);
328
- constraint = dsl.substring(colonIndex + 1);
329
- }
330
-
331
- // 特殊处理 phone:country
332
- if (type === "phone") {
333
- const country = constraint || "cn";
334
- const config = patterns.phone[country];
335
- if (!config) throw new Error(`Unsupported country: ${country}`);
336
- return {
337
- type: "string",
338
- pattern: config.pattern.source,
339
- minLength: config.min,
340
- maxLength: config.max,
341
- _customMessages: { pattern: config.key || config.msg },
342
- };
343
- }
344
-
345
- // 特殊处理 idCard:country
346
- if (type === "idCard") {
347
- const country = constraint || "cn";
348
- const config = patterns.idCard[country.toLowerCase()];
349
- if (!config)
350
- throw new Error(`Unsupported country for idCard: ${country}`);
351
- return {
352
- type: "string",
353
- pattern: config.pattern.source,
354
- minLength: config.min,
355
- maxLength: config.max,
356
- _customMessages: { pattern: config.key || config.msg },
357
- };
358
- }
359
-
360
- // 特殊处理 creditCard:type
361
- if (type === "creditCard") {
362
- const cardType = constraint || "visa";
363
- const config = patterns.creditCard[cardType.toLowerCase()];
364
- if (!config) throw new Error(`Unsupported credit card type: ${cardType}`);
365
- return {
366
- type: "string",
367
- pattern: config.pattern.source,
368
- _customMessages: { pattern: config.key || config.msg },
369
- };
370
- }
371
-
372
- // 特殊处理 licensePlate:country
373
- if (type === "licensePlate") {
374
- const country = constraint || "cn";
375
- const config = patterns.licensePlate[country.toLowerCase()];
376
- if (!config)
377
- throw new Error(`Unsupported country for licensePlate: ${country}`);
378
- return {
379
- type: "string",
380
- pattern: config.pattern.source,
381
- _customMessages: { pattern: config.key || config.msg },
382
- };
383
- }
384
-
385
- // 特殊处理 postalCode:country
386
- if (type === "postalCode") {
387
- const country = constraint || "cn";
388
- const config = patterns.postalCode[country.toLowerCase()];
389
- if (!config)
390
- throw new Error(`Unsupported country for postalCode: ${country}`);
391
- return {
392
- type: "string",
393
- pattern: config.pattern.source,
394
- _customMessages: { pattern: config.key || config.msg },
395
- };
396
- }
397
-
398
- // 特殊处理 passport:country
399
- if (type === "passport") {
400
- const country = constraint || "cn";
401
- const config = patterns.passport[country.toLowerCase()];
402
- if (!config)
403
- throw new Error(`Unsupported country for passport: ${country}`);
404
- return {
405
- type: "string",
406
- pattern: config.pattern.source,
407
- _customMessages: { pattern: config.key || config.msg },
408
- };
409
- }
410
-
411
- // 获取基础类型
412
- const schema = this._getBaseType(type);
413
-
414
- // 处理约束
415
- if (constraint) {
416
- Object.assign(schema, this._parseConstraint(schema.type, constraint));
417
- }
418
-
419
- return schema;
420
- }
421
-
422
- /**
423
- * 获取基础类型Schema
424
- * @private
425
- */
426
- _getBaseType(type) {
427
- // 🔴 优先查询自定义类型(插件注册的)
428
- if (DslBuilder._customTypes.has(type)) {
429
- const customSchema = DslBuilder._customTypes.get(type);
430
- // 如果是函数,调用它生成Schema
431
- if (typeof customSchema === "function") {
432
- return customSchema();
433
- }
434
- // 否则返回Schema对象的深拷贝(避免污染)
435
- return JSON.parse(JSON.stringify(customSchema));
436
- }
437
-
438
- // 🔴 查询内置类型
439
- const typeMap = {
440
- string: { type: "string" },
441
- number: { type: "number" },
442
- integer: { type: "integer" },
443
- boolean: { type: "boolean" },
444
- object: { type: "object" },
445
- array: { type: "array" },
446
- null: { type: "null" },
447
- email: { type: "string", format: "email" },
448
- url: { type: "string", format: "uri" },
449
- uuid: { type: "string", format: "uuid" },
450
- date: { type: "string", format: "date" },
451
- datetime: { type: "string", format: "date-time" },
452
- time: { type: "string", format: "time" },
453
- ipv4: { type: "string", format: "ipv4" },
454
- ipv6: { type: "string", format: "ipv6" },
455
- binary: { type: "string", contentEncoding: "base64" },
456
- objectId: {
457
- type: "string",
458
- pattern: "^[0-9a-fA-F]{24}$",
459
- _customMessages: { pattern: "pattern.objectId" },
460
- },
461
- hexColor: {
462
- type: "string",
463
- pattern: "^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",
464
- _customMessages: { pattern: "pattern.hexColor" },
465
- },
466
- macAddress: {
467
- type: "string",
468
- pattern: "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$",
469
- _customMessages: { pattern: "pattern.macAddress" },
470
- },
471
- cron: {
472
- type: "string",
473
- pattern:
474
- "^(\\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\\*\\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\\*|([0-9]|1[0-9]|2[0-3])|\\*\\/([0-9]|1[0-9]|2[0-3])) (\\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\\*\\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\\*|([1-9]|1[0-2])|\\*\\/([1-9]|1[0-2])) (\\*|([0-6])|\\*\\/([0-6]))$",
475
- _customMessages: { pattern: "pattern.cron" },
476
- },
477
- slug: {
478
- type: "string",
479
- pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
480
- _customMessages: { pattern: "pattern.slug" },
481
- },
482
- any: {},
483
- // v1.0.2 新增类型
484
- alphanum: { type: "string", alphanum: true },
485
- lower: { type: "string", lowercase: true },
486
- upper: { type: "string", uppercase: true },
487
- json: { type: "string", jsonString: true },
488
- port: { type: "integer", port: true },
489
- };
490
-
491
- return typeMap[type] || { type: "string" };
492
- }
493
-
494
- /**
495
- * 解析约束
496
- * @private
497
- *
498
- * @example
499
- * // 比较运算符 (v1.2.0+)
500
- * _parseConstraint('number', '>0') // { exclusiveMinimum: 0 }
501
- * _parseConstraint('number', '>=18') // { minimum: 18 }
502
- * _parseConstraint('number', '<100') // { exclusiveMaximum: 100 }
503
- * _parseConstraint('number', '<=100') // { maximum: 100 }
504
- * _parseConstraint('number', '=100') // { enum: [100] }
505
- */
506
- _parseConstraint(type, constraint) {
507
- const result = {};
508
-
509
- if (type === "string" || type === "number" || type === "integer") {
510
- // ========== 比较运算符(v1.1.2新增,仅number/integer,最高优先级)==========
511
- if (type === "number" || type === "integer") {
512
- // 1. 大于等于: >=18, >=-10 (支持负数)
513
- const gteMatch = constraint.match(/^>=(-?\d+(?:\.\d+)?)$/);
514
- if (gteMatch) {
515
- result.minimum = parseFloat(gteMatch[1]);
516
- return result;
517
- }
518
-
519
- // 2. 小于等于: <=100, <=-10 (支持负数)
520
- const lteMatch = constraint.match(/^<=(-?\d+(?:\.\d+)?)$/);
521
- if (lteMatch) {
522
- result.maximum = parseFloat(lteMatch[1]);
523
- return result;
524
- }
525
-
526
- // 3. 大于: >0, >-10 (不包括边界值,支持负数)
527
- const gtMatch = constraint.match(/^>(-?\d+(?:\.\d+)?)$/);
528
- if (gtMatch) {
529
- result.exclusiveMinimum = parseFloat(gtMatch[1]);
530
- return result;
531
- }
532
-
533
- // 4. 小于: <100, <-10 (不包括边界值,支持负数)
534
- const ltMatch = constraint.match(/^<(-?\d+(?:\.\d+)?)$/);
535
- if (ltMatch) {
536
- result.exclusiveMaximum = parseFloat(ltMatch[1]);
537
- return result;
538
- }
539
-
540
- // 5. 等于: =100, =-10 (支持负数)
541
- const eqMatch = constraint.match(/^=(-?\d+(?:\.\d+)?)$/);
542
- if (eqMatch) {
543
- result.enum = [parseFloat(eqMatch[1])];
544
- return result;
545
- }
546
- }
547
-
548
- // ========== 范围约束: min-max ==========
549
- if (constraint.includes("-")) {
550
- const [min, max] = constraint.split("-").map((v) => v.trim());
551
-
552
- if (type === "string") {
553
- if (min) result.minLength = parseInt(min);
554
- if (max) result.maxLength = parseInt(max);
555
- } else {
556
- if (min) result.minimum = parseFloat(min);
557
- if (max) result.maximum = parseFloat(max);
558
- }
559
- } else {
560
- // 单个值
561
- const value = constraint.trim();
562
- if (value) {
563
- if (type === "string") {
564
- // 🔴 String单值 = 精确长度(常用于验证码、国家代码等)
565
- result.exactLength = parseInt(value);
566
- } else {
567
- // Number单值 = 最大值(符合直觉:不超过某值)
568
- result.maximum = parseFloat(value);
569
- }
570
- }
571
- }
572
- }
573
-
574
- return result;
575
- }
576
-
577
- /**
578
- * 检查是否为已知类型
579
- * @private
580
- */
581
- _isKnownType(type) {
582
- const knownTypes = [
583
- "string",
584
- "number",
585
- "integer",
586
- "boolean",
587
- "object",
588
- "array",
589
- "null",
590
- "email",
591
- "url",
592
- "uuid",
593
- "date",
594
- "datetime",
595
- "time",
596
- "ipv4",
597
- "ipv6",
598
- "binary",
599
- "objectId",
600
- "hexColor",
601
- "macAddress",
602
- "cron",
603
- "any",
604
- "phone",
605
- "idCard",
606
- "creditCard",
607
- "licensePlate",
608
- "postalCode",
609
- "passport",
610
- // v1.0.2 新增
611
- "alphanum",
612
- "lower",
613
- "upper",
614
- "json",
615
- "port",
616
- ];
617
- return knownTypes.includes(type);
618
- }
619
-
620
- /**
621
- * 自动检测枚举类型
622
- * @private
623
- */
624
- _detectEnumType(enumValues) {
625
- const values = enumValues.split("|").map((v) => v.trim());
626
-
627
- // 检查是否全部为布尔值
628
- const allBoolean = values.every((v) => v === "true" || v === "false");
629
- if (allBoolean) return "boolean";
630
-
631
- // 检查是否全部为数字
632
- const allNumber = values.every((v) => !isNaN(parseFloat(v)) && isFinite(v));
633
- if (allNumber) return "number";
634
-
635
- // 默认字符串
636
- return "string";
637
- }
638
-
639
- /**
640
- * 解析枚举值
641
- * @private
642
- */
643
- _parseEnum(enumType, enumValues) {
644
- let values = enumValues.split("|").map((v) => v.trim());
645
-
646
- // 类型转换
647
- if (enumType === "boolean") {
648
- values = values.map((v) => {
649
- if (v === "true") return true;
650
- if (v === "false") return false;
651
- throw new Error(
652
- `Invalid boolean enum value: ${v}. Must be 'true' or 'false'`,
653
- );
654
- });
655
- return { type: "boolean", enum: values };
656
- } else if (enumType === "number") {
657
- values = values.map((v) => {
658
- const num = parseFloat(v);
659
- if (isNaN(num)) throw new Error(`Invalid number enum value: ${v}`);
660
- return num;
661
- });
662
- return { type: "number", enum: values };
663
- } else if (enumType === "integer") {
664
- values = values.map((v) => {
665
- const num = parseInt(v, 10);
666
- if (isNaN(num)) throw new Error(`Invalid integer enum value: ${v}`);
667
- return num;
668
- });
669
- return { type: "integer", enum: values };
670
- } else {
671
- // 字符串枚举(默认)
672
- return { type: "string", enum: values };
673
- }
674
- }
675
-
676
- /**
677
- * 添加正则表达式验证
678
- * @param {RegExp|string} regex - 正则表达式
679
- * @param {string} [message] - 自定义错误消息
680
- * @returns {DslBuilder}
681
- *
682
- * @example
683
- * dsl('string:3-32!')
684
- * .pattern(/^[a-zA-Z0-9_]+$/, '只能包含字母、数字和下划线')
685
- */
686
- pattern(regex, message) {
687
- this._baseSchema.pattern = regex instanceof RegExp ? regex.source : regex;
688
-
689
- if (message) {
690
- this._customMessages["string.pattern"] = message;
691
- }
692
-
693
- return this;
694
- }
695
-
696
- /**
697
- * 自定义错误消息
698
- * @param {Object} messages - 错误消息对象
699
- * @returns {DslBuilder}
700
- *
701
- * @example
702
- * dsl('string:3-32!')
703
- * .messages({
704
- * 'string.min': '至少{{#limit}}个字符',
705
- * 'string.max': '最多{{#limit}}个字符'
706
- * })
707
- */
708
- messages(messages) {
709
- Object.assign(this._customMessages, messages);
710
- return this;
711
- }
712
-
713
- /**
714
- * 设置字段标签(用于错误消息)
715
- * @param {string} labelText - 标签文本
716
- * @returns {DslBuilder}
717
- *
718
- * @example
719
- * dsl('email!').label('邮箱地址')
720
- */
721
- label(labelText) {
722
- this._label = labelText;
723
- return this;
724
- }
725
-
726
- /**
727
- * 添加自定义验证器
728
- * @param {Function} validatorFn - 验证函数
729
- * @returns {DslBuilder}
730
- *
731
- * 支持多种返回方式:
732
- * 1. 不返回/返回 undefined → 验证通过
733
- * 2. 返回 true → 验证通过
734
- * 3. 返回 false → 验证失败(使用默认消息)
735
- * 4. 返回字符串 → 验证失败(字符串作为错误消息)
736
- * 5. 返回对象 { error, message } → 验证失败(自定义错误)
737
- * 6. 抛出异常 → 验证失败(异常消息作为错误)
738
- *
739
- * @example
740
- * // 方式1: 不返回任何值(推荐)
741
- * .custom(async (value) => {
742
- * const exists = await checkEmailExists(value);
743
- * if (exists) return '邮箱已被占用';
744
- * })
745
- *
746
- * // 方式2: 返回错误消息字符串
747
- * .custom((value) => {
748
- * if (value.includes('admin')) return '不能包含敏感词';
749
- * })
750
- *
751
- * // 方式3: 返回错误对象
752
- * .custom(async (value) => {
753
- * const exists = await checkExists(value);
754
- * if (exists) {
755
- * return { error: 'email.exists', message: '邮箱已被占用' };
756
- * }
757
- * })
758
- *
759
- * // 方式4: 抛出异常
760
- * .custom(async (value) => {
761
- * const user = await findUser(value);
762
- * if (!user) throw new Error('用户不存在');
763
- * })
764
- */
765
- custom(validatorFn) {
766
- if (typeof validatorFn !== "function") {
767
- throw new Error("Custom validator must be a function");
768
- }
769
- this._customValidators.push(validatorFn);
770
- return this;
771
- }
772
-
773
- /**
774
- * 设置描述
775
- * @param {string} text - 描述文本
776
- * @returns {DslBuilder}
777
- *
778
- * @example
779
- * dsl('string:3-32!').description('用户登录名')
780
- */
781
- description(text) {
782
- this._description = text;
783
- return this;
784
- }
785
-
786
- /**
787
- * 设置默认值
788
- * @param {*} value - 默认值
789
- * @returns {DslBuilder}
790
- */
791
- default(value) {
792
- this._baseSchema.default = value;
793
- return this;
794
- }
795
-
796
- /**
797
- * 转换为 JSON Schema
798
- * @returns {Object} JSON Schema对象
799
- */
800
- toSchema() {
801
- const schema = { ...this._baseSchema };
802
-
803
- // 添加描述
804
- if (this._description) {
805
- schema.description = this._description;
806
- }
807
-
808
- // 添加自定义消息
809
- if (Object.keys(this._customMessages).length > 0) {
810
- schema._customMessages = this._customMessages;
811
- }
812
-
813
- // 添加标签
814
- if (this._label) {
815
- schema._label = this._label;
816
- }
817
-
818
- // 添加自定义验证器
819
- if (this._customValidators.length > 0) {
820
- schema._customValidators = this._customValidators;
821
- }
822
-
823
- // 添加when条件
824
- if (this._whenConditions.length > 0) {
825
- schema._whenConditions = this._whenConditions;
826
- }
827
-
828
- // 添加必填标记
829
- schema._required = this._required;
830
-
831
- return schema;
832
- }
833
-
834
- /**
835
- * 输出纯净的 JSON Schema(无内部标记字段)
836
- *
837
- * 与 toSchema() 不同,toJsonSchema() 会自动清理所有 schema-dsl 内部标记:
838
- * - 下划线前缀字段:_required / _customMessages / _label / _customValidators / _whenConditions
839
- * - 自定义验证关键字:exactLength / alphanum / lowercase / uppercase / trim / jsonString /
840
- * port / requiredAll / strictSchema / noSparse / includesRequired / dateFormat /
841
- * dateGreater / dateLess / precision / multipleOf
842
- *
843
- * 返回的对象可直接嵌入 OpenAPI / JSON Schema 等标准文档中,无需下游再做清理。
844
- *
845
- * @returns {Object} 纯净的 JSON Schema 对象(符合 JSON Schema 标准)
846
- *
847
- * @example
848
- * const builder = new DslBuilder('string:3-32!');
849
- * builder.toSchema(); // { type: 'string', minLength: 3, maxLength: 32, _required: true }
850
- * builder.toJsonSchema(); // { type: 'string', minLength: 3, maxLength: 32 }
851
- *
852
- * @example
853
- * const builder = new DslBuilder('email!');
854
- * builder.messages({ format: '邮箱格式不正确' });
855
- * builder.toSchema(); // { type: 'string', format: 'email', _required: true, _customMessages: { format: '...' } }
856
- * builder.toJsonSchema(); // { type: 'string', format: 'email' }
857
- *
858
- * @since v1.2.5
859
- */
860
- toJsonSchema() {
861
- const raw = this.toSchema();
862
- const cleaned = {};
863
-
864
- for (const key of Object.keys(raw)) {
865
- // 跳过下划线前缀的内部字段
866
- if (key.startsWith("_")) continue;
867
-
868
- // 跳过 schema-dsl 自定义验证关键字(非 JSON Schema 标准)
869
- if (DslBuilder._internalKeys.has(key)) continue;
870
-
871
- cleaned[key] = raw[key];
872
- }
873
-
874
- return cleaned;
875
- }
876
-
877
- /**
878
- * 验证数据
879
- * @param {*} data - 待验证数据
880
- * @param {Object} [context] - 验证上下文
881
- * @returns {Promise<Object>} 验证结果
882
- */
883
- async validate(data, context = {}) {
884
- const Validator = require("./Validator");
885
- const validator = new Validator();
886
- const schema = this.toSchema();
887
-
888
- return validator.validate(schema, data);
889
- }
890
-
891
- /**
892
- * 验证Schema嵌套深度
893
- * @static
894
- * @param {Object} schema - Schema对象
895
- * @param {number} maxDepth - 最大深度(默认3)
896
- * @returns {Object} { valid, depth, path, message }
897
- */
898
- static validateNestingDepth(schema, maxDepth = 3) {
899
- let maxFound = 0;
900
- let deepestPath = "";
901
-
902
- function traverse(obj, depth = 0, path = "", isRoot = false) {
903
- // 更新最大深度(仅当节点是容器时,即包含 properties 或 items)
904
- // 这样叶子节点(如 string 字段)不会增加嵌套深度
905
- if (!isRoot && (obj.properties || obj.items)) {
906
- if (depth > maxFound) {
907
- maxFound = depth;
908
- deepestPath = path;
909
- }
910
- }
911
-
912
- if (obj && typeof obj === "object") {
913
- if (obj.properties) {
914
- const nextDepth = depth + 1;
915
- Object.keys(obj.properties).forEach((key) => {
916
- traverse(
917
- obj.properties[key],
918
- nextDepth,
919
- `${path}.${key}`.replace(/^\./, ""),
920
- false,
921
- );
922
- });
923
- }
924
- if (obj.items) {
925
- // 数组items不增加深度,或者根据需求增加
926
- // 这里保持原逻辑:数组本身算一层,items内部继续
927
- traverse(obj.items, depth, `${path}[]`, false);
928
- }
929
- }
930
- }
931
-
932
- traverse(schema, 0, "", true);
933
-
934
- return {
935
- valid: maxFound <= maxDepth,
936
- depth: maxFound,
937
- path: deepestPath,
938
- message:
939
- maxFound > maxDepth
940
- ? `嵌套深度${maxFound}超过限制${maxDepth},路径: ${deepestPath}`
941
- : `嵌套深度${maxFound}符合要求`,
942
- };
943
- }
944
-
945
- // ========== 默认验证方法 ==========
946
-
947
- /**
948
- * 设置格式
949
- * @param {string} format - 格式名称 (email, url, uuid, etc.)
950
- * @returns {DslBuilder}
951
- */
952
- format(format) {
953
- this._baseSchema.format = format;
954
- return this;
955
- }
956
-
957
- /**
958
- * 手机号别名
959
- * @param {string} country
960
- * @returns {DslBuilder}
961
- */
962
- phoneNumber(country) {
963
- return this.phone(country);
964
- }
965
-
966
- /**
967
- * 身份证验证
968
- * @param {string} country - 国家代码 (目前仅支持 'cn')
969
- * @returns {DslBuilder}
970
- */
971
- idCard(country = "cn") {
972
- if (country.toLowerCase() !== "cn") {
973
- throw new Error(`Unsupported country for idCard: ${country}`);
974
- }
975
-
976
- // 中国身份证正则 (18位)
977
- const pattern =
978
- /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
979
-
980
- // 自动设置长度
981
- if (!this._baseSchema.minLength) this._baseSchema.minLength = 18;
982
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 18;
983
-
984
- return this.pattern(pattern).messages({
985
- pattern: "pattern.idCard.cn",
986
- });
987
- }
988
-
989
- /**
990
- * URL Slug 验证
991
- * @returns {DslBuilder}
992
- */
993
- slug() {
994
- // 只能包含小写字母、数字和连字符,不能以连字符开头或结尾
995
- const pattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
996
-
997
- return this.pattern(pattern).messages({
998
- pattern: "pattern.slug",
999
- });
1000
- }
1001
-
1002
- /**
1003
- * 用户名验证(自动设置合理约束)
1004
- * @param {string|Object} preset - 预设长度或选项
1005
- * - '5-20' → 长度5-20
1006
- * - 'short' → 3-16(短用户名)
1007
- * - 'medium' → 3-32(中等,默认)
1008
- * - 'long' → 3-64(长用户名)
1009
- * - { minLength, maxLength, allowUnderscore, allowNumber }
1010
- * @returns {DslBuilder}
1011
- *
1012
- * @example
1013
- * // 简洁写法(推荐)
1014
- * username: 'string!'.username() // 自动3-32
1015
- * username: 'string!'.username('5-20') // 长度5-20
1016
- * username: 'string!'.username('short') // 短用户名3-16
1017
- * username: 'string!'.username('long') // 长用户名3-64
1018
- */
1019
- username(preset = "medium") {
1020
- let minLength,
1021
- maxLength,
1022
- allowUnderscore = true,
1023
- allowNumber = true;
1024
-
1025
- // 解析预设
1026
- if (typeof preset === "string") {
1027
- // 字符串范围格式:'5-20'
1028
- const rangeMatch = preset.match(/^(\d+)-(\d+)$/);
1029
- if (rangeMatch) {
1030
- minLength = parseInt(rangeMatch[1], 10);
1031
- maxLength = parseInt(rangeMatch[2], 10);
1032
- }
1033
- // 预设枚举
1034
- else {
1035
- const presets = {
1036
- short: { min: 3, max: 16 },
1037
- medium: { min: 3, max: 32 },
1038
- long: { min: 3, max: 64 },
1039
- };
1040
- const p = presets[preset] || presets.medium;
1041
- minLength = p.min;
1042
- maxLength = p.max;
1043
- }
1044
- }
1045
- // 对象参数
1046
- else if (typeof preset === "object") {
1047
- minLength = preset.minLength || 3;
1048
- maxLength = preset.maxLength || 32;
1049
- allowUnderscore = preset.allowUnderscore !== false;
1050
- allowNumber = preset.allowNumber !== false;
1051
- }
1052
-
1053
- // 自动设置长度约束(如果未设置)
1054
- if (!this._baseSchema.minLength) this._baseSchema.minLength = minLength;
1055
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = maxLength;
1056
-
1057
- // 设置正则验证
1058
- let pattern = "^[a-zA-Z]";
1059
- if (allowUnderscore && allowNumber) {
1060
- pattern += "[a-zA-Z0-9_]*$";
1061
- } else if (allowNumber) {
1062
- pattern += "[a-zA-Z0-9]*$";
1063
- } else {
1064
- pattern += "[a-zA-Z]*$";
1065
- }
1066
-
1067
- return this.pattern(new RegExp(pattern)).messages({
1068
- pattern: "pattern.username",
1069
- });
1070
- }
1071
-
1072
- /**
1073
- * 密码强度验证(自动设置合理约束)
1074
- * @param {string} strength - 强度级别
1075
- * @returns {DslBuilder}
1076
- *
1077
- * @example
1078
- * password: 'string!'.password('strong') // 自动设置8-64长度
1079
- */
1080
- password(strength = "medium") {
1081
- const patterns = {
1082
- weak: /.{6,}/,
1083
- medium: /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/,
1084
- strong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/,
1085
- veryStrong: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{10,}$/,
1086
- };
1087
-
1088
- const minLengths = { weak: 6, medium: 8, strong: 8, veryStrong: 10 };
1089
-
1090
- const pattern = patterns[strength];
1091
- if (!pattern) {
1092
- throw new Error(`Invalid password strength: ${strength}`);
1093
- }
1094
-
1095
- // 自动设置长度约束
1096
- if (!this._baseSchema.minLength)
1097
- this._baseSchema.minLength = minLengths[strength];
1098
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = 64;
1099
-
1100
- return this.pattern(pattern).messages({
1101
- pattern: `pattern.password.${strength}`,
1102
- });
1103
- }
1104
-
1105
- /**
1106
- * 手机号验证(自动设置合理约束)
1107
- * @param {string} country - 国家代码: cn|us|uk|hk|tw|international
1108
- * @returns {DslBuilder}
1109
- *
1110
- * @example
1111
- * phone: 'string!'.phone('cn') // ✅ 推荐
1112
- * phone: 'number!'.phone('cn') // ✅ 自动纠正为 string
1113
- */
1114
- phone(country = "cn") {
1115
- // ✨ 自动纠正类型为 string(手机号不应该是 number)
1116
- if (
1117
- this._baseSchema.type === "number" ||
1118
- this._baseSchema.type === "integer"
1119
- ) {
1120
- this._baseSchema.type = "string";
1121
- // 清理 number 类型的属性
1122
- delete this._baseSchema.minimum;
1123
- delete this._baseSchema.maximum;
1124
- }
1125
-
1126
- const config = patterns.phone[country];
1127
- if (!config) {
1128
- throw new Error(`Unsupported country: ${country}`);
1129
- }
1130
-
1131
- // 自动设置长度约束
1132
- if (!this._baseSchema.minLength) this._baseSchema.minLength = config.min;
1133
- if (!this._baseSchema.maxLength) this._baseSchema.maxLength = config.max;
1134
-
1135
- return this.pattern(config.pattern).messages({ pattern: config.key });
1136
- }
1137
-
1138
- /**
1139
- * 信用卡验证
1140
- * @param {string} type - 卡类型: visa|mastercard|amex|discover|jcb|unionpay
1141
- * @returns {DslBuilder}
1142
- */
1143
- creditCard(type = "visa") {
1144
- const config = patterns.creditCard[type.toLowerCase()];
1145
- if (!config) {
1146
- throw new Error(`Unsupported credit card type: ${type}`);
1147
- }
1148
-
1149
- return this.pattern(config.pattern).messages({ pattern: config.key });
1150
- }
1151
-
1152
- /**
1153
- * 车牌号验证
1154
- * @param {string} country - 国家代码
1155
- * @returns {DslBuilder}
1156
- */
1157
- licensePlate(country = "cn") {
1158
- const config = patterns.licensePlate[country.toLowerCase()];
1159
- if (!config) {
1160
- throw new Error(`Unsupported country for licensePlate: ${country}`);
1161
- }
1162
- return this.pattern(config.pattern).messages({ pattern: config.key });
1163
- }
1164
-
1165
- /**
1166
- * 邮政编码验证
1167
- * @param {string} country - 国家代码
1168
- * @returns {DslBuilder}
1169
- */
1170
- postalCode(country = "cn") {
1171
- const config = patterns.postalCode[country.toLowerCase()];
1172
- if (!config) {
1173
- throw new Error(`Unsupported country for postalCode: ${country}`);
1174
- }
1175
- return this.pattern(config.pattern).messages({ pattern: config.key });
1176
- }
1177
-
1178
- /**
1179
- * 护照号码验证
1180
- * @param {string} country - 国家代码
1181
- * @returns {DslBuilder}
1182
- */
1183
- passport(country = "cn") {
1184
- const config = patterns.passport[country.toLowerCase()];
1185
- if (!config) {
1186
- throw new Error(`Unsupported country for passport: ${country}`);
1187
- }
1188
- return this.pattern(config.pattern).messages({ pattern: config.key });
1189
- }
1190
-
1191
- // ========== v1.0.2 新增验证器方法 ==========
1192
-
1193
- /**
1194
- * String 最小长度(使用AJV原生minLength)
1195
- * @param {number} n - 最小长度
1196
- * @returns {DslBuilder}
1197
- *
1198
- * @example
1199
- * dsl('string!').min(3) // 最少3个字符
1200
- */
1201
- min(n) {
1202
- if (this._baseSchema.type !== "string") {
1203
- throw new Error("min() only applies to string type");
1204
- }
1205
- this._baseSchema.minLength = n;
1206
- return this;
1207
- }
1208
-
1209
- /**
1210
- * String 最大长度(使用AJV原生maxLength)
1211
- * @param {number} n - 最大长度
1212
- * @returns {DslBuilder}
1213
- *
1214
- * @example
1215
- * dsl('string!').max(32) // 最多32个字符
1216
- */
1217
- max(n) {
1218
- if (this._baseSchema.type !== "string") {
1219
- throw new Error("max() only applies to string type");
1220
- }
1221
- this._baseSchema.maxLength = n;
1222
- return this;
1223
- }
1224
-
1225
- /**
1226
- * String 精确长度
1227
- * @param {number} n - 精确长度
1228
- * @returns {DslBuilder}
1229
- *
1230
- * @example
1231
- * dsl('string!').length(11) // 必须是11个字符
1232
- */
1233
- length(n) {
1234
- if (this._baseSchema.type !== "string") {
1235
- throw new Error("length() only applies to string type");
1236
- }
1237
- this._baseSchema.exactLength = n;
1238
- return this;
1239
- }
1240
-
1241
- /**
1242
- * String 只能包含字母和数字
1243
- * @returns {DslBuilder}
1244
- *
1245
- * @example
1246
- * dsl('string!').alphanum() // 只能是字母和数字
1247
- */
1248
- alphanum() {
1249
- if (this._baseSchema.type !== "string") {
1250
- throw new Error("alphanum() only applies to string type");
1251
- }
1252
- this._baseSchema.alphanum = true;
1253
- return this;
1254
- }
1255
-
1256
- /**
1257
- * String 不能包含前后空格
1258
- * @returns {DslBuilder}
1259
- *
1260
- * @example
1261
- * dsl('string!').trim() // 不能有前后空格
1262
- */
1263
- trim() {
1264
- if (this._baseSchema.type !== "string") {
1265
- throw new Error("trim() only applies to string type");
1266
- }
1267
- this._baseSchema.trim = true;
1268
- return this;
1269
- }
1270
-
1271
- /**
1272
- * String 必须是小写
1273
- * @returns {DslBuilder}
1274
- *
1275
- * @example
1276
- * dsl('string!').lowercase() // 必须全小写
1277
- */
1278
- lowercase() {
1279
- if (this._baseSchema.type !== "string") {
1280
- throw new Error("lowercase() only applies to string type");
1281
- }
1282
- this._baseSchema.lowercase = true;
1283
- return this;
1284
- }
1285
-
1286
- /**
1287
- * String 必须是大写
1288
- * @returns {DslBuilder}
1289
- *
1290
- * @example
1291
- * dsl('string!').uppercase() // 必须全大写
1292
- */
1293
- uppercase() {
1294
- if (this._baseSchema.type !== "string") {
1295
- throw new Error("uppercase() only applies to string type");
1296
- }
1297
- this._baseSchema.uppercase = true;
1298
- return this;
1299
- }
1300
-
1301
- /**
1302
- * Number 小数位数限制
1303
- * @param {number} n - 最大小数位数
1304
- * @returns {DslBuilder}
1305
- *
1306
- * @example
1307
- * dsl('number!').precision(2) // 最多2位小数
1308
- */
1309
- precision(n) {
1310
- if (
1311
- this._baseSchema.type !== "number" &&
1312
- this._baseSchema.type !== "integer"
1313
- ) {
1314
- throw new Error("precision() only applies to number type");
1315
- }
1316
- this._baseSchema.precision = n;
1317
- return this;
1318
- }
1319
-
1320
- /**
1321
- * Number 倍数验证(使用AJV原生multipleOf)
1322
- * @param {number} n - 必须是此数的倍数
1323
- * @returns {DslBuilder}
1324
- *
1325
- * @example
1326
- * dsl('number!').multiple(5) // 必须是5的倍数
1327
- */
1328
- multiple(n) {
1329
- if (
1330
- this._baseSchema.type !== "number" &&
1331
- this._baseSchema.type !== "integer"
1332
- ) {
1333
- throw new Error("multiple() only applies to number type");
1334
- }
1335
- this._baseSchema.multipleOf = n;
1336
- return this;
1337
- }
1338
-
1339
- /**
1340
- * Number 端口号验证(1-65535)
1341
- * @returns {DslBuilder}
1342
- *
1343
- * @example
1344
- * dsl('integer!').port() // 必须是有效端口号
1345
- */
1346
- port() {
1347
- if (
1348
- this._baseSchema.type !== "number" &&
1349
- this._baseSchema.type !== "integer"
1350
- ) {
1351
- throw new Error("port() only applies to number type");
1352
- }
1353
- this._baseSchema.port = true;
1354
- return this;
1355
- }
1356
-
1357
- /**
1358
- * Object 要求所有属性都必须存在
1359
- * @returns {DslBuilder}
1360
- *
1361
- * @example
1362
- * dsl({ name: 'string', age: 'number' }).requireAll()
1363
- */
1364
- requireAll() {
1365
- if (this._baseSchema.type !== "object") {
1366
- throw new Error("requireAll() only applies to object type");
1367
- }
1368
- this._baseSchema.requiredAll = true;
1369
- return this;
1370
- }
1371
-
1372
- /**
1373
- * Object 严格模式,不允许额外属性
1374
- * @returns {DslBuilder}
1375
- *
1376
- * @example
1377
- * dsl({ name: 'string!' }).strict()
1378
- */
1379
- strict() {
1380
- if (this._baseSchema.type !== "object") {
1381
- throw new Error("strict() only applies to object type");
1382
- }
1383
- this._baseSchema.strictSchema = true;
1384
- return this;
1385
- }
1386
-
1387
- /**
1388
- * Array 不允许稀疏数组
1389
- * @returns {DslBuilder}
1390
- *
1391
- * @example
1392
- * dsl('array<string>').noSparse()
1393
- */
1394
- noSparse() {
1395
- if (this._baseSchema.type !== "array") {
1396
- throw new Error("noSparse() only applies to array type");
1397
- }
1398
- this._baseSchema.noSparse = true;
1399
- return this;
1400
- }
1401
-
1402
- /**
1403
- * Array 必须包含指定元素
1404
- * @param {Array} items - 必须包含的元素
1405
- * @returns {DslBuilder}
1406
- *
1407
- * @example
1408
- * dsl('array<string>').includesRequired(['admin', 'user'])
1409
- */
1410
- includesRequired(items) {
1411
- if (this._baseSchema.type !== "array") {
1412
- throw new Error("includesRequired() only applies to array type");
1413
- }
1414
- if (!Array.isArray(items)) {
1415
- throw new Error("includesRequired() requires an array parameter");
1416
- }
1417
- this._baseSchema.includesRequired = items;
1418
- return this;
1419
- }
1420
-
1421
- /**
1422
- * Date 自定义日期格式验证
1423
- * @param {string} fmt - 日期格式(YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY, ISO8601)
1424
- * @returns {DslBuilder}
1425
- *
1426
- * @example
1427
- * dsl('string!').dateFormat('YYYY-MM-DD')
1428
- */
1429
- dateFormat(fmt) {
1430
- if (this._baseSchema.type !== "string") {
1431
- throw new Error("dateFormat() only applies to string type");
1432
- }
1433
- this._baseSchema.dateFormat = fmt;
1434
- return this;
1435
- }
1436
-
1437
- /**
1438
- * Date 必须晚于指定日期
1439
- * @param {string} date - 比较日期
1440
- * @returns {DslBuilder}
1441
- *
1442
- * @example
1443
- * dsl('date!').after('2024-01-01')
1444
- */
1445
- after(date) {
1446
- if (this._baseSchema.type !== "string") {
1447
- throw new Error("after() only applies to string type");
1448
- }
1449
- this._baseSchema.dateGreater = date;
1450
- return this;
1451
- }
1452
-
1453
- /**
1454
- * Date 必须早于指定日期
1455
- * @param {string} date - 比较日期
1456
- * @returns {DslBuilder}
1457
- *
1458
- * @example
1459
- * dsl('date!').before('2025-12-31')
1460
- */
1461
- before(date) {
1462
- if (this._baseSchema.type !== "string") {
1463
- throw new Error("before() only applies to string type");
1464
- }
1465
- this._baseSchema.dateLess = date;
1466
- return this;
1467
- }
1468
-
1469
- /**
1470
- * Pattern 域名验证
1471
- * @returns {DslBuilder}
1472
- *
1473
- * @example
1474
- * dsl('string!').domain()
1475
- */
1476
- domain() {
1477
- if (this._baseSchema.type !== "string") {
1478
- throw new Error("domain() only applies to string type");
1479
- }
1480
- const config = patterns.common.domain;
1481
- return this.pattern(config.pattern).messages({ pattern: config.key });
1482
- }
1483
-
1484
- /**
1485
- * Pattern IP地址验证(IPv4或IPv6)
1486
- * @returns {DslBuilder}
1487
- *
1488
- * @example
1489
- * dsl('string!').ip()
1490
- */
1491
- ip() {
1492
- if (this._baseSchema.type !== "string") {
1493
- throw new Error("ip() only applies to string type");
1494
- }
1495
- const config = patterns.common.ip;
1496
- return this.pattern(config.pattern).messages({ pattern: config.key });
1497
- }
1498
-
1499
- /**
1500
- * Pattern Base64编码验证
1501
- * @returns {DslBuilder}
1502
- *
1503
- * @example
1504
- * dsl('string!').base64()
1505
- */
1506
- base64() {
1507
- if (this._baseSchema.type !== "string") {
1508
- throw new Error("base64() only applies to string type");
1509
- }
1510
- const config = patterns.common.base64;
1511
- return this.pattern(config.pattern).messages({ pattern: config.key });
1512
- }
1513
-
1514
- /**
1515
- * Pattern JWT令牌验证
1516
- * @returns {DslBuilder}
1517
- *
1518
- * @example
1519
- * dsl('string!').jwt()
1520
- */
1521
- jwt() {
1522
- if (this._baseSchema.type !== "string") {
1523
- throw new Error("jwt() only applies to string type");
1524
- }
1525
- const config = patterns.common.jwt;
1526
- return this.pattern(config.pattern).messages({ pattern: config.key });
1527
- }
1528
-
1529
- /**
1530
- * Pattern JSON字符串验证
1531
- * @returns {DslBuilder}
1532
- *
1533
- * @example
1534
- * dsl('string!').json()
1535
- */
1536
- json() {
1537
- if (this._baseSchema.type !== "string") {
1538
- throw new Error("json() only applies to string type");
1539
- }
1540
- this._baseSchema.jsonString = true;
1541
- return this;
1542
- }
1543
-
1544
- /**
1545
- * Pattern URL slug验证 (v1.0.3)
1546
- * URL slug只能包含小写字母、数字和连字符
1547
- * @returns {DslBuilder}
1548
- *
1549
- * @example
1550
- * dsl('string!').slug() // my-blog-post, hello-world-123
1551
- */
1552
- slug() {
1553
- if (this._baseSchema.type !== "string") {
1554
- throw new Error("slug() only applies to string type");
1555
- }
1556
- this._baseSchema.pattern = "^[a-z0-9]+(?:-[a-z0-9]+)*$";
1557
- this._baseSchema._customMessages = this._baseSchema._customMessages || {};
1558
- this._baseSchema._customMessages["pattern"] = "pattern.slug";
1559
- return this;
1560
- }
1561
-
1562
- /**
1563
- * 日期大于验证 (v1.0.2)
1564
- * @param {string} date - 对比日期
1565
- * @returns {DslBuilder}
1566
- *
1567
- * @example
1568
- * dsl('string!').dateGreater('2025-01-01')
1569
- */
1570
- dateGreater(date) {
1571
- this._baseSchema.dateGreater = date;
1572
- return this;
1573
- }
1574
-
1575
- /**
1576
- * 日期小于验证 (v1.0.2)
1577
- * @param {string} date - 对比日期
1578
- * @returns {DslBuilder}
1579
- *
1580
- * @example
1581
- * dsl('string!').dateLess('2025-12-31')
1582
- */
1583
- dateLess(date) {
1584
- this._baseSchema.dateLess = date;
1585
- return this;
1586
- }
1587
- }
1588
-
1589
- module.exports = DslBuilder;