befly 3.8.25 → 3.8.29

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.
Files changed (62) hide show
  1. package/config.ts +8 -9
  2. package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
  3. package/hooks/auth.ts +3 -11
  4. package/hooks/cors.ts +1 -4
  5. package/hooks/parser.ts +6 -8
  6. package/hooks/permission.ts +9 -12
  7. package/hooks/validator.ts +6 -9
  8. package/lib/cacheHelper.ts +0 -4
  9. package/lib/{database.ts → connect.ts} +65 -18
  10. package/lib/logger.ts +1 -17
  11. package/lib/redisHelper.ts +6 -5
  12. package/loader/loadApis.ts +3 -3
  13. package/loader/loadHooks.ts +15 -41
  14. package/loader/loadPlugins.ts +10 -16
  15. package/main.ts +25 -28
  16. package/package.json +4 -4
  17. package/plugins/cache.ts +2 -2
  18. package/plugins/cipher.ts +15 -0
  19. package/plugins/config.ts +16 -0
  20. package/plugins/db.ts +7 -17
  21. package/plugins/jwt.ts +15 -0
  22. package/plugins/logger.ts +1 -1
  23. package/plugins/redis.ts +4 -4
  24. package/plugins/tool.ts +50 -0
  25. package/router/api.ts +56 -42
  26. package/router/static.ts +12 -12
  27. package/sync/syncAll.ts +2 -20
  28. package/sync/syncApi.ts +7 -7
  29. package/sync/syncDb/apply.ts +10 -12
  30. package/sync/syncDb/constants.ts +64 -12
  31. package/sync/syncDb/ddl.ts +9 -8
  32. package/sync/syncDb/helpers.ts +7 -119
  33. package/sync/syncDb/schema.ts +16 -19
  34. package/sync/syncDb/sqlite.ts +1 -3
  35. package/sync/syncDb/table.ts +13 -146
  36. package/sync/syncDb/tableCreate.ts +28 -12
  37. package/sync/syncDb/types.ts +126 -0
  38. package/sync/syncDb/version.ts +4 -7
  39. package/sync/syncDb.ts +151 -6
  40. package/sync/syncDev.ts +19 -15
  41. package/sync/syncMenu.ts +87 -75
  42. package/tests/redisHelper.test.ts +15 -16
  43. package/tests/sync-connection.test.ts +189 -0
  44. package/tests/syncDb-apply.test.ts +288 -0
  45. package/tests/syncDb-constants.test.ts +151 -0
  46. package/tests/syncDb-ddl.test.ts +206 -0
  47. package/tests/syncDb-helpers.test.ts +113 -0
  48. package/tests/syncDb-schema.test.ts +178 -0
  49. package/tests/syncDb-types.test.ts +130 -0
  50. package/tsconfig.json +2 -2
  51. package/types/api.d.ts +1 -1
  52. package/types/befly.d.ts +23 -21
  53. package/types/common.d.ts +0 -29
  54. package/types/context.d.ts +8 -6
  55. package/types/hook.d.ts +3 -4
  56. package/types/plugin.d.ts +3 -0
  57. package/hooks/errorHandler.ts +0 -23
  58. package/hooks/requestId.ts +0 -24
  59. package/hooks/requestLogger.ts +0 -25
  60. package/hooks/responseFormatter.ts +0 -64
  61. package/router/root.ts +0 -56
  62. package/sync/syncDb/index.ts +0 -164
