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,477 @@
1
+ import { describe, expect, test, afterEach } from "bun:test";
2
+
3
+ import { beflyConfig } from "../befly.config.js";
4
+ import rateLimitHook from "../hooks/rateLimit.js";
5
+
6
+ type MockBefly = {
7
+ redis?: {
8
+ incrWithExpire: (key: string, seconds: number) => Promise<number>;
9
+ };
10
+ };
11
+
12
+ type MockCtx = {
13
+ api: { name: string };
14
+ req: Request;
15
+ route: string;
16
+ ip: string;
17
+ user: Record<string, any>;
18
+ requestId: string;
19
+ corsHeaders: Record<string, string>;
20
+ response?: Response;
21
+ };
22
+
23
+ const originalRateLimitConfigJson = JSON.stringify(beflyConfig.rateLimit);
24
+
25
+ afterEach(() => {
26
+ beflyConfig.rateLimit = JSON.parse(originalRateLimitConfigJson);
27
+ });
28
+
29
+ describe("hook - rateLimit", () => {
30
+ test("命中规则:未超限不拦截,超限后拦截", async () => {
31
+ beflyConfig.rateLimit = {
32
+ enable: 1,
33
+ defaultLimit: 0,
34
+ defaultWindow: 0,
35
+ key: "ip",
36
+ rules: [
37
+ {
38
+ route: "/api/auth/*",
39
+ limit: 2,
40
+ window: 60,
41
+ key: "ip"
42
+ }
43
+ ]
44
+ };
45
+
46
+ let counter = 0;
47
+ const befly: MockBefly = {
48
+ redis: {
49
+ incrWithExpire: async () => {
50
+ counter += 1;
51
+ return counter;
52
+ }
53
+ }
54
+ };
55
+
56
+ const ctx: MockCtx = {
57
+ api: { name: "login" },
58
+ req: new Request("http://localhost/api/auth/login", { method: "POST" }),
59
+ route: "POST/api/auth/login",
60
+ ip: "1.1.1.1",
61
+ user: { id: 123 },
62
+ requestId: "rid_rate_1",
63
+ corsHeaders: {}
64
+ };
65
+
66
+ await rateLimitHook.handler(befly as any, ctx as any);
67
+ expect(ctx.response).toBeUndefined();
68
+
69
+ await rateLimitHook.handler(befly as any, ctx as any);
70
+ expect(ctx.response).toBeUndefined();
71
+
72
+ await rateLimitHook.handler(befly as any, ctx as any);
73
+ expect(ctx.response).toBeInstanceOf(Response);
74
+
75
+ const payload = await (ctx.response as Response).json();
76
+ expect(payload).toEqual({
77
+ code: 1,
78
+ msg: "请求过于频繁,请稍后再试",
79
+ data: null,
80
+ detail: {
81
+ limit: 2,
82
+ window: 60
83
+ }
84
+ });
85
+ });
86
+
87
+ test("key 维度为 user:counterKey 包含用户 id", async () => {
88
+ beflyConfig.rateLimit = {
89
+ enable: 1,
90
+ defaultLimit: 0,
91
+ defaultWindow: 0,
92
+ key: "ip",
93
+ rules: [
94
+ {
95
+ route: "*",
96
+ limit: 10,
97
+ window: 60,
98
+ key: "user"
99
+ }
100
+ ]
101
+ };
102
+
103
+ let lastKey = "";
104
+ const befly: MockBefly = {
105
+ redis: {
106
+ incrWithExpire: async (key) => {
107
+ lastKey = key;
108
+ return 1;
109
+ }
110
+ }
111
+ };
112
+
113
+ const ctx: MockCtx = {
114
+ api: { name: "x" },
115
+ req: new Request("http://localhost/api/user/profile", { method: "POST" }),
116
+ route: "POST/api/user/profile",
117
+ ip: "2.2.2.2",
118
+ user: { id: 9 },
119
+ requestId: "rid_rate_2",
120
+ corsHeaders: {}
121
+ };
122
+
123
+ await rateLimitHook.handler(befly as any, ctx as any);
124
+
125
+ expect(lastKey.includes(":user:9")).toBe(true);
126
+ expect(lastKey.startsWith("rate_limit:POST/api/user/profile:")).toBe(true);
127
+ });
128
+
129
+ test("key 维度为 ip_user:counterKey 同时包含 ip 与用户 id", async () => {
130
+ beflyConfig.rateLimit = {
131
+ enable: 1,
132
+ defaultLimit: 0,
133
+ defaultWindow: 0,
134
+ key: "ip",
135
+ rules: [
136
+ {
137
+ route: "*",
138
+ limit: 10,
139
+ window: 60,
140
+ key: "ip_user"
141
+ }
142
+ ]
143
+ };
144
+
145
+ let lastKey = "";
146
+ const befly: MockBefly = {
147
+ redis: {
148
+ incrWithExpire: async (key) => {
149
+ lastKey = key;
150
+ return 1;
151
+ }
152
+ }
153
+ };
154
+
155
+ const ctx: MockCtx = {
156
+ api: { name: "x" },
157
+ req: new Request("http://localhost/api/user/profile", { method: "POST" }),
158
+ route: "POST/api/user/profile",
159
+ ip: "2.2.2.2",
160
+ user: { id: 9 },
161
+ requestId: "rid_rate_2_ip_user",
162
+ corsHeaders: {}
163
+ };
164
+
165
+ await rateLimitHook.handler(befly as any, ctx as any);
166
+
167
+ expect(lastKey.includes("ip:2.2.2.2:user:9")).toBe(true);
168
+ expect(lastKey.startsWith("rate_limit:POST/api/user/profile:")).toBe(true);
169
+ });
170
+
171
+ test("规则优先级:精确规则优先于通配与前缀", async () => {
172
+ // 特意把更宽泛的规则放在前面,验证实现会优先选择更具体的规则
173
+ beflyConfig.rateLimit = {
174
+ enable: 1,
175
+ defaultLimit: 0,
176
+ defaultWindow: 0,
177
+ key: "ip",
178
+ rules: [
179
+ {
180
+ route: "*",
181
+ limit: 1,
182
+ window: 60,
183
+ key: "ip"
184
+ },
185
+ {
186
+ route: "/api/auth/*",
187
+ limit: 1,
188
+ window: 60,
189
+ key: "ip"
190
+ },
191
+ {
192
+ route: "POST/api/auth/login",
193
+ limit: 100,
194
+ window: 60,
195
+ key: "ip"
196
+ }
197
+ ]
198
+ };
199
+
200
+ let counter = 0;
201
+ const befly: MockBefly = {
202
+ redis: {
203
+ incrWithExpire: async () => {
204
+ counter += 1;
205
+ return counter;
206
+ }
207
+ }
208
+ };
209
+
210
+ const ctx: MockCtx = {
211
+ api: { name: "login" },
212
+ req: new Request("http://localhost/api/auth/login", { method: "POST" }),
213
+ route: "POST/api/auth/login",
214
+ ip: "8.8.8.8",
215
+ user: {},
216
+ requestId: "rid_rate_priority_1",
217
+ corsHeaders: {}
218
+ };
219
+
220
+ await rateLimitHook.handler(befly as any, ctx as any);
221
+ expect(ctx.response).toBeUndefined();
222
+
223
+ await rateLimitHook.handler(befly as any, ctx as any);
224
+ expect(ctx.response).toBeUndefined();
225
+ });
226
+
227
+ test("key=user 且缺失 userId 时回退为按 IP 计数", async () => {
228
+ beflyConfig.rateLimit = {
229
+ enable: 1,
230
+ defaultLimit: 0,
231
+ defaultWindow: 0,
232
+ key: "ip",
233
+ rules: [
234
+ {
235
+ route: "*",
236
+ limit: 10,
237
+ window: 60,
238
+ key: "user"
239
+ }
240
+ ]
241
+ };
242
+
243
+ let lastKey = "";
244
+ const befly: MockBefly = {
245
+ redis: {
246
+ incrWithExpire: async (key) => {
247
+ lastKey = key;
248
+ return 1;
249
+ }
250
+ }
251
+ };
252
+
253
+ const ctx: MockCtx = {
254
+ api: { name: "x" },
255
+ req: new Request("http://localhost/api/user/profile", { method: "POST" }),
256
+ route: "POST/api/user/profile",
257
+ ip: "6.6.6.6",
258
+ user: {},
259
+ requestId: "rid_rate_user_fallback_ip",
260
+ corsHeaders: {}
261
+ };
262
+
263
+ await rateLimitHook.handler(befly as any, ctx as any);
264
+
265
+ expect(lastKey.includes("ip:6.6.6.6")).toBe(true);
266
+ expect(lastKey.includes("anonymous")).toBe(false);
267
+ });
268
+
269
+ test("key=user:userId=0 不应被当作缺失", async () => {
270
+ beflyConfig.rateLimit = {
271
+ enable: 1,
272
+ defaultLimit: 0,
273
+ defaultWindow: 0,
274
+ key: "ip",
275
+ rules: [
276
+ {
277
+ route: "*",
278
+ limit: 10,
279
+ window: 60,
280
+ key: "user"
281
+ }
282
+ ]
283
+ };
284
+
285
+ let lastKey = "";
286
+ const befly: MockBefly = {
287
+ redis: {
288
+ incrWithExpire: async (key) => {
289
+ lastKey = key;
290
+ return 1;
291
+ }
292
+ }
293
+ };
294
+
295
+ const ctx: MockCtx = {
296
+ api: { name: "x" },
297
+ req: new Request("http://localhost/api/user/profile", { method: "POST" }),
298
+ route: "POST/api/user/profile",
299
+ ip: "7.7.7.7",
300
+ user: { id: 0 },
301
+ requestId: "rid_rate_user_id_zero",
302
+ corsHeaders: {}
303
+ };
304
+
305
+ await rateLimitHook.handler(befly as any, ctx as any);
306
+ expect(lastKey.includes(":user:0")).toBe(true);
307
+ });
308
+
309
+ test("skipRoutes:命中后不计数也不拦截", async () => {
310
+ beflyConfig.rateLimit = {
311
+ enable: 1,
312
+ defaultLimit: 1,
313
+ defaultWindow: 60,
314
+ key: "ip",
315
+ skipRoutes: ["/api/health"],
316
+ rules: []
317
+ };
318
+
319
+ let called = 0;
320
+ const befly: MockBefly = {
321
+ redis: {
322
+ incrWithExpire: async () => {
323
+ called += 1;
324
+ return 999;
325
+ }
326
+ }
327
+ };
328
+
329
+ const ctx: MockCtx = {
330
+ api: { name: "health" },
331
+ req: new Request("http://localhost/api/health", { method: "POST" }),
332
+ route: "POST/api/health",
333
+ ip: "9.9.9.9",
334
+ user: {},
335
+ requestId: "rid_rate_skip_1",
336
+ corsHeaders: {}
337
+ };
338
+
339
+ await rateLimitHook.handler(befly as any, ctx as any);
340
+ await rateLimitHook.handler(befly as any, ctx as any);
341
+
342
+ expect(called).toBe(0);
343
+ expect(ctx.response).toBeUndefined();
344
+ });
345
+
346
+ test("route 匹配:支持 METHOD/api 前缀与精确匹配", async () => {
347
+ beflyConfig.rateLimit = {
348
+ enable: 1,
349
+ defaultLimit: 0,
350
+ defaultWindow: 0,
351
+ key: "ip",
352
+ rules: [
353
+ {
354
+ route: "POST/api/auth/*",
355
+ limit: 1,
356
+ window: 60,
357
+ key: "ip"
358
+ },
359
+ {
360
+ route: "POST/api/user/profile",
361
+ limit: 1,
362
+ window: 60,
363
+ key: "ip"
364
+ }
365
+ ]
366
+ };
367
+
368
+ const keys: string[] = [];
369
+ const befly: MockBefly = {
370
+ redis: {
371
+ incrWithExpire: async (key) => {
372
+ keys.push(key);
373
+ return 1;
374
+ }
375
+ }
376
+ };
377
+
378
+ const ctx1: MockCtx = {
379
+ api: { name: "login" },
380
+ req: new Request("http://localhost/api/auth/login", { method: "POST" }),
381
+ route: "POST/api/auth/login",
382
+ ip: "4.4.4.4",
383
+ user: {},
384
+ requestId: "rid_rate_match_1",
385
+ corsHeaders: {}
386
+ };
387
+
388
+ const ctx2: MockCtx = {
389
+ api: { name: "profile" },
390
+ req: new Request("http://localhost/api/user/profile", { method: "POST" }),
391
+ route: "POST/api/user/profile",
392
+ ip: "4.4.4.4",
393
+ user: {},
394
+ requestId: "rid_rate_match_2",
395
+ corsHeaders: {}
396
+ };
397
+
398
+ await rateLimitHook.handler(befly as any, ctx1 as any);
399
+ await rateLimitHook.handler(befly as any, ctx2 as any);
400
+
401
+ expect(keys.length).toBe(2);
402
+ expect(keys[0].startsWith("rate_limit:POST/api/auth/login:")).toBe(true);
403
+ expect(keys[1].startsWith("rate_limit:POST/api/user/profile:")).toBe(true);
404
+ });
405
+
406
+ test("OPTIONS 请求不计数也不拦截", async () => {
407
+ beflyConfig.rateLimit = {
408
+ enable: 1,
409
+ defaultLimit: 1,
410
+ defaultWindow: 60,
411
+ key: "ip",
412
+ rules: []
413
+ };
414
+
415
+ let called = 0;
416
+ const befly: MockBefly = {
417
+ redis: {
418
+ incrWithExpire: async () => {
419
+ called += 1;
420
+ return 999;
421
+ }
422
+ }
423
+ };
424
+
425
+ const ctx: MockCtx = {
426
+ api: { name: "opt" },
427
+ req: new Request("http://localhost/api/user/profile", { method: "OPTIONS" }),
428
+ route: "OPTIONS/api/user/profile",
429
+ ip: "5.5.5.5",
430
+ user: {},
431
+ requestId: "rid_rate_options",
432
+ corsHeaders: {}
433
+ };
434
+
435
+ await rateLimitHook.handler(befly as any, ctx as any);
436
+ expect(called).toBe(0);
437
+ expect(ctx.response).toBeUndefined();
438
+ });
439
+
440
+ test("无 redis 时降级为内存计数", async () => {
441
+ beflyConfig.rateLimit = {
442
+ enable: 1,
443
+ defaultLimit: 0,
444
+ defaultWindow: 0,
445
+ key: "ip",
446
+ rules: [
447
+ {
448
+ route: "POST/api/test/memory",
449
+ limit: 2,
450
+ window: 60,
451
+ key: "ip"
452
+ }
453
+ ]
454
+ };
455
+
456
+ const befly: MockBefly = {};
457
+
458
+ const ctx: MockCtx = {
459
+ api: { name: "mem" },
460
+ req: new Request("http://localhost/api/test/memory", { method: "POST" }),
461
+ route: "POST/api/test/memory",
462
+ ip: "3.3.3.3",
463
+ user: {},
464
+ requestId: "rid_rate_3",
465
+ corsHeaders: {}
466
+ };
467
+
468
+ await rateLimitHook.handler(befly as any, ctx as any);
469
+ expect(ctx.response).toBeUndefined();
470
+
471
+ await rateLimitHook.handler(befly as any, ctx as any);
472
+ expect(ctx.response).toBeUndefined();
473
+
474
+ await rateLimitHook.handler(befly as any, ctx as any);
475
+ expect(ctx.response).toBeInstanceOf(Response);
476
+ });
477
+ });