befly 3.71.0 → 3.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/validator.js CHANGED
@@ -1,368 +1,272 @@
1
- /**
2
- * 数据验证器 - Befly 项目专用
3
- * 纯静态类设计,简洁易用
4
- */
5
-
6
1
  import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
7
2
  import RegexAliases from "../configs/regexpAlias.json";
8
- import { isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isNullable, isNumber, isPlainObject, isRegexInput, isString } from "../utils/is.js";
3
+ import { configSchema } from "../schemas/config.js";
4
+ import { isFiniteNumber, isIntegerNumber, isJsonPrimitive, isNoTrimStringAllowEmpty, isNullable, isPlainObject, isRegexInput, isString, isValidTimeZone } from "../utils/is.js";
9
5
  import { getCompiledRegex } from "../utils/regexpUtil.js";
10
6
 
11
7
  const INPUT_TYPE_SET = new Set(FIELD_RULE_INPUT_TYPES);
12
- const RULE_ALLOWED_KEYS = new Set(["input", "check", "name", "min", "max", "detail"]);
13
-
14
- /**
15
- * 递归校验 JSON 结构叶子节点是否满足数值规则。
16
- */
17
- function checkJsonLeaves(value, rule) {
18
- if (isJsonPrimitive(value)) {
19
- if (rule === "number") return isFiniteNumber(value);
20
- return isIntegerNumber(value);
21
- }
8
+ const FIELD_RULE_KEYS = new Set(["input", "check", "name", "min", "max", "detail"]);
9
+
10
+ function addIssue(issues, path, message) {
11
+ issues.push({ path: Array.isArray(path) ? path : [path], message: message });
12
+ }
13
+
14
+ function isJsonScalar(value) {
15
+ return isJsonPrimitive(value) && (typeof value !== "number" || Number.isFinite(value));
16
+ }
22
17
 
18
+ function isScalarArray(value) {
19
+ return Array.isArray(value) && value.every((item) => isJsonScalar(item));
20
+ }
21
+
22
+ function isFlatJson(value, kind = "any") {
23
23
  if (Array.isArray(value)) {
24
- for (const item of value) {
25
- if (!checkJsonLeaves(item, rule)) return false;
26
- }
27
- return true;
24
+ return value.every((item) => isJsonScalar(item) && (kind === "any" || (kind === "number" ? isFiniteNumber(item) : isIntegerNumber(item))));
28
25
  }
29
26
 
30
- if (isJsonObject(value)) {
31
- for (const v of Object.values(value)) {
32
- if (v === undefined) continue;
33
- if (!checkJsonLeaves(v, rule)) return false;
34
- }
35
- return true;
27
+ if (!isPlainObject(value)) {
28
+ return false;
36
29
  }
37
30
 
38
- return false;
31
+ return Object.values(value).every((item) => isJsonScalar(item) && (kind === "any" || (kind === "number" ? isFiniteNumber(item) : isIntegerNumber(item))));
39
32
  }
40
33
 
41
- function convertStringNumber(value, error, integer = false) {
42
- if (!isString(value)) {
43
- return { value: null, error: error };
34
+ function toNumber(value, integer = false) {
35
+ if (isFiniteNumber(value) && (!integer || isIntegerNumber(value))) {
36
+ return { value: value, error: null };
44
37
  }
45
38
 
46
- const normalized = value.trim();
47
- if (normalized.length === 0) {
48
- return { value: null, error: error };
39
+ if (!isString(value) || value.trim().length === 0) {
40
+ return { value: null, error: integer ? "必须是整数" : "必须是数字" };
49
41
  }
50
42
 
51
- const num = Number(normalized);
52
- if (!Number.isFinite(num)) {
53
- return { value: null, error: error };
43
+ const number = Number(value.trim());
44
+ if (!Number.isFinite(number) || (integer && !Number.isInteger(number))) {
45
+ return { value: null, error: integer ? "必须是整数" : "必须是数字" };
54
46
  }
55
47
 
56
- if (integer && !Number.isInteger(num)) {
57
- return { value: null, error: error };
58
- }
59
-
60
- return { value: num, error: null };
48
+ return { value: number, error: null };
61
49
  }
62
50
 
63
- /**
64
- * 验证器类
65
- */
66
- export class Validator {
67
- /**
68
- * 验证数据对象
69
- */
70
- static validate(data, rules, required = []) {
71
- const fieldErrors = {};
72
-
73
- // 参数检查
74
- if (!isPlainObject(data)) {
75
- return this.buildResult({ rootError: "数据必须是对象格式" });
76
- }
77
- if (!isPlainObject(rules)) {
78
- return this.buildResult({ rootError: "验证规则必须是对象格式" });
79
- }
80
-
81
- // 检查必填字段
82
- for (const field of required) {
83
- const value = data[field];
84
- if (isNullable(value)) {
85
- const rawRule = rules[field];
86
- const label = isPlainObject(rawRule) && isString(rawRule["name"]) ? rawRule["name"] : field;
87
- fieldErrors[field] = `${label}为必填项`;
88
- }
89
- }
90
-
91
- // 验证有值的字段
92
- for (const [field, rawRule] of Object.entries(rules)) {
93
- if (fieldErrors[field]) continue;
94
- // 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
95
- if (isNullable(data[field]) && !required.includes(field)) continue;
96
-
97
- if (!isPlainObject(rawRule)) {
98
- fieldErrors[field] = `${field}验证规则必须是对象格式`;
99
- continue;
100
- }
101
-
102
- const invalidKeys = [];
103
- for (const key of Object.keys(rawRule)) {
104
- if (!RULE_ALLOWED_KEYS.has(key)) {
105
- invalidKeys.push(key);
106
- }
107
- }
108
- if (invalidKeys.length > 0) {
109
- fieldErrors[field] = `${field}存在非法属性: ${invalidKeys.join(",")}`;
110
- continue;
111
- }
112
-
113
- const ruleName = rawRule["name"];
114
- const ruleInput = rawRule["input"];
115
- if (!isString(ruleName) || !isString(ruleInput)) {
116
- fieldErrors[field] = `${field}验证规则无效`;
117
- continue;
118
- }
119
-
120
- if (!INPUT_TYPE_SET.has(ruleInput)) {
121
- fieldErrors[field] = `${field}input 不支持: ${ruleInput}`;
122
- continue;
123
- }
124
-
125
- const rule = {
126
- name: ruleName,
127
- input: ruleInput
128
- };
51
+ function getEnumValues(check, numeric = false, integer = false) {
52
+ if (!isString(check) || check.length === 0) {
53
+ return null;
54
+ }
129
55
 
130
- const check = rawRule["check"];
131
- if (isString(check)) rule.check = check;
56
+ const values = check
57
+ .split("|")
58
+ .map((item) => item.trim())
59
+ .filter(Boolean);
132
60
 
133
- const detail = rawRule["detail"];
134
- if (isString(detail)) rule.detail = detail;
61
+ if (values.length === 0) {
62
+ return null;
63
+ }
135
64
 
136
- const min = rawRule["min"];
137
- if (isNumber(min) || min === null) rule.min = min;
65
+ if (!numeric) {
66
+ return values;
67
+ }
138
68
 
139
- const max = rawRule["max"];
140
- if (isNumber(max) || max === null) rule.max = max;
69
+ const numbers = values.map(Number);
70
+ if (numbers.some((value) => !Number.isFinite(value) || (integer && !Number.isInteger(value)))) {
71
+ return null;
72
+ }
141
73
 
142
- const error = this.checkField(data[field], rule, field);
143
- if (error) fieldErrors[field] = error;
144
- }
74
+ return numbers;
75
+ }
145
76
 
146
- return this.buildResult(fieldErrors);
77
+ function checkFieldValue(value, rule) {
78
+ const min = isFiniteNumber(rule.min) ? rule.min : FIELD_RULE_DEFAULT_MIN;
79
+ const max = isFiniteNumber(rule.max) ? rule.max : FIELD_RULE_DEFAULT_MAX;
80
+ const input = rule.input;
81
+ let converted = value;
82
+
83
+ if (input === "number" || input === "integer" || input === "enumNumber" || input === "enumInteger") {
84
+ const parsed = toNumber(value, input === "integer" || input === "enumInteger");
85
+ if (parsed.error) return parsed.error;
86
+ converted = parsed.value;
87
+ } else if (input === "string" || input === "char" || input === "regexp" || input === "enum") {
88
+ if (!isString(value)) return "必须是字符串";
89
+ } else if (input === "array" || input === "arrayNumber" || input === "arrayInteger") {
90
+ if (!isScalarArray(value)) return "必须是仅包含基础值的一层数组";
91
+ if (input === "arrayNumber" && !value.every(isFiniteNumber)) return "数组元素必须是数字";
92
+ if (input === "arrayInteger" && !value.every(isIntegerNumber)) return "数组元素必须是整数";
93
+ } else if (input === "json" || input === "jsonNumber" || input === "jsonInteger") {
94
+ const kind = input === "json" ? "any" : input === "jsonNumber" ? "number" : "integer";
95
+ if (!isFlatJson(value, kind)) return kind === "any" ? "必须是一层JSON对象或数组,成员只能是基础值" : `JSON值必须是${kind === "number" ? "数字" : "整数"}`;
147
96
  }
148
97
 
149
- /**
150
- * 验证单个值(带类型转换)
151
- */
152
- static single(value, fieldDef) {
153
- if (!isString(fieldDef.input) || !INPUT_TYPE_SET.has(fieldDef.input)) {
154
- const input = isString(fieldDef.input) ? fieldDef.input : "";
155
- return { value: null, error: `input 不支持: ${input}` };
156
- }
157
-
158
- // 处理空值
159
- if (isNullable(value, [""])) {
160
- return { value: value, error: null };
161
- }
98
+ if (input === "number" || input === "integer") {
99
+ if (converted < min) return `不能小于${min}`;
100
+ if (converted > max) return `不能大于${max}`;
101
+ }
162
102
 
163
- // 类型转换
164
- const converted = this.convert(value, fieldDef.input);
165
- if (converted.error) {
166
- return { value: null, error: converted.error };
103
+ if (input === "string" || input === "char" || input === "regexp" || input === "enum") {
104
+ if (input === "char" && converted.length !== 1) return "必须是单字符";
105
+ if (converted.length < min) return `长度不能少于${min}个字符`;
106
+ if (converted.length > max) return `长度不能超过${max}个字符`;
107
+
108
+ if (input === "regexp") {
109
+ if (!isRegexInput(rule.check)) return "check 规则无效";
110
+ const pattern = rule.check.startsWith("@") ? RegexAliases[rule.check.slice(1)] || rule.check : rule.check;
111
+ try {
112
+ if (!getCompiledRegex(pattern).test(converted)) return "格式不正确";
113
+ } catch {
114
+ return "check 规则无效";
115
+ }
167
116
  }
168
117
 
169
- // 规则验证
170
- const error = this.checkRule(converted.value, fieldDef);
171
- if (error) {
172
- return { value: null, error: error };
118
+ if (input === "enum") {
119
+ const values = getEnumValues(rule.check);
120
+ if (!values) return "check 规则无效";
121
+ if (!values.includes(converted)) return "值不在枚举范围内";
173
122
  }
174
-
175
- return { value: converted.value, error: null };
176
123
  }
177
124
 
178
- // ========== 私有方法 ==========
179
-
180
- /** 构建结果对象 */
181
- static buildResult(fieldErrors) {
182
- const errors = Object.values(fieldErrors);
183
- const errorFields = Object.keys(fieldErrors);
184
- const failed = errors.length > 0;
185
-
186
- return {
187
- code: failed ? 1 : 0,
188
- failed: failed,
189
- firstError: failed ? (errors[0] ?? null) : null,
190
- errors: errors,
191
- errorFields: errorFields,
192
- fieldErrors: fieldErrors
193
- };
125
+ if (input === "enumNumber" || input === "enumInteger") {
126
+ const values = getEnumValues(rule.check, true, input === "enumInteger");
127
+ if (!values) return "check 规则无效";
128
+ if (!values.includes(converted)) return "值不在枚举范围内";
194
129
  }
195
130
 
196
- /** 验证单个字段 */
197
- static checkField(value, fieldDef, fieldName) {
198
- const label = fieldDef.name || fieldName;
199
- const converted = this.convert(value, fieldDef.input);
200
- if (converted.error) {
201
- return `${label}${converted.error}`;
202
- }
203
-
204
- const error = this.checkRule(converted.value, fieldDef);
205
- return error ? `${label}${error}` : null;
131
+ if (input === "array" || input === "arrayNumber" || input === "arrayInteger") {
132
+ if (value.length < min) return `至少需要${min}个元素`;
133
+ if (value.length > max) return `最多只能有${max}个元素`;
206
134
  }
207
135
 
208
- /** 类型转换 */
209
- static convert(value, input) {
210
- switch (input) {
211
- case "number": {
212
- if (isFiniteNumber(value)) return { value: value, error: null };
213
- return convertStringNumber(value, "必须是数字");
214
- }
215
-
216
- case "integer": {
217
- if (isIntegerNumber(value)) return { value: value, error: null };
218
- return convertStringNumber(value, "必须是整数", true);
219
- }
220
-
221
- case "string": {
222
- return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
223
- }
136
+ return null;
137
+ }
224
138
 
225
- case "char":
226
- if (!isString(value)) return { value: null, error: "必须是字符串" };
227
- return value.length === 1 ? { value: value, error: null } : { value: null, error: "必须是单字符" };
139
+ function buildFieldResult(fieldErrors) {
140
+ const errors = Object.values(fieldErrors);
141
+ const errorFields = Object.keys(fieldErrors);
142
+ const failed = errors.length > 0;
143
+
144
+ return {
145
+ code: failed ? 1 : 0,
146
+ failed: failed,
147
+ firstError: failed ? errors[0] : null,
148
+ errors: errors,
149
+ errorFields: errorFields,
150
+ fieldErrors: fieldErrors
151
+ };
152
+ }
228
153
 
229
- case "array":
230
- return Array.isArray(value) ? { value: value, error: null } : { value: null, error: "必须是数组" };
154
+ export function validateFieldRule(rule) {
155
+ const issues = [];
156
+ if (!isPlainObject(rule)) {
157
+ addIssue(issues, [], "验证规则必须是对象格式");
158
+ return issues;
159
+ }
231
160
 
232
- case "arrayNumber": {
233
- if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
234
- for (const item of value) {
235
- if (!isFiniteNumber(item)) return { value: null, error: "数组元素必须是数字" };
236
- }
237
- return { value: value, error: null };
238
- }
161
+ for (const key of Object.keys(rule)) {
162
+ if (!FIELD_RULE_KEYS.has(key)) addIssue(issues, [key], "不允许出现");
163
+ }
239
164
 
240
- case "arrayInteger": {
241
- if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
242
- for (const item of value) {
243
- if (!isIntegerNumber(item)) return { value: null, error: "数组元素必须是整数" };
244
- }
245
- return { value: value, error: null };
246
- }
165
+ if (!isString(rule.name) || rule.name.length === 0 || !isNoTrimStringAllowEmpty(rule.name)) addIssue(issues, ["name"], "必须是非空且无首尾空格的字符串");
166
+ if (!isString(rule.input) || !INPUT_TYPE_SET.has(rule.input)) addIssue(issues, ["input"], "不支持该类型");
167
+ if (rule.check !== undefined && (!isString(rule.check) || !isNoTrimStringAllowEmpty(rule.check))) addIssue(issues, ["check"], "必须是无首尾空格的字符串");
168
+ if (rule.detail !== undefined && !isString(rule.detail)) addIssue(issues, ["detail"], "必须是字符串");
169
+ if (rule.min !== undefined && rule.min !== null && !isFiniteNumber(rule.min)) addIssue(issues, ["min"], "必须是有限数字或 null");
170
+ if (rule.max !== undefined && rule.max !== null && !isFiniteNumber(rule.max)) addIssue(issues, ["max"], "必须是有限数字或 null");
247
171
 
248
- case "json":
249
- if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
250
- return { value: value, error: null };
172
+ if (issues.length > 0) return issues;
251
173
 
252
- case "jsonNumber":
253
- if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
254
- return checkJsonLeaves(value, "number") ? { value: value, error: null } : { value: null, error: "JSON值必须是数字" };
174
+ const min = rule.min === null || rule.min === undefined ? FIELD_RULE_DEFAULT_MIN : rule.min;
175
+ const max = rule.max === null || rule.max === undefined ? FIELD_RULE_DEFAULT_MAX : rule.max;
176
+ if (min > max) addIssue(issues, ["min"], "不能大于 max");
255
177
 
256
- case "jsonInteger":
257
- if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
258
- return checkJsonLeaves(value, "integer") ? { value: value, error: null } : { value: null, error: "JSON值必须是整数" };
178
+ if (["regexp", "enum", "enumNumber", "enumInteger"].includes(rule.input) && !rule.check) {
179
+ addIssue(issues, ["check"], `${rule.input} 必须提供 check`);
180
+ }
181
+ if (rule.input === "regexp" && rule.check && !isRegexInput(rule.check)) addIssue(issues, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
182
+ if (["enum", "enumNumber", "enumInteger"].includes(rule.input) && rule.check && !getEnumValues(rule.check, rule.input !== "enum", rule.input === "enumInteger")) {
183
+ addIssue(issues, ["check"], `${rule.input}.check 必须是非空枚举值列表`);
184
+ }
259
185
 
260
- case "regexp":
261
- case "enum":
262
- return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
186
+ return issues;
187
+ }
263
188
 
264
- case "enumNumber": {
265
- if (isFiniteNumber(value)) return { value: value, error: null };
266
- return convertStringNumber(value, "必须是数字");
267
- }
189
+ export function validateFields(data, fields, required = []) {
190
+ const fieldErrors = {};
191
+ if (!isPlainObject(data)) return buildFieldResult({ rootError: "数据必须是对象格式" });
192
+ if (!isPlainObject(fields)) return buildFieldResult({ rootError: "验证规则必须是对象格式" });
268
193
 
269
- case "enumInteger": {
270
- if (isIntegerNumber(value)) return { value: value, error: null };
271
- return convertStringNumber(value, "必须是整数", true);
272
- }
273
-
274
- default: {
275
- return { value: value, error: null };
276
- }
194
+ for (const field of required) {
195
+ if (isNullable(data[field])) {
196
+ fieldErrors[field] = `${isPlainObject(fields[field]) && isString(fields[field].name) ? fields[field].name : field}为必填项`;
277
197
  }
278
198
  }
279
199
 
280
- /** 规则验证 */
281
- static checkRule(value, fieldDef) {
282
- const min = isNumber(fieldDef.min) ? fieldDef.min : FIELD_RULE_DEFAULT_MIN;
283
- const max = isNumber(fieldDef.max) ? fieldDef.max : FIELD_RULE_DEFAULT_MAX;
200
+ for (const [field, rule] of Object.entries(fields)) {
201
+ if (fieldErrors[field] || (isNullable(data[field]) && !required.includes(field))) continue;
202
+ const error = checkFieldValue(data[field], rule);
203
+ if (error) fieldErrors[field] = `${rule.name}${error}`;
204
+ }
284
205
 
285
- if (!isString(fieldDef.input)) {
286
- return "input 不支持";
287
- }
206
+ return buildFieldResult(fieldErrors);
207
+ }
288
208
 
289
- if (fieldDef.input === "number" || fieldDef.input === "integer") {
290
- if (typeof value !== "number") return fieldDef.input === "integer" ? "必须是整数" : "必须是数字";
291
- if (value < min) return `不能小于${min}`;
292
- if (value > max) return `不能大于${max}`;
293
- } else if (fieldDef.input === "string" || fieldDef.input === "char" || fieldDef.input === "regexp" || fieldDef.input === "enum") {
294
- if (!isString(value)) return "必须是字符串";
295
- if (fieldDef.input === "char" && value.length !== 1) return "必须是单字符";
296
- if (value.length < min) return `长度不能少于${min}个字符`;
297
- if (value.length > max) return `长度不能超过${max}个字符`;
298
-
299
- if (fieldDef.input === "regexp") {
300
- if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
301
- if (!isRegexInput(fieldDef.check)) return "check 规则无效";
302
- const regex = this.resolveRegex(fieldDef.check);
303
- if (!this.testRegex(regex, value)) return "格式不正确";
304
- }
209
+ function addUnknownKeys(issues, value, allowedKeys, path) {
210
+ for (const key of Object.keys(value)) {
211
+ if (!allowedKeys.has(key)) addIssue(issues, [...path, key], "不允许出现");
212
+ }
213
+ }
305
214
 
306
- if (fieldDef.input === "enum") {
307
- if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
308
- const enums = fieldDef.check
309
- .split("|")
310
- .map((x) => x.trim())
311
- .filter((x) => x !== "");
312
- if (enums.length === 0) return "check 规则无效";
313
- if (!enums.includes(value)) return "值不在枚举范围内";
314
- }
315
- } else if (fieldDef.input === "enumNumber" || fieldDef.input === "enumInteger") {
316
- if (typeof value !== "number") return fieldDef.input === "enumInteger" ? "必须是整数" : "必须是数字";
317
- if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
318
-
319
- const enumValues = [];
320
- for (const item of fieldDef.check.split("|")) {
321
- const normalized = item.trim();
322
- if (normalized.length === 0) {
323
- continue;
324
- }
325
-
326
- const num = Number(normalized);
327
- if (!Number.isFinite(num)) {
328
- return "check 规则无效";
329
- }
330
-
331
- if (fieldDef.input === "enumInteger" && !Number.isInteger(num)) {
332
- return "check 规则无效";
333
- }
334
-
335
- enumValues.push(num);
336
- }
215
+ function isConfigCheckValid(value, check) {
216
+ if (check === "emailOrEmpty") return value === "" || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
217
+ if (check === "timeZone") return isValidTimeZone(value);
218
+ if (check === "redisPrefix") return !value.includes(":");
219
+ return false;
220
+ }
337
221
 
338
- if (enumValues.length === 0) return "check 规则无效";
339
- if (!enumValues.includes(value)) return "值不在枚举范围内";
340
- } else if (fieldDef.input === "array" || fieldDef.input === "arrayNumber" || fieldDef.input === "arrayInteger") {
341
- if (!Array.isArray(value)) return "必须是数组";
342
- if (value.length < min) return `至少需要${min}个元素`;
343
- if (value.length > max) return `最多只能有${max}个元素`;
344
- } else if (fieldDef.input === "json" || fieldDef.input === "jsonNumber" || fieldDef.input === "jsonInteger") {
345
- if (!isJsonStructure(value)) return "必须是JSON对象或数组";
346
- }
222
+ function checkConfigLeaf(issues, value, def, path) {
223
+ if (value === undefined) {
224
+ if (!def.optional) addIssue(issues, path, "为必填项");
225
+ return;
226
+ }
347
227
 
348
- return null;
228
+ if (def.type === "string") {
229
+ if (!isString(value)) addIssue(issues, path, "必须是字符串");
230
+ else if (def.noTrim && !isNoTrimStringAllowEmpty(value)) addIssue(issues, path, "不允许首尾空格");
231
+ else if (def.min !== undefined && value.length < def.min) addIssue(issues, path, `长度不能少于${def.min}个字符`);
232
+ } else if (def.type === "number") {
233
+ if (!isFiniteNumber(value)) addIssue(issues, path, "必须是有限数字");
234
+ else if (def.integer && !isIntegerNumber(value)) addIssue(issues, path, "必须是整数");
235
+ else if (def.min !== undefined && value < def.min) addIssue(issues, path, `不能小于${def.min}`);
236
+ else if (def.max !== undefined && value > def.max) addIssue(issues, path, `不能大于${def.max}`);
237
+ } else if (def.type === "boolInt") {
238
+ if (![0, 1, true, false].includes(value)) addIssue(issues, path, "仅允许 0|1|true|false");
239
+ } else if (def.type === "values") {
240
+ if (!def.values.includes(value)) addIssue(issues, path, `仅允许 ${def.values.join("|")}`);
241
+ } else if (def.type === "stringArray") {
242
+ if (!Array.isArray(value) || !value.every((item) => isString(item) && (!def.noTrim || isNoTrimStringAllowEmpty(item)))) addIssue(issues, path, "必须是仅包含无首尾空格字符串的一层数组");
349
243
  }
350
244
 
351
- /** 解析正则别名 */
352
- static resolveRegex(pattern) {
353
- if (pattern.startsWith("@")) {
354
- const key = pattern.substring(1);
355
- return RegexAliases[key] || pattern;
356
- }
357
- return pattern;
245
+ if (def.pattern && isString(value) && !def.pattern.test(value)) addIssue(issues, path, "格式不正确");
246
+ if (def.check && isString(value) && !isConfigCheckValid(value, def.check)) addIssue(issues, path, "无效值");
247
+ }
248
+
249
+ export function validateConfig(config) {
250
+ const issues = [];
251
+ if (!isPlainObject(config)) {
252
+ addIssue(issues, [], "配置必须是对象格式");
253
+ return issues;
358
254
  }
359
255
 
360
- /** 测试正则 */
361
- static testRegex(pattern, value) {
362
- try {
363
- return getCompiledRegex(pattern).test(value);
364
- } catch {
365
- return false;
256
+ const { root, groups } = configSchema;
257
+ addUnknownKeys(issues, config, new Set([...Object.keys(root), ...Object.keys(groups)]), []);
258
+ for (const [key, def] of Object.entries(root)) checkConfigLeaf(issues, config[key], def, [key]);
259
+
260
+ for (const [groupName, fields] of Object.entries(groups)) {
261
+ const group = config[groupName];
262
+ if (group === undefined && groupName === "email") continue;
263
+ if (!isPlainObject(group)) {
264
+ addIssue(issues, [groupName], "必须是对象格式");
265
+ continue;
366
266
  }
267
+ addUnknownKeys(issues, group, new Set(Object.keys(fields)), [groupName]);
268
+ for (const [fieldName, def] of Object.entries(fields)) checkConfigLeaf(issues, group[fieldName], def, [groupName, fieldName]);
367
269
  }
270
+
271
+ return issues;
368
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.71.0",
3
+ "version": "3.73.0",
4
4
  "gitHead": "49c39d36695036e85fc64083cc43c1652fff96cb",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
@@ -26,6 +26,7 @@
26
26
  "lib",
27
27
  "plugins",
28
28
  "router",
29
+ "schemas",
29
30
  "scripts",
30
31
  "sql",
31
32
  "sync",
@@ -56,8 +57,7 @@
56
57
  },
57
58
  "dependencies": {
58
59
  "pathe": "^2.0.3",
59
- "picomatch": "^4.0.5",
60
- "zod": "^4.4.3"
60
+ "picomatch": "^4.0.5"
61
61
  },
62
62
  "engines": {
63
63
  "bun": ">=1.3.0"
package/plugins/cron.js CHANGED
@@ -16,7 +16,7 @@ const jobs = new Map();
16
16
  * @returns {CronJob|null}
17
17
  */
18
18
  function register(context, name, schedule, task) {
19
- if (!isPrimaryProcess(context.env)) {
19
+ if (!isPrimaryProcess()) {
20
20
  return null;
21
21
  }
22
22
 
@@ -42,7 +42,7 @@ function register(context, name, schedule, task) {
42
42
  * 注册一个按秒执行的定时任务
43
43
  */
44
44
  function second(context, name, seconds, task) {
45
- if (!isPrimaryProcess(context.env)) {
45
+ if (!isPrimaryProcess()) {
46
46
  return null;
47
47
  }
48
48