befly 3.9.38 → 3.9.40
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/README.md +37 -38
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +1 -1
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +8 -8
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +48 -44
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -52
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -65
- package/sync/syncMenu.ts +190 -55
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
package/lib/validator.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 数据验证器 - Befly 项目专用
|
|
3
3
|
* 纯静态类设计,简洁易用
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import type { TableDefinition, FieldDefinition, ValidateResult, SingleResult } from "../types/validate.js";
|
|
7
|
+
|
|
8
|
+
import { RegexAliases, getCompiledRegex } from "../utils/regex.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* 验证器类
|
|
@@ -30,17 +31,17 @@ export class Validator {
|
|
|
30
31
|
const fieldErrors: Record<string, string> = {};
|
|
31
32
|
|
|
32
33
|
// 参数检查
|
|
33
|
-
if (!data || typeof data !==
|
|
34
|
-
return this.buildResult({ _error:
|
|
34
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
35
|
+
return this.buildResult({ _error: "数据必须是对象格式" });
|
|
35
36
|
}
|
|
36
|
-
if (!rules || typeof rules !==
|
|
37
|
-
return this.buildResult({ _error:
|
|
37
|
+
if (!rules || typeof rules !== "object") {
|
|
38
|
+
return this.buildResult({ _error: "验证规则必须是对象格式" });
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
// 检查必填字段
|
|
41
42
|
for (const field of required) {
|
|
42
43
|
const value = data[field];
|
|
43
|
-
if (value === undefined || value === null || value ===
|
|
44
|
+
if (value === undefined || value === null || value === "") {
|
|
44
45
|
const label = rules[field]?.name || field;
|
|
45
46
|
fieldErrors[field] = `${label}为必填项`;
|
|
46
47
|
}
|
|
@@ -49,7 +50,8 @@ export class Validator {
|
|
|
49
50
|
// 验证有值的字段
|
|
50
51
|
for (const [field, rule] of Object.entries(rules)) {
|
|
51
52
|
if (fieldErrors[field]) continue;
|
|
52
|
-
|
|
53
|
+
// 字段值为 undefined 时跳过验证(除非是必填字段,但必填字段已在上面检查过)
|
|
54
|
+
if (data[field] === undefined && !required.includes(field)) continue;
|
|
53
55
|
|
|
54
56
|
const error = this.checkField(data[field], rule, field);
|
|
55
57
|
if (error) fieldErrors[field] = error;
|
|
@@ -65,7 +67,7 @@ export class Validator {
|
|
|
65
67
|
const { type, default: defaultValue } = fieldDef;
|
|
66
68
|
|
|
67
69
|
// 处理空值
|
|
68
|
-
if (value === undefined || value === null || value ===
|
|
70
|
+
if (value === undefined || value === null || value === "") {
|
|
69
71
|
return { value: this.defaultFor(type, defaultValue), error: null };
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -118,23 +120,36 @@ export class Validator {
|
|
|
118
120
|
/** 类型转换 */
|
|
119
121
|
private static convert(value: any, type: string): { value: any; error: string | null } {
|
|
120
122
|
switch (type.toLowerCase()) {
|
|
121
|
-
case
|
|
122
|
-
if (typeof value ===
|
|
123
|
-
return Number.isNaN(value) || !isFinite(value) ? { value: null, error:
|
|
123
|
+
case "number":
|
|
124
|
+
if (typeof value === "number") {
|
|
125
|
+
return Number.isNaN(value) || !isFinite(value) ? { value: null, error: "必须是有效数字" } : { value: value, error: null };
|
|
124
126
|
}
|
|
125
|
-
if (typeof value ===
|
|
127
|
+
if (typeof value === "string") {
|
|
126
128
|
const num = Number(value);
|
|
127
|
-
return Number.isNaN(num) || !isFinite(num) ? { value: null, error:
|
|
129
|
+
return Number.isNaN(num) || !isFinite(num) ? { value: null, error: "必须是数字" } : { value: num, error: null };
|
|
128
130
|
}
|
|
129
|
-
return { value: null, error:
|
|
131
|
+
return { value: null, error: "必须是数字" };
|
|
130
132
|
|
|
131
|
-
case
|
|
132
|
-
case
|
|
133
|
-
return typeof value ===
|
|
133
|
+
case "string":
|
|
134
|
+
case "text":
|
|
135
|
+
return typeof value === "string" ? { value: value, error: null } : { value: null, error: "必须是字符串" };
|
|
134
136
|
|
|
135
|
-
case
|
|
136
|
-
case
|
|
137
|
-
return Array.isArray(value) ? { value: value, error: null } : { value: null, error:
|
|
137
|
+
case "array_string":
|
|
138
|
+
case "array_text":
|
|
139
|
+
return Array.isArray(value) ? { value: value, error: null } : { value: null, error: "必须是数组" };
|
|
140
|
+
|
|
141
|
+
case "array_number_string":
|
|
142
|
+
case "array_number_text":
|
|
143
|
+
if (!Array.isArray(value)) {
|
|
144
|
+
return { value: null, error: "必须是数组" };
|
|
145
|
+
}
|
|
146
|
+
// 验证数组元素必须是数字
|
|
147
|
+
for (const item of value) {
|
|
148
|
+
if (typeof item !== "number" || !isFinite(item)) {
|
|
149
|
+
return { value: null, error: "数组元素必须是数字" };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { value: value, error: null };
|
|
138
153
|
|
|
139
154
|
default:
|
|
140
155
|
return { value: value, error: null };
|
|
@@ -147,26 +162,28 @@ export class Validator {
|
|
|
147
162
|
const regex = this.resolveRegex(regexp);
|
|
148
163
|
|
|
149
164
|
switch (type.toLowerCase()) {
|
|
150
|
-
case
|
|
165
|
+
case "number":
|
|
151
166
|
if (min !== null && value < min) return `不能小于${min}`;
|
|
152
167
|
if (max !== null && max > 0 && value > max) return `不能大于${max}`;
|
|
153
|
-
if (regex && !this.testRegex(regex, String(value))) return
|
|
168
|
+
if (regex && !this.testRegex(regex, String(value))) return "格式不正确";
|
|
154
169
|
break;
|
|
155
170
|
|
|
156
|
-
case
|
|
157
|
-
case
|
|
171
|
+
case "string":
|
|
172
|
+
case "text":
|
|
158
173
|
if (min !== null && value.length < min) return `长度不能少于${min}个字符`;
|
|
159
174
|
if (max !== null && max > 0 && value.length > max) return `长度不能超过${max}个字符`;
|
|
160
|
-
if (regex && !this.testRegex(regex, value)) return
|
|
175
|
+
if (regex && !this.testRegex(regex, value)) return "格式不正确";
|
|
161
176
|
break;
|
|
162
177
|
|
|
163
|
-
case
|
|
164
|
-
case
|
|
178
|
+
case "array_string":
|
|
179
|
+
case "array_text":
|
|
180
|
+
case "array_number_string":
|
|
181
|
+
case "array_number_text":
|
|
165
182
|
if (min !== null && value.length < min) return `至少需要${min}个元素`;
|
|
166
183
|
if (max !== null && max > 0 && value.length > max) return `最多只能有${max}个元素`;
|
|
167
184
|
if (regex) {
|
|
168
185
|
for (const item of value) {
|
|
169
|
-
if (!this.testRegex(regex, String(item))) return
|
|
186
|
+
if (!this.testRegex(regex, String(item))) return "元素格式不正确";
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
break;
|
|
@@ -177,7 +194,7 @@ export class Validator {
|
|
|
177
194
|
/** 解析正则别名 */
|
|
178
195
|
private static resolveRegex(regexp: string | null): string | null {
|
|
179
196
|
if (!regexp) return null;
|
|
180
|
-
if (regexp.startsWith(
|
|
197
|
+
if (regexp.startsWith("@")) {
|
|
181
198
|
const key = regexp.substring(1) as keyof typeof RegexAliases;
|
|
182
199
|
return RegexAliases[key] || regexp;
|
|
183
200
|
}
|
|
@@ -195,10 +212,11 @@ export class Validator {
|
|
|
195
212
|
|
|
196
213
|
/** 获取默认值 */
|
|
197
214
|
private static defaultFor(type: string, defaultValue: any): any {
|
|
215
|
+
// 如果字段定义了默认值,则使用字段默认值(优先级最高)
|
|
198
216
|
if (defaultValue !== null && defaultValue !== undefined) {
|
|
199
217
|
// 数组默认值
|
|
200
|
-
if ((type ===
|
|
201
|
-
if (defaultValue ===
|
|
218
|
+
if ((type === "array_string" || type === "array_text" || type === "array_number_string" || type === "array_number_text") && typeof defaultValue === "string") {
|
|
219
|
+
if (defaultValue === "[]") return [];
|
|
202
220
|
try {
|
|
203
221
|
const parsed = JSON.parse(defaultValue);
|
|
204
222
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -207,22 +225,24 @@ export class Validator {
|
|
|
207
225
|
}
|
|
208
226
|
}
|
|
209
227
|
// 数字默认值
|
|
210
|
-
if (type ===
|
|
228
|
+
if (type === "number" && typeof defaultValue === "string") {
|
|
211
229
|
const num = Number(defaultValue);
|
|
212
230
|
return isNaN(num) ? 0 : num;
|
|
213
231
|
}
|
|
214
232
|
return defaultValue;
|
|
215
233
|
}
|
|
216
234
|
|
|
217
|
-
//
|
|
235
|
+
// 类型默认值(字段未定义 default 时使用)
|
|
218
236
|
switch (type.toLowerCase()) {
|
|
219
|
-
case
|
|
237
|
+
case "number":
|
|
220
238
|
return 0;
|
|
221
|
-
case
|
|
222
|
-
case
|
|
239
|
+
case "array_string":
|
|
240
|
+
case "array_text":
|
|
241
|
+
case "array_number_string":
|
|
242
|
+
case "array_number_text":
|
|
223
243
|
return [];
|
|
224
244
|
default:
|
|
225
|
-
return
|
|
245
|
+
return "";
|
|
226
246
|
}
|
|
227
247
|
}
|
|
228
248
|
}
|
package/loader/loadApis.ts
CHANGED
|
@@ -3,55 +3,48 @@
|
|
|
3
3
|
* 负责扫描和加载所有 API 路由(组件、项目)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
// 外部依赖
|
|
10
|
-
import { relative, basename, join } from 'pathe';
|
|
11
|
-
import { isPlainObject } from 'es-toolkit/compat';
|
|
12
|
-
import { scanFiles } from 'befly-shared/scanFiles';
|
|
13
|
-
import { scanAddons, getAddonDir, addonDirExists } from 'befly-shared/addonHelper';
|
|
6
|
+
// 类型导入
|
|
7
|
+
import type { ApiRoute } from "../types/api.js";
|
|
14
8
|
|
|
9
|
+
import { Logger } from "../lib/logger.js";
|
|
10
|
+
import { projectApiDir } from "../paths.js";
|
|
11
|
+
import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
|
|
12
|
+
import { makeRouteKey } from "../utils/route.js";
|
|
15
13
|
// 相对导入
|
|
16
|
-
import {
|
|
17
|
-
import { projectApiDir } from '../paths.js';
|
|
18
|
-
|
|
19
|
-
// 类型导入
|
|
20
|
-
import type { ApiRoute } from '../types/api.js';
|
|
14
|
+
import { scanFiles } from "../utils/scanFiles.js";
|
|
21
15
|
|
|
22
16
|
/**
|
|
23
17
|
* 预定义的默认字段
|
|
24
18
|
*/
|
|
25
19
|
const PRESET_FIELDS: Record<string, any> = {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
"@id": { name: "ID", type: "number", min: 1, max: null },
|
|
21
|
+
"@page": { name: "页码", type: "number", min: 1, max: 9999, default: 1 },
|
|
22
|
+
"@limit": { name: "每页数量", type: "number", min: 1, max: 100, default: 30 },
|
|
23
|
+
"@keyword": { name: "关键词", type: "string", min: 0, max: 50 },
|
|
24
|
+
"@state": { name: "状态", type: "number", regex: "^[0-2]$" }
|
|
31
25
|
};
|
|
32
26
|
|
|
33
27
|
/**
|
|
34
|
-
*
|
|
28
|
+
* 处理字段定义:将 @ 符号引用替换为实际字段定义
|
|
35
29
|
*/
|
|
36
30
|
function processFields(fields: Record<string, any>, apiName: string, routePath: string): Record<string, any> {
|
|
37
|
-
if (!fields || typeof fields !==
|
|
31
|
+
if (!fields || typeof fields !== "object") return fields;
|
|
38
32
|
|
|
39
33
|
const processed: Record<string, any> = {};
|
|
40
34
|
for (const [key, value] of Object.entries(fields)) {
|
|
41
|
-
|
|
42
|
-
if (typeof value === 'string' && value.startsWith('@')) {
|
|
35
|
+
if (typeof value === "string" && value.startsWith("@")) {
|
|
43
36
|
if (PRESET_FIELDS[value]) {
|
|
44
37
|
processed[key] = PRESET_FIELDS[value];
|
|
45
|
-
|
|
46
|
-
// 未找到预定义字段,抛出错误
|
|
47
|
-
const validKeys = Object.keys(PRESET_FIELDS).join(', ');
|
|
48
|
-
throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
|
|
38
|
+
continue;
|
|
49
39
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
|
|
41
|
+
const validKeys = Object.keys(PRESET_FIELDS).join(", ");
|
|
42
|
+
throw new Error(`API [${apiName}] (${routePath}) 字段 [${key}] 引用了未定义的预设字段 "${value}"。可用的预设字段有: ${validKeys}`);
|
|
53
43
|
}
|
|
44
|
+
|
|
45
|
+
processed[key] = value;
|
|
54
46
|
}
|
|
47
|
+
|
|
55
48
|
return processed;
|
|
56
49
|
}
|
|
57
50
|
|
|
@@ -66,31 +59,31 @@ export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
|
|
|
66
59
|
const projectApiList = projectApiFiles.map((file) => ({
|
|
67
60
|
filePath: file.filePath,
|
|
68
61
|
relativePath: file.relativePath,
|
|
69
|
-
type:
|
|
70
|
-
routePrefix:
|
|
71
|
-
typeName:
|
|
62
|
+
type: "project" as const,
|
|
63
|
+
routePrefix: "/",
|
|
64
|
+
typeName: "项目"
|
|
72
65
|
}));
|
|
73
66
|
|
|
74
67
|
// 2. 扫描组件 API
|
|
75
68
|
const addonApiList: Array<{
|
|
76
69
|
filePath: string;
|
|
77
70
|
relativePath: string;
|
|
78
|
-
type:
|
|
71
|
+
type: "addon";
|
|
79
72
|
routePrefix: string;
|
|
80
73
|
typeName: string;
|
|
81
74
|
}> = [];
|
|
82
75
|
const addons = scanAddons();
|
|
83
76
|
for (const addon of addons) {
|
|
84
|
-
if (!addonDirExists(addon,
|
|
77
|
+
if (!addonDirExists(addon, "apis")) continue;
|
|
85
78
|
|
|
86
|
-
const addonApiDir = getAddonDir(addon,
|
|
79
|
+
const addonApiDir = getAddonDir(addon, "apis");
|
|
87
80
|
const addonApiFiles = await scanFiles(addonApiDir);
|
|
88
81
|
|
|
89
82
|
for (const file of addonApiFiles) {
|
|
90
83
|
addonApiList.push({
|
|
91
84
|
filePath: file.filePath,
|
|
92
85
|
relativePath: file.relativePath,
|
|
93
|
-
type:
|
|
86
|
+
type: "addon" as const,
|
|
94
87
|
routePrefix: `/addon/${addon}/`, // 组件 API 默认带斜杠
|
|
95
88
|
typeName: `组件${addon}`
|
|
96
89
|
});
|
|
@@ -104,39 +97,50 @@ export async function loadApis(apis: Map<string, ApiRoute>): Promise<void> {
|
|
|
104
97
|
for (const apiFile of allApiFiles) {
|
|
105
98
|
try {
|
|
106
99
|
// Windows 下路径需要转换为正斜杠格式
|
|
107
|
-
const normalizedFilePath = apiFile.filePath.replace(/\\/g,
|
|
100
|
+
const normalizedFilePath = apiFile.filePath.replace(/\\/g, "/");
|
|
108
101
|
const apiImport = await import(normalizedFilePath);
|
|
109
102
|
const api = apiImport.default;
|
|
110
103
|
|
|
104
|
+
api.name = api.name || apiFile.relativePath;
|
|
105
|
+
|
|
111
106
|
// 设置默认值
|
|
112
|
-
const methodStr = (api.method ||
|
|
107
|
+
const methodStr = (api.method || "POST").toUpperCase();
|
|
113
108
|
api.auth = api.auth !== undefined ? api.auth : true;
|
|
114
109
|
|
|
115
110
|
// 构建路由路径(用于错误提示)
|
|
116
111
|
const routePath = `/api${apiFile.routePrefix}${apiFile.relativePath}`;
|
|
117
112
|
|
|
118
113
|
// 处理字段定义,将 @ 引用替换为实际字段定义
|
|
119
|
-
api.fields = processFields(api.fields || {}, api.name
|
|
114
|
+
api.fields = processFields(api.fields || {}, api.name, routePath);
|
|
120
115
|
api.required = api.required || [];
|
|
121
116
|
|
|
122
117
|
// 支持逗号分隔的多方法,拆分后分别注册
|
|
123
118
|
const methods = methodStr
|
|
124
|
-
.split(
|
|
119
|
+
.split(",")
|
|
125
120
|
.map((m: string) => m.trim())
|
|
126
121
|
.filter((m: string) => m);
|
|
127
122
|
for (const method of methods) {
|
|
128
|
-
const route =
|
|
123
|
+
const route = makeRouteKey(method, routePath);
|
|
129
124
|
// 为每个方法创建独立的路由对象
|
|
130
|
-
const routeApi = {
|
|
125
|
+
const routeApi: ApiRoute = {
|
|
126
|
+
name: api.name,
|
|
127
|
+
handler: api.handler,
|
|
128
|
+
method: method,
|
|
129
|
+
auth: api.auth,
|
|
130
|
+
fields: api.fields,
|
|
131
|
+
required: api.required,
|
|
132
|
+
rawBody: api.rawBody,
|
|
133
|
+
route: route
|
|
134
|
+
};
|
|
131
135
|
apis.set(route, routeApi);
|
|
132
136
|
}
|
|
133
137
|
} catch (error: any) {
|
|
134
|
-
Logger.error({ err: error, api: apiFile.relativePath, type: apiFile.typeName },
|
|
138
|
+
Logger.error({ err: error, api: apiFile.relativePath, type: apiFile.typeName }, "接口加载失败");
|
|
135
139
|
process.exit(1);
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
} catch (error: any) {
|
|
139
|
-
Logger.error({ err: error },
|
|
143
|
+
Logger.error({ err: error }, "加载 API 时发生错误");
|
|
140
144
|
process.exit(1);
|
|
141
145
|
}
|
|
142
146
|
}
|
package/loader/loadHooks.ts
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 钩子加载器
|
|
3
|
-
*
|
|
3
|
+
* 默认只加载核心钩子
|
|
4
|
+
* 可选:通过配置开启组件/项目钩子(默认关闭以保持稳定性与可预期性)
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
|
-
// 相对导入
|
|
7
|
-
import { Logger } from '../lib/logger.js';
|
|
8
|
-
import { coreHookDir } from '../paths.js';
|
|
9
|
-
import { scanModules } from '../util.js';
|
|
10
|
-
import { beflyConfig } from '../befly.config.js';
|
|
11
|
-
|
|
12
7
|
// 类型导入
|
|
13
|
-
import type { Hook } from
|
|
8
|
+
import type { Hook } from "../types/hook.js";
|
|
9
|
+
|
|
10
|
+
import { beflyConfig } from "../befly.config.js";
|
|
11
|
+
import { Logger } from "../lib/logger.js";
|
|
12
|
+
import { coreHookDir, projectHookDir } from "../paths.js";
|
|
13
|
+
// 相对导入
|
|
14
|
+
import { scanAddons, getAddonDir, addonDirExists } from "../utils/addonHelper.js";
|
|
15
|
+
import { scanModules } from "../utils/modules.js";
|
|
14
16
|
|
|
15
17
|
export async function loadHooks(hooks: Hook[]): Promise<void> {
|
|
16
18
|
try {
|
|
19
|
+
const allHooks: Hook[] = [];
|
|
20
|
+
|
|
17
21
|
// 1. 扫描核心钩子
|
|
18
|
-
const coreHooks = await scanModules<Hook>(coreHookDir,
|
|
22
|
+
const coreHooks = await scanModules<Hook>(coreHookDir, "core", "钩子");
|
|
23
|
+
allHooks.push(...coreHooks);
|
|
24
|
+
|
|
25
|
+
// 2. 可选:扫描组件钩子(默认关闭)
|
|
26
|
+
const enableAddonHooks = Boolean((beflyConfig as any).enableAddonHooks);
|
|
27
|
+
if (enableAddonHooks) {
|
|
28
|
+
const addonHooks: Hook[] = [];
|
|
29
|
+
const addons = scanAddons();
|
|
30
|
+
for (const addon of addons) {
|
|
31
|
+
if (!addonDirExists(addon, "hooks")) continue;
|
|
32
|
+
const dir = getAddonDir(addon, "hooks");
|
|
33
|
+
const items = await scanModules<Hook>(dir, "addon", "钩子", addon);
|
|
34
|
+
addonHooks.push(...items);
|
|
35
|
+
}
|
|
36
|
+
allHooks.push(...addonHooks);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 3. 可选:扫描项目钩子(默认关闭)
|
|
40
|
+
const enableAppHooks = Boolean((beflyConfig as any).enableAppHooks);
|
|
41
|
+
if (enableAppHooks) {
|
|
42
|
+
const appHooks = await scanModules<Hook>(projectHookDir, "app", "钩子");
|
|
43
|
+
allHooks.push(...appHooks);
|
|
44
|
+
}
|
|
19
45
|
|
|
20
|
-
//
|
|
46
|
+
// 4. 过滤禁用的钩子
|
|
21
47
|
const disableHooks = beflyConfig.disableHooks || [];
|
|
22
|
-
const enabledHooks =
|
|
48
|
+
const enabledHooks = allHooks.filter((hook) => hook.name && !disableHooks.includes(hook.name));
|
|
23
49
|
|
|
24
50
|
if (disableHooks.length > 0) {
|
|
25
|
-
Logger.info({ hooks: disableHooks },
|
|
51
|
+
Logger.info({ hooks: disableHooks }, "禁用钩子");
|
|
26
52
|
}
|
|
27
53
|
|
|
28
|
-
//
|
|
54
|
+
// 5. 按 order 排序
|
|
29
55
|
const sortedHooks = enabledHooks.sort((a, b) => {
|
|
30
56
|
const orderA = a.order ?? 999;
|
|
31
57
|
const orderB = b.order ?? 999;
|
|
@@ -34,7 +60,7 @@ export async function loadHooks(hooks: Hook[]): Promise<void> {
|
|
|
34
60
|
|
|
35
61
|
hooks.push(...sortedHooks);
|
|
36
62
|
} catch (error: any) {
|
|
37
|
-
Logger.error({ err: error },
|
|
63
|
+
Logger.error({ err: error }, "加载钩子时发生错误");
|
|
38
64
|
process.exit(1);
|
|
39
65
|
}
|
|
40
66
|
}
|
package/loader/loadPlugins.ts
CHANGED
|
@@ -3,34 +3,33 @@
|
|
|
3
3
|
* 负责扫描和初始化所有插件(核心、组件、项目)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import type { BeflyContext } from "../types/befly.js";
|
|
7
|
+
import type { Plugin } from "../types/plugin.js";
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import type { Plugin } from '../types/plugin.js';
|
|
14
|
-
import type { BeflyContext } from '../types/befly.js';
|
|
9
|
+
import { beflyConfig } from "../befly.config.js";
|
|
10
|
+
import { Logger } from "../lib/logger.js";
|
|
11
|
+
import { corePluginDir, projectPluginDir } from "../paths.js";
|
|
12
|
+
import { scanAddons, getAddonDir } from "../utils/addonHelper.js";
|
|
13
|
+
import { sortModules, scanModules } from "../utils/modules.js";
|
|
15
14
|
|
|
16
15
|
export async function loadPlugins(plugins: Plugin[], context: BeflyContext): Promise<void> {
|
|
17
16
|
try {
|
|
18
17
|
const allPlugins: Plugin[] = [];
|
|
19
18
|
|
|
20
19
|
// 1. 扫描核心插件
|
|
21
|
-
const corePlugins = await scanModules<Plugin>(corePluginDir,
|
|
20
|
+
const corePlugins = await scanModules<Plugin>(corePluginDir, "core", "插件");
|
|
22
21
|
|
|
23
22
|
// 2. 扫描组件插件
|
|
24
23
|
const addonPlugins: Plugin[] = [];
|
|
25
24
|
const addons = scanAddons();
|
|
26
25
|
for (const addon of addons) {
|
|
27
|
-
const dir = getAddonDir(addon,
|
|
28
|
-
const plugins = await scanModules<Plugin>(dir,
|
|
26
|
+
const dir = getAddonDir(addon, "plugins");
|
|
27
|
+
const plugins = await scanModules<Plugin>(dir, "addon", "插件", addon);
|
|
29
28
|
addonPlugins.push(...plugins);
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
// 3. 扫描项目插件
|
|
33
|
-
const appPlugins = await scanModules<Plugin>(projectPluginDir,
|
|
32
|
+
const appPlugins = await scanModules<Plugin>(projectPluginDir, "app", "插件");
|
|
34
33
|
|
|
35
34
|
// 4. 合并所有插件
|
|
36
35
|
allPlugins.push(...corePlugins);
|
|
@@ -42,13 +41,13 @@ export async function loadPlugins(plugins: Plugin[], context: BeflyContext): Pro
|
|
|
42
41
|
const enabledPlugins = allPlugins.filter((plugin) => plugin.name && !disablePlugins.includes(plugin.name));
|
|
43
42
|
|
|
44
43
|
if (disablePlugins.length > 0) {
|
|
45
|
-
Logger.info({ plugins: disablePlugins },
|
|
44
|
+
Logger.info({ plugins: disablePlugins }, "禁用插件");
|
|
46
45
|
}
|
|
47
46
|
|
|
48
47
|
// 6. 排序与初始化
|
|
49
48
|
const sortedPlugins = sortModules(enabledPlugins);
|
|
50
49
|
if (sortedPlugins === false) {
|
|
51
|
-
Logger.error(
|
|
50
|
+
Logger.error("插件依赖关系错误,请检查 after 属性");
|
|
52
51
|
process.exit(1);
|
|
53
52
|
}
|
|
54
53
|
|
|
@@ -56,17 +55,17 @@ export async function loadPlugins(plugins: Plugin[], context: BeflyContext): Pro
|
|
|
56
55
|
try {
|
|
57
56
|
plugins.push(plugin);
|
|
58
57
|
|
|
59
|
-
const pluginInstance = typeof plugin.handler ===
|
|
58
|
+
const pluginInstance = typeof plugin.handler === "function" ? await plugin.handler(context) : {};
|
|
60
59
|
|
|
61
60
|
// 直接挂载到 befly 下
|
|
62
61
|
(context as any)[plugin.name!] = pluginInstance;
|
|
63
62
|
} catch (error: any) {
|
|
64
|
-
Logger.error({ err: error, plugin: plugin.name },
|
|
63
|
+
Logger.error({ err: error, plugin: plugin.name }, "插件初始化失败");
|
|
65
64
|
process.exit(1);
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
} catch (error: any) {
|
|
69
|
-
Logger.error({ err: error },
|
|
68
|
+
Logger.error({ err: error }, "加载插件时发生错误");
|
|
70
69
|
process.exit(1);
|
|
71
70
|
}
|
|
72
71
|
}
|