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.
- package/apis/auth/login.js +2 -1
- package/checks/api.js +60 -4
- package/checks/table.js +47 -13
- package/configs/constConfig.js +11 -0
- package/lib/dbHelper/dataOps.js +1 -0
- package/lib/dbHelper/index.js +1 -1
- package/lib/validator.js +108 -49
- package/package.json +2 -2
- package/sync/dev.js +16 -11
- package/sync/menu.js +6 -4
- package/tables/admin.json +10 -5
- package/tables/dict.json +4 -2
- package/tables/dictType.json +2 -1
- package/tables/emailLog.json +2 -1
- package/tables/role.json +2 -1
- package/tables/sysConfig.json +4 -2
- package/utils/cors.js +34 -1
package/apis/auth/login.js
CHANGED
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:
|
|
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
|
|
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) ?
|
|
38
|
-
const max = isNullable(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);
|
package/configs/constConfig.js
CHANGED
|
@@ -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;
|
package/lib/dbHelper/dataOps.js
CHANGED
package/lib/dbHelper/index.js
CHANGED
|
@@ -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 =
|
|
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 {
|
|
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(
|
|
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 =
|
|
61
|
+
const value = data[field];
|
|
63
62
|
if (isNullable(value)) {
|
|
64
|
-
const rawRule =
|
|
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(
|
|
70
|
+
for (const [field, rawRule] of Object.entries(rules)) {
|
|
72
71
|
if (fieldErrors[field]) continue;
|
|
73
72
|
// 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
|
|
74
|
-
if (isNullable(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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 "
|
|
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
|
|
244
|
-
const
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const isEnum = !INPUT_TYPE_SET.has(inputKey) && isEnumInput(inputRaw);
|
|
279
|
+
if (!isString(fieldDef.input)) {
|
|
280
|
+
return "input 不支持";
|
|
281
|
+
}
|
|
251
282
|
|
|
252
|
-
if (
|
|
253
|
-
if (typeof value !== "number") return
|
|
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 (
|
|
287
|
+
} else if (fieldDef.input === "string" || fieldDef.input === "char" || fieldDef.input === "regexp" || fieldDef.input === "enum") {
|
|
257
288
|
if (!isString(value)) return "必须是字符串";
|
|
258
|
-
if (
|
|
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 (
|
|
263
|
-
|
|
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 (
|
|
268
|
-
|
|
269
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
|
4
|
-
"gitHead": "
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
89
|
+
await ctx.mysql.updBatch(MENU_TABLE_NAME, updList);
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
if (insList.length > 0) {
|
|
91
|
-
await ctx.mysql.insBatch(
|
|
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(
|
|
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": "
|
|
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": "
|
|
23
|
+
"input": "regexp",
|
|
24
|
+
"check": "@email",
|
|
23
25
|
"max": 100
|
|
24
26
|
},
|
|
25
27
|
"phone": {
|
|
26
28
|
"name": "手机号",
|
|
27
|
-
"input": "
|
|
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": "
|
|
40
|
+
"input": "regexp",
|
|
41
|
+
"check": "@alphanumeric_",
|
|
38
42
|
"min": 2,
|
|
39
43
|
"max": 50
|
|
40
44
|
},
|
|
41
45
|
"roleType": {
|
|
42
46
|
"name": "角色类型",
|
|
43
|
-
"input": "
|
|
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": "
|
|
4
|
+
"input": "regexp",
|
|
5
|
+
"check": "@alphanumeric_",
|
|
5
6
|
"min": 2,
|
|
6
7
|
"max": 50
|
|
7
8
|
},
|
|
8
9
|
"key": {
|
|
9
10
|
"name": "字典键",
|
|
10
|
-
"input": "
|
|
11
|
+
"input": "regexp",
|
|
12
|
+
"check": "@alphanumeric_",
|
|
11
13
|
"min": 1,
|
|
12
14
|
"max": 50
|
|
13
15
|
},
|
package/tables/dictType.json
CHANGED
package/tables/emailLog.json
CHANGED
package/tables/role.json
CHANGED
package/tables/sysConfig.json
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
},
|
|
8
8
|
"code": {
|
|
9
9
|
"name": "配置代码",
|
|
10
|
-
"input": "
|
|
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": "
|
|
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":
|
|
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
|