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/checks/table.js
CHANGED
|
@@ -1,126 +1,42 @@
|
|
|
1
|
-
import * as z from "zod";
|
|
2
|
-
|
|
3
|
-
import { FIELD_RULE_DEFAULT_MAX, FIELD_RULE_DEFAULT_MIN, FIELD_RULE_INPUT_TYPES } from "../configs/constConfig.js";
|
|
4
1
|
import { Logger } from "../lib/logger.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { validateFieldRule } from "../lib/validator.js";
|
|
3
|
+
import { formatValidationIssues } from "../utils/formatValidationIssues.js";
|
|
4
|
+
import { isPlainObject } from "../utils/is.js";
|
|
7
5
|
import { snakeCase } from "../utils/util.js";
|
|
8
6
|
|
|
9
|
-
z.config(z.locales.zhCN());
|
|
10
|
-
|
|
11
7
|
const lowerCamelRegex = /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/;
|
|
12
8
|
const lowerSnakeRegex = /^_?[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
|
|
13
|
-
const noTrimString = z.string().refine(isNoTrimStringAllowEmpty, "不允许首尾空格");
|
|
14
|
-
const inputSchema = z.enum(FIELD_RULE_INPUT_TYPES);
|
|
15
9
|
|
|
16
10
|
function isValidFieldName(fieldName) {
|
|
17
|
-
|
|
18
|
-
return lowerSnakeRegex.test(fieldName);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return lowerCamelRegex.test(fieldName);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function addIssue(context, path, message) {
|
|
25
|
-
context.addIssue({
|
|
26
|
-
path: path,
|
|
27
|
-
message: message
|
|
28
|
-
});
|
|
11
|
+
return fieldName.includes("_") ? lowerSnakeRegex.test(fieldName) : lowerCamelRegex.test(fieldName);
|
|
29
12
|
}
|
|
30
13
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.superRefine((value, context) => {
|
|
42
|
-
const min = isNullable(value.min) ? FIELD_RULE_DEFAULT_MIN : value.min;
|
|
43
|
-
const max = isNullable(value.max) ? FIELD_RULE_DEFAULT_MAX : value.max;
|
|
44
|
-
if (min > max) {
|
|
45
|
-
addIssue(context, ["min"], "min 不能大于 max");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const checkValue = value.check;
|
|
49
|
-
const input = value.input;
|
|
50
|
-
|
|
51
|
-
if ((input === "enum" || input === "enumNumber" || input === "enumInteger" || input === "regexp") && (!checkValue || checkValue.length === 0)) {
|
|
52
|
-
addIssue(context, ["check"], `${input} 必须提供 check`);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (input === "regexp" && checkValue && !isRegexInput(checkValue)) {
|
|
57
|
-
addIssue(context, ["check"], "regexp.check 必须是正则字面量或 @ 别名");
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if ((input === "enum" || input === "enumNumber" || input === "enumInteger") && checkValue) {
|
|
62
|
-
const items = String(checkValue)
|
|
63
|
-
.split("|")
|
|
64
|
-
.map((item) => item.trim())
|
|
65
|
-
.filter((item) => item.length > 0);
|
|
66
|
-
|
|
67
|
-
if (items.length === 0) {
|
|
68
|
-
addIssue(context, ["check"], `${input}.check 必须是非空枚举值列表`);
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (input === "enumNumber" || input === "enumInteger") {
|
|
73
|
-
for (const item of items) {
|
|
74
|
-
const num = Number(item);
|
|
75
|
-
if (!Number.isFinite(num)) {
|
|
76
|
-
addIssue(context, ["check"], `${input}.check 存在非法数字值: ${item}`);
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (input === "enumInteger" && !Number.isInteger(num)) {
|
|
80
|
-
addIssue(context, ["check"], `${input}.check 存在非法整数值: ${item}`);
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
14
|
+
export async function checkTable(tables) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
if (!isPlainObject(tables)) {
|
|
17
|
+
issues.push({ path: [], message: "必须是对象格式" });
|
|
18
|
+
} else {
|
|
19
|
+
for (const [tableName, fields] of Object.entries(tables)) {
|
|
20
|
+
if (!lowerCamelRegex.test(tableName)) issues.push({ path: [tableName], message: "表名必须为 lowerCamelCase" });
|
|
21
|
+
if (!isPlainObject(fields)) {
|
|
22
|
+
issues.push({ path: [tableName], message: "必须是对象格式" });
|
|
23
|
+
continue;
|
|
84
24
|
}
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
25
|
|
|
88
|
-
const
|
|
89
|
-
|
|
26
|
+
const fieldNameMap = new Map();
|
|
27
|
+
for (const [fieldName, rule] of Object.entries(fields)) {
|
|
28
|
+
if (!isValidFieldName(fieldName)) issues.push({ path: [tableName, fieldName], message: "字段名必须为 lowerCamelCase 或 snake_case" });
|
|
29
|
+
for (const issue of validateFieldRule(rule)) issues.push({ path: [tableName, fieldName, ...issue.path], message: issue.message });
|
|
90
30
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
addIssue(context, [fieldName], `字段名 ${fieldName} 与 ${previousFieldName} 指向同一数据库字段 ${dbFieldName}`);
|
|
97
|
-
continue;
|
|
31
|
+
const dbFieldName = snakeCase(fieldName);
|
|
32
|
+
const previousFieldName = fieldNameMap.get(dbFieldName);
|
|
33
|
+
if (previousFieldName) issues.push({ path: [tableName, fieldName], message: `字段名 ${fieldName} 与 ${previousFieldName} 指向同一数据库字段 ${dbFieldName}` });
|
|
34
|
+
else fieldNameMap.set(dbFieldName, fieldName);
|
|
35
|
+
}
|
|
98
36
|
}
|
|
99
|
-
|
|
100
|
-
fieldNameMap.set(dbFieldName, fieldName);
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
const tableRegistrySchema = z.record(z.string().regex(lowerCamelRegex), tableContentSchema);
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 检查表定义文件
|
|
107
|
-
* @throws 当检查失败时抛出异常
|
|
108
|
-
*/
|
|
109
|
-
export async function checkTable(tables) {
|
|
110
|
-
const result = tableRegistrySchema.safeParse(tables);
|
|
111
|
-
if (result.success) {
|
|
112
|
-
return false;
|
|
113
37
|
}
|
|
114
38
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
{
|
|
118
|
-
errors: formatZodIssues(result.error.issues, {
|
|
119
|
-
items: tables,
|
|
120
|
-
itemLabel: "table"
|
|
121
|
-
})
|
|
122
|
-
},
|
|
123
|
-
false
|
|
124
|
-
);
|
|
39
|
+
if (issues.length === 0) return false;
|
|
40
|
+
Logger.warn("表结构校验失败", { errors: formatValidationIssues(issues, { item: tables, itemLabel: "table" }) }, false);
|
|
125
41
|
return true;
|
|
126
42
|
}
|
package/configs/beflyConfig.json
CHANGED
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
},
|
|
56
56
|
|
|
57
57
|
"cors": {
|
|
58
|
-
"origin": "*",
|
|
59
58
|
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
|
|
60
59
|
"allowedHeaders": "Content-Type,Authorization",
|
|
61
60
|
"exposedHeaders": "",
|
|
@@ -68,7 +67,6 @@
|
|
|
68
67
|
"defaultLimit": 1000,
|
|
69
68
|
"defaultWindow": 60,
|
|
70
69
|
"key": "ip",
|
|
71
|
-
"skipRoutes": []
|
|
72
|
-
"rules": []
|
|
70
|
+
"skipRoutes": []
|
|
73
71
|
}
|
|
74
72
|
}
|
package/hooks/rateLimit.js
CHANGED
|
@@ -5,72 +5,13 @@ import { ErrorResponse } from "../utils/response.js";
|
|
|
5
5
|
|
|
6
6
|
const RATE_LIMIT_ENABLED_VALUES = new Set([1, true, "1", "true"]);
|
|
7
7
|
|
|
8
|
-
function isRateLimitEnabled(value) {
|
|
9
|
-
return RATE_LIMIT_ENABLED_VALUES.has(value);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
8
|
function toPositiveInt(value, fallbackValue) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
if (!Number.isFinite(num) || num <= 0) {
|
|
16
|
-
return fallbackValue;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return Math.floor(num);
|
|
9
|
+
const number = Number(value);
|
|
10
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallbackValue;
|
|
20
11
|
}
|
|
21
12
|
|
|
22
13
|
function toSafeString(value, fallbackValue) {
|
|
23
|
-
|
|
24
|
-
return fallbackValue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const trimmed = value.trim();
|
|
28
|
-
|
|
29
|
-
if (!trimmed) {
|
|
30
|
-
return fallbackValue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return trimmed;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function toRuleMethods(rule) {
|
|
37
|
-
if (Array.isArray(rule?.methods)) {
|
|
38
|
-
const values = [];
|
|
39
|
-
|
|
40
|
-
for (const method of rule.methods) {
|
|
41
|
-
const text = toSafeString(method, "");
|
|
42
|
-
if (!text) {
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
values.push(text.toUpperCase());
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return values;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (typeof rule?.method === "string") {
|
|
53
|
-
const value = toSafeString(rule.method, "");
|
|
54
|
-
if (value) {
|
|
55
|
-
return [value.toUpperCase()];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function normalizeRule(rawRule, defaultLimit, defaultWindow, defaultKeyType) {
|
|
63
|
-
const rule = rawRule && typeof rawRule === "object" ? rawRule : {};
|
|
64
|
-
const pathPattern = toSafeString(rule.path || rule.apiPath || rule.route, "");
|
|
65
|
-
const methods = toRuleMethods(rule);
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
pathPattern: pathPattern,
|
|
69
|
-
methods: methods,
|
|
70
|
-
limit: toPositiveInt(rule.limit, defaultLimit),
|
|
71
|
-
window: toPositiveInt(rule.window, defaultWindow),
|
|
72
|
-
keyType: toSafeString(rule.key, defaultKeyType)
|
|
73
|
-
};
|
|
14
|
+
return typeof value === "string" && value.trim() ? value.trim() : fallbackValue;
|
|
74
15
|
}
|
|
75
16
|
|
|
76
17
|
function createMatcher(pattern) {
|
|
@@ -82,117 +23,31 @@ function createMatcher(pattern) {
|
|
|
82
23
|
}
|
|
83
24
|
|
|
84
25
|
function normalizeConfig(config) {
|
|
85
|
-
const defaultLimit = toPositiveInt(config?.defaultLimit, 1000);
|
|
86
|
-
const defaultWindow = toPositiveInt(config?.defaultWindow, 60);
|
|
87
|
-
const defaultKeyType = toSafeString(config?.key, "ip");
|
|
88
|
-
|
|
89
26
|
const skipMatchers = [];
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const pattern = toSafeString(route, "");
|
|
94
|
-
if (!pattern) {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const matcher = createMatcher(pattern);
|
|
99
|
-
if (!matcher) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
skipMatchers.push(matcher);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const rules = [];
|
|
107
|
-
const rawRules = Array.isArray(config?.rules) ? config.rules : [];
|
|
108
|
-
|
|
109
|
-
for (const rawRule of rawRules) {
|
|
110
|
-
const normalizedRule = normalizeRule(rawRule, defaultLimit, defaultWindow, defaultKeyType);
|
|
111
|
-
|
|
112
|
-
if (!normalizedRule.pathPattern) {
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const matcher = createMatcher(normalizedRule.pathPattern);
|
|
117
|
-
if (!matcher) {
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
rules.push({
|
|
122
|
-
matcher: matcher,
|
|
123
|
-
methods: normalizedRule.methods,
|
|
124
|
-
limit: normalizedRule.limit,
|
|
125
|
-
window: normalizedRule.window,
|
|
126
|
-
keyType: normalizedRule.keyType
|
|
127
|
-
});
|
|
27
|
+
for (const route of Array.isArray(config?.skipRoutes) ? config.skipRoutes : []) {
|
|
28
|
+
const matcher = createMatcher(toSafeString(route, ""));
|
|
29
|
+
if (matcher) skipMatchers.push(matcher);
|
|
128
30
|
}
|
|
129
31
|
|
|
130
32
|
return {
|
|
131
|
-
enabled:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
skipMatchers: skipMatchers
|
|
136
|
-
rules: rules
|
|
33
|
+
enabled: RATE_LIMIT_ENABLED_VALUES.has(config?.enable),
|
|
34
|
+
limit: toPositiveInt(config?.defaultLimit, 1000),
|
|
35
|
+
window: toPositiveInt(config?.defaultWindow, 60),
|
|
36
|
+
keyType: toSafeString(config?.key, "ip"),
|
|
37
|
+
skipMatchers: skipMatchers
|
|
137
38
|
};
|
|
138
39
|
}
|
|
139
40
|
|
|
140
41
|
function resolveScopeByKeyType(keyType, ctx) {
|
|
141
|
-
if (keyType === "userId") {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return `ip:${ctx.ip || "unknown"}`;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (keyType === "roleCode") {
|
|
150
|
-
return `role:${ctx.roleCode || "guest"}`;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (keyType === "apiPath") {
|
|
154
|
-
return `path:${ctx.apiPath || "unknown"}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (keyType === "global") {
|
|
158
|
-
return "global";
|
|
159
|
-
}
|
|
160
|
-
|
|
42
|
+
if (keyType === "userId") return isValidPositiveInt(ctx.userId) ? `user:${ctx.userId}` : `ip:${ctx.ip || "unknown"}`;
|
|
43
|
+
if (keyType === "roleCode") return `role:${ctx.roleCode || "guest"}`;
|
|
44
|
+
if (keyType === "apiPath") return `path:${ctx.apiPath || "unknown"}`;
|
|
45
|
+
if (keyType === "global") return "global";
|
|
161
46
|
return `ip:${ctx.ip || "unknown"}`;
|
|
162
47
|
}
|
|
163
48
|
|
|
164
|
-
function resolveRateRule(normalized, ctx) {
|
|
165
|
-
for (const rule of normalized.rules) {
|
|
166
|
-
if (!rule.matcher(ctx.apiPath)) {
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (rule.methods.length > 0 && !rule.methods.includes(ctx.method)) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
limit: rule.limit,
|
|
176
|
-
window: rule.window,
|
|
177
|
-
keyType: rule.keyType
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
limit: normalized.defaultLimit,
|
|
183
|
-
window: normalized.defaultWindow,
|
|
184
|
-
keyType: normalized.defaultKeyType
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
49
|
function shouldSkipRateLimit(normalized, ctx) {
|
|
189
|
-
|
|
190
|
-
if (matcher(ctx.apiPath)) {
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return false;
|
|
50
|
+
return normalized.skipMatchers.some((matcher) => matcher(ctx.apiPath));
|
|
196
51
|
}
|
|
197
52
|
|
|
198
53
|
function buildRateLimitRedisKey(ctx, keyType, scope) {
|
|
@@ -202,42 +57,26 @@ function buildRateLimitRedisKey(ctx, keyType, scope) {
|
|
|
202
57
|
export default {
|
|
203
58
|
order: 1.5,
|
|
204
59
|
handler: async (befly, ctx) => {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
if (!normalized.enabled) {
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (!befly?.redis || typeof befly.redis.incrWithExpire !== "function") {
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (shouldSkipRateLimit(normalized, ctx)) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const rateRule = resolveRateRule(normalized, ctx);
|
|
220
|
-
const scope = resolveScopeByKeyType(rateRule.keyType, ctx);
|
|
221
|
-
const redisKey = buildRateLimitRedisKey(ctx, rateRule.keyType, scope);
|
|
222
|
-
const current = await befly.redis.incrWithExpire(redisKey, rateRule.window);
|
|
60
|
+
const rateLimit = normalizeConfig(befly?.config?.rateLimit);
|
|
61
|
+
if (!rateLimit.enabled || !befly?.redis || typeof befly.redis.incrWithExpire !== "function" || shouldSkipRateLimit(rateLimit, ctx)) return;
|
|
223
62
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
63
|
+
const scope = resolveScopeByKeyType(rateLimit.keyType, ctx);
|
|
64
|
+
const current = await befly.redis.incrWithExpire(buildRateLimitRedisKey(ctx, rateLimit.keyType, scope), rateLimit.window);
|
|
65
|
+
if (current <= rateLimit.limit) return;
|
|
227
66
|
|
|
228
67
|
ctx.corsHeaders = ctx.corsHeaders || {};
|
|
229
|
-
ctx.corsHeaders["Retry-After"] = String(
|
|
68
|
+
ctx.corsHeaders["Retry-After"] = String(rateLimit.window);
|
|
230
69
|
ctx.response = ErrorResponse(
|
|
231
70
|
ctx,
|
|
232
71
|
"请求过于频繁,请稍后再试",
|
|
233
72
|
1,
|
|
234
73
|
null,
|
|
235
74
|
{
|
|
236
|
-
limit:
|
|
237
|
-
window:
|
|
75
|
+
limit: rateLimit.limit,
|
|
76
|
+
window: rateLimit.window,
|
|
238
77
|
current: current,
|
|
239
78
|
apiPath: ctx.apiPath,
|
|
240
|
-
key:
|
|
79
|
+
key: rateLimit.keyType
|
|
241
80
|
},
|
|
242
81
|
"rateLimit"
|
|
243
82
|
);
|
package/hooks/validator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// 相对导入
|
|
2
|
-
import {
|
|
2
|
+
import { validateFields } from "../lib/validator.js";
|
|
3
3
|
import { isPlainObject, isString } from "../utils/is.js";
|
|
4
4
|
import { ErrorResponse } from "../utils/response.js";
|
|
5
5
|
import { snakeCase } from "../utils/util.js";
|
|
@@ -37,7 +37,7 @@ export default {
|
|
|
37
37
|
ctx.body = nextBody;
|
|
38
38
|
|
|
39
39
|
// 验证参数
|
|
40
|
-
const result =
|
|
40
|
+
const result = validateFields(ctx.body, ctx.apiFields, ctx.apiRequired);
|
|
41
41
|
|
|
42
42
|
if (result.code !== 0) {
|
|
43
43
|
ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors, "validator");
|
package/index.js
CHANGED
|
@@ -55,7 +55,7 @@ function prefixMenuPaths(menus, prefix) {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
async function
|
|
58
|
+
async function syncReady(ctx) {
|
|
59
59
|
const missingCtxKeys = ["ctx.redis", "ctx.mysql", "ctx.cache"].filter((key) => !ctx[key.slice(4)]);
|
|
60
60
|
if (missingCtxKeys.length > 0) {
|
|
61
61
|
throw new Error(`启动失败:${missingCtxKeys.join("、")} 未初始化`, {
|
|
@@ -88,6 +88,7 @@ async function ensureSyncPrerequisites(ctx) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export async function createBefly(config = {}, menus = []) {
|
|
91
|
+
const mergeStartTime = Bun.nanoseconds();
|
|
91
92
|
const mergedConfig = deepMerge(beflyConfig, config);
|
|
92
93
|
const mergedMenus = deepMerge(prefixMenuPaths(beflyMenus, "core"), menus);
|
|
93
94
|
|
|
@@ -95,15 +96,35 @@ export async function createBefly(config = {}, menus = []) {
|
|
|
95
96
|
runtimeEnv: mergedConfig.runMode,
|
|
96
97
|
...mergedConfig.logger
|
|
97
98
|
});
|
|
99
|
+
Logger.info(`启动 合并配置 耗时 ${calcPerfTime(mergeStartTime)}`);
|
|
98
100
|
|
|
101
|
+
let stageStartTime = Bun.nanoseconds();
|
|
99
102
|
const configHasError = await checkConfig(mergedConfig);
|
|
103
|
+
Logger.info(`启动 检查配置 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
104
|
+
|
|
105
|
+
stageStartTime = Bun.nanoseconds();
|
|
100
106
|
const { apis, tables, plugins, hooks } = await scanSources();
|
|
107
|
+
Logger.info(`启动 扫描源码 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
101
108
|
|
|
109
|
+
stageStartTime = Bun.nanoseconds();
|
|
102
110
|
const apiHasError = await checkApi(apis);
|
|
111
|
+
Logger.info(`启动 检查接口 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
112
|
+
|
|
113
|
+
stageStartTime = Bun.nanoseconds();
|
|
103
114
|
const tableHasError = await checkTable(tables);
|
|
115
|
+
Logger.info(`启动 检查表 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
116
|
+
|
|
117
|
+
stageStartTime = Bun.nanoseconds();
|
|
104
118
|
const pluginHasError = await checkPlugin(plugins);
|
|
119
|
+
Logger.info(`启动 检查插件 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
120
|
+
|
|
121
|
+
stageStartTime = Bun.nanoseconds();
|
|
105
122
|
const hookHasError = await checkHook(hooks);
|
|
123
|
+
Logger.info(`启动 检查钩子 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
124
|
+
|
|
125
|
+
stageStartTime = Bun.nanoseconds();
|
|
106
126
|
const menuHasError = await checkMenu(mergedMenus);
|
|
127
|
+
Logger.info(`启动 检查菜单 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
107
128
|
|
|
108
129
|
if (configHasError || apiHasError || tableHasError || pluginHasError || hookHasError || menuHasError) {
|
|
109
130
|
throw new Error("检查失败:存在配置/结构问题", {
|
|
@@ -167,24 +188,46 @@ export class Befly {
|
|
|
167
188
|
|
|
168
189
|
// 启动期建立基础连接(SQL + Redis)
|
|
169
190
|
// 说明:连接职责收敛到启动期单点;插件只消费已连接实例(Connect.getSql/getRedis)。
|
|
191
|
+
let stageStartTime = Bun.nanoseconds();
|
|
170
192
|
await Connect.connectRedis(this.context.config.redis);
|
|
193
|
+
Logger.info(`启动 连接 Redis 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
194
|
+
|
|
195
|
+
stageStartTime = Bun.nanoseconds();
|
|
171
196
|
await Connect.connectMysql(this.context.config.mysql);
|
|
197
|
+
Logger.info(`启动 连接 Mysql 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
172
198
|
|
|
173
199
|
// 加载插件
|
|
174
200
|
for (const item of this.plugins.toSorted((a, b) => a.order - b.order)) {
|
|
201
|
+
const pluginStartTime = Bun.nanoseconds();
|
|
175
202
|
this.context[item.fileName] = await item.handler(this.context);
|
|
203
|
+
Logger.info(`启动 插件 ${item.fileName} 耗时 ${calcPerfTime(pluginStartTime)}`);
|
|
176
204
|
}
|
|
177
|
-
|
|
205
|
+
|
|
206
|
+
stageStartTime = Bun.nanoseconds();
|
|
207
|
+
await syncReady(this.context);
|
|
208
|
+
Logger.info(`启动 同步就绪 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
178
209
|
|
|
179
210
|
// 自动同步(PM2 cluster:主进程执行,其它进程等待同步完成)
|
|
180
211
|
if (isPrimaryProcess()) {
|
|
212
|
+
stageStartTime = Bun.nanoseconds();
|
|
181
213
|
await syncApi(this.context, this.apis);
|
|
214
|
+
Logger.info(`启动 同步接口 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
215
|
+
|
|
216
|
+
stageStartTime = Bun.nanoseconds();
|
|
182
217
|
await syncMenu(this.context, this.menus);
|
|
218
|
+
Logger.info(`启动 同步菜单 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
219
|
+
|
|
220
|
+
stageStartTime = Bun.nanoseconds();
|
|
183
221
|
await syncDev(this.context);
|
|
222
|
+
Logger.info(`启动 同步开发账号 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
223
|
+
|
|
224
|
+
stageStartTime = Bun.nanoseconds();
|
|
184
225
|
await syncCache(this.context);
|
|
226
|
+
Logger.info(`启动 同步缓存 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
185
227
|
}
|
|
186
228
|
|
|
187
229
|
// 加载钩子
|
|
230
|
+
stageStartTime = Bun.nanoseconds();
|
|
188
231
|
this.hooks = this.hooks.toSorted((a, b) => a.order - b.order);
|
|
189
232
|
|
|
190
233
|
// 加载所有 API
|
|
@@ -193,7 +236,9 @@ export class Befly {
|
|
|
193
236
|
// 启动 HTTP服务器
|
|
194
237
|
const apiFetch = apiHandler(this.apis, this.hooks, this.context);
|
|
195
238
|
const staticFetch = staticHandler(this.context.config.cors, this.context.config.upload);
|
|
239
|
+
Logger.info(`启动 装配接口 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
196
240
|
|
|
241
|
+
stageStartTime = Bun.nanoseconds();
|
|
197
242
|
const server = Bun.serve({
|
|
198
243
|
port: this.context.config.appPort || 3000,
|
|
199
244
|
hostname: this.context.config.appHost || "0.0.0.0",
|
|
@@ -230,6 +275,7 @@ export class Befly {
|
|
|
230
275
|
return Response.json({ code: 1, msg: "内部服务器错误" }, { status: 200 });
|
|
231
276
|
}
|
|
232
277
|
});
|
|
278
|
+
Logger.info(`启动 监听服务 耗时 ${calcPerfTime(stageStartTime)}`);
|
|
233
279
|
|
|
234
280
|
const finalStartupTime = calcPerfTime(serverStartTime);
|
|
235
281
|
|
package/lib/connect.js
CHANGED
|
@@ -88,7 +88,6 @@ export class Connect {
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
await this.mysqlClient`SELECT 1`;
|
|
91
|
-
Logger.info(`Mysql 连接成功 (${config.hostname})`);
|
|
92
91
|
} catch (error) {
|
|
93
92
|
await this.handleConnectError("mysqlClient", "Mysql", "mysql", config.hostname, error);
|
|
94
93
|
}
|
|
@@ -101,11 +100,6 @@ export class Connect {
|
|
|
101
100
|
static async connectRedis(config) {
|
|
102
101
|
try {
|
|
103
102
|
this.redisClient = new RedisClient(buildRedisUrl(config));
|
|
104
|
-
// Called when successfully connected to Redis server
|
|
105
|
-
this.redisClient.onconnect = () => {
|
|
106
|
-
Logger.info(`Redis 连接成功 (${config.hostname})`);
|
|
107
|
-
};
|
|
108
|
-
|
|
109
103
|
// Called when disconnected from Redis server
|
|
110
104
|
this.redisClient.onclose = (error) => {
|
|
111
105
|
if (error) {
|
package/lib/dbHelper.js
CHANGED
|
@@ -1052,20 +1052,11 @@ class DbHelper {
|
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
1054
|
async trans(callback) {
|
|
1055
|
-
const abortMark = "__beflyTransAbort";
|
|
1056
|
-
|
|
1057
1055
|
if (this.isTransaction) {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
code: "runtime"
|
|
1063
|
-
});
|
|
1064
|
-
abortError[abortMark] = true;
|
|
1065
|
-
abortError.payload = result;
|
|
1066
|
-
throw abortError;
|
|
1067
|
-
}
|
|
1068
|
-
return result;
|
|
1056
|
+
throw new Error("不支持嵌套事务", {
|
|
1057
|
+
cause: null,
|
|
1058
|
+
code: "policy"
|
|
1059
|
+
});
|
|
1069
1060
|
}
|
|
1070
1061
|
|
|
1071
1062
|
if (typeof this.sql?.begin !== "function") {
|
|
@@ -1075,34 +1066,17 @@ class DbHelper {
|
|
|
1075
1066
|
});
|
|
1076
1067
|
}
|
|
1077
1068
|
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
tables: this.tables
|
|
1087
|
-
});
|
|
1088
|
-
const result = await callback(trans);
|
|
1089
|
-
if (result?.code !== undefined && result.code !== 0) {
|
|
1090
|
-
const abortError = new Error("TRANSACTION_ABORT", {
|
|
1091
|
-
cause: null,
|
|
1092
|
-
code: "runtime"
|
|
1093
|
-
});
|
|
1094
|
-
abortError[abortMark] = true;
|
|
1095
|
-
abortError.payload = result;
|
|
1096
|
-
throw abortError;
|
|
1097
|
-
}
|
|
1098
|
-
return result;
|
|
1069
|
+
return await this.sql.begin(async (tx) => {
|
|
1070
|
+
const trans = new this.constructor({
|
|
1071
|
+
redis: this.redis,
|
|
1072
|
+
dbName: this.dbName,
|
|
1073
|
+
sql: tx,
|
|
1074
|
+
isTransaction: true,
|
|
1075
|
+
beflyMode: this.beflyMode,
|
|
1076
|
+
tables: this.tables
|
|
1099
1077
|
});
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
return error.payload;
|
|
1103
|
-
}
|
|
1104
|
-
throw error;
|
|
1105
|
-
}
|
|
1078
|
+
return await callback(trans);
|
|
1079
|
+
});
|
|
1106
1080
|
}
|
|
1107
1081
|
}
|
|
1108
1082
|
|