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.
Files changed (155) hide show
  1. package/README.md +37 -38
  2. package/befly.config.ts +62 -40
  3. package/checks/checkApi.ts +16 -16
  4. package/checks/checkApp.ts +19 -25
  5. package/checks/checkTable.ts +42 -42
  6. package/docs/README.md +42 -35
  7. package/docs/{api.md → api/api.md} +223 -231
  8. package/docs/cipher.md +71 -69
  9. package/docs/database.md +143 -141
  10. package/docs/{examples.md → guide/examples.md} +181 -181
  11. package/docs/guide/quickstart.md +331 -0
  12. package/docs/hooks/auth.md +38 -0
  13. package/docs/hooks/cors.md +28 -0
  14. package/docs/{hook.md → hooks/hook.md} +140 -57
  15. package/docs/hooks/parser.md +19 -0
  16. package/docs/hooks/rateLimit.md +47 -0
  17. package/docs/{redis.md → infra/redis.md} +84 -93
  18. package/docs/plugins/cipher.md +61 -0
  19. package/docs/plugins/database.md +128 -0
  20. package/docs/{plugin.md → plugins/plugin.md} +83 -81
  21. package/docs/quickstart.md +26 -26
  22. package/docs/{addon.md → reference/addon.md} +46 -46
  23. package/docs/{config.md → reference/config.md} +32 -80
  24. package/docs/{logger.md → reference/logger.md} +52 -52
  25. package/docs/{sync.md → reference/sync.md} +32 -35
  26. package/docs/{table.md → reference/table.md} +1 -1
  27. package/docs/{validator.md → reference/validator.md} +57 -57
  28. package/hooks/auth.ts +8 -4
  29. package/hooks/cors.ts +13 -13
  30. package/hooks/parser.ts +37 -17
  31. package/hooks/permission.ts +26 -14
  32. package/hooks/rateLimit.ts +276 -0
  33. package/hooks/validator.ts +8 -8
  34. package/lib/asyncContext.ts +43 -0
  35. package/lib/cacheHelper.ts +212 -77
  36. package/lib/cacheKeys.ts +38 -0
  37. package/lib/cipher.ts +30 -30
  38. package/lib/connect.ts +28 -28
  39. package/lib/dbHelper.ts +183 -102
  40. package/lib/jwt.ts +16 -16
  41. package/lib/logger.ts +610 -19
  42. package/lib/redisHelper.ts +185 -44
  43. package/lib/sqlBuilder.ts +90 -91
  44. package/lib/validator.ts +59 -39
  45. package/loader/loadApis.ts +48 -44
  46. package/loader/loadHooks.ts +40 -14
  47. package/loader/loadPlugins.ts +16 -17
  48. package/main.ts +57 -47
  49. package/package.json +47 -45
  50. package/paths.ts +15 -14
  51. package/plugins/cache.ts +5 -4
  52. package/plugins/cipher.ts +3 -3
  53. package/plugins/config.ts +2 -2
  54. package/plugins/db.ts +9 -9
  55. package/plugins/jwt.ts +3 -3
  56. package/plugins/logger.ts +8 -12
  57. package/plugins/redis.ts +8 -8
  58. package/plugins/tool.ts +6 -6
  59. package/router/api.ts +85 -56
  60. package/router/static.ts +12 -12
  61. package/sync/syncAll.ts +12 -12
  62. package/sync/syncApi.ts +55 -52
  63. package/sync/syncDb/apply.ts +20 -19
  64. package/sync/syncDb/constants.ts +25 -23
  65. package/sync/syncDb/ddl.ts +35 -36
  66. package/sync/syncDb/helpers.ts +6 -9
  67. package/sync/syncDb/schema.ts +10 -9
  68. package/sync/syncDb/sqlite.ts +7 -8
  69. package/sync/syncDb/table.ts +37 -35
  70. package/sync/syncDb/tableCreate.ts +21 -20
  71. package/sync/syncDb/types.ts +23 -20
  72. package/sync/syncDb/version.ts +10 -10
  73. package/sync/syncDb.ts +43 -36
  74. package/sync/syncDev.ts +74 -65
  75. package/sync/syncMenu.ts +190 -55
  76. package/tests/api-integration-array-number.test.ts +282 -0
  77. package/tests/befly-config-env.test.ts +78 -0
  78. package/tests/cacheHelper.test.ts +135 -104
  79. package/tests/cacheKeys.test.ts +41 -0
  80. package/tests/cipher.test.ts +90 -89
  81. package/tests/dbHelper-advanced.test.ts +140 -134
  82. package/tests/dbHelper-all-array-types.test.ts +316 -0
  83. package/tests/dbHelper-array-serialization.test.ts +258 -0
  84. package/tests/dbHelper-columns.test.ts +56 -55
  85. package/tests/dbHelper-execute.test.ts +45 -44
  86. package/tests/dbHelper-joins.test.ts +124 -119
  87. package/tests/fields-redis-cache.test.ts +29 -27
  88. package/tests/fields-validate.test.ts +38 -38
  89. package/tests/getClientIp.test.ts +54 -0
  90. package/tests/integration.test.ts +69 -67
  91. package/tests/jwt.test.ts +27 -26
  92. package/tests/logger.test.ts +267 -34
  93. package/tests/rateLimit-hook.test.ts +477 -0
  94. package/tests/redisHelper.test.ts +187 -188
  95. package/tests/redisKeys.test.ts +6 -73
  96. package/tests/scanConfig.test.ts +144 -0
  97. package/tests/sqlBuilder-advanced.test.ts +217 -215
  98. package/tests/sqlBuilder.test.ts +92 -91
  99. package/tests/sync-connection.test.ts +29 -29
  100. package/tests/syncDb-apply.test.ts +97 -96
  101. package/tests/syncDb-array-number.test.ts +160 -0
  102. package/tests/syncDb-constants.test.ts +48 -47
  103. package/tests/syncDb-ddl.test.ts +99 -98
  104. package/tests/syncDb-helpers.test.ts +29 -28
  105. package/tests/syncDb-schema.test.ts +61 -60
  106. package/tests/syncDb-types.test.ts +60 -59
  107. package/tests/syncMenu-paths.test.ts +68 -0
  108. package/tests/util.test.ts +42 -41
  109. package/tests/validator-array-number.test.ts +310 -0
  110. package/tests/validator-default.test.ts +373 -0
  111. package/tests/validator.test.ts +271 -266
  112. package/tsconfig.json +4 -5
  113. package/types/api.d.ts +7 -12
  114. package/types/befly.d.ts +60 -13
  115. package/types/cache.d.ts +8 -4
  116. package/types/common.d.ts +17 -9
  117. package/types/context.d.ts +2 -2
  118. package/types/crypto.d.ts +23 -0
  119. package/types/database.d.ts +19 -19
  120. package/types/hook.d.ts +2 -2
  121. package/types/jwt.d.ts +118 -0
  122. package/types/logger.d.ts +30 -0
  123. package/types/plugin.d.ts +4 -4
  124. package/types/redis.d.ts +7 -3
  125. package/types/roleApisCache.ts +23 -0
  126. package/types/sync.d.ts +10 -10
  127. package/types/table.d.ts +50 -9
  128. package/types/validate.d.ts +69 -0
  129. package/utils/addonHelper.ts +90 -0
  130. package/utils/arrayKeysToCamel.ts +18 -0
  131. package/utils/calcPerfTime.ts +13 -0
  132. package/utils/configTypes.ts +3 -0
  133. package/utils/cors.ts +19 -0
  134. package/utils/fieldClear.ts +75 -0
  135. package/utils/genShortId.ts +12 -0
  136. package/utils/getClientIp.ts +45 -0
  137. package/utils/keysToCamel.ts +22 -0
  138. package/utils/keysToSnake.ts +22 -0
  139. package/utils/modules.ts +98 -0
  140. package/utils/pickFields.ts +19 -0
  141. package/utils/process.ts +56 -0
  142. package/utils/regex.ts +225 -0
  143. package/utils/response.ts +115 -0
  144. package/utils/route.ts +23 -0
  145. package/utils/scanConfig.ts +142 -0
  146. package/utils/scanFiles.ts +48 -0
  147. package/.prettierignore +0 -2
  148. package/.prettierrc +0 -12
  149. package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
  150. package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
  151. package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
  152. package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
  153. package/hooks/requestLogger.ts +0 -84
  154. package/types/index.ts +0 -24
  155. package/util.ts +0 -283
