befly 3.8.19 → 3.8.21
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 +7 -6
- package/bunfig.toml +1 -1
- package/checks/checkApi.ts +92 -0
- package/checks/checkApp.ts +31 -0
- package/{check.ts → checks/checkTable.ts} +28 -159
- package/config.ts +71 -0
- package/hooks/auth.ts +30 -0
- package/hooks/cors.ts +48 -0
- package/hooks/errorHandler.ts +23 -0
- package/hooks/parser.ts +67 -0
- package/hooks/permission.ts +54 -0
- package/hooks/rateLimit.ts +70 -0
- package/hooks/requestId.ts +24 -0
- package/hooks/requestLogger.ts +25 -0
- package/hooks/responseFormatter.ts +64 -0
- package/hooks/validator.ts +34 -0
- package/lib/database.ts +28 -25
- package/lib/dbHelper.ts +3 -3
- package/lib/jwt.ts +90 -99
- package/lib/logger.ts +44 -23
- package/lib/redisHelper.ts +19 -22
- package/loader/loadApis.ts +69 -114
- package/loader/loadHooks.ts +65 -0
- package/loader/loadPlugins.ts +50 -219
- package/main.ts +106 -133
- package/package.json +23 -14
- package/paths.ts +20 -0
- package/plugins/cache.ts +1 -3
- package/plugins/db.ts +8 -11
- package/plugins/logger.ts +5 -3
- package/plugins/redis.ts +10 -14
- package/router/api.ts +60 -106
- package/router/root.ts +15 -12
- package/router/static.ts +54 -58
- package/sync/syncAll.ts +58 -0
- package/sync/syncApi.ts +264 -0
- package/sync/syncDb/apply.ts +194 -0
- package/sync/syncDb/constants.ts +76 -0
- package/sync/syncDb/ddl.ts +194 -0
- package/sync/syncDb/helpers.ts +200 -0
- package/sync/syncDb/index.ts +164 -0
- package/sync/syncDb/schema.ts +201 -0
- package/sync/syncDb/sqlite.ts +50 -0
- package/sync/syncDb/table.ts +321 -0
- package/sync/syncDb/tableCreate.ts +146 -0
- package/sync/syncDb/version.ts +72 -0
- package/sync/syncDb.ts +19 -0
- package/sync/syncDev.ts +206 -0
- package/sync/syncMenu.ts +331 -0
- package/tests/cipher.test.ts +248 -0
- package/tests/dbHelper-advanced.test.ts +717 -0
- package/tests/dbHelper-columns.test.ts +266 -0
- package/tests/dbHelper-execute.test.ts +240 -0
- package/tests/fields-redis-cache.test.ts +123 -0
- package/tests/fields-validate.test.ts +99 -0
- package/tests/integration.test.ts +202 -0
- package/tests/jwt.test.ts +122 -0
- package/tests/logger.test.ts +94 -0
- package/tests/redisHelper.test.ts +231 -0
- package/tests/sqlBuilder-advanced.test.ts +593 -0
- package/tests/sqlBuilder.test.ts +184 -0
- package/tests/util.test.ts +95 -0
- package/tests/validator-advanced.test.ts +653 -0
- package/tests/validator.test.ts +148 -0
- package/tests/xml.test.ts +101 -0
- package/tsconfig.json +2 -4
- package/types/api.d.ts +6 -0
- package/types/befly.d.ts +152 -28
- package/types/context.d.ts +29 -3
- package/types/hook.d.ts +35 -0
- package/types/index.ts +14 -1
- package/types/plugin.d.ts +6 -7
- package/types/sync.d.ts +403 -0
- package/env.ts +0 -106
- package/lib/middleware.ts +0 -275
- package/types/env.ts +0 -65
- package/types/util.d.ts +0 -45
- package/util.ts +0 -257
package/README.md
CHANGED
|
@@ -55,7 +55,6 @@ bun run main.ts
|
|
|
55
55
|
|
|
56
56
|
```typescript
|
|
57
57
|
// apis/user/hello.ts
|
|
58
|
-
import { Yes } from 'befly';
|
|
59
58
|
import type { ApiRoute } from 'befly';
|
|
60
59
|
|
|
61
60
|
export default {
|
|
@@ -63,9 +62,12 @@ export default {
|
|
|
63
62
|
auth: false, // 公开接口
|
|
64
63
|
fields: {},
|
|
65
64
|
handler: async (befly, ctx) => {
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
65
|
+
return {
|
|
66
|
+
msg: 'Hello, Befly!',
|
|
67
|
+
data: {
|
|
68
|
+
timestamp: Date.now()
|
|
69
|
+
}
|
|
70
|
+
};
|
|
69
71
|
}
|
|
70
72
|
} as ApiRoute;
|
|
71
73
|
```
|
|
@@ -77,7 +79,6 @@ export default {
|
|
|
77
79
|
### TypeScript 全面支持
|
|
78
80
|
|
|
79
81
|
```typescript
|
|
80
|
-
import { Yes } from 'befly';
|
|
81
82
|
import type { ApiRoute, BeflyContext } from 'befly';
|
|
82
83
|
import type { User } from './types/models';
|
|
83
84
|
|
|
@@ -97,7 +98,7 @@ export default {
|
|
|
97
98
|
where: { id }
|
|
98
99
|
});
|
|
99
100
|
|
|
100
|
-
return
|
|
101
|
+
return { msg: '查询成功', data: user };
|
|
101
102
|
}
|
|
102
103
|
} as ApiRoute;
|
|
103
104
|
```
|
package/bunfig.toml
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// 内部依赖
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// 外部依赖
|
|
5
|
+
import { isPlainObject } from 'es-toolkit/compat';
|
|
6
|
+
import { scanAddons, getAddonDir, addonDirExists, scanFiles } from 'befly-util';
|
|
7
|
+
|
|
8
|
+
// 相对导入
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
10
|
+
import { projectApiDir } from '../paths.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 检查所有 API 定义
|
|
14
|
+
*/
|
|
15
|
+
export async function checkApi(): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
// 收集所有 API 文件
|
|
18
|
+
const allApiFiles: Array<{ file: string; displayName: string; apiPath: string }> = [];
|
|
19
|
+
|
|
20
|
+
// 收集项目 API 文件
|
|
21
|
+
if (existsSync(projectApiDir)) {
|
|
22
|
+
const files = await scanFiles(projectApiDir);
|
|
23
|
+
for (const { filePath, relativePath } of files) {
|
|
24
|
+
allApiFiles.push({
|
|
25
|
+
file: filePath,
|
|
26
|
+
displayName: '用户',
|
|
27
|
+
apiPath: relativePath
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 收集组件 API 文件
|
|
33
|
+
const addons = scanAddons();
|
|
34
|
+
for (const addon of addons) {
|
|
35
|
+
if (!addonDirExists(addon, 'apis')) continue;
|
|
36
|
+
const addonApiDir = getAddonDir(addon, 'apis');
|
|
37
|
+
|
|
38
|
+
const files = await scanFiles(addonApiDir);
|
|
39
|
+
for (const { filePath, relativePath } of files) {
|
|
40
|
+
allApiFiles.push({
|
|
41
|
+
file: filePath,
|
|
42
|
+
displayName: `组件${addon}`,
|
|
43
|
+
apiPath: relativePath
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 合并进行验证逻辑
|
|
49
|
+
for (const item of allApiFiles) {
|
|
50
|
+
const { apiPath } = item;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Windows 下路径需要转换为正斜杠格式
|
|
54
|
+
const filePath = item.file.replace(/\\/g, '/');
|
|
55
|
+
const apiImport = await import(filePath);
|
|
56
|
+
const api = apiImport.default;
|
|
57
|
+
|
|
58
|
+
// 验证必填属性:name 和 handler
|
|
59
|
+
if (typeof api.name !== 'string' || api.name.trim() === '') {
|
|
60
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (typeof api.handler !== 'function') {
|
|
64
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 验证可选属性的类型(如果提供了)
|
|
69
|
+
if (api.method && !['GET', 'POST'].includes(api.method.toUpperCase())) {
|
|
70
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
|
|
71
|
+
}
|
|
72
|
+
if (api.auth !== undefined && typeof api.auth !== 'boolean') {
|
|
73
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
|
|
74
|
+
}
|
|
75
|
+
if (api.fields && !isPlainObject(api.fields)) {
|
|
76
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
|
|
77
|
+
}
|
|
78
|
+
if (api.required && !Array.isArray(api.required)) {
|
|
79
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
|
|
80
|
+
}
|
|
81
|
+
if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
|
|
82
|
+
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
|
|
83
|
+
}
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
Logger.error('API 定义检查过程中出错', error);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 内部依赖
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
// 相对导入
|
|
6
|
+
import { Logger } from '../lib/logger.js';
|
|
7
|
+
import { projectApiDir, projectDir } from '../paths.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 检查项目结构
|
|
11
|
+
*/
|
|
12
|
+
export async function checkApp(): Promise<void> {
|
|
13
|
+
try {
|
|
14
|
+
// 检查项目 apis 目录下是否存在名为 addon 的目录
|
|
15
|
+
if (existsSync(projectApiDir)) {
|
|
16
|
+
const addonDir = join(projectApiDir, 'addon');
|
|
17
|
+
if (existsSync(addonDir)) {
|
|
18
|
+
throw new Error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 检查并创建 logs 目录
|
|
23
|
+
const logsDir = join(projectDir, 'logs');
|
|
24
|
+
if (!existsSync(logsDir)) {
|
|
25
|
+
mkdirSync(logsDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
} catch (error: any) {
|
|
28
|
+
Logger.error('项目结构检查过程中出错', error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
// 内部依赖
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
|
|
4
|
+
// 外部依赖
|
|
5
|
+
import { basename } from 'pathe';
|
|
6
|
+
import { scanAddons, getAddonDir, scanFiles } from 'befly-util';
|
|
7
|
+
|
|
8
|
+
// 相对导入
|
|
9
|
+
import { Logger } from '../lib/logger.js';
|
|
10
|
+
import { projectTableDir } from '../paths.js';
|
|
5
11
|
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
9
|
-
import { isPlainObject } from 'es-toolkit/compat';
|
|
10
|
-
import { Logger } from './lib/logger.js';
|
|
11
|
-
import { projectTableDir, projectApiDir, projectDir } from './paths.js';
|
|
12
|
-
import { scanAddons, getAddonDir, addonDirExists } from './util.js';
|
|
13
|
-
import type { FieldDefinition } from './types/common.d.ts';
|
|
12
|
+
// 类型导入
|
|
13
|
+
import type { FieldDefinition } from '../types/common.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* 表文件信息接口
|
|
@@ -22,6 +22,8 @@ interface TableFileInfo {
|
|
|
22
22
|
type: 'project' | 'addon';
|
|
23
23
|
/** 如果是 addon 类型,记录 addon 名称 */
|
|
24
24
|
addonName?: string;
|
|
25
|
+
/** 类型名称(用于日志) */
|
|
26
|
+
typeName: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -57,24 +59,19 @@ const MAX_VARCHAR_LENGTH = 65535;
|
|
|
57
59
|
* 检查表定义文件
|
|
58
60
|
* @throws 当检查失败时抛出异常
|
|
59
61
|
*/
|
|
60
|
-
export
|
|
62
|
+
export async function checkTable(): Promise<void> {
|
|
61
63
|
try {
|
|
62
|
-
const tablesGlob = new Bun.Glob('*.json');
|
|
63
|
-
|
|
64
64
|
// 收集所有表文件
|
|
65
65
|
const allTableFiles: TableFileInfo[] = [];
|
|
66
66
|
let hasError = false;
|
|
67
67
|
|
|
68
68
|
// 收集项目表字段定义文件(如果目录存在)
|
|
69
69
|
if (existsSync(projectTableDir)) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
absolute: true,
|
|
73
|
-
onlyFiles: true
|
|
74
|
-
})) {
|
|
70
|
+
const files = await scanFiles(projectTableDir, '*.json', false);
|
|
71
|
+
for (const { filePath } of files) {
|
|
75
72
|
allTableFiles.push({
|
|
76
|
-
file:
|
|
77
|
-
|
|
73
|
+
file: filePath,
|
|
74
|
+
type: 'project',
|
|
78
75
|
typeName: '项目'
|
|
79
76
|
});
|
|
80
77
|
}
|
|
@@ -90,14 +87,11 @@ export const checkTable = async function (): Promise<boolean> {
|
|
|
90
87
|
continue;
|
|
91
88
|
}
|
|
92
89
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
absolute: true,
|
|
96
|
-
onlyFiles: true
|
|
97
|
-
})) {
|
|
90
|
+
const files = await scanFiles(addonTablesDir, '*.json', false);
|
|
91
|
+
for (const { filePath } of files) {
|
|
98
92
|
allTableFiles.push({
|
|
99
|
-
file:
|
|
100
|
-
|
|
93
|
+
file: filePath,
|
|
94
|
+
type: 'addon',
|
|
101
95
|
typeName: `组件${addonName}`,
|
|
102
96
|
addonName: addonName
|
|
103
97
|
});
|
|
@@ -243,136 +237,11 @@ export const checkTable = async function (): Promise<boolean> {
|
|
|
243
237
|
}
|
|
244
238
|
}
|
|
245
239
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
Logger.error('数据表定义检查过程中出错', error);
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* 检查所有 API 定义
|
|
255
|
-
*/
|
|
256
|
-
export const checkApi = async function (): Promise<boolean> {
|
|
257
|
-
try {
|
|
258
|
-
const apiGlob = new Bun.Glob('**/*.{ts,js}');
|
|
259
|
-
|
|
260
|
-
// 收集所有 API 文件
|
|
261
|
-
const allApiFiles: Array<{ file: string; displayName: string }> = [];
|
|
262
|
-
|
|
263
|
-
// 收集项目 API 文件
|
|
264
|
-
if (existsSync(projectApiDir)) {
|
|
265
|
-
for await (const file of apiGlob.scan({
|
|
266
|
-
cwd: projectApiDir,
|
|
267
|
-
onlyFiles: true,
|
|
268
|
-
absolute: true
|
|
269
|
-
})) {
|
|
270
|
-
if (file.endsWith('.d.ts')) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
allApiFiles.push({
|
|
274
|
-
file: file,
|
|
275
|
-
displayName: '用户'
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// 收集组件 API 文件
|
|
281
|
-
const addons = scanAddons();
|
|
282
|
-
for (const addon of addons) {
|
|
283
|
-
if (!addonDirExists(addon, 'apis')) continue;
|
|
284
|
-
const addonApiDir = getAddonDir(addon, 'apis');
|
|
285
|
-
|
|
286
|
-
for await (const file of apiGlob.scan({
|
|
287
|
-
cwd: addonApiDir,
|
|
288
|
-
onlyFiles: true,
|
|
289
|
-
absolute: true
|
|
290
|
-
})) {
|
|
291
|
-
if (file.endsWith('.d.ts')) {
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
allApiFiles.push({
|
|
295
|
-
file: file,
|
|
296
|
-
displayName: `组件${addon}`
|
|
297
|
-
});
|
|
298
|
-
}
|
|
240
|
+
if (hasError) {
|
|
241
|
+
throw new Error('表结构检查失败');
|
|
299
242
|
}
|
|
300
|
-
|
|
301
|
-
// 合并进行验证逻辑
|
|
302
|
-
for (const item of allApiFiles) {
|
|
303
|
-
const fileName = basename(item.file).replace(/\.(ts|js)$/, '');
|
|
304
|
-
const apiPath = relative(item.displayName === '用户' ? projectApiDir : getAddonDir(item.displayName.replace('组件', ''), 'apis'), item.file).replace(/\.(ts|js)$/, '');
|
|
305
|
-
|
|
306
|
-
// 跳过以下划线开头的文件
|
|
307
|
-
if (apiPath.indexOf('_') !== -1) continue;
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
// Windows 下路径需要转换为正斜杠格式
|
|
311
|
-
const filePath = item.file.replace(/\\/g, '/');
|
|
312
|
-
const apiImport = await import(filePath);
|
|
313
|
-
const api = apiImport.default;
|
|
314
|
-
|
|
315
|
-
// 验证必填属性:name 和 handler
|
|
316
|
-
if (typeof api.name !== 'string' || api.name.trim() === '') {
|
|
317
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 name 属性必须是非空字符串`);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (typeof api.handler !== 'function') {
|
|
321
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 handler 属性必须是函数`);
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// 验证可选属性的类型(如果提供了)
|
|
326
|
-
if (api.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'].includes(api.method.toUpperCase())) {
|
|
327
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 method 属性必须是有效的 HTTP 方法`);
|
|
328
|
-
}
|
|
329
|
-
if (api.auth !== undefined && typeof api.auth !== 'boolean') {
|
|
330
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 auth 属性必须是布尔值 (true=需登录, false=公开)`);
|
|
331
|
-
}
|
|
332
|
-
if (api.fields && !isPlainObject(api.fields)) {
|
|
333
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 fields 属性必须是对象`);
|
|
334
|
-
}
|
|
335
|
-
if (api.required && !Array.isArray(api.required)) {
|
|
336
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是数组`);
|
|
337
|
-
}
|
|
338
|
-
if (api.required && api.required.some((item: any) => typeof item !== 'string')) {
|
|
339
|
-
Logger.warn(`[${item.displayName}] 接口 ${apiPath} 的 required 属性必须是字符串数组`);
|
|
340
|
-
}
|
|
341
|
-
} catch (error: any) {
|
|
342
|
-
Logger.error(`[${item.displayName}] 接口 ${apiPath} 解析失败`, error);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return true;
|
|
347
|
-
} catch (error: any) {
|
|
348
|
-
Logger.error('API 定义检查过程中出错', error);
|
|
349
|
-
return false;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* 检查项目结构
|
|
355
|
-
*/
|
|
356
|
-
export const checkApp = async function (): Promise<boolean> {
|
|
357
|
-
try {
|
|
358
|
-
// 检查项目 apis 目录下是否存在名为 addon 的目录
|
|
359
|
-
if (existsSync(projectApiDir)) {
|
|
360
|
-
const addonDir = join(projectApiDir, 'addon');
|
|
361
|
-
if (existsSync(addonDir)) {
|
|
362
|
-
Logger.error('项目 apis 目录下不能存在名为 addon 的目录,addon 是保留名称,用于组件接口路由');
|
|
363
|
-
return false;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 检查并创建 logs 目录
|
|
368
|
-
const logsDir = join(projectDir, 'logs');
|
|
369
|
-
if (!existsSync(logsDir)) {
|
|
370
|
-
mkdirSync(logsDir, { recursive: true });
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return true;
|
|
374
243
|
} catch (error: any) {
|
|
375
|
-
Logger.error('
|
|
376
|
-
|
|
244
|
+
Logger.error('数据表定义检查过程中出错', error);
|
|
245
|
+
throw error;
|
|
377
246
|
}
|
|
378
|
-
}
|
|
247
|
+
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Befly 默认配置
|
|
3
|
+
* 包含所有配置项的默认值
|
|
4
|
+
*/
|
|
5
|
+
import type { BeflyOptions } from './types/befly.js';
|
|
6
|
+
|
|
7
|
+
export const defaultOptions: Required<Omit<BeflyOptions, 'devPassword'>> = {
|
|
8
|
+
// ========== 核心参数 ==========
|
|
9
|
+
nodeEnv: 'development',
|
|
10
|
+
appName: 'Befly App',
|
|
11
|
+
appPort: 3000,
|
|
12
|
+
appHost: '127.0.0.1',
|
|
13
|
+
devEmail: 'dev@qq.com',
|
|
14
|
+
bodyLimit: 1048576, // 1MB
|
|
15
|
+
tz: 'Asia/Shanghai',
|
|
16
|
+
dbCache: 0,
|
|
17
|
+
|
|
18
|
+
// ========== 日志配置 ==========
|
|
19
|
+
logger: {
|
|
20
|
+
debug: 1,
|
|
21
|
+
excludeFields: 'password,token,secret',
|
|
22
|
+
dir: './logs',
|
|
23
|
+
console: 1,
|
|
24
|
+
maxSize: 10485760 // 10MB
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// ========== 数据库配置 ==========
|
|
28
|
+
db: {
|
|
29
|
+
enable: 0,
|
|
30
|
+
type: 'sqlite',
|
|
31
|
+
host: '127.0.0.1',
|
|
32
|
+
port: 3306,
|
|
33
|
+
username: 'root',
|
|
34
|
+
password: '',
|
|
35
|
+
database: 'befly',
|
|
36
|
+
poolMax: 1
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// ========== Redis 配置 ==========
|
|
40
|
+
redis: {
|
|
41
|
+
host: '127.0.0.1',
|
|
42
|
+
port: 6379,
|
|
43
|
+
username: '',
|
|
44
|
+
password: '',
|
|
45
|
+
db: 0,
|
|
46
|
+
prefix: 'befly:'
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
// ========== 认证配置 ==========
|
|
50
|
+
auth: {
|
|
51
|
+
secret: 'befly-secret',
|
|
52
|
+
expiresIn: '7d',
|
|
53
|
+
algorithm: 'HS256'
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// ========== CORS 配置 ==========
|
|
57
|
+
cors: {
|
|
58
|
+
origin: '*',
|
|
59
|
+
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
60
|
+
allowedHeaders: 'Content-Type,Authorization',
|
|
61
|
+
exposedHeaders: '',
|
|
62
|
+
maxAge: 86400,
|
|
63
|
+
credentials: 'true'
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// ========== 禁用配置 ==========
|
|
67
|
+
/** 禁用的钩子列表 */
|
|
68
|
+
disableHooks: [],
|
|
69
|
+
/** 禁用的插件列表 */
|
|
70
|
+
disablePlugins: []
|
|
71
|
+
};
|
package/hooks/auth.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Hook } from '../types/hook.js';
|
|
2
|
+
import { Jwt } from '../lib/jwt.js';
|
|
3
|
+
|
|
4
|
+
const hook: Hook = {
|
|
5
|
+
after: ['cors'],
|
|
6
|
+
order: 5,
|
|
7
|
+
handler: async (befly, ctx, next) => {
|
|
8
|
+
// 初始化配置(如果有)
|
|
9
|
+
// 注意:Hook 没有 onInit,如果需要初始化,可以在 handler 首次执行时做,或者保留 Plugin 机制专门做初始化
|
|
10
|
+
// 这里 auth 插件原本有 onInit 来配置 Jwt,现在需要迁移
|
|
11
|
+
// 临时方案:直接在 handler 里判断是否配置过,或者让 Jwt 自身处理配置
|
|
12
|
+
|
|
13
|
+
const authHeader = ctx.req.headers.get('authorization');
|
|
14
|
+
|
|
15
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
16
|
+
const token = authHeader.substring(7);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const payload = await Jwt.verify(token);
|
|
20
|
+
ctx.user = payload;
|
|
21
|
+
} catch (error: any) {
|
|
22
|
+
ctx.user = {};
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
ctx.user = {};
|
|
26
|
+
}
|
|
27
|
+
await next();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
export default hook;
|
package/hooks/cors.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// 相对导入
|
|
2
|
+
import { setCorsOptions } from '../util.js';
|
|
3
|
+
|
|
4
|
+
// 类型导入
|
|
5
|
+
import type { Hook } from '../types/hook.js';
|
|
6
|
+
import type { CorsConfig } from '../types/befly.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CORS 跨域处理钩子
|
|
10
|
+
* 设置跨域响应头并处理 OPTIONS 预检请求
|
|
11
|
+
*/
|
|
12
|
+
const hook: Hook = {
|
|
13
|
+
after: ['errorHandler'],
|
|
14
|
+
order: 2,
|
|
15
|
+
handler: async (befly, ctx, next) => {
|
|
16
|
+
const req = ctx.req;
|
|
17
|
+
|
|
18
|
+
// 合并默认配置和用户配置
|
|
19
|
+
const defaultConfig: CorsConfig = {
|
|
20
|
+
origin: '*',
|
|
21
|
+
methods: 'GET, POST, PUT, DELETE, OPTIONS',
|
|
22
|
+
allowedHeaders: 'Content-Type, Authorization, authorization, token',
|
|
23
|
+
exposedHeaders: 'Content-Range, X-Content-Range, Authorization, authorization, token',
|
|
24
|
+
maxAge: 86400,
|
|
25
|
+
credentials: 'true'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const userConfig = (hook as any).config || {};
|
|
29
|
+
const config = { ...defaultConfig, ...userConfig };
|
|
30
|
+
|
|
31
|
+
// 设置 CORS 响应头
|
|
32
|
+
const headers = setCorsOptions(req, config);
|
|
33
|
+
|
|
34
|
+
ctx.corsHeaders = headers;
|
|
35
|
+
|
|
36
|
+
// 处理 OPTIONS 预检请求
|
|
37
|
+
if (req.method === 'OPTIONS') {
|
|
38
|
+
ctx.response = new Response(null, {
|
|
39
|
+
status: 204,
|
|
40
|
+
headers: headers
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
await next();
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
export default hook;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// 相对导入
|
|
2
|
+
import { Logger } from '../lib/logger.js';
|
|
3
|
+
import { JsonResponse } from '../util.js';
|
|
4
|
+
|
|
5
|
+
// 类型导入
|
|
6
|
+
import type { Hook } from '../types/hook.js';
|
|
7
|
+
|
|
8
|
+
const hook: Hook = {
|
|
9
|
+
order: 1,
|
|
10
|
+
handler: async (befly, ctx, next) => {
|
|
11
|
+
try {
|
|
12
|
+
await next();
|
|
13
|
+
} catch (err: any) {
|
|
14
|
+
// 记录错误信息
|
|
15
|
+
const apiPath = ctx.api ? `${ctx.req.method}${new URL(ctx.req.url).pathname}` : ctx.req.url;
|
|
16
|
+
Logger.error(`Request Error: ${apiPath}`, err);
|
|
17
|
+
|
|
18
|
+
// 设置错误响应
|
|
19
|
+
ctx.response = JsonResponse(ctx, '内部服务错误');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
export default hook;
|
package/hooks/parser.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// 外部依赖
|
|
2
|
+
import { isPlainObject, isEmpty } from 'es-toolkit/compat';
|
|
3
|
+
import { pickFields } from 'befly-util';
|
|
4
|
+
|
|
5
|
+
// 相对导入
|
|
6
|
+
import { Xml } from '../lib/xml.js';
|
|
7
|
+
import { JsonResponse } from '../util.js';
|
|
8
|
+
|
|
9
|
+
// 类型导入
|
|
10
|
+
import type { Hook } from '../types/hook.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 请求参数解析钩子
|
|
14
|
+
* - GET 请求:解析 URL 查询参数
|
|
15
|
+
* - POST 请求:解析 JSON 或 XML 请求体
|
|
16
|
+
* - 根据 API 定义的 fields 过滤字段
|
|
17
|
+
*/
|
|
18
|
+
const hook: Hook = {
|
|
19
|
+
after: ['auth'],
|
|
20
|
+
order: 10,
|
|
21
|
+
handler: async (befly, ctx, next) => {
|
|
22
|
+
if (!ctx.api) return next();
|
|
23
|
+
|
|
24
|
+
// GET 请求:解析查询参数
|
|
25
|
+
if (ctx.req.method === 'GET') {
|
|
26
|
+
const url = new URL(ctx.req.url);
|
|
27
|
+
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
28
|
+
ctx.body = pickFields(Object.fromEntries(url.searchParams), Object.keys(ctx.api.fields));
|
|
29
|
+
} else {
|
|
30
|
+
ctx.body = Object.fromEntries(url.searchParams);
|
|
31
|
+
}
|
|
32
|
+
} else if (ctx.req.method === 'POST') {
|
|
33
|
+
// POST 请求:解析请求体
|
|
34
|
+
const contentType = ctx.req.headers.get('content-type') || '';
|
|
35
|
+
try {
|
|
36
|
+
// JSON 格式
|
|
37
|
+
if (contentType.includes('application/json')) {
|
|
38
|
+
const body = await ctx.req.json();
|
|
39
|
+
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
40
|
+
ctx.body = pickFields(body, Object.keys(ctx.api.fields));
|
|
41
|
+
} else {
|
|
42
|
+
ctx.body = body;
|
|
43
|
+
}
|
|
44
|
+
} else if (contentType.includes('application/xml') || contentType.includes('text/xml')) {
|
|
45
|
+
// XML 格式
|
|
46
|
+
const text = await ctx.req.text();
|
|
47
|
+
const body = await Xml.parse(text);
|
|
48
|
+
if (isPlainObject(ctx.api.fields) && !isEmpty(ctx.api.fields)) {
|
|
49
|
+
ctx.body = pickFields(body, Object.keys(ctx.api.fields));
|
|
50
|
+
} else {
|
|
51
|
+
ctx.body = body;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// 不支持的 Content-Type
|
|
55
|
+
ctx.response = JsonResponse(ctx, '无效的请求参数格式');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
// 解析失败
|
|
60
|
+
ctx.response = JsonResponse(ctx, '无效的请求参数格式');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await next();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
export default hook;
|