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.
Files changed (78) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +1 -1
  3. package/checks/checkApi.ts +92 -0
  4. package/checks/checkApp.ts +31 -0
  5. package/{check.ts → checks/checkTable.ts} +28 -159
  6. package/config.ts +71 -0
  7. package/hooks/auth.ts +30 -0
  8. package/hooks/cors.ts +48 -0
  9. package/hooks/errorHandler.ts +23 -0
  10. package/hooks/parser.ts +67 -0
  11. package/hooks/permission.ts +54 -0
  12. package/hooks/rateLimit.ts +70 -0
  13. package/hooks/requestId.ts +24 -0
  14. package/hooks/requestLogger.ts +25 -0
  15. package/hooks/responseFormatter.ts +64 -0
  16. package/hooks/validator.ts +34 -0
  17. package/lib/database.ts +28 -25
  18. package/lib/dbHelper.ts +3 -3
  19. package/lib/jwt.ts +90 -99
  20. package/lib/logger.ts +44 -23
  21. package/lib/redisHelper.ts +19 -22
  22. package/loader/loadApis.ts +69 -114
  23. package/loader/loadHooks.ts +65 -0
  24. package/loader/loadPlugins.ts +50 -219
  25. package/main.ts +106 -133
  26. package/package.json +23 -14
  27. package/paths.ts +20 -0
  28. package/plugins/cache.ts +1 -3
  29. package/plugins/db.ts +8 -11
  30. package/plugins/logger.ts +5 -3
  31. package/plugins/redis.ts +10 -14
  32. package/router/api.ts +60 -106
  33. package/router/root.ts +15 -12
  34. package/router/static.ts +54 -58
  35. package/sync/syncAll.ts +58 -0
  36. package/sync/syncApi.ts +264 -0
  37. package/sync/syncDb/apply.ts +194 -0
  38. package/sync/syncDb/constants.ts +76 -0
  39. package/sync/syncDb/ddl.ts +194 -0
  40. package/sync/syncDb/helpers.ts +200 -0
  41. package/sync/syncDb/index.ts +164 -0
  42. package/sync/syncDb/schema.ts +201 -0
  43. package/sync/syncDb/sqlite.ts +50 -0
  44. package/sync/syncDb/table.ts +321 -0
  45. package/sync/syncDb/tableCreate.ts +146 -0
  46. package/sync/syncDb/version.ts +72 -0
  47. package/sync/syncDb.ts +19 -0
  48. package/sync/syncDev.ts +206 -0
  49. package/sync/syncMenu.ts +331 -0
  50. package/tests/cipher.test.ts +248 -0
  51. package/tests/dbHelper-advanced.test.ts +717 -0
  52. package/tests/dbHelper-columns.test.ts +266 -0
  53. package/tests/dbHelper-execute.test.ts +240 -0
  54. package/tests/fields-redis-cache.test.ts +123 -0
  55. package/tests/fields-validate.test.ts +99 -0
  56. package/tests/integration.test.ts +202 -0
  57. package/tests/jwt.test.ts +122 -0
  58. package/tests/logger.test.ts +94 -0
  59. package/tests/redisHelper.test.ts +231 -0
  60. package/tests/sqlBuilder-advanced.test.ts +593 -0
  61. package/tests/sqlBuilder.test.ts +184 -0
  62. package/tests/util.test.ts +95 -0
  63. package/tests/validator-advanced.test.ts +653 -0
  64. package/tests/validator.test.ts +148 -0
  65. package/tests/xml.test.ts +101 -0
  66. package/tsconfig.json +2 -4
  67. package/types/api.d.ts +6 -0
  68. package/types/befly.d.ts +152 -28
  69. package/types/context.d.ts +29 -3
  70. package/types/hook.d.ts +35 -0
  71. package/types/index.ts +14 -1
  72. package/types/plugin.d.ts +6 -7
  73. package/types/sync.d.ts +403 -0
  74. package/env.ts +0 -106
  75. package/lib/middleware.ts +0 -275
  76. package/types/env.ts +0 -65
  77. package/types/util.d.ts +0 -45
  78. package/util.ts +0 -257
