befly 3.8.29 → 3.8.31

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 (70) hide show
  1. package/README.md +91 -6
  2. package/checks/checkApi.ts +2 -1
  3. package/checks/checkApp.ts +31 -1
  4. package/checks/checkTable.ts +3 -2
  5. package/hooks/cors.ts +3 -3
  6. package/hooks/parser.ts +8 -6
  7. package/hooks/permission.ts +12 -5
  8. package/hooks/validator.ts +1 -1
  9. package/lib/cacheHelper.ts +73 -65
  10. package/lib/cipher.ts +2 -1
  11. package/lib/connect.ts +23 -52
  12. package/lib/dbHelper.ts +14 -11
  13. package/lib/jwt.ts +58 -437
  14. package/lib/logger.ts +76 -197
  15. package/lib/redisHelper.ts +163 -1
  16. package/lib/sqlBuilder.ts +2 -1
  17. package/lib/validator.ts +150 -384
  18. package/loader/loadApis.ts +4 -7
  19. package/loader/loadHooks.ts +6 -5
  20. package/loader/loadPlugins.ts +11 -13
  21. package/main.ts +26 -53
  22. package/package.json +10 -8
  23. package/paths.ts +0 -6
  24. package/plugins/cipher.ts +1 -1
  25. package/plugins/config.ts +3 -4
  26. package/plugins/db.ts +6 -7
  27. package/plugins/jwt.ts +7 -6
  28. package/plugins/logger.ts +6 -6
  29. package/plugins/redis.ts +9 -13
  30. package/router/api.ts +2 -2
  31. package/router/static.ts +4 -8
  32. package/sync/syncAll.ts +8 -13
  33. package/sync/syncApi.ts +14 -10
  34. package/sync/syncDb/apply.ts +1 -2
  35. package/sync/syncDb.ts +12 -15
  36. package/sync/syncDev.ts +19 -56
  37. package/sync/syncMenu.ts +182 -137
  38. package/tests/cacheHelper.test.ts +327 -0
  39. package/tests/dbHelper-columns.test.ts +5 -20
  40. package/tests/dbHelper-execute.test.ts +14 -68
  41. package/tests/fields-redis-cache.test.ts +5 -3
  42. package/tests/integration.test.ts +17 -32
  43. package/tests/jwt.test.ts +36 -94
  44. package/tests/logger.test.ts +32 -34
  45. package/tests/redisHelper.test.ts +271 -2
  46. package/tests/redisKeys.test.ts +76 -0
  47. package/tests/sync-connection.test.ts +0 -6
  48. package/tests/syncDb-constants.test.ts +12 -12
  49. package/tests/util.test.ts +5 -1
  50. package/tests/validator.test.ts +611 -85
  51. package/types/befly.d.ts +9 -15
  52. package/types/cache.d.ts +73 -0
  53. package/types/common.d.ts +10 -128
  54. package/types/database.d.ts +221 -5
  55. package/types/index.ts +6 -5
  56. package/types/plugin.d.ts +1 -4
  57. package/types/redis.d.ts +37 -2
  58. package/types/table.d.ts +175 -0
  59. package/config.ts +0 -70
  60. package/hooks/_rateLimit.ts +0 -64
  61. package/lib/regexAliases.ts +0 -59
  62. package/lib/xml.ts +0 -383
  63. package/tests/validator-advanced.test.ts +0 -653
  64. package/tests/xml.test.ts +0 -101
  65. package/types/addon.d.ts +0 -50
  66. package/types/crypto.d.ts +0 -23
  67. package/types/jwt.d.ts +0 -99
  68. package/types/logger.d.ts +0 -43
  69. package/types/tool.d.ts +0 -67
  70. package/types/validator.d.ts +0 -43
package/lib/logger.ts CHANGED
@@ -1,216 +1,95 @@
1
1
  /**
2
- * 日志系统 - Befly 项目专用
3
- * 直接集成环境变量,提供开箱即用的日志功能
2
+ * 日志系统 - 基于 pino 实现
4
3
  */
5
4
 
5
+ import pino from 'pino';
6
6
  import { join } from 'pathe';
