befly 3.72.0 → 3.74.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/checks/api.js +50 -107
- package/checks/config.js +5 -133
- package/checks/hook.js +28 -31
- package/checks/menu.js +36 -50
- package/checks/plugin.js +28 -31
- package/checks/table.js +25 -109
- package/configs/beflyConfig.json +1 -3
- package/hooks/rateLimit.js +25 -186
- package/hooks/validator.js +2 -2
- package/index.js +48 -2
- package/lib/connect.js +0 -6
- package/lib/dbHelper.js +14 -40
- package/lib/validator.js +208 -304
- package/package.json +3 -3
- package/plugins/tool.js +16 -0
- package/router/api.js +4 -0
- package/schemas/config.js +67 -0
- package/utils/cors.js +2 -2
- package/utils/formatValidationIssues.js +28 -0
- package/utils/is.js +1 -1
- package/utils/formatZodIssues.js +0 -98
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 {
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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 (
|
|
31
|
-
|
|
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
|
|
31
|
+
return Object.values(value).every((item) => isJsonScalar(item) && (kind === "any" || (kind === "number" ? isFiniteNumber(item) : isIntegerNumber(item))));
|
|
39
32
|
}
|
|
40
33
|
|
|
41
|
-
function
|
|
42
|
-
if (!
|
|
43
|
-
return { value:
|
|
34
|
+
function toNumber(value, integer = false) {
|
|
35
|
+
if (isFiniteNumber(value) && (!integer || isIntegerNumber(value))) {
|
|
36
|
+
return { value: value, error: null };
|
|
44
37
|
}
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
52
|
-
if (!Number.isFinite(
|
|
53
|
-
return { value: null, 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
56
|
+
const values = check
|
|
57
|
+
.split("|")
|
|
58
|
+
.map((item) => item.trim())
|
|
59
|
+
.filter(Boolean);
|
|
132
60
|
|
|
133
|
-
|
|
134
|
-
|
|
61
|
+
if (values.length === 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
135
64
|
|
|
136
|
-
|
|
137
|
-
|
|
65
|
+
if (!numeric) {
|
|
66
|
+
return values;
|
|
67
|
+
}
|
|
138
68
|
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
74
|
+
return numbers;
|
|
75
|
+
}
|
|
145
76
|
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
165
|
-
if (converted.
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
154
|
+
export function validateFieldRule(rule) {
|
|
155
|
+
const issues = [];
|
|
156
|
+
if (!isPlainObject(rule)) {
|
|
157
|
+
addIssue(issues, [], "验证规则必须是对象格式");
|
|
158
|
+
return issues;
|
|
159
|
+
}
|
|
231
160
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
|
|
186
|
+
return issues;
|
|
187
|
+
}
|
|
263
188
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
206
|
+
return buildFieldResult(fieldErrors);
|
|
207
|
+
}
|
|
288
208
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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.
|
|
3
|
+
"version": "3.74.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/tool.js
CHANGED
|
@@ -47,6 +47,21 @@ export function No(msg, data = null, other = {}) {
|
|
|
47
47
|
return out;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 业务失败并抛出错误
|
|
52
|
+
*
|
|
53
|
+
* 仅用于事务内需要回滚的业务失败;抛出后由路由统一转换为 { code: 1 } 响应。
|
|
54
|
+
* @param msg - 消息
|
|
55
|
+
* @param data - 数据(可选)
|
|
56
|
+
* @param other - 其他字段(可选)
|
|
57
|
+
*/
|
|
58
|
+
export function Fail(msg, data = null, other = {}) {
|
|
59
|
+
const error = new Error(msg);
|
|
60
|
+
error.isBeflyResponse = true;
|
|
61
|
+
error.payload = No(msg, data, other);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
|
|
50
65
|
/**
|
|
51
66
|
* 统一响应函数
|
|
52
67
|
*
|
|
@@ -109,6 +124,7 @@ export default {
|
|
|
109
124
|
return {
|
|
110
125
|
Yes: Yes,
|
|
111
126
|
No: No,
|
|
127
|
+
Fail: Fail,
|
|
112
128
|
Raw: Raw
|
|
113
129
|
};
|
|
114
130
|
}
|
package/router/api.js
CHANGED
|
@@ -158,6 +158,10 @@ export function apiHandler(apis, hooks, context) {
|
|
|
158
158
|
// 7. 返回响应(自动处理 response/result/日志)
|
|
159
159
|
return FinalResponse(ctx);
|
|
160
160
|
} catch (err) {
|
|
161
|
+
if (err && err.isBeflyResponse === true) {
|
|
162
|
+
ctx.result = err.payload;
|
|
163
|
+
return FinalResponse(ctx);
|
|
164
|
+
}
|
|
161
165
|
// 全局错误处理
|
|
162
166
|
Logger.error("请求错误", err, {
|
|
163
167
|
path: ctx.apiPath,
|