@@ -0,0 +1,54 @@
1
+ // 相对导入
2
+ import { JsonResponse } from '../util.js';
3
+
4
+ // 类型导入
5
+ import type { Hook } from '../types/hook.js';
6
+
7
+ /**
8
+ * 权限检查钩子
9
+ * - 接口无需权限(auth=false):直接通过
10
+ * - 用户未登录:返回 401
11
+ * - 开发者角色(dev):最高权限,直接通过
12
+ * - 其他角色:检查 Redis 中的角色权限集合
13
+ */
14
+ const hook: Hook = {
15
+ after: ['parser'],
16
+ order: 20,
17
+ handler: async (befly, ctx, next) => {
18
+ if (!ctx.api) return next();
19
+
20
+ // 1. 接口无需权限
21
+ if (ctx.api.auth === false) {
22
+ return next();
23
+ }
24
+
25
+ // 2. 用户未登录
26
+ if (!ctx.user || !ctx.user.userId) {
27
+ ctx.response = JsonResponse(ctx, '未登录', 401);
28
+ return;
29
+ }
30
+
31
+ // 3. 开发者权限(最高权限)
32
+ if (ctx.user.roleCode === 'dev') {
33
+ return next();
34
+ }
35
+
36
+ // 4. 角色权限检查
37
+ let hasPermission = false;
38
+ if (ctx.user.roleCode && befly.redis) {
39
+ // 验证角色权限
40
+ const apiPath = `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
41
+ const roleApisKey = `role:apis:${ctx.user.roleCode}`;
42
+ const isMember = await befly.redis.sismember(roleApisKey, apiPath);
43
+ hasPermission = isMember === 1;
44
+ }
45
+
46
+ if (!hasPermission) {
47
+ ctx.response = JsonResponse(ctx, '无权访问', 403);
48
+ return;
49
+ }
50
+
51
+ await next();
52
+ }
53
+ };
54
+ export default hook;
@@ -0,0 +1,70 @@
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
+ /**
9
+ * 接口限流插件
10
+ *
11
+ * 功能:
12
+ * 1. 基于 Redis 实现滑动窗口或固定窗口限流
13
+ * 2. 支持配置格式 "count/seconds" (e.g. "10/60")
14
+ * 3. 针对每个用户(userId)或IP进行限制
15
+ */
16
+ const hook: Hook = {
17
+ // 必须在 auth 之后(获取 userId),但在业务逻辑之前
18
+ after: ['parser'],
19
+ order: 25,
20
+
21
+ handler: async (befly, ctx, next) => {
22
+ const { api } = ctx;
23
+
24
+ // 1. 检查配置
25
+ if (!api || !api.rateLimit || !befly.redis) {
26
+ return next();
27
+ }
28
+
29
+ // 2. 解析配置 "10/60" -> count=10, seconds=60
30
+ const [countStr, secondsStr] = api.rateLimit.split('/');
31
+ const limitCount = parseInt(countStr, 10);
32
+ const limitSeconds = parseInt(secondsStr, 10);
33
+
34
+ if (isNaN(limitCount) || isNaN(limitSeconds)) {
35
+ Logger.warn(`[RateLimit] Invalid config: ${api.rateLimit}`);
36
+ return next();
37
+ }
38
+
39
+ // 3. 生成 Key
40
+ // 优先使用 userId,否则使用 IP
41
+ const identifier = ctx.user?.userId || ctx.ip || 'unknown';
42
+ const apiPath = ctx.route || `${ctx.req.method}${new URL(ctx.req.url).pathname}`;
43
+ const key = `rate_limit:${apiPath}:${identifier}`;
44
+
45
+ try {
46
+ // 4. 执行限流逻辑 (使用 Redis INCR)
47
+ // 这是一个简单的固定窗口算法,对于严格场景可能需要 Lua 脚本实现滑动窗口
48
+ const current = await befly.redis.incr(key);
49
+
50
+ // 5. 设置过期时间 (如果是新 Key)
51
+ if (current === 1) {
52
+ await befly.redis.expire(key, limitSeconds);
53
+ }
54
+
55
+ // 6. 判断是否超限
56
+ if (current > limitCount) {
57
+ ctx.response = JsonResponse(ctx, '请求过于频繁,请稍后再试', 429);
58
+ return;
59
+ }
60
+
61
+ return next();
62
+ } catch (err) {
63
+ Logger.error('[RateLimit] Redis error:', err);
64
+ // Redis 故障时,默认放行,避免阻塞业务
65
+ return next();
66
+ }
67
+ }
68
+ };
69
+
70
+ export default hook;
@@ -0,0 +1,24 @@
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;
@@ -0,0 +1,25 @@
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;
@@ -0,0 +1,64 @@
1
+ // 相对导入
2
+ import { JsonResponse } from '../util.js';
3
+
4
+ // 类型导入
5
+ import type { Hook } from '../types/hook.js';
6
+
7
+ const hook: Hook = {
8
+ after: ['requestId'],
9
+ order: 100,
10
+ handler: async (befly, ctx, next) => {
11
+ await next();
12
+
13
+ // 如果已经有 response,直接返回
14
+ if (ctx.response) {
15
+ return;
16
+ }
17
+
18
+ // 如果有 result,格式化为 JSON 响应
19
+ if (ctx.result !== undefined) {
20
+ let result = ctx.result;
21
+
22
+ // 1. 如果是字符串,自动包裹为成功响应
23
+ if (typeof result === 'string') {
24
+ result = {
25
+ code: 0,
26
+ msg: result,
27
+ data: {}
28
+ };
29
+ }
30
+ // 2. 如果是对象,自动补充 code: 0
31
+ else if (result && typeof result === 'object') {
32
+ if (!('code' in result)) {
33
+ result = {
34
+ code: 0,
35
+ ...result
36
+ };
37
+ }
38
+ }
39
+
40
+ // 处理 BigInt 序列化问题
41
+ if (result && typeof result === 'object') {
42
+ const jsonString = JSON.stringify(result, (key, value) => (typeof value === 'bigint' ? value.toString() : value));
43
+ ctx.response = new Response(jsonString, {
44
+ headers: {
45
+ ...ctx.corsHeaders,
46
+ 'Content-Type': 'application/json'
47
+ }
48
+ });
49
+ } else {
50
+ // 简单类型直接返回
51
+ ctx.response = Response.json(result, {
52
+ headers: ctx.corsHeaders
53
+ });
54
+ }
55
+ return;
56
+ }
57
+
58
+ // 如果还没有响应,且不是 OPTIONS 请求,则设置默认 JSON 响应
59
+ if (ctx.req.method !== 'OPTIONS' && !ctx.response) {
60
+ ctx.response = JsonResponse(ctx, 'No response generated');
61
+ }
62
+ }
63
+ };
64
+ export default hook;
@@ -0,0 +1,34 @@
1
+ // 相对导入
2
+ import { Validator } from '../lib/validator.js';
3
+ import { JsonResponse } from '../util.js';
4
+
5
+ // 类型导入
6
+ import type { Hook } from '../types/hook.js';
7
+
8
+ /**
9
+ * 参数验证钩子
10
+ * 根据 API 定义的 fields 和 required 验证请求参数
11
+ */
12
+ const hook: Hook = {
13
+ after: ['parser'],
14
+ order: 15,
15
+ handler: async (befly, ctx, next) => {
16
+ if (!ctx.api) return next();
17
+
18
+ // 无需验证
19
+ if (!ctx.api.fields) {
20
+ return next();
21
+ }
22
+
23
+ // 验证参数
24
+ const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
25
+
26
+ if (result.code !== 0) {
27
+ ctx.response = JsonResponse(ctx, '无效的请求参数格式', 1, result.fields);
28
+ return;
29
+ }
30
+
31
+ await next();
32
+ }
33
+ };
34
+ export default hook;
package/lib/database.ts CHANGED
@@ -4,11 +4,10 @@
4
4
  */
5
5
 
6
6
  import { SQL, RedisClient } from 'bun';
7
- import { Env } from '../env.js';
8
7
  import { Logger } from './logger.js';
9
8
  import { DbHelper } from './dbHelper.js';
10
9
  import { RedisHelper } from './redisHelper.js';
11
- import type { BeflyContext } from '../types/befly.js';
10
+ import type { BeflyContext, DatabaseConfig, RedisConfig } from '../types/befly.js';
12
11
  import type { SqlClientOptions } from '../types/database.js';
13
12
 
14
13
  /**
@@ -26,50 +25,49 @@ export class Database {
26
25
 
27
26
  /**
28
27
  * 连接 SQL 数据库
29
- * @param options - SQL 客户端配置选项
28
+ * @param config - 数据库配置
30
29
  * @returns SQL 客户端实例
31
30
  */
32
- static async connectSql(options: SqlClientOptions = {}): Promise<SQL> {
31
+ static async connectSql(config: DatabaseConfig): Promise<SQL> {
33
32
  // 构建数据库连接字符串
34
- const type = Env.DB_TYPE || '';
35
- const host = Env.DB_HOST || '';
36
- const port = Env.DB_PORT;
37
- const user = encodeURIComponent(Env.DB_USER || '');
38
- const password = encodeURIComponent(Env.DB_PASS || '');
39
- const database = encodeURIComponent(Env.DB_NAME || '');
33
+ const type = config.type || 'mysql';
34
+ const host = config.host || '127.0.0.1';
35
+ const port = config.port || 3306;
36
+ const user = encodeURIComponent(config.username || 'root');
37
+ const password = encodeURIComponent(config.password || 'root');
38
+ const database = encodeURIComponent(config.database || 'befly_demo');
40
39
 
41
40
  let finalUrl: string;
42
41
  if (type === 'sqlite') {
43
42
  finalUrl = database;
44
43
  } else {
45
44
  if (!host || !database) {
46
- throw new Error('数据库配置不完整,请检查环境变量');
45
+ throw new Error('数据库配置不完整,请检查配置参数');
47
46
  }
48
47
  finalUrl = `${type}://${user}:${password}@${host}:${port}/${database}`;
49
48
  }
50
49
 
51
50
  let sql: SQL;
52
51
 
53
- if (Env.DB_TYPE === 'sqlite') {
52
+ if (type === 'sqlite') {
54
53
  sql = new SQL(finalUrl);
55
54
  } else {
56
55
  sql = new SQL({
57
56
  url: finalUrl,
58
- max: options.max ?? 1,
59
- bigint: false,
60
- ...options
57
+ max: config.poolMax ?? 1,
58
+ bigint: false
61
59
  });
62
60
  }
63
61
 
64
62
  try {
65
- const timeout = options.connectionTimeout ?? 30000;
63
+ const timeout = 30000;
66
64
 
67
65
  const healthCheckPromise = (async () => {
68
66
  let version = '';
69
- if (Env.DB_TYPE === 'sqlite') {
67
+ if (type === 'sqlite') {
70
68
  const v = await sql`SELECT sqlite_version() AS version`;
71
69
  version = v?.[0]?.version;
72
- } else if (Env.DB_TYPE === 'postgresql' || Env.DB_TYPE === 'postgres') {
70
+ } else if (type === 'postgresql' || type === 'postgres') {
73
71
  const v = await sql`SELECT version() AS version`;
74
72
  version = v?.[0]?.version;
75
73
  } else {
@@ -157,21 +155,26 @@ export class Database {
157
155
 
158
156
  /**
159
157
  * 连接 Redis
158
+ * @param config - Redis 配置
160
159
  * @returns Redis 客户端实例
161
160
  */
162
- static async connectRedis(): Promise<RedisClient> {
161
+ static async connectRedis(config: RedisConfig = {}): Promise<RedisClient> {
163
162
  try {
164
163
  // 构建 Redis URL
165
- const { REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB } = Env;
164
+ const host = config.host || '127.0.0.1';
165
+ const port = config.port || 6379;
166
+ const username = config.username || '';
167
+ const password = config.password || '';
168
+ const db = config.db || 0;
166
169
 
167
170
  let auth = '';
168
- if (REDIS_USERNAME && REDIS_PASSWORD) {
169
- auth = `${REDIS_USERNAME}:${REDIS_PASSWORD}@`;
170
- } else if (REDIS_PASSWORD) {
171
- auth = `:${REDIS_PASSWORD}@`;
171
+ if (username && password) {
172
+ auth = `${username}:${password}@`;
173
+ } else if (password) {
174
+ auth = `:${password}@`;
172
175
  }
173
176
 
174
- const url = `redis://${auth}${REDIS_HOST}:${REDIS_PORT}/${REDIS_DB}`;
177
+ const url = `redis://${auth}${host}:${port}/${db}`;
175
178
 
176
179
  const redis = new RedisClient(url, {
177
180
  connectionTimeout: 30000,
package/lib/dbHelper.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { snakeCase } from 'es-toolkit/string';
7
7
  import { SqlBuilder } from './sqlBuilder.js';
8
- import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear } from '../util.js';
8
+ import { keysToCamel, arrayKeysToCamel, keysToSnake, fieldClear } from 'befly-util';
9
9
  import { Logger } from '../lib/logger.js';
10
10
  import type { WhereConditions } from '../types/common.js';
11
11
  import type { BeflyContext } from '../types/befly.js';
@@ -204,8 +204,8 @@ export class DbHelper {
204
204
  /**
205
205
  * 清理数据或 where 条件(默认排除 null 和 undefined)
206
206
  */
207
- private cleanFields<T extends Record<string, any>>(data: T | undefined | null, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> {
208
- return fieldClear(data || ({} as T), excludeValues, keepValues);
207
+ public cleanFields<T extends Record<string, any>>(data: T, excludeValues: any[] = [null, undefined], keepValues: Record<string, any> = {}): Partial<T> {
208
+ return fieldClear(data || ({} as T), { excludeValues, keepMap: keepValues });
209
209
  }
210
210
 
211
211
  /**