befly 3.18.22 → 3.19.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.
@@ -18,7 +18,8 @@ export default {
18
18
  password: adminTable.password,
19
19
  loginType: {
20
20
  name: "登录类型",
21
- input: "/^(username|email|phone)$/",
21
+ input: "enum",
22
+ check: "username|email|phone",
22
23
  min: null,
23
24
  max: null
24
25
  }
package/checks/api.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import * as z from "zod";
2
2
 
3
+ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
3
4
  import { Logger } from "../lib/logger.js";
4
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty } from "../utils/is.js";
6
+ import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
6
7
 
7
8
  z.config(z.locales.zhCN());
8
9
 
@@ -10,14 +11,69 @@ const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首
10
11
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
11
12
  const apiPathRegex = /^\/api\/(?:core\/[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*|(?!app(?:\/|$))[a-z][a-zA-Z0-9]*(?:\/[a-z][a-zA-Z0-9]*)*)$/;
12
13
 
14
+ function addIssue(context, path, message) {
15
+ context.addIssue({
16
+ path: path,
17
+ message: message
18
+ });
19
+ }
20
+
13
21
  const fieldSchema = z
14
22
  .object({
15
23
  name: noTrimString.min(1),
16
- input: noTrimString.min(1),
24
+ input: z.enum(FIELD_RULE_INPUT_TYPES),
25
+ check: noTrimString.optional(),
17
26
  min: z.number().nullable().optional(),
18
- max: z.number().nullable().optional()
27
+ max: z.number().nullable().optional(),
28
+ detail: z.string().optional()
19
29
  })
20
- .strict();
30
+ .strict()
31
+ .superRefine((value, context) => {
32
+ const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
33
+ const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
34
+ if (min > max) {
35
+ addIssue(context, ["min"], "min 不能大于 max");
36
+ }
37
+
38
+ const checkValue = value.check;
39
+ const input = value.input;
40
+
41
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
42
+ addIssue(context, ["check"], `${input} 必须提供 check`);
43
+ return;
44
+ }
45
+
46
+ if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
47
+ addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
48
+ return;
49
+ }
50
+
51
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
52
+ const items = String(checkValue)
53
+ .split("|")
54
+ .map((item) => item.trim())
55
+ .filter((item) => item.length > 0);
56
+
57
+ if (items.length === 0) {
58
+ addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
59
+ return;
60
+ }
61
+
62
+ if (input === "enumNumber" || input === "enumInteger") {
63
+ for (const item of items) {
64
+ const num = Number(item);
65
+ if (!Number.isFinite(num)) {
66
+ addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
67
+ return;
68
+ }
69
+ if (input === "enumInteger" && !Number.isInteger(num)) {
70
+ addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
71
+ return;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ });
21
77
 
22
78
  const fieldsSchema = z.record(z.string().regex(lowerCamelRegex), fieldSchema);
23
79
 
package/checks/table.js CHANGED
@@ -1,22 +1,15 @@
1
1
  import * as z from "zod";
2
2
 
3
+ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
3
4
  import { Logger } from "../lib/logger.js";
4
5
  import { formatZodIssues } from "../utils/formatZodIssues.js";
5
- import { isNoTrimStringAllowEmpty, isNullable } from "../utils/is.js";
6
+ import { isNoTrimStringAllowEmpty, isNullable, isRegexInput } from "../utils/is.js";
6
7
 
7
8
  z.config(z.locales.zhCN());
8
9
 
9
10
  const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
10
11
  const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
11
- const INPUT_TYPES = ["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"];
12
-
13
- const inputRegexLiteral = /^\/.*?\/[gimsuy]*$/;
14
- const inputEnumRegex = /^[^/].*\|.*$/;
15
- const inputAliasRegex = /^@.+$/;
16
-
17
- const inputSchema = z.union([z.enum(INPUT_TYPES), z.string().regex(inputRegexLiteral), z.string().regex(inputEnumRegex), z.string().regex(inputAliasRegex)]).refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
18
- const DEFAULT_MIN = 0;
19
- const DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
12
+ const inputSchema = z.enum(FIELD_RULE_INPUT_TYPES);
20
13
 
21
14
  function addIssue(context, path, message) {
22
15
  context.addIssue({
@@ -29,16 +22,57 @@ const fieldDefSchema = z
29
22
  .object({
30
23
  name: z.string().min(1),
31
24
  input: inputSchema,
25
+ check: noTrimString.optional(),
32
26
  min: z.number().nullable().optional(),
33
- max: z.number().nullable().optional()
27
+ max: z.number().nullable().optional(),
28
+ detail: z.string().optional()
34
29
  })
35
30
  .strict()
36
31
  .superRefine((value, context) => {
37
- const min = isNullable(value.min) ? DEFAULT_MIN : value.min;
38
- const max = isNullable(value.max) ? DEFAULT_MAX : value.max;
32
+ const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
33
+ const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
39
34
  if (min > max) {
40
35
  addIssue(context, ["min"], "min 不能大于 max");
41
36
  }
37
+
38
+ const checkValue = value.check;
39
+ const input = value.input;
40
+
41
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
42
+ addIssue(context, ["check"], `${input} 必须提供 check`);
43
+ return;
44
+ }
45
+
46
+ if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
47
+ addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
48
+ return;
49
+ }
50
+
51
+ if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
52
+ const items = String(checkValue)
53
+ .split("|")
54
+ .map((item) => item.trim())
55
+ .filter((item) => item.length > 0);
56
+
57
+ if (items.length === 0) {
58
+ addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
59
+ return;
60
+ }
61
+
62
+ if (input === "enumNumber" || input === "enumInteger") {
63
+ for (const item of items) {
64
+ const num = Number(item);
65
+ if (!Number.isFinite(num)) {
66
+ addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
67
+ return;
68
+ }
69
+ if (input === "enumInteger" && !Number.isInteger(num)) {
70
+ addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
71
+ return;
72
+ }
73
+ }
74
+ }
75
+ }
42
76
  });
43
77
 
44
78
  const tableContentSchema = z.record(z.string().regex(lowerCamelRegex), fieldDefSchema);
@@ -32,3 +32,14 @@ export const RUN_MODE_VALUES = ["development", "production"];
32
32
 
33
33
  // 数据库 ID 生成模式可选值
34
34
  export const DB_ID_MODE_VALUES = ["timeId", "autoId"];
35
+
36
+ // ==========================
37
+ // 字段验证规则配置
38
+ // ==========================
39
+
40
+ // 字段 input 可选值(统一用于 validator / checkTable / checkApi)
41
+ export const FIELD_RULE_INPUT_TYPES = ["number", "integer", "string", "char", "array", "arrayNumber", "arrayInteger", "json", "jsonNumber", "jsonInteger", "regexp", "enum", "enumNumber", "enumInteger"];
42
+
43
+ // 字段 min/max 缺省值
44
+ export const FIELD_RULE_DEFAULT_MIN = 0;
45
+ export const FIELD_RULE_DEFAULT_MAX = Number.MAX_SAFE_INTEGER;
@@ -551,6 +551,7 @@ export const dataOpsMethods = {
551
551
  redis: this.redis,
552
552
  dbName: this.dbName,
553
553
  sql: tx,
554
+ isTransaction: true,
554
555
  idMode: this.idMode,
555
556
  beflyMode: this.beflyMode
556
557
  });
@@ -15,7 +15,7 @@ function DbHelper(options) {
15
15
  this.dbName = options.dbName;
16
16
 
17
17
  this.sql = options.sql || null;
18
- this.isTransaction = Boolean(options.sql);
18
+ this.isTransaction = options.isTransaction === true;
19
19
  this.idMode = options.idMode === "autoId" ? "autoId" : "timeId";
20
20
  this.beflyMode = options.beflyMode === 0 ? 0 : 1;
21
21
  }
package/lib/validator.js CHANGED
@@ -4,10 +4,12 @@
4
4
  */
5
5
 
6
6
  import RegexAliases from "../configs/regexpAlias.json";
7
+ import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
7
8
  import { getCompiledRegex } from "../utils/regexpUtil.js";
8
- import { isEnumInput, isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isNullable, isNumber, isPlainObject, isRegexInput, isString } from "../utils/is.js";
9
+ import { isFiniteNumber, isIntegerNumber, isJsonObject, isJsonPrimitive, isJsonStructure, isNullable, isNumber, isPlainObject, isRegexInput, isString } from "../utils/is.js";
9
10
 
10
- const INPUT_TYPE_SET = new Set(["number", "integer", "string", "char", "array", "array_number", "array_integer", "json", "json_number", "json_integer"]);
11
+ const INPUT_TYPE_SET = new Set(FIELD_RULE_INPUT_TYPES);
12
+ const RULE_ALLOWED_KEYS = new Set(["input", "check", "name", "min", "max", "detail"]);
11
13
 
12
14
  /**
13
15
  * 递归校验 JSON 结构叶子节点是否满足数值规则。
@@ -54,30 +56,38 @@ export class Validator {
54
56
  return this.buildResult({ _error: "验证规则必须是对象格式" });
55
57
  }
56
58
 
57
- const dataRecord = data;
58
- const rulesRecord = rules;
59
-
60
59
  // 检查必填字段
61
60
  for (const field of required) {
62
- const value = dataRecord[field];
61
+ const value = data[field];
63
62
  if (isNullable(value)) {
64
- const rawRule = rulesRecord[field];
63
+ const rawRule = rules[field];
65
64
  const label = isPlainObject(rawRule) && isString(rawRule["name"]) ? rawRule["name"] : field;
66
65
  fieldErrors[field] = `${label}为必填项`;
67
66
  }
68
67
  }
69
68
 
70
69
  // 验证有值的字段
71
- for (const [field, rawRule] of Object.entries(rulesRecord)) {
70
+ for (const [field, rawRule] of Object.entries(rules)) {
72
71
  if (fieldErrors[field]) continue;
73
72
  // 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
74
- if (isNullable(dataRecord[field]) && !required.includes(field)) continue;
73
+ if (isNullable(data[field]) && !required.includes(field)) continue;
75
74
 
76
75
  if (!isPlainObject(rawRule)) {
77
76
  fieldErrors[field] = `${field}验证规则必须是对象格式`;
78
77
  continue;
79
78
  }
80
79
 
80
+ const invalidKeys = [];
81
+ for (const key of Object.keys(rawRule)) {
82
+ if (!RULE_ALLOWED_KEYS.has(key)) {
83
+ invalidKeys.push(key);
84
+ }
85
+ }
86
+ if (invalidKeys.length > 0) {
87
+ fieldErrors[field] = `${field}存在非法属性: ${invalidKeys.join(",")}`;
88
+ continue;
89
+ }
90
+
81
91
  const ruleName = rawRule["name"];
82
92
  const ruleInput = rawRule["input"];
83
93
  if (!isString(ruleName) || !isString(ruleInput)) {
@@ -85,18 +95,29 @@ export class Validator {
85
95
  continue;
86
96
  }
87
97
 
98
+ if (!INPUT_TYPE_SET.has(ruleInput)) {
99
+ fieldErrors[field] = `${field}input 不支持: ${ruleInput}`;
100
+ continue;
101
+ }
102
+
88
103
  const rule = {
89
104
  name: ruleName,
90
105
  input: ruleInput
91
106
  };
92
107
 
108
+ const check = rawRule["check"];
109
+ if (isString(check)) rule.check = check;
110
+
111
+ const detail = rawRule["detail"];
112
+ if (isString(detail)) rule.detail = detail;
113
+
93
114
  const min = rawRule["min"];
94
115
  if (isNumber(min) || min === null) rule.min = min;
95
116
 
96
117
  const max = rawRule["max"];
97
118
  if (isNumber(max) || max === null) rule.max = max;
98
119
 
99
- const error = this.checkField(dataRecord[field], rule, field);
120
+ const error = this.checkField(data[field], rule, field);
100
121
  if (error) fieldErrors[field] = error;
101
122
  }
102
123
 
@@ -107,7 +128,10 @@ export class Validator {
107
128
  * 验证单个值(带类型转换)
108
129
  */
109
130
  static single(value, fieldDef) {
110
- const input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
131
+ if (!isString(fieldDef.input) || !INPUT_TYPE_SET.has(fieldDef.input)) {
132
+ const input = isString(fieldDef.input) ? fieldDef.input : "";
133
+ return { value: null, error: `input 不支持: ${input}` };
134
+ }
111
135
 
112
136
  // 处理空值
113
137
  if (isNullable(value, [""])) {
@@ -115,7 +139,7 @@ export class Validator {
115
139
  }
116
140
 
117
141
  // 类型转换
118
- const converted = this.convert(value, input);
142
+ const converted = this.convert(value, fieldDef.input);
119
143
  if (converted.error) {
120
144
  return { value: null, error: converted.error };
121
145
  }
@@ -150,9 +174,7 @@ export class Validator {
150
174
  /** 验证单个字段 */
151
175
  static checkField(value, fieldDef, fieldName) {
152
176
  const label = fieldDef.name || fieldName;
153
-
154
- const input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
155
- const converted = this.convert(value, input);
177
+ const converted = this.convert(value, fieldDef.input);
156
178
  if (converted.error) {
157
179
  return `${label}${converted.error}`;
158
180
  }
@@ -163,18 +185,7 @@ export class Validator {
163
185
 
164
186
  /** 类型转换 */
165
187
  static convert(value, input) {
166
- const inputRaw = String(input || "").trim();
167
- const inputKey = inputRaw.toLowerCase();
168
-
169
- if (!INPUT_TYPE_SET.has(inputKey) && isRegexInput(inputRaw)) {
170
- return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
171
- }
172
-
173
- if (!INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw)) {
174
- return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
175
- }
176
-
177
- switch (inputKey) {
188
+ switch (input) {
178
189
  case "number": {
179
190
  if (isFiniteNumber(value)) return { value: value, error: null };
180
191
  if (isString(value)) {
@@ -204,7 +215,7 @@ export class Validator {
204
215
  case "array":
205
216
  return Array.isArray(value) ? { value: value, error: null } : { value: null, error: "必须是数组" };
206
217
 
207
- case "array_number": {
218
+ case "arrayNumber": {
208
219
  if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
209
220
  for (const item of value) {
210
221
  if (!isFiniteNumber(item)) return { value: null, error: "数组元素必须是数字" };
@@ -212,7 +223,7 @@ export class Validator {
212
223
  return { value: value, error: null };
213
224
  }
214
225
 
215
- case "array_integer": {
226
+ case "arrayInteger": {
216
227
  if (!Array.isArray(value)) return { value: null, error: "必须是数组" };
217
228
  for (const item of value) {
218
229
  if (!isIntegerNumber(item)) return { value: null, error: "数组元素必须是整数" };
@@ -224,14 +235,36 @@ export class Validator {
224
235
  if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
225
236
  return { value: value, error: null };
226
237
 
227
- case "json_number":
238
+ case "jsonNumber":
228
239
  if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
229
240
  return checkJsonLeaves(value, "number") ? { value: value, error: null } : { value: null, error: "JSON值必须是数字" };
230
241
 
231
- case "json_integer":
242
+ case "jsonInteger":
232
243
  if (!isJsonStructure(value)) return { value: null, error: "必须是JSON对象或数组" };
233
244
  return checkJsonLeaves(value, "integer") ? { value: value, error: null } : { value: null, error: "JSON值必须是整数" };
234
245
 
246
+ case "regexp":
247
+ case "enum":
248
+ return isString(value) ? { value: value, error: null } : { value: null, error: "必须是字符串" };
249
+
250
+ case "enumNumber": {
251
+ if (isFiniteNumber(value)) return { value: value, error: null };
252
+ if (isString(value)) {
253
+ const num = Number(value);
254
+ return Number.isFinite(num) ? { value: num, error: null } : { value: null, error: "必须是数字" };
255
+ }
256
+ return { value: null, error: "必须是数字" };
257
+ }
258
+
259
+ case "enumInteger": {
260
+ if (isIntegerNumber(value)) return { value: value, error: null };
261
+ if (isString(value)) {
262
+ const num = Number(value);
263
+ return Number.isFinite(num) && Number.isInteger(num) ? { value: num, error: null } : { value: null, error: "必须是整数" };
264
+ }
265
+ return { value: null, error: "必须是整数" };
266
+ }
267
+
235
268
  default: {
236
269
  return { value: value, error: null };
237
270
  }
@@ -240,43 +273,69 @@ export class Validator {
240
273
 
241
274
  /** 规则验证 */
242
275
  static checkRule(value, fieldDef) {
243
- const input = isString(fieldDef.input) ? fieldDef.input.trim() : "";
244
- const min = isNumber(fieldDef.min) ? fieldDef.min : 0;
245
- const max = isNumber(fieldDef.max) ? fieldDef.max : Number.MAX_SAFE_INTEGER;
276
+ const min = isNumber(fieldDef.min) ? fieldDef.min : FIELD_RULE_DEFAULT_MIN;
277
+ const max = isNumber(fieldDef.max) ? fieldDef.max : FIELD_RULE_DEFAULT_MAX;
246
278
 
247
- const inputRaw = String(input || "").trim();
248
- const inputKey = inputRaw.toLowerCase();
249
- const isRegex = !INPUT_TYPE_SET.has(inputKey) && isRegexInput(inputRaw);
250
- const isEnum = !INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw);
279
+ if (!isString(fieldDef.input)) {
280
+ return "input 不支持";
281
+ }
251
282
 
252
- if (inputKey === "number" || inputKey === "integer") {
253
- if (typeof value !== "number") return inputKey === "integer" ? "必须是整数" : "必须是数字";
283
+ if (fieldDef.input === "number" || fieldDef.input === "integer") {
284
+ if (typeof value !== "number") return fieldDef.input === "integer" ? "必须是整数" : "必须是数字";
254
285
  if (value < min) return `不能小于${min}`;
255
286
  if (value > max) return `不能大于${max}`;
256
- } else if (inputKey === "string" || inputKey === "char" || isRegex || isEnum) {
287
+ } else if (fieldDef.input === "string" || fieldDef.input === "char" || fieldDef.input === "regexp" || fieldDef.input === "enum") {
257
288
  if (!isString(value)) return "必须是字符串";
258
- if (inputKey === "char" && value.length !== 1) return "必须是单字符";
289
+ if (fieldDef.input === "char" && value.length !== 1) return "必须是单字符";
259
290
  if (value.length < min) return `长度不能少于${min}个字符`;
260
291
  if (value.length > max) return `长度不能超过${max}个字符`;
261
292
 
262
- if (isRegex) {
263
- const regex = this.resolveRegex(inputRaw);
293
+ if (fieldDef.input === "regexp") {
294
+ if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
295
+ if (!isRegexInput(fieldDef.check)) return "check 规则无效";
296
+ const regex = this.resolveRegex(fieldDef.check);
264
297
  if (!this.testRegex(regex, value)) return "格式不正确";
265
298
  }
266
299
 
267
- if (isEnum) {
268
- const enums = String(inputRaw || "")
269
- .trim()
300
+ if (fieldDef.input === "enum") {
301
+ if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
302
+ const enums = fieldDef.check
270
303
  .split("|")
271
304
  .map((x) => x.trim())
272
305
  .filter((x) => x !== "");
306
+ if (enums.length === 0) return "check 规则无效";
273
307
  if (!enums.includes(value)) return "值不在枚举范围内";
274
308
  }
275
- } else if (inputKey === "array" || inputKey === "array_number" || inputKey === "array_integer") {
309
+ } else if (fieldDef.input === "enumNumber" || fieldDef.input === "enumInteger") {
310
+ if (typeof value !== "number") return fieldDef.input === "enumInteger" ? "必须是整数" : "必须是数字";
311
+ if (!isString(fieldDef.check) || fieldDef.check.length === 0) return "缺少 check 规则";
312
+
313
+ const enumValues = [];
314
+ for (const item of fieldDef.check.split("|")) {
315
+ const normalized = item.trim();
316
+ if (normalized.length === 0) {
317
+ continue;
318
+ }
319
+
320
+ const num = Number(normalized);
321
+ if (!Number.isFinite(num)) {
322
+ return "check 规则无效";
323
+ }
324
+
325
+ if (fieldDef.input === "enumInteger" && !Number.isInteger(num)) {
326
+ return "check 规则无效";
327
+ }
328
+
329
+ enumValues.push(num);
330
+ }
331
+
332
+ if (enumValues.length === 0) return "check 规则无效";
333
+ if (!enumValues.includes(value)) return "值不在枚举范围内";
334
+ } else if (fieldDef.input === "array" || fieldDef.input === "arrayNumber" || fieldDef.input === "arrayInteger") {
276
335
  if (!Array.isArray(value)) return "必须是数组";
277
336
  if (value.length < min) return `至少需要${min}个元素`;
278
337
  if (value.length > max) return `最多只能有${max}个元素`;
279
- } else if (inputKey === "json" || inputKey === "json_number" || inputKey === "json_integer") {
338
+ } else if (fieldDef.input === "json" || fieldDef.input === "jsonNumber" || fieldDef.input === "jsonInteger") {
280
339
  if (!isJsonStructure(value)) return "必须是JSON对象或数组";
281
340
  }
282
341
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.18.22",
4
- "gitHead": "fdf4570d70dbec2c68b317db54bdc9c8a7229c11",
3
+ "version": "3.19.0",
4
+ "gitHead": "ccc66d5da9c6b84f8346087adc1d24412ce65bec",
5
5
  "private": false,
6
6
  "description": "Befly - 为 Bun 专属打造的 JavaScript API 接口框架核心引擎",
7
7
  "keywords": [
package/sync/dev.js CHANGED
@@ -1,28 +1,33 @@
1
1
  import { isNumber } from "../utils/is.js";
2
2
 
3
+ const MENU_TABLE_NAME = "beflyMenu";
4
+ const API_TABLE_NAME = "beflyApi";
5
+ const ROLE_TABLE_NAME = "beflyRole";
6
+ const ADMIN_TABLE_NAME = "beflyAdmin";
7
+
3
8
  export async function syncDev(ctx) {
4
9
  try {
5
10
  const allMenus = await ctx.mysql.getAll({
6
- table: "beflyMenu",
11
+ table: MENU_TABLE_NAME,
7
12
  fields: ["path"],
8
13
  where: { state$gte: 0 },
9
14
  orderBy: ["id#ASC"]
10
15
  });
11
16
 
12
17
  const allApis = await ctx.mysql.getAll({
13
- table: "beflyApi",
18
+ table: API_TABLE_NAME,
14
19
  fields: ["path", "auth"],
15
20
  where: { state$gte: 0 },
16
21
  orderBy: ["id#ASC"]
17
22
  });
18
23
 
19
24
  const devRole = await ctx.mysql.getOne({
20
- table: "beflyRole",
25
+ table: ROLE_TABLE_NAME,
21
26
  where: { code: "dev", state$gte: 0 }
22
27
  });
23
28
 
24
29
  const devAdmin = await ctx.mysql.getOne({
25
- table: "beflyAdmin",
30
+ table: ADMIN_TABLE_NAME,
26
31
  where: { username: "dev", state$gte: 0 }
27
32
  });
28
33
 
@@ -47,7 +52,7 @@ export async function syncDev(ctx) {
47
52
 
48
53
  if (isNumber(devRole.data.id)) {
49
54
  await ctx.mysql.updData({
50
- table: "beflyRole",
55
+ table: ROLE_TABLE_NAME,
51
56
  where: { code: "dev", state$gte: 0 },
52
57
  data: {
53
58
  name: devRoleData.name,
@@ -60,7 +65,7 @@ export async function syncDev(ctx) {
60
65
  });
61
66
  } else {
62
67
  await ctx.mysql.insData({
63
- table: "beflyRole",
68
+ table: ROLE_TABLE_NAME,
64
69
  data: devRoleData
65
70
  });
66
71
  }
@@ -78,7 +83,7 @@ export async function syncDev(ctx) {
78
83
 
79
84
  if (isNumber(devAdmin.data.id)) {
80
85
  await ctx.mysql.updData({
81
- table: "beflyAdmin",
86
+ table: ADMIN_TABLE_NAME,
82
87
  where: { username: "dev" },
83
88
  data: {
84
89
  nickname: devAdminData.nickname,
@@ -91,7 +96,7 @@ export async function syncDev(ctx) {
91
96
  });
92
97
  } else {
93
98
  await ctx.mysql.insData({
94
- table: "beflyAdmin",
99
+ table: ADMIN_TABLE_NAME,
95
100
  data: {
96
101
  nickname: devAdminData.nickname,
97
102
  email: devAdminData.email,
@@ -127,7 +132,7 @@ export async function syncDev(ctx) {
127
132
 
128
133
  for (const roleConfig of roles) {
129
134
  const existingRole = await ctx.mysql.getOne({
130
- table: "beflyRole",
135
+ table: ROLE_TABLE_NAME,
131
136
  where: {
132
137
  code: roleConfig.code,
133
138
  state$gte: 0
@@ -191,7 +196,7 @@ export async function syncDev(ctx) {
191
196
  if (existingRole.data?.id) {
192
197
  // 角色存在则强制更新(不做差异判断)
193
198
  await ctx.mysql.updData({
194
- table: "beflyRole",
199
+ table: ROLE_TABLE_NAME,
195
200
  where: { code: roleConfig.code, state$gte: 0 },
196
201
  data: {
197
202
  name: roleConfig.name,
@@ -203,7 +208,7 @@ export async function syncDev(ctx) {
203
208
  });
204
209
  } else {
205
210
  await ctx.mysql.insData({
206
- table: "beflyRole",
211
+ table: ROLE_TABLE_NAME,
207
212
  data: {
208
213
  code: roleConfig.code,
209
214
  name: roleConfig.name,
package/sync/menu.js CHANGED
@@ -1,3 +1,5 @@
1
+ const MENU_TABLE_NAME = "beflyMenu";
2
+
1
3
  export function flattenMenusToDefMap(menus) {
2
4
  // 读取配置菜单:扁平化为 path => { name, sort, parentPath }
3
5
  // - 以 path 为唯一键:后出现的覆盖先出现的(与旧逻辑“同 path 多次同步同一条记录”一致)
@@ -40,7 +42,7 @@ export async function syncMenu(ctx, menus) {
40
42
  // 2) 批量同步(事务内):按 path diff 执行批量 insert/update/delete
41
43
  // 读取全部菜单
42
44
  const allExistingMenus = await ctx.mysql.getAll({
43
- table: "beflyMenu",
45
+ table: MENU_TABLE_NAME,
44
46
  fields: ["id", "name", "path", "parentPath", "sort", "state"],
45
47
  where: { state$gte: 0 }
46
48
  });
@@ -84,16 +86,16 @@ export async function syncMenu(ctx, menus) {
84
86
  }
85
87
 
86
88
  if (updList.length > 0) {
87
- await ctx.mysql.updBatch("beflyMenu", updList);
89
+ await ctx.mysql.updBatch(MENU_TABLE_NAME, updList);
88
90
  }
89
91
 
90
92
  if (insList.length > 0) {
91
- await ctx.mysql.insBatch("beflyMenu", insList);
93
+ await ctx.mysql.insBatch(MENU_TABLE_NAME, insList);
92
94
  }
93
95
 
94
96
  // 3) 删除差集(DB - 配置)
95
97
  const delIds = Array.from(delIdSet);
96
98
  if (delIds.length > 0) {
97
- await ctx.mysql.delForceBatch("beflyMenu", delIds);
99
+ await ctx.mysql.delForceBatch(MENU_TABLE_NAME, delIds);
98
100
  }
99
101
  }
package/tables/admin.json CHANGED
@@ -7,7 +7,8 @@
7
7
  },
8
8
  "username": {
9
9
  "name": "用户名",
10
- "input": "@alphanumeric_",
10
+ "input": "regexp",
11
+ "check": "@alphanumeric_",
11
12
  "min": 2,
12
13
  "max": 30
13
14
  },
@@ -19,12 +20,14 @@
19
20
  },
20
21
  "email": {
21
22
  "name": "邮箱",
22
- "input": "@email",
23
+ "input": "regexp",
24
+ "check": "@email",
23
25
  "max": 100
24
26
  },
25
27
  "phone": {
26
28
  "name": "手机号",
27
- "input": "@phone",
29
+ "input": "regexp",
30
+ "check": "@phone",
28
31
  "max": 20
29
32
  },
30
33
  "avatar": {
@@ -34,13 +37,15 @@
34
37
  },
35
38
  "roleCode": {
36
39
  "name": "角色编码",
37
- "input": "@alphanumeric_",
40
+ "input": "regexp",
41
+ "check": "@alphanumeric_",
38
42
  "min": 2,
39
43
  "max": 50
40
44
  },
41
45
  "roleType": {
42
46
  "name": "角色类型",
43
- "input": "admin|user",
47
+ "input": "enum",
48
+ "check": "admin|user",
44
49
  "min": 4,
45
50
  "max": 5
46
51
  },
package/tables/dict.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "typeCode": {
3
3
  "name": "字典类型代码",
4
- "input": "@alphanumeric_",
4
+ "input": "regexp",
5
+ "check": "@alphanumeric_",
5
6
  "min": 2,
6
7
  "max": 50
7
8
  },
8
9
  "key": {
9
10
  "name": "字典键",
10
- "input": "@alphanumeric_",
11
+ "input": "regexp",
12
+ "check": "@alphanumeric_",
11
13
  "min": 1,
12
14
  "max": 50
13
15
  },
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "code": {
3
3
  "name": "类型代码",
4
- "input": "@alphanumeric_",
4
+ "input": "regexp",
5
+ "check": "@alphanumeric_",
5
6
  "min": 2,
6
7
  "max": 50
7
8
  },
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "toEmail": {
17
17
  "name": "收件人邮箱",
18
- "input": "@email",
18
+ "input": "regexp",
19
+ "check": "@email",
19
20
  "min": 5,
20
21
  "max": 200
21
22
  },
package/tables/role.json CHANGED
@@ -7,7 +7,8 @@
7
7
  },
8
8
  "code": {
9
9
  "name": "角色编码",
10
- "input": "@alphanumericDash_",
10
+ "input": "regexp",
11
+ "check": "@alphanumericDash_",
11
12
  "min": 2,
12
13
  "max": 50
13
14
  },
@@ -7,7 +7,8 @@
7
7
  },
8
8
  "code": {
9
9
  "name": "配置代码",
10
- "input": "@alphanumeric_",
10
+ "input": "regexp",
11
+ "check": "@alphanumeric_",
11
12
  "min": 2,
12
13
  "max": 100
13
14
  },
@@ -17,7 +18,8 @@
17
18
  },
18
19
  "valueType": {
19
20
  "name": "值类型",
20
- "input": "string|number|boolean|json",
21
+ "input": "enum",
22
+ "check": "string|number|boolean|json",
21
23
  "max": 20
22
24
  },
23
25
  "group": {
package/utils/cors.js CHANGED
@@ -16,11 +16,44 @@ export function setCorsOptions(req, config = {}) {
16
16
 
17
17
  const merged = Object.assign({}, defaultConfig, config || {});
18
18
  const origin = merged.origin;
19
+ const requestedHeaders = req.headers.get("Access-Control-Request-Headers") || "";
20
+
21
+ const allowedHeaderItems = [];
22
+ const allowedHeaderMap = Object.create(null);
23
+
24
+ const pushHeaderIfNeeded = (rawValue) => {
25
+ if (typeof rawValue !== "string") {
26
+ return;
27
+ }
28
+
29
+ const value = rawValue.trim();
30
+ if (value.length < 1) {
31
+ return;
32
+ }
33
+
34
+ const key = value.toLowerCase();
35
+ if (allowedHeaderMap[key]) {
36
+ return;
37
+ }
38
+
39
+ allowedHeaderMap[key] = 1;
40
+ allowedHeaderItems.push(value);
41
+ };
42
+
43
+ for (const item of String(merged.allowedHeaders || "").split(",")) {
44
+ pushHeaderIfNeeded(item);
45
+ }
46
+
47
+ for (const item of String(requestedHeaders).split(",")) {
48
+ pushHeaderIfNeeded(item);
49
+ }
50
+
51
+ const allowHeaders = allowedHeaderItems.join(", ");
19
52
 
20
53
  return {
21
54
  "Access-Control-Allow-Origin": origin === "*" ? req.headers.get("origin") || "*" : origin,
22
55
  "Access-Control-Allow-Methods": merged.methods,
23
- "Access-Control-Allow-Headers": merged.allowedHeaders,
56
+ "Access-Control-Allow-Headers": allowHeaders,
24
57
  "Access-Control-Expose-Headers": merged.exposedHeaders,
25
58
  "Access-Control-Max-Age": String(merged.maxAge),
26
59
  "Access-Control-Allow-Credentials": merged.credentials