7
- import { appendFile, stat } from 'node:fs/promises';
8
- import type { LogLevel } from '../types/common.js';
9
- import type { LoggerConfig } from '../types/befly.js';
7
+
8
+ import type { LoggerConfig } from 'befly-shared/types';
9
+
10
+ let instance: pino.Logger | null = null;
11
+ let mockInstance: pino.Logger | null = null;
12
+ let config: LoggerConfig = {
13
+ debug: 0,
14
+ dir: './logs',
15
+ console: 1,
16
+ maxSize: 10
17
+ };
10
18
 
11
19
  /**
12
- * 日志消息类型
20
+ * 配置日志
13
21
  */
14
- type LogMessage = string | number | boolean | null | undefined | Record<string, any> | any[];
22
+ export function configure(cfg: LoggerConfig): void {
23
+ config = { ...config, ...cfg };
24
+ instance = null;
25
+ }
15
26
 
16
27
  /**
17
- * 格式化日期时间
28
+ * 设置 Mock Logger(用于测试)
29
+ * @param mock - Mock pino 实例,传 null 清除 mock
18
30
  */
19
- function formatDate(): string {
20
- const now = new Date();
21
- const year = now.getFullYear();
22
- const month = String(now.getMonth() + 1).padStart(2, '0');
23
- const day = String(now.getDate()).padStart(2, '0');
24
- const hours = String(now.getHours()).padStart(2, '0');
25
- const minutes = String(now.getMinutes()).padStart(2, '0');
26
- const seconds = String(now.getSeconds()).padStart(2, '0');
27
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
31
+ export function setMockLogger(mock: pino.Logger | null): void {
32
+ mockInstance = mock;
28
33
  }
29
34
 
30
35
  /**
31
- * 日志器类
36
+ * 获取 pino 日志实例
32
37
  */
33
- export class Logger {
34
- /** 当前使用的日志文件缓存 */
35
- private static currentFiles: Map<string, string> = new Map();
36
-
37
- /** 日志配置 */
38
- private static config: LoggerConfig = {
39
- debug: 1,
40
- excludeFields: 'password,token,secret',
41
- dir: './logs',
42
- console: 1,
43
- maxSize: 10 * 1024 * 1024
44
- };
45
-
46
- /**
47
- * 配置日志器
48
- * @param config - 日志配置
49
- */
50
- static configure(config: LoggerConfig) {
51
- this.config = { ...this.config, ...config };
52
- }
53
-
54
- /**
55
- * 记录日志
56
- * @param level - 日志级别
57
- * @param message - 日志消息
58
- */
59
- static async log(level: LogLevel, message: LogMessage): Promise<void> {
60
- // debug 日志特殊处理:仅当 LOG_DEBUG=1 时才记录
61
- if (level === 'debug' && this.config.debug !== 1) return;
62
-
63
- // 格式化消息
64
- const timestamp = formatDate();
65
-
66
- // 处理消息内容
67
- let content = '';
68
- if (typeof message === 'object' && message !== null && Object.keys(message).length > 0) {
69
- content = JSON.stringify(message, null, 0).replace(/\\"/g, '"');
70
- } else {
71
- content = String(message);
38
+ export function getLogger(): pino.Logger {
39
+ // 优先返回 mock 实例(用于测试)
40
+ if (mockInstance) return mockInstance;
41
+
42
+ if (instance) return instance;
43
+
44
+ const level = config.debug === 1 ? 'debug' : 'info';
45
+ const targets: pino.TransportTargetOptions[] = [];
46
+
47
+ // 文件输出
48
+ targets.push({
49
+ target: 'pino-roll',
50
+ level: level,
51
+ options: {
52
+ file: join(config.dir || './logs', 'app'),
53
+ frequency: 'daily',
54
+ size: `${config.maxSize || 10}m`,
55
+ mkdir: true,
56
+ dateFormat: 'yyyy-MM-dd'
72
57
  }
73
-
74
- // 格式化日志消息
75
- const levelStr = level.toUpperCase().padStart(5);
76
- const logMessage = `[${timestamp}] ${levelStr} - ${content}`;
77
-
78
- // 控制台输出
79
- if (this.config.console === 1) {
80
- console.log(logMessage);
81
- }
82
-
83
- // 文件输出
84
- await this.writeToFile(logMessage, level);
58
+ });
59
+
60
+ // 控制台输出
61
+ if (config.console === 1) {
62
+ targets.push({
63
+ target: 'pino/file',
64
+ level: level,
65
+ options: { destination: 1 }
66
+ });
85
67
  }
86
68
 
87
- /**
88
- * 记录成功日志(使用 info 级别)
89
- * @param message - 日志消息
90
- */
91
- static async success(message: LogMessage): Promise<void> {
92
- await this.log('info', message);
93
- }
94
-
95
- /**
96
- * 写入日志文件
97
- * @param message - 格式化后的消息
98
- * @param level - 日志级别
99
- */
100
- static async writeToFile(message: string, level: LogLevel = 'info'): Promise<void> {
101
- try {
102
- const logDir = this.config.dir || './logs';
103
- // 确定文件前缀
104
- const prefix = level === 'debug' ? 'debug' : new Date().toISOString().split('T')[0];
105
-
106
- // 检查缓存的当前文件是否仍然可用
107
- let currentLogFile = this.currentFiles.get(prefix);
108
-
109
- if (currentLogFile) {
110
- try {
111
- const stats = await stat(currentLogFile);
112
- if (stats.size >= (this.config.maxSize || 10 * 1024 * 1024)) {
113
- this.currentFiles.delete(prefix);
114
- currentLogFile = undefined;
115
- }
116
- } catch {
117
- this.currentFiles.delete(prefix);
118
- currentLogFile = undefined;
119
- }
120
- }
121
-
122
- // 查找或创建新文件
123
- if (!currentLogFile) {
124
- const glob = new Bun.Glob(`${prefix}.*.log`);
125
- const files = await Array.fromAsync(glob.scan(this.config.dir || 'logs'));
126
-
127
- // 按索引排序并查找可用文件
128
- const getIndex = (f: string) => parseInt(f.match(/\.(\d+)\.log$/)?.[1] || '0');
129
- files.sort((a, b) => getIndex(a) - getIndex(b));
130
-
131
- let foundFile = false;
132
- for (let i = files.length - 1; i >= 0; i--) {
133
- const filePath = join(this.config.dir || 'logs', files[i]);
134
- try {
135
- const stats = await stat(filePath);
136
- // 检查文件大小
137
- if (stats.size < (this.config.maxSize || 10 * 1024 * 1024)) {
138
- currentLogFile = filePath;
139
- foundFile = true;
140
- break;
141
- }
142
- } catch {
143
- continue;
144
- }
145
- }
146
-
147
- // 没有可用文件,创建新文件
148
- if (!foundFile) {
149
- const maxIndex = files.length > 0 ? Math.max(...files.map(getIndex)) : -1;
150
- currentLogFile = join(this.config.dir || 'logs', `${prefix}.${maxIndex + 1}.log`);
151
- }
69
+ instance = pino({
70
+ level: level,
71
+ transport: { targets: targets }
72
+ });
152
73
 
153
- this.currentFiles.set(prefix, currentLogFile);
154
- }
155
-
156
- await appendFile(currentLogFile, message + '\n', 'utf8');
157
- } catch (error: any) {
158
- console.error('写入日志文件失败:', error?.message || error);
159
- }
160
- }
161
-
162
- /**
163
- * 记录错误日志
164
- * @param name - 错误名称/位置
165
- * @param error - 错误对象或消息
166
- */
167
- static async error(name: string, error?: any): Promise<void> {
168
- if (!error) {
169
- return this.log('error', name);
170
- }
171
-
172
- // 构建错误消息
173
- const parts = [name];
174
- if (error?.message || error?.stack) {
175
- if (error.message) parts.push(error.message);
176
- if (error.stack) parts.push('\n' + error.stack);
177
- } else {
178
- const errorStr = typeof error === 'object' ? JSON.stringify(error) : String(error);
179
- parts.push(errorStr);
180
- }
181
-
182
- await this.log('error', parts.join(' - '));
183
- }
184
-
185
- /**
186
- * 记录警告日志
187
- * @param message - 日志消息
188
- */
189
- static async warn(message: LogMessage): Promise<void> {
190
- await this.log('warn', message);
191
- }
192
-
193
- /**
194
- * 记录信息日志
195
- * @param message - 日志消息
196
- */
197
- static async info(message: LogMessage): Promise<void> {
198
- await this.log('info', message);
199
- }
200
-
201
- /**
202
- * 记录调试日志
203
- * 受 enableDebug 配置控制,仅当 enableDebug=true 时才记录
204
- * @param message - 日志消息
205
- */
206
- static async debug(message: LogMessage): Promise<void> {
207
- await this.log('debug', message);
208
- }
209
-
210
- /**
211
- * 清除文件缓存
212
- */
213
- static clearCache(): void {
214
- this.currentFiles.clear();
215
- }
74
+ return instance;
216
75
  }
76
+
77
+ /**
78
+ * 日志实例(延迟初始化)
79
+ */
80
+ export const Logger = {
81
+ get info() {
82
+ return getLogger().info.bind(getLogger());
83
+ },
84
+ get warn() {
85
+ return getLogger().warn.bind(getLogger());
86
+ },
87
+ get error() {
88
+ return getLogger().error.bind(getLogger());
89
+ },
90
+ get debug() {
91
+ return getLogger().debug.bind(getLogger());
92
+ },
93
+ configure: configure,
94
+ setMock: setMockLogger
95
+ };
@@ -6,7 +6,7 @@
6
6
  import { SQL, RedisClient } from 'bun';
7
7
  import { Logger } from './logger.js';
8
8
  import { Connect } from './connect.js';
9
- import type { KeyValue } from '../types/common.js';
9
+ import type { KeyValue } from 'befly-shared/types';
10
10
 
11
11
  /**
12
12
  * Redis 助手类
@@ -79,6 +79,11 @@ export class RedisHelper {
79
79
  }
80
80
  }
81
81
 
82
+ // ==================== ID 生成 ====================
83
+ // 注意:ID 生成功能强依赖 Redis 原子操作(INCR/INCRBY)保证分布式唯一性
84
+ // 主要被 DbHelper.insData/insBatch 使用
85
+ // 如未来有其他 ID 生成需求,可考虑提取到独立模块
86
+
82
87
  /**
83
88
  * 生成基于时间的唯一 ID
84
89
  * 格式: 秒级时间戳(10位) + 4位自增 = 14位纯数字
@@ -209,6 +214,25 @@ export class RedisHelper {
209
214
  }
210
215
  }
211
216
 
217
+ /**
218
+ * 批量获取剩余过期时间(利用 Bun Redis 自动管道优化)
219
+ * @param keys - 键名数组
220
+ * @returns TTL 数组(-2 表示键不存在,-1 表示无过期时间)
221
+ */
222
+ async ttlBatch(keys: string[]): Promise<number[]> {
223
+ if (keys.length === 0) {
224
+ return [];
225
+ }
226
+
227
+ try {
228
+ const results = await Promise.all(keys.map((key) => this.ttl(key)));
229
+ return results;
230
+ } catch (error: any) {
231
+ Logger.error('Redis ttlBatch 错误', error);
232
+ return keys.map(() => -1);
233
+ }
234
+ }
235
+
212
236
  /**
213
237
  * 向 Set 中添加一个或多个成员
214
238
  * @param key - 键名
@@ -273,6 +297,44 @@ export class RedisHelper {
273
297
  }
274
298
  }
275
299
 
300
+ /**
301
+ * 批量向多个 Set 添加成员(利用 Bun Redis 自动管道优化)
302
+ * @param items - [{ key, members }] 数组
303
+ * @returns 成功添加的总成员数量
304
+ */
305
+ async saddBatch(items: Array<{ key: string; members: string[] }>): Promise<number> {
306
+ if (items.length === 0) {
307
+ return 0;
308
+ }
309
+
310
+ try {
311
+ const results = await Promise.all(items.map((item) => this.sadd(item.key, item.members)));
312
+ return results.reduce((sum, count) => sum + count, 0);
313
+ } catch (error: any) {
314
+ Logger.error('Redis saddBatch 错误', error);
315
+ return 0;
316
+ }
317
+ }
318
+
319
+ /**
320
+ * 批量检查成员是否在 Set 中(利用 Bun Redis 自动管道优化)
321
+ * @param items - [{ key, member }] 数组
322
+ * @returns 布尔数组(true 表示存在,false 表示不存在)
323
+ */
324
+ async sismemberBatch(items: Array<{ key: string; member: string }>): Promise<boolean[]> {
325
+ if (items.length === 0) {
326
+ return [];
327
+ }
328
+
329
+ try {
330
+ const results = await Promise.all(items.map((item) => this.sismember(item.key, item.member)));
331
+ return results.map((r) => r > 0);
332
+ } catch (error: any) {
333
+ Logger.error('Redis sismemberBatch 错误', error);
334
+ return items.map(() => false);
335
+ }
336
+ }
337
+
276
338
  /**
277
339
  * 删除键
278
340
  * @param key - 键名
@@ -288,6 +350,106 @@ export class RedisHelper {
288
350
  }
289
351
  }
290
352
 
353
+ /**
354
+ * 批量删除键(利用 Bun Redis 自动管道优化)
355
+ * @param keys - 键名数组
356
+ * @returns 成功删除的键数量
357
+ */
358
+ async delBatch(keys: string[]): Promise<number> {
359
+ if (keys.length === 0) {
360
+ return 0;
361
+ }
362
+
363
+ try {
364
+ const results = await Promise.all(
365
+ keys.map((key) => {
366
+ const pkey = `${this.prefix}${key}`;
367
+ return this.client.del(pkey);
368
+ })
369
+ );
370
+ return results.reduce((sum, count) => sum + count, 0);
371
+ } catch (error: any) {
372
+ Logger.error('Redis delBatch 错误', error);
373
+ return 0;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * 批量设置对象(利用 Bun Redis 自动管道优化)
379
+ * @param items - 键值对数组 [{ key, value, ttl? }]
380
+ * @returns 成功设置的数量
381
+ */
382
+ async setBatch<T = any>(items: Array<{ key: string; value: T; ttl?: number | null }>): Promise<number> {
383
+ if (items.length === 0) {
384
+ return 0;
385
+ }
386
+
387
+ try {
388
+ const results = await Promise.all(items.map((item) => this.setObject(item.key, item.value, item.ttl ?? null)));
389
+ return results.filter((r) => r !== null).length;
390
+ } catch (error: any) {
391
+ Logger.error('Redis setBatch 错误', error);
392
+ return 0;
393
+ }
394
+ }
395
+
396
+ /**
397
+ * 批量获取对象(利用 Bun Redis 自动管道优化)
398
+ * @param keys - 键名数组
399
+ * @returns 对象数组(不存在的键返回 null)
400
+ */
401
+ async getBatch<T = any>(keys: string[]): Promise<Array<T | null>> {
402
+ if (keys.length === 0) {
403
+ return [];
404
+ }
405
+
406
+ try {
407
+ const results = await Promise.all(keys.map((key) => this.getObject<T>(key)));
408
+ return results;
409
+ } catch (error: any) {
410
+ Logger.error('Redis getBatch 错误', error);
411
+ return keys.map(() => null);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * 批量检查键是否存在(利用 Bun Redis 自动管道优化)
417
+ * @param keys - 键名数组
418
+ * @returns 布尔数组(true 表示存在,false 表示不存在)
419
+ */
420
+ async existsBatch(keys: string[]): Promise<boolean[]> {
421
+ if (keys.length === 0) {
422
+ return [];
423
+ }
424
+
425
+ try {
426
+ const results = await Promise.all(keys.map((key) => this.exists(key)));
427
+ return results.map((r) => r > 0);
428
+ } catch (error: any) {
429
+ Logger.error('Redis existsBatch 错误', error);
430
+ return keys.map(() => false);
431
+ }
432
+ }
433
+
434
+ /**
435
+ * 批量设置过期时间(利用 Bun Redis 自动管道优化)
436
+ * @param items - 键名和过期时间数组 [{ key, seconds }]
437
+ * @returns 成功设置的数量
438
+ */
439
+ async expireBatch(items: Array<{ key: string; seconds: number }>): Promise<number> {
440
+ if (items.length === 0) {
441
+ return 0;
442
+ }
443
+
444
+ try {
445
+ const results = await Promise.all(items.map((item) => this.expire(item.key, item.seconds)));
446
+ return results.filter((r) => r > 0).length;
447
+ } catch (error: any) {
448
+ Logger.error('Redis expireBatch 错误', error);
449
+ return 0;
450
+ }
451
+ }
452
+
291
453
  /**
292
454
  * 测试 Redis 连接
293
455
  * @returns ping 响应结果
package/lib/sqlBuilder.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  * 提供链式 API 构建 SQL 查询
4
4
  */
5
5
 
6
- import type { WhereConditions, SqlValue, OrderByField } from '../types/common.js';
6
+ import type { WhereConditions, OrderByField } from '../types/common.js';
7
+ import type { SqlValue } from 'befly-shared/types';
7
8
 
8
9
  /**
9
10
  * SQL 构建器类