@@ -0,0 +1,276 @@
1
+ // 类型导入
2
+ import type { Hook } from "../types/hook.js";
3
+
4
+ import { beflyConfig } from "../befly.config.js";
5
+ // 相对导入
6
+ import { ErrorResponse } from "../utils/response.js";
7
+
8
+ /**
9
+ * 限流 key 维度
10
+ * - ip: 仅按 IP
11
+ * - user: 仅按用户(缺失 ctx.user.id 时回退为按 IP,避免匿名共享同一计数桶)
12
+ * - ip_user: IP + 用户(缺失用户时视为 anonymous)
13
+ */
14
+ type RateLimitKeyMode = "ip" | "user" | "ip_user";
15
+
16
+ /**
17
+ * 单条限流规则
18
+ * route 匹配串:
19
+ * - 精确:"POST/api/auth/login"
20
+ * - 前缀:"POST/api/auth/*" 或 "/api/auth/*"
21
+ * - 全量:"*"
22
+ */
23
+ type RateLimitRule = {
24
+ route: string;
25
+ /** 窗口期内允许次数 */
26
+ limit: number;
27
+ /** 窗口期秒数 */
28
+ window: number;
29
+ /** 计数维度(默认 ip) */
30
+ key?: RateLimitKeyMode;
31
+ };
32
+
33
+ /** 全局请求限流配置(Hook) */
34
+ type RateLimitConfig = {
35
+ /** 是否启用 (0/1) */
36
+ enable?: number;
37
+ /** 未命中 rules 时的默认次数(<=0 表示不启用默认规则) */
38
+ defaultLimit?: number;
39
+ /** 未命中 rules 时的默认窗口秒数(<=0 表示不启用默认规则) */
40
+ defaultWindow?: number;
41
+ /** 默认计数维度(默认 ip) */
42
+ key?: RateLimitKeyMode;
43
+ /**
44
+ * 直接跳过限流的路由列表(优先级最高)
45
+ * - 精确:"POST/api/health" 或 "/api/health"
46
+ * - 前缀:"POST/api/health/*" 或 "/api/health/*"
47
+ */
48
+ skipRoutes?: string[];
49
+ /** 路由规则列表 */
50
+ rules?: RateLimitRule[];
51
+ };
52
+
53
+ type MemoryBucket = {
54
+ count: number;
55
+ resetAt: number;
56
+ };
57
+
58
+ const memoryBuckets = new Map<string, MemoryBucket>();
59
+ let nextSweepAt = 0;
60
+
61
+ function matchRoute(ruleRoute: string, actualRoute: string): boolean {
62
+ if (ruleRoute === "*") return true;
63
+
64
+ if (ruleRoute.endsWith("*")) {
65
+ const prefix = ruleRoute.slice(0, -1);
66
+ if (prefix.startsWith("/")) {
67
+ return actualRoute.includes(prefix);
68
+ }
69
+ return actualRoute.startsWith(prefix);
70
+ }
71
+
72
+ if (ruleRoute.startsWith("/")) {
73
+ return actualRoute.endsWith(ruleRoute);
74
+ }
75
+
76
+ return actualRoute === ruleRoute;
77
+ }
78
+
79
+ function calcRouteMatchScore(ruleRoute: string, actualRoute: string): number {
80
+ if (!ruleRoute || typeof ruleRoute !== "string") return -1;
81
+
82
+ // 兜底通配:最低优先级
83
+ if (ruleRoute === "*") return 0;
84
+
85
+ // 完全精确:最高优先级
86
+ if (ruleRoute === actualRoute) return 400000 + ruleRoute.length;
87
+
88
+ // 以 / 开头:只匹配 path(忽略 method),用于 /api/* 这类
89
+ if (ruleRoute.startsWith("/")) {
90
+ if (ruleRoute.endsWith("*")) {
91
+ const prefix = ruleRoute.slice(0, -1);
92
+ if (actualRoute.includes(prefix)) {
93
+ return 100000 + prefix.length;
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ if (actualRoute.endsWith(ruleRoute)) {
99
+ return 300000 + ruleRoute.length;
100
+ }
101
+
102
+ return -1;
103
+ }
104
+
105
+ // 不以 / 开头:匹配包含 method 的完整串,如 POST/api/auth/*
106
+ if (ruleRoute.endsWith("*")) {
107
+ const prefix = ruleRoute.slice(0, -1);
108
+ if (actualRoute.startsWith(prefix)) {
109
+ return 200000 + prefix.length;
110
+ }
111
+ return -1;
112
+ }
113
+
114
+ // 其他兜底(理论上到不了,因为精确已在上面处理;这里为了兼容 matchRoute 的未来扩展)
115
+ if (matchRoute(ruleRoute, actualRoute)) {
116
+ return 50000 + ruleRoute.length;
117
+ }
118
+
119
+ return -1;
120
+ }
121
+
122
+ function shouldSkip(config: RateLimitConfig, actualRoute: string): boolean {
123
+ const skipRoutes = Array.isArray(config.skipRoutes) ? config.skipRoutes : [];
124
+ if (skipRoutes.length === 0) return false;
125
+
126
+ for (const skip of skipRoutes) {
127
+ if (typeof skip !== "string" || !skip) continue;
128
+ if (matchRoute(skip, actualRoute)) return true;
129
+ }
130
+
131
+ return false;
132
+ }
133
+
134
+ function pickRule(config: RateLimitConfig, actualRoute: string): RateLimitRule | null {
135
+ const rules = Array.isArray(config.rules) ? config.rules : [];
136
+
137
+ let bestRule: RateLimitRule | null = null;
138
+ let bestScore = -1;
139
+
140
+ // 多条命中时,优先更“具体”的规则(精确 > 前缀 > 通配);同等具体度按 rules 的先后顺序
141
+ for (const rule of rules) {
142
+ if (!rule || typeof rule.route !== "string") continue;
143
+
144
+ const score = calcRouteMatchScore(rule.route, actualRoute);
145
+ if (score > bestScore) {
146
+ bestRule = rule;
147
+ bestScore = score;
148
+ }
149
+ }
150
+
151
+ if (bestRule) return bestRule;
152
+
153
+ const defaultLimit = typeof config.defaultLimit === "number" ? config.defaultLimit : 0;
154
+ const defaultWindow = typeof config.defaultWindow === "number" ? config.defaultWindow : 0;
155
+
156
+ if (defaultLimit > 0 && defaultWindow > 0) {
157
+ return {
158
+ route: "*",
159
+ limit: defaultLimit,
160
+ window: defaultWindow,
161
+ key: config.key
162
+ };
163
+ }
164
+
165
+ return null;
166
+ }
167
+
168
+ function buildIdentity(ctx: any, mode: RateLimitKeyMode): string {
169
+ const ip = typeof ctx.ip === "string" ? ctx.ip : "unknown";
170
+
171
+ const userIdValue = ctx.user && (typeof ctx.user.id === "number" || typeof ctx.user.id === "string") ? ctx.user.id : null;
172
+ const userId = userIdValue !== null ? String(userIdValue) : "";
173
+
174
+ if (mode === "ip") {
175
+ return `ip:${ip}`;
176
+ }
177
+
178
+ if (mode === "user") {
179
+ // 未登录/无 userId:回退为按 IP 计数,避免所有匿名用户共享同一 bucket
180
+ if (userId) return `user:${userId}`;
181
+ return `ip:${ip}`;
182
+ }
183
+
184
+ if (mode === "ip_user") {
185
+ if (userId) {
186
+ return `ip:${ip}:user:${userId}`;
187
+ }
188
+ return `ip:${ip}:user:anonymous`;
189
+ }
190
+
191
+ return `ip:${ip}`;
192
+ }
193
+
194
+ function hitMemoryBucket(key: string, windowSeconds: number): number {
195
+ const now = Date.now();
196
+
197
+ if (now >= nextSweepAt) {
198
+ nextSweepAt = now + 60_000;
199
+ for (const [k, v] of memoryBuckets.entries()) {
200
+ if (!v || typeof v.resetAt !== "number") {
201
+ memoryBuckets.delete(k);
202
+ continue;
203
+ }
204
+ if (v.resetAt <= now) {
205
+ memoryBuckets.delete(k);
206
+ }
207
+ }
208
+ }
209
+
210
+ const existing = memoryBuckets.get(key);
211
+ if (!existing || existing.resetAt <= now) {
212
+ const bucket: MemoryBucket = {
213
+ count: 1,
214
+ resetAt: now + windowSeconds * 1000
215
+ };
216
+ memoryBuckets.set(key, bucket);
217
+ return bucket.count;
218
+ }
219
+
220
+ existing.count += 1;
221
+ return existing.count;
222
+ }
223
+
224
+ /**
225
+ * 请求限流钩子(全局)
226
+ * - 通过 beflyConfig.rateLimit 开启/配置
227
+ * - 默认启用:可通过配置禁用或调整阈值
228
+ */
229
+ const hook: Hook = {
230
+ order: 7,
231
+ handler: async (befly, ctx) => {
232
+ const config = beflyConfig.rateLimit as RateLimitConfig | undefined;
233
+
234
+ if (!config || config.enable !== 1) return;
235
+ if (!ctx.api) return;
236
+ if (ctx.req && ctx.req.method === "OPTIONS") return;
237
+
238
+ // 跳过名单:命中后不计数也不拦截(优先级最高)
239
+ if (shouldSkip(config, ctx.route)) return;
240
+
241
+ const rule = pickRule(config, ctx.route);
242
+ if (!rule) return;
243
+
244
+ const limit = typeof rule.limit === "number" ? rule.limit : 0;
245
+ const windowSeconds = typeof rule.window === "number" ? rule.window : 0;
246
+ if (limit <= 0 || windowSeconds <= 0) return;
247
+
248
+ const keyMode = rule.key || config.key || "ip";
249
+ const identity = buildIdentity(ctx, keyMode);
250
+ const counterKey = `rate_limit:${ctx.route}:${identity}`;
251
+
252
+ let count = 0;
253
+ if (befly.redis) {
254
+ count = await befly.redis.incrWithExpire(counterKey, windowSeconds);
255
+ } else {
256
+ count = hitMemoryBucket(counterKey, windowSeconds);
257
+ }
258
+
259
+ if (count > limit) {
260
+ ctx.response = ErrorResponse(
261
+ ctx,
262
+ "请求过于频繁,请稍后再试",
263
+ 1,
264
+ null,
265
+ {
266
+ limit: limit,
267
+ window: windowSeconds
268
+ },
269
+ "rateLimit"
270
+ );
271
+ return;
272
+ }
273
+ }
274
+ };
275
+
276
+ export default hook;
@@ -1,16 +1,16 @@
1
- // 相对导入
2
- import { Validator } from '../lib/validator.js';
3
- import { ErrorResponse } from '../util.js';
4
-
5
1
  // 类型导入
6
- import type { Hook } from '../types/hook.js';
2
+ import type { Hook } from "../types/hook.js";
3
+
4
+ // 相对导入
5
+ import { Validator } from "../lib/validator.js";
6
+ import { ErrorResponse } from "../utils/response.js";
7
7
 
8
8
  /**
9
9
  * 参数验证钩子
10
10
  * 根据 API 定义的 fields 和 required 验证请求参数
11
11
  */
12
12
  const hook: Hook = {
13
- order: 6,
13
+ order: 8,
14
14
  handler: async (befly, ctx) => {
15
15
  if (!ctx.api) return;
16
16
 
@@ -22,7 +22,7 @@ const hook: Hook = {
22
22
  // 应用字段默认值
23
23
  for (const [field, fieldDef] of Object.entries(ctx.api.fields)) {
24
24
  // 字段未传值且定义了默认值时,应用默认值
25
- if (ctx.body[field] === undefined && (fieldDef as any)?.default !== undefined) {
25
+ if (ctx.body[field] === undefined && (fieldDef as any)?.default !== undefined && (fieldDef as any)?.default !== null) {
26
26
  ctx.body[field] = (fieldDef as any).default;
27
27
  }
28
28
  }
@@ -31,7 +31,7 @@ const hook: Hook = {
31
31
  const result = Validator.validate(ctx.body, ctx.api.fields, ctx.api.required || []);
32
32
 
33
33
  if (result.code !== 0) {
34
- ctx.response = ErrorResponse(ctx, result.firstError || '参数验证失败', 1, null, result.fieldErrors);
34
+ ctx.response = ErrorResponse(ctx, result.firstError || "参数验证失败", 1, null, result.fieldErrors, "validator");
35
35
  return;
36
36
  }
37
37
  }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * AsyncLocalStorage 请求级上下文
3
+ *
4
+ * 目标:
5
+ * - 给 Logger/慢查询日志提供 requestId/route/userId 等元信息
6
+ * - 避免在深层工具函数中层层传参
7
+ */
8
+
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+
11
+ export type RequestMeta = {
12
+ requestId: string;
13
+ method: string;
14
+ route: string;
15
+ ip: string;
16
+ userId?: string | number | null;
17
+ roleCode?: string | null;
18
+ nickname?: string | null;
19
+ roleType?: string | null;
20
+ now: number;
21
+ };
22
+
23
+ const storage = new AsyncLocalStorage<RequestMeta>();
24
+
25
+ export function withCtx<T>(meta: RequestMeta, fn: () => T): T {
26
+ return storage.run(meta, fn);
27
+ }
28
+
29
+ export function getCtx(): RequestMeta | null {
30
+ const store = storage.getStore();
31
+ if (!store) return null;
32
+ return store;
33
+ }
34
+
35
+ export function setCtxUser(userId: string | number | null | undefined, roleCode?: string | null | undefined, nickname?: string | null | undefined, roleType?: string | null | undefined): void {
36
+ const store = storage.getStore();
37
+ if (!store) return;
38
+
39
+ store.userId = userId;
40
+ store.roleCode = roleCode;
41
+ store.nickname = nickname;
42
+ store.roleType = roleType;
43
+ }