@@ -0,0 +1,113 @@
1
+ /**
2
+ * syncDb 辅助工具模块测试
3
+ *
4
+ * 测试 helpers.ts 中的函数:
5
+ * - quoteIdentifier
6
+ * - escapeComment
7
+ * - applyFieldDefaults
8
+ */
9
+
10
+ import { describe, test, expect, beforeAll } from 'bun:test';
11
+ import { setDbType } from '../sync/syncDb/constants.js';
12
+
13
+ // 设置数据库类型为 MySQL
14
+ setDbType('mysql');
15
+
16
+ let quoteIdentifier: any;
17
+ let escapeComment: any;
18
+ let applyFieldDefaults: any;
19
+
20
+ beforeAll(async () => {
21
+ const helpers = await import('../sync/syncDb/helpers.js');
22
+ quoteIdentifier = helpers.quoteIdentifier;
23
+ escapeComment = helpers.escapeComment;
24
+ applyFieldDefaults = helpers.applyFieldDefaults;
25
+ });
26
+
27
+ describe('quoteIdentifier (MySQL)', () => {
28
+ test('使用反引号包裹标识符', () => {
29
+ expect(quoteIdentifier('user_table')).toBe('`user_table`');
30
+ });
31
+
32
+ test('处理普通表名', () => {
33
+ expect(quoteIdentifier('admin')).toBe('`admin`');
34
+ });
35
+
36
+ test('处理带下划线的表名', () => {
37
+ expect(quoteIdentifier('addon_admin_menu')).toBe('`addon_admin_menu`');
38
+ });
39
+ });
40
+
41
+ describe('escapeComment', () => {
42
+ test('普通注释不变', () => {
43
+ expect(escapeComment('用户名称')).toBe('用户名称');
44
+ });
45
+
46
+ test('双引号被转义', () => {
47
+ expect(escapeComment('用户"昵称"')).toBe('用户\\"昵称\\"');
48
+ });
49
+
50
+ test('空字符串', () => {
51
+ expect(escapeComment('')).toBe('');
52
+ });
53
+ });
54
+
55
+ describe('applyFieldDefaults', () => {
56
+ test('为空字段定义应用默认值', () => {
57
+ const fieldDef: any = {
58
+ name: '用户名',
59
+ type: 'string'
60
+ };
61
+
62
+ applyFieldDefaults(fieldDef);
63
+
64
+ expect(fieldDef.detail).toBe('');
65
+ expect(fieldDef.min).toBe(0);
66
+ expect(fieldDef.max).toBe(100);
67
+ expect(fieldDef.default).toBe(null);
68
+ expect(fieldDef.index).toBe(false);
69
+ expect(fieldDef.unique).toBe(false);
70
+ expect(fieldDef.comment).toBe('');
71
+ expect(fieldDef.nullable).toBe(false);
72
+ expect(fieldDef.unsigned).toBe(true);
73
+ expect(fieldDef.regexp).toBe(null);
74
+ });
75
+
76
+ test('保留已有值', () => {
77
+ const fieldDef: any = {
78
+ name: '用户名',
79
+ type: 'string',
80
+ max: 200,
81
+ index: true,
82
+ unique: true,
83
+ nullable: true
84
+ };
85
+
86
+ applyFieldDefaults(fieldDef);
87
+
88
+ expect(fieldDef.max).toBe(200);
89
+ expect(fieldDef.index).toBe(true);
90
+ expect(fieldDef.unique).toBe(true);
91
+ expect(fieldDef.nullable).toBe(true);
92
+ });
93
+
94
+ test('处理 0 和 false 值', () => {
95
+ const fieldDef: any = {
96
+ name: '排序',
97
+ type: 'number',
98
+ min: 0,
99
+ max: 0,
100
+ default: 0,
101
+ index: false,
102
+ unsigned: false
103
+ };
104
+
105
+ applyFieldDefaults(fieldDef);
106
+
107
+ expect(fieldDef.min).toBe(0);
108
+ expect(fieldDef.max).toBe(0);
109
+ expect(fieldDef.default).toBe(0);
110
+ expect(fieldDef.index).toBe(false);
111
+ expect(fieldDef.unsigned).toBe(false);
112
+ });
113
+ });
@@ -0,0 +1,178 @@
1
+ /**
2
+ * syncDb 表结构查询模块测试
3
+ *
4
+ * 测试 schema.ts 中的函数(纯逻辑测试,不需要数据库连接):
5
+ * - tableExists
6
+ * - getTableColumns
7
+ * - getTableIndexes
8
+ *
9
+ * 注意:这些是模拟测试,实际数据库操作需要集成测试
10
+ */
11
+
12
+ import { describe, test, expect, beforeAll, mock } from 'bun:test';
13
+ import { setDbType } from '../sync/syncDb/constants.js';
14
+
15
+ // 设置数据库类型为 MySQL
16
+ setDbType('mysql');
17
+
18
+ let tableExists: any;
19
+ let getTableColumns: any;
20
+ let getTableIndexes: any;
21
+
22
+ beforeAll(async () => {
23
+ const schema = await import('../sync/syncDb/schema.js');
24
+ tableExists = schema.tableExists;
25
+ getTableColumns = schema.getTableColumns;
26
+ getTableIndexes = schema.getTableIndexes;
27
+ });
28
+
29
+ describe('tableExists', () => {
30
+ test('sql 客户端未初始化时抛出错误', async () => {
31
+ try {
32
+ await tableExists(null, 'user');
33
+ expect(true).toBe(false); // 不应该到这里
34
+ } catch (error: any) {
35
+ expect(error.message).toBe('SQL 客户端未初始化');
36
+ }
37
+ });
38
+
39
+ test('传入有效 sql 客户端时正常执行', async () => {
40
+ // 创建模拟 SQL 客户端
41
+ const mockSql = Object.assign(
42
+ async function (strings: TemplateStringsArray, ...values: any[]) {
43
+ // 模拟 MySQL 查询返回
44
+ return [{ count: 1 }];
45
+ },
46
+ {
47
+ unsafe: async (query: string) => []
48
+ }
49
+ );
50
+
51
+ const result = await tableExists(mockSql, 'user', 'test_db');
52
+ expect(result).toBe(true);
53
+ });
54
+
55
+ test('表不存在时返回 false', async () => {
56
+ const mockSql = Object.assign(
57
+ async function (strings: TemplateStringsArray, ...values: any[]) {
58
+ return [{ count: 0 }];
59
+ },
60
+ {
61
+ unsafe: async (query: string) => []
62
+ }
63
+ );
64
+
65
+ const result = await tableExists(mockSql, 'nonexistent', 'test_db');
66
+ expect(result).toBe(false);
67
+ });
68
+ });
69
+
70
+ describe('getTableColumns', () => {
71
+ test('返回正确的列信息结构', async () => {
72
+ const mockSql = Object.assign(
73
+ async function (strings: TemplateStringsArray, ...values: any[]) {
74
+ // 模拟 MySQL information_schema 返回
75
+ return [
76
+ {
77
+ COLUMN_NAME: 'id',
78
+ DATA_TYPE: 'bigint',
79
+ CHARACTER_MAXIMUM_LENGTH: null,
80
+ IS_NULLABLE: 'NO',
81
+ COLUMN_DEFAULT: null,
82
+ COLUMN_COMMENT: '主键ID',
83
+ COLUMN_TYPE: 'bigint unsigned'
84
+ },
85
+ {
86
+ COLUMN_NAME: 'user_name',
87
+ DATA_TYPE: 'varchar',
88
+ CHARACTER_MAXIMUM_LENGTH: 50,
89
+ IS_NULLABLE: 'NO',
90
+ COLUMN_DEFAULT: '',
91
+ COLUMN_COMMENT: '用户名',
92
+ COLUMN_TYPE: 'varchar(50)'
93
+ },
94
+ {
95
+ COLUMN_NAME: 'age',
96
+ DATA_TYPE: 'bigint',
97
+ CHARACTER_MAXIMUM_LENGTH: null,
98
+ IS_NULLABLE: 'YES',
99
+ COLUMN_DEFAULT: '0',
100
+ COLUMN_COMMENT: '年龄',
101
+ COLUMN_TYPE: 'bigint'
102
+ }
103
+ ];
104
+ },
105
+ {
106
+ unsafe: async (query: string) => []
107
+ }
108
+ );
109
+
110
+ const columns = await getTableColumns(mockSql, 'user', 'test_db');
111
+
112
+ expect(columns.id).toBeDefined();
113
+ expect(columns.id.type).toBe('bigint');
114
+ expect(columns.id.nullable).toBe(false);
115
+ expect(columns.id.comment).toBe('主键ID');
116
+
117
+ expect(columns.user_name).toBeDefined();
118
+ expect(columns.user_name.type).toBe('varchar');
119
+ expect(columns.user_name.max).toBe(50);
120
+ expect(columns.user_name.nullable).toBe(false);
121
+ expect(columns.user_name.defaultValue).toBe('');
122
+
123
+ expect(columns.age).toBeDefined();
124
+ expect(columns.age.nullable).toBe(true);
125
+ expect(columns.age.defaultValue).toBe('0');
126
+ });
127
+ });
128
+
129
+ describe('getTableIndexes', () => {
130
+ test('返回正确的索引信息结构', async () => {
131
+ const mockSql = Object.assign(
132
+ async function (strings: TemplateStringsArray, ...values: any[]) {
133
+ // 模拟 MySQL information_schema.STATISTICS 返回
134
+ // 注意:PRIMARY 索引被排除
135
+ return [
136
+ { INDEX_NAME: 'idx_created_at', COLUMN_NAME: 'created_at' },
137
+ { INDEX_NAME: 'idx_user_name', COLUMN_NAME: 'user_name' }
138
+ ];
139
+ },
140
+ {
141
+ unsafe: async (query: string) => []
142
+ }
143
+ );
144
+
145
+ const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
146
+
147
+ // PRIMARY 索引被排除,不应存在
148
+ expect(indexes.PRIMARY).toBeUndefined();
149
+
150
+ expect(indexes.idx_created_at).toBeDefined();
151
+ expect(indexes.idx_created_at).toContain('created_at');
152
+
153
+ expect(indexes.idx_user_name).toBeDefined();
154
+ expect(indexes.idx_user_name).toContain('user_name');
155
+ });
156
+
157
+ test('复合索引包含多个列', async () => {
158
+ const mockSql = Object.assign(
159
+ async function (strings: TemplateStringsArray, ...values: any[]) {
160
+ // 模拟复合索引,同一索引名包含多个列
161
+ return [
162
+ { INDEX_NAME: 'idx_composite', COLUMN_NAME: 'user_id' },
163
+ { INDEX_NAME: 'idx_composite', COLUMN_NAME: 'created_at' }
164
+ ];
165
+ },
166
+ {
167
+ unsafe: async (query: string) => []
168
+ }
169
+ );
170
+
171
+ const indexes = await getTableIndexes(mockSql, 'user', 'test_db');
172
+
173
+ expect(indexes.idx_composite).toBeDefined();
174
+ expect(indexes.idx_composite.length).toBe(2);
175
+ expect(indexes.idx_composite).toContain('user_id');
176
+ expect(indexes.idx_composite).toContain('created_at');
177
+ });
178
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * syncDb 类型处理模块测试
3
+ *
4
+ * 测试 types.ts 中的函数:
5
+ * - isStringOrArrayType
6
+ * - getSqlType
7
+ * - resolveDefaultValue
8
+ * - generateDefaultSql
9
+ */
10
+
11
+ import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
12
+ import { setDbType } from '../sync/syncDb/constants.js';
13
+
14
+ // 设置数据库类型为 MySQL
15
+ setDbType('mysql');
16
+
17
+ // 动态导入以确保环境变量生效
18
+ let isStringOrArrayType: any;
19
+ let getSqlType: any;
20
+ let resolveDefaultValue: any;
21
+ let generateDefaultSql: any;
22
+
23
+ beforeAll(async () => {
24
+ const types = await import('../sync/syncDb/types.js');
25
+ isStringOrArrayType = types.isStringOrArrayType;
26
+ getSqlType = types.getSqlType;
27
+ resolveDefaultValue = types.resolveDefaultValue;
28
+ generateDefaultSql = types.generateDefaultSql;
29
+ });
30
+
31
+ describe('isStringOrArrayType', () => {
32
+ test('string 类型返回 true', () => {
33
+ expect(isStringOrArrayType('string')).toBe(true);
34
+ });
35
+
36
+ test('array_string 类型返回 true', () => {
37
+ expect(isStringOrArrayType('array_string')).toBe(true);
38
+ });
39
+
40
+ test('number 类型返回 false', () => {
41
+ expect(isStringOrArrayType('number')).toBe(false);
42
+ });
43
+
44
+ test('text 类型返回 false', () => {
45
+ expect(isStringOrArrayType('text')).toBe(false);
46
+ });
47
+
48
+ test('array_text 类型返回 false', () => {
49
+ expect(isStringOrArrayType('array_text')).toBe(false);
50
+ });
51
+ });
52
+
53
+ describe('resolveDefaultValue', () => {
54
+ test('null 值 + string 类型 => 空字符串', () => {
55
+ expect(resolveDefaultValue(null, 'string')).toBe('');
56
+ });
57
+
58
+ test('null 值 + number 类型 => 0', () => {
59
+ expect(resolveDefaultValue(null, 'number')).toBe(0);
60
+ });
61
+
62
+ test('"null" 字符串 + number 类型 => 0', () => {
63
+ expect(resolveDefaultValue('null', 'number')).toBe(0);
64
+ });
65
+
66
+ test('null 值 + array 类型 => "[]"', () => {
67
+ expect(resolveDefaultValue(null, 'array')).toBe('[]');
68
+ });
69
+
70
+ test('null 值 + text 类型 => "null"', () => {
71
+ expect(resolveDefaultValue(null, 'text')).toBe('null');
72
+ });
73
+
74
+ test('有实际值时直接返回', () => {
75
+ expect(resolveDefaultValue('admin', 'string')).toBe('admin');
76
+ expect(resolveDefaultValue(100, 'number')).toBe(100);
77
+ expect(resolveDefaultValue(0, 'number')).toBe(0);
78
+ });
79
+ });
80
+
81
+ describe('generateDefaultSql', () => {
82
+ test('number 类型生成数字默认值', () => {
83
+ expect(generateDefaultSql(0, 'number')).toBe(' DEFAULT 0');
84
+ expect(generateDefaultSql(100, 'number')).toBe(' DEFAULT 100');
85
+ });
86
+
87
+ test('string 类型生成带引号默认值', () => {
88
+ expect(generateDefaultSql('admin', 'string')).toBe(" DEFAULT 'admin'");
89
+ expect(generateDefaultSql('', 'string')).toBe(" DEFAULT ''");
90
+ });
91
+
92
+ test('text 类型不生成默认值', () => {
93
+ expect(generateDefaultSql('null', 'text')).toBe('');
94
+ });
95
+
96
+ test('array 类型生成 JSON 数组默认值', () => {
97
+ expect(generateDefaultSql('[]', 'array')).toBe(" DEFAULT '[]'");
98
+ });
99
+
100
+ test('单引号被正确转义', () => {
101
+ expect(generateDefaultSql("it's", 'string')).toBe(" DEFAULT 'it''s'");
102
+ });
103
+ });
104
+
105
+ describe('getSqlType', () => {
106
+ test('string 类型带长度', () => {
107
+ const result = getSqlType('string', 100);
108
+ expect(result).toBe('VARCHAR(100)');
109
+ });
110
+
111
+ test('array_string 类型带长度', () => {
112
+ const result = getSqlType('array_string', 500);
113
+ expect(result).toBe('VARCHAR(500)');
114
+ });
115
+
116
+ test('number 类型无符号', () => {
117
+ const result = getSqlType('number', null, true);
118
+ expect(result).toBe('BIGINT UNSIGNED');
119
+ });
120
+
121
+ test('number 类型有符号', () => {
122
+ const result = getSqlType('number', null, false);
123
+ expect(result).toBe('BIGINT');
124
+ });
125
+
126
+ test('text 类型', () => {
127
+ const result = getSqlType('text', null);
128
+ expect(result).toBe('MEDIUMTEXT');
129
+ });
130
+ });
package/tsconfig.json CHANGED
@@ -17,8 +17,8 @@
17
17
  "alwaysStrict": true,
18
18
 
19
19
  // 额外检查
20
- "noUnusedLocals": true,
21
- "noUnusedParameters": true,
20
+ "noUnusedLocals": false,
21
+ "noUnusedParameters": false,
22
22
  "noImplicitReturns": true,
23
23
  "noFallthroughCasesInSwitch": true,
24
24
  "noUncheckedIndexedAccess": false,
package/types/api.d.ts CHANGED
@@ -35,7 +35,7 @@ export type AuthType = boolean | 'admin' | 'user' | string[];
35
35
  /**
36
36
  * API 处理器函数类型
37
37
  */
38
- export type ApiHandler<T = any, R = any> = (befly: BeflyContext, ctx: RequestContext, req?: Request) => Promise<Response | R> | Response | R;
38
+ export type ApiHandler<T = any, R = any> = (befly: BeflyContext, ctx: RequestContext) => Promise<Response | R> | Response | R;
39
39
 
40
40
  /**
41
41
  * 字段规则定义
package/types/befly.d.ts CHANGED
@@ -146,32 +146,34 @@ export interface BeflyOptions {
146
146
  * 包含所有插件挂载的实例
147
147
  */
148
148
  export interface BeflyContext extends KeyValue {
149
- // ========== 核心插件(带下划线前缀) ==========
150
- /** 数据库助手 (db 插件) */
151
- _db?: DbHelper;
152
-
153
- /** Redis 助手 (redis 插件) */
154
- _redis?: RedisHelper;
155
-
156
- /** 日志器 (logger 插件) */
157
- _logger?: typeof Logger;
158
-
159
- /** 缓存助手 (cache 插件) */
160
- _cache?: CacheHelper;
161
-
162
- // ========== 核心插件便捷访问(无前缀) ==========
163
- /** 数据库助手便捷访问 */
149
+ // ========== 核心插件 ==========
150
+ /** 数据库助手 */
164
151
  db?: DbHelper;
165
152
 
166
- /** Redis 助手便捷访问 */
153
+ /** Redis 助手 */
167
154
  redis?: RedisHelper;
168
155
 
169
- /** 日志器便捷访问 */
156
+ /** 日志器 */
170
157
  logger?: typeof Logger;
171
158
 
172
- /** 缓存助手便捷访问 */
159
+ /** 缓存助手 */
173
160
  cache?: CacheHelper;
174
161
 
162
+ /** 工具函数 */
163
+ tool?: {
164
+ Yes: (msg: string, data?: any, other?: Record<string, any>) => { code: 0; msg: string; data: any };
165
+ No: (msg: string, data?: any, other?: Record<string, any>) => { code: 1; msg: string; data: any };
166
+ };
167
+
168
+ /** 加密解密 */
169
+ cipher?: typeof Cipher;
170
+
171
+ /** JWT 令牌 */
172
+ jwt?: typeof Jwt;
173
+
174
+ /** 项目配置 */
175
+ config?: BeflyOptions;
176
+
175
177
  // ========== 动态插件 ==========
176
178
  /** 组件插件:addon_{addonName}_{pluginName} */
177
179
  /** 项目插件:app_{pluginName} */
@@ -183,13 +185,13 @@ export interface BeflyContext extends KeyValue {
183
185
  */
184
186
  export interface Befly {
185
187
  /** API 路由映射 */
186
- apiRoutes: Map<string, ApiRoute>;
188
+ apis: Map<string, ApiRoute>;
187
189
 
188
190
  /** 插件列表 */
189
- pluginLists: Plugin[];
191
+ plugins: Plugin[];
190
192
 
191
193
  /** 应用上下文 */
192
- appContext: KeyValue;
194
+ context: KeyValue;
193
195
 
194
196
  /** 应用选项 */
195
197
  appOptions: BeflyOptions;
package/types/common.d.ts CHANGED
@@ -128,35 +128,6 @@ export interface LoggerConfig {
128
128
  };
129
129
  }
130
130
 
131
- /**
132
- * 环境变量类型
133
- */
134
- export interface EnvConfig {
135
- // 服务配置
136
- appName: string;
137
- appHost: string;
138
- appPort: number;
139
-
140
- // 数据库配置
141
- mysqlHost: string;
142
- mysqlPort: number;
143
- mysqlUsername: string;
144
- mysqlPassword: string;
145
- mysqlDatabase: string;
146
-
147
- // Redis 配置
148
- redisHost?: string;
149
- redisPort?: number;
150
- redisPassword?: string;
151
-
152
- // JWT 配置
153
- jwtSecret: string;
154
- jwtExpires?: string;
155
-
156
- // 其他配置
157
- [key: string]: any;
158
- }
159
-
160
131
  /**
161
132
  * 工具函数返回类型
162
133
  */
@@ -17,17 +17,19 @@ export interface RequestContext {
17
17
  /** 请求开始时间(毫秒) */
18
18
  now: number;
19
19
  /** 客户端 IP 地址 */
20
- ip?: string;
20
+ ip: string;
21
+ /** 请求头 */
22
+ headers: Headers;
21
23
  /** API 路由路径(如 POST/api/user/login) */
22
- route?: string;
24
+ route: string;
25
+ /** 请求唯一 ID */
26
+ requestId: string;
27
+ /** CORS 响应头 */
28
+ corsHeaders: Record<string, string>;
23
29
  /** 当前请求的 API 路由对象 */
24
30
  api?: ApiRoute;
25
31
  /** 响应对象(如果设置了此属性,将直接返回该响应) */
26
32
  response?: Response;
27
33
  /** 原始处理结果(未转换为 Response 对象前) */
28
34
  result?: any;
29
- /** 请求唯一 ID */
30
- requestId?: string;
31
- /** CORS 响应头 */
32
- corsHeaders?: Record<string, string>;
33
35
  }
package/types/hook.d.ts CHANGED
@@ -6,10 +6,9 @@ import type { BeflyContext } from './befly.js';
6
6
  import type { RequestContext } from './context.js';
7
7
 
8
8
  /**
9
- * 钩子处理函数类型
9
+ * 钩子处理函数类型(串联模式,无 next 参数)
10
10
  */
11
- export type Next = () => Promise<void>;
12
- export type HookHandler = (befly: BeflyContext, ctx: RequestContext, next: Next) => Promise<void> | void;
11
+ export type HookHandler = (befly: BeflyContext, ctx: RequestContext) => Promise<void> | void;
13
12
 
14
13
  /**
15
14
  * 钩子配置类型
@@ -21,7 +20,7 @@ export interface Hook {
21
20
  /** 依赖的钩子列表(在这些钩子之后执行) */
22
21
  after?: string[];
23
22
 
24
- /** 执行顺序(数字越小越先执行,同级别时使用) */
23
+ /** 执行顺序(数字越小越先执行) */
25
24
  order?: number;
26
25
 
27
26
  /** 钩子处理函数 */
package/types/plugin.d.ts CHANGED
@@ -28,6 +28,9 @@ export interface Plugin {
28
28
  after?: string[];
29
29
 
30
30
  /** 插件初始化函数 */
31
+ handler?: (context: BeflyContext, config?: Record<string, any>) => any | Promise<any>;
32
+
33
+ /** @deprecated use handler instead */
31
34
  onInit?: PluginInitFunction;
32
35
 
33
36
  /** 插件配置 */
@@ -1,23 +0,0 @@
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;
@@ -1,24 +0,0 @@
1
- import type { Hook } from '../types/hook.js';
2
- import { logContextStorage } from '../lib/logger.js';
3
-
4
- const hook: Hook = {
5
- after: ['errorHandler'],
6
- order: 3,
7
- handler: async (befly, ctx, next) => {
8
- // 生成唯一请求 ID
9
- const requestId = crypto.randomUUID();
10
- ctx.requestId = requestId;
11
-
12
- // 添加到 CORS 响应头
13
- if (!ctx.corsHeaders) {
14
- ctx.corsHeaders = {};
15
- }
16
- ctx.corsHeaders['X-Request-ID'] = requestId;
17
-
18
- // 在 AsyncLocalStorage 上下文中执行后续钩子
19
- await logContextStorage.run({ requestId }, async () => {
20
- await next();
21
- });
22
- }
23
- };
24
- export default hook;
@@ -1,25 +0,0 @@
1
- // 相对导入
2
- import { Logger } from '../lib/logger.js';
3
-
4
- // 类型导入
5
- import type { Hook } from '../types/hook.js';
6
-
7
- /**
8
- * 请求日志记录钩子
9
- * 记录请求方法、路径、用户信息和响应时间
10
- */
11
- const hook: Hook = {
12
- after: ['parser'],
13
- order: 30,
14
- handler: async (befly, ctx, next) => {
15
- await next();
16
-
17
- if (ctx.api) {
18
- const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
19
- const duration = Date.now() - ctx.now;
20
- const user = ctx.user?.userId ? `[User:${ctx.user.userId}]` : '[Guest]';
21
- Logger.info(`[${ctx.req.method}] ${apiPath} ${user} ${duration}ms`);
22
- }
23
- }
24
- };
25
- export default hook;