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.
- package/config.ts +8 -9
- package/hooks/{rateLimit.ts → _rateLimit.ts} +7 -13
- package/hooks/auth.ts +3 -11
- package/hooks/cors.ts +1 -4
- package/hooks/parser.ts +6 -8
- package/hooks/permission.ts +9 -12
- package/hooks/validator.ts +6 -9
- package/lib/cacheHelper.ts +0 -4
- package/lib/{database.ts → connect.ts} +65 -18
- package/lib/logger.ts +1 -17
- package/lib/redisHelper.ts +6 -5
- package/loader/loadApis.ts +3 -3
- package/loader/loadHooks.ts +15 -41
- package/loader/loadPlugins.ts +10 -16
- package/main.ts +25 -28
- package/package.json +4 -4
- package/plugins/cache.ts +2 -2
- package/plugins/cipher.ts +15 -0
- package/plugins/config.ts +16 -0
- package/plugins/db.ts +7 -17
- package/plugins/jwt.ts +15 -0
- package/plugins/logger.ts +1 -1
- package/plugins/redis.ts +4 -4
- package/plugins/tool.ts +50 -0
- package/router/api.ts +56 -42
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +2 -20
- package/sync/syncApi.ts +7 -7
- package/sync/syncDb/apply.ts +10 -12
- package/sync/syncDb/constants.ts +64 -12
- package/sync/syncDb/ddl.ts +9 -8
- package/sync/syncDb/helpers.ts +7 -119
- package/sync/syncDb/schema.ts +16 -19
- package/sync/syncDb/sqlite.ts +1 -3
- package/sync/syncDb/table.ts +13 -146
- package/sync/syncDb/tableCreate.ts +28 -12
- package/sync/syncDb/types.ts +126 -0
- package/sync/syncDb/version.ts +4 -7
- package/sync/syncDb.ts +151 -6
- package/sync/syncDev.ts +19 -15
- package/sync/syncMenu.ts +87 -75
- package/tests/redisHelper.test.ts +15 -16
- package/tests/sync-connection.test.ts +189 -0
- package/tests/syncDb-apply.test.ts +288 -0
- package/tests/syncDb-constants.test.ts +151 -0
- package/tests/syncDb-ddl.test.ts +206 -0
- package/tests/syncDb-helpers.test.ts +113 -0
- package/tests/syncDb-schema.test.ts +178 -0
- package/tests/syncDb-types.test.ts +130 -0
- package/tsconfig.json +2 -2
- package/types/api.d.ts +1 -1
- package/types/befly.d.ts +23 -21
- package/types/common.d.ts +0 -29
- package/types/context.d.ts +8 -6
- package/types/hook.d.ts +3 -4
- package/types/plugin.d.ts +3 -0
- package/hooks/errorHandler.ts +0 -23
- package/hooks/requestId.ts +0 -24
- package/hooks/requestLogger.ts +0 -25
- package/hooks/responseFormatter.ts +0 -64
- package/router/root.ts +0 -56
- package/sync/syncDb/index.ts +0 -164
package/sync/syncDev.ts
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* - 表名: addon_admin_admin
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
10
|
+
import { scanAddons, getAddonDir, normalizeModuleForSync } from 'befly-util';
|
|
11
|
+
|
|
12
12
|
import { Logger } from '../lib/logger.js';
|
|
13
|
+
import { Cipher } from '../lib/cipher.js';
|
|
14
|
+
import { Connect } from '../lib/connect.js';
|
|
15
|
+
import { DbHelper } from '../lib/dbHelper.js';
|
|
13
16
|
import type { SyncDevOptions, SyncDevStats, BeflyOptions } from '../types/index.js';
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -22,33 +25,34 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
22
25
|
return;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!devPassword) {
|
|
28
|
+
if (!config.devPassword) {
|
|
29
|
+
// 未配置开发者密码,跳过同步
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// 连接数据库(SQL + Redis)
|
|
33
|
-
await
|
|
34
|
+
await Connect.connect(config);
|
|
34
35
|
|
|
35
|
-
const helper =
|
|
36
|
+
const helper = Connect.getDbHelper();
|
|
36
37
|
|
|
37
38
|
// 检查 addon_admin_admin 表是否存在
|
|
38
39
|
const existAdmin = await helper.tableExists('addon_admin_admin');
|
|
39
40
|
if (!existAdmin) {
|
|
41
|
+
Logger.debug('[SyncDev] 表 addon_admin_admin 不存在,跳过开发者账号同步');
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
// 检查 addon_admin_role 表是否存在
|
|
44
46
|
const existRole = await helper.tableExists('addon_admin_role');
|
|
45
47
|
if (!existRole) {
|
|
48
|
+
Logger.debug('[SyncDev] 表 addon_admin_role 不存在,跳过开发者账号同步');
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
// 检查 addon_admin_menu 表是否存在
|
|
50
53
|
const existMenu = await helper.tableExists('addon_admin_menu');
|
|
51
54
|
if (!existMenu) {
|
|
55
|
+
Logger.debug('[SyncDev] 表 addon_admin_menu 不存在,跳过开发者账号同步');
|
|
52
56
|
return;
|
|
53
57
|
}
|
|
54
58
|
|
|
@@ -59,6 +63,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
if (!allMenus || !Array.isArray(allMenus)) {
|
|
66
|
+
Logger.debug('[SyncDev] 菜单数据为空,跳过开发者账号同步');
|
|
62
67
|
return;
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -113,13 +118,12 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// 使用 bcrypt 加密密码
|
|
116
|
-
const hashed = await Cipher.hashPassword(devPassword);
|
|
121
|
+
const hashed = await Cipher.hashPassword(config.devPassword);
|
|
117
122
|
|
|
118
123
|
// 准备开发管理员数据
|
|
119
124
|
const devData = {
|
|
120
|
-
name: '开发者',
|
|
121
125
|
nickname: '开发者',
|
|
122
|
-
email: devEmail,
|
|
126
|
+
email: config.devEmail,
|
|
123
127
|
username: 'dev',
|
|
124
128
|
password: hashed,
|
|
125
129
|
roleId: devRole.id,
|
|
@@ -130,14 +134,14 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
130
134
|
// 查询现有账号
|
|
131
135
|
const existing = await helper.getOne({
|
|
132
136
|
table: 'addon_admin_admin',
|
|
133
|
-
where: { email: devEmail }
|
|
137
|
+
where: { email: config.devEmail }
|
|
134
138
|
});
|
|
135
139
|
|
|
136
140
|
if (existing) {
|
|
137
141
|
// 更新现有账号
|
|
138
142
|
await helper.updData({
|
|
139
143
|
table: 'addon_admin_admin',
|
|
140
|
-
where: { email: devEmail },
|
|
144
|
+
where: { email: config.devEmail },
|
|
141
145
|
data: devData
|
|
142
146
|
});
|
|
143
147
|
} else {
|
|
@@ -167,7 +171,7 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
167
171
|
fields: ['id', 'name', 'path', 'method', 'description', 'addonName']
|
|
168
172
|
});
|
|
169
173
|
|
|
170
|
-
const redis =
|
|
174
|
+
const redis = Connect.getRedis();
|
|
171
175
|
|
|
172
176
|
// 为每个角色缓存接口权限
|
|
173
177
|
for (const role of roles) {
|
|
@@ -201,6 +205,6 @@ export async function syncDevCommand(config: BeflyOptions, options: SyncDevOptio
|
|
|
201
205
|
Logger.error('同步开发者管理员失败', error);
|
|
202
206
|
throw error;
|
|
203
207
|
} finally {
|
|
204
|
-
await
|
|
208
|
+
await Connect.disconnect();
|
|
205
209
|
}
|
|
206
210
|
}
|
package/sync/syncMenu.ts
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SyncMenu 命令 - 同步菜单数据到数据库
|
|
3
|
-
*
|
|
3
|
+
* 说明:根据配置文件增量同步菜单数据(最多3级:父级、子级、孙级)
|
|
4
4
|
*
|
|
5
5
|
* 流程:
|
|
6
|
-
* 1.
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
12
|
-
* 7.
|
|
6
|
+
* 1. 扫描所有 addon 的 addon.config.js/ts 配置文件
|
|
7
|
+
* 2. 扫描项目根目录的 app.config.js/ts 配置文件
|
|
8
|
+
* 3. 项目的 app.config 优先级最高,可以覆盖 addon 的菜单配置
|
|
9
|
+
* 4. 文件不存在或格式错误时默认为空数组
|
|
10
|
+
* 5. 根据菜单的 path 字段检查是否存在
|
|
11
|
+
* 6. 存在则更新其他字段(name、sort、type、pid)
|
|
12
|
+
* 7. 不存在则新增菜单记录
|
|
13
|
+
* 8. 强制删除配置中不存在的菜单记录
|
|
13
14
|
* 注:state 字段由框架自动管理(1=正常,2=禁用,0=删除)
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { join } from 'pathe';
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { cloneDeep } from 'es-toolkit';
|
|
19
|
+
import { Connect } from '../lib/connect.js';
|
|
19
20
|
import { RedisHelper } from '../lib/redisHelper.js';
|
|
20
|
-
import { scanAddons, getAddonDir } from 'befly-util';
|
|
21
|
+
import { scanAddons, getAddonDir, scanConfig } from 'befly-util';
|
|
21
22
|
import { Logger } from '../lib/logger.js';
|
|
22
23
|
import { projectDir } from '../paths.js';
|
|
23
24
|
|
|
24
25
|
import type { SyncMenuOptions, MenuConfig, BeflyOptions } from '../types/index.js';
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
*
|
|
28
|
+
* 递归转换菜单路径
|
|
29
|
+
* @param menu 菜单对象(会被修改)
|
|
30
|
+
* @param transform 路径转换函数
|
|
29
31
|
*/
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const content = await import(filePath, { with: { type: 'json' } });
|
|
37
|
-
|
|
38
|
-
// 验证是否为数组
|
|
39
|
-
if (!Array.isArray(content.default)) {
|
|
40
|
-
return [];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return content.default;
|
|
44
|
-
} catch (error: any) {
|
|
45
|
-
Logger.warn(`读取菜单配置失败 ${filePath}: ${error.message}`);
|
|
46
|
-
return [];
|
|
32
|
+
function transformMenuPaths(menu: MenuConfig, transform: (path: string) => string): void {
|
|
33
|
+
if (menu.path && menu.path.startsWith('/')) {
|
|
34
|
+
menu.path = transform(menu.path);
|
|
47
35
|
}
|
|
36
|
+
menu.children?.forEach((child) => transformMenuPaths(child, transform));
|
|
48
37
|
}
|
|
49
38
|
|
|
50
39
|
/**
|
|
@@ -57,25 +46,15 @@ async function readMenuConfig(filePath: string): Promise<MenuConfig[]> {
|
|
|
57
46
|
*/
|
|
58
47
|
function addAddonPrefix(menus: MenuConfig[], addonName: string): MenuConfig[] {
|
|
59
48
|
return menus.map((menu) => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (newMenu.path && newMenu.path.startsWith('/')) {
|
|
64
|
-
newMenu.path = `/addon/${addonName}${newMenu.path}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// 递归处理子菜单
|
|
68
|
-
if (newMenu.children && newMenu.children.length > 0) {
|
|
69
|
-
newMenu.children = addAddonPrefix(newMenu.children, addonName);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return newMenu;
|
|
49
|
+
const cloned = cloneDeep(menu);
|
|
50
|
+
transformMenuPaths(cloned, (path) => `/addon/${addonName}${path}`);
|
|
51
|
+
return cloned;
|
|
73
52
|
});
|
|
74
53
|
}
|
|
75
54
|
|
|
76
55
|
/**
|
|
77
56
|
* 合并菜单配置
|
|
78
|
-
* 优先级:项目
|
|
57
|
+
* 优先级:项目 package.json > addon package.json
|
|
79
58
|
* 支持三级菜单结构:父级、子级、孙级
|
|
80
59
|
*/
|
|
81
60
|
function mergeMenuConfigs(allMenus: Array<{ menus: MenuConfig[]; addonName: string }>): MenuConfig[] {
|
|
@@ -251,6 +230,58 @@ async function deleteObsoleteRecords(helper: any, configPaths: Set<string>): Pro
|
|
|
251
230
|
}
|
|
252
231
|
}
|
|
253
232
|
|
|
233
|
+
/**
|
|
234
|
+
* 加载所有菜单配置(addon + 项目)
|
|
235
|
+
* @returns 菜单配置数组
|
|
236
|
+
*/
|
|
237
|
+
async function loadMenuConfigs(): Promise<Array<{ menus: MenuConfig[]; addonName: string }>> {
|
|
238
|
+
const allMenus: Array<{ menus: MenuConfig[]; addonName: string }> = [];
|
|
239
|
+
|
|
240
|
+
// 1. 加载所有 addon 配置
|
|
241
|
+
const addonNames = scanAddons();
|
|
242
|
+
|
|
243
|
+
for (const addonName of addonNames) {
|
|
244
|
+
try {
|
|
245
|
+
const addonDir = getAddonDir(addonName, '');
|
|
246
|
+
const addonConfigData = await scanConfig({
|
|
247
|
+
dirs: [addonDir],
|
|
248
|
+
files: ['addon.config'],
|
|
249
|
+
mode: 'first',
|
|
250
|
+
paths: ['menus']
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const addonMenus = addonConfigData?.menus || [];
|
|
254
|
+
if (Array.isArray(addonMenus) && addonMenus.length > 0) {
|
|
255
|
+
// 为 addon 菜单添加路径前缀
|
|
256
|
+
const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
|
|
257
|
+
allMenus.push({ menus: menusWithPrefix, addonName: addonName });
|
|
258
|
+
}
|
|
259
|
+
} catch (error: any) {
|
|
260
|
+
Logger.warn(`读取 addon 配置失败 ${addonName}: ${error.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 2. 加载项目配置
|
|
265
|
+
try {
|
|
266
|
+
const appConfigData = await scanConfig({
|
|
267
|
+
dirs: [projectDir],
|
|
268
|
+
files: ['app.config'],
|
|
269
|
+
mode: 'first',
|
|
270
|
+
paths: ['menus']
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const appMenus = appConfigData?.menus || [];
|
|
274
|
+
if (Array.isArray(appMenus) && appMenus.length > 0) {
|
|
275
|
+
// 项目菜单不添加前缀
|
|
276
|
+
allMenus.push({ menus: appMenus, addonName: 'app' });
|
|
277
|
+
}
|
|
278
|
+
} catch (error: any) {
|
|
279
|
+
Logger.warn(`读取项目配置失败: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return allMenus;
|
|
283
|
+
}
|
|
284
|
+
|
|
254
285
|
/**
|
|
255
286
|
* SyncMenu 命令主函数
|
|
256
287
|
*/
|
|
@@ -261,37 +292,18 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
|
|
|
261
292
|
return;
|
|
262
293
|
}
|
|
263
294
|
|
|
264
|
-
// 1.
|
|
265
|
-
const allMenus
|
|
266
|
-
|
|
267
|
-
const addonNames = scanAddons();
|
|
268
|
-
|
|
269
|
-
for (const addonName of addonNames) {
|
|
270
|
-
const addonMenuPath = getAddonDir(addonName, 'menu.json');
|
|
271
|
-
if (existsSync(addonMenuPath)) {
|
|
272
|
-
const addonMenus = await readMenuConfig(addonMenuPath);
|
|
273
|
-
if (addonMenus.length > 0) {
|
|
274
|
-
// 为 addon 菜单添加路径前缀
|
|
275
|
-
const menusWithPrefix = addAddonPrefix(addonMenus, addonName);
|
|
276
|
-
allMenus.push({ menus: menusWithPrefix, addonName: addonName });
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
295
|
+
// 1. 加载所有菜单配置(addon + 项目)
|
|
296
|
+
const allMenus = await loadMenuConfigs();
|
|
280
297
|
|
|
281
|
-
// 2.
|
|
282
|
-
const projectMenuPath = join(projectDir, 'menu.json');
|
|
283
|
-
const projectMenus = await readMenuConfig(projectMenuPath);
|
|
284
|
-
if (projectMenus.length > 0) {
|
|
285
|
-
allMenus.push({ menus: projectMenus, addonName: 'project' });
|
|
286
|
-
} // 3. 合并菜单配置(项目配置优先)
|
|
298
|
+
// 2. 合并菜单配置
|
|
287
299
|
const mergedMenus = mergeMenuConfigs(allMenus);
|
|
288
300
|
|
|
289
301
|
// 连接数据库(SQL + Redis)
|
|
290
|
-
await
|
|
302
|
+
await Connect.connect(config);
|
|
291
303
|
|
|
292
|
-
const helper =
|
|
304
|
+
const helper = Connect.getDbHelper();
|
|
293
305
|
|
|
294
|
-
//
|
|
306
|
+
// 3. 检查表是否存在(addon_admin_menu 来自 addon-admin 组件)
|
|
295
307
|
const exists = await helper.tableExists('addon_admin_menu');
|
|
296
308
|
|
|
297
309
|
if (!exists) {
|
|
@@ -299,23 +311,23 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
|
|
|
299
311
|
return;
|
|
300
312
|
}
|
|
301
313
|
|
|
302
|
-
//
|
|
314
|
+
// 4. 收集配置文件中所有菜单的 path
|
|
303
315
|
const configPaths = collectPaths(mergedMenus);
|
|
304
316
|
|
|
305
|
-
//
|
|
317
|
+
// 5. 同步菜单
|
|
306
318
|
await syncMenus(helper, mergedMenus);
|
|
307
319
|
|
|
308
|
-
//
|
|
320
|
+
// 6. 删除文件中不存在的菜单(强制删除)
|
|
309
321
|
await deleteObsoleteRecords(helper, configPaths);
|
|
310
322
|
|
|
311
|
-
//
|
|
323
|
+
// 7. 获取最终菜单数据(用于缓存)
|
|
312
324
|
const allMenusData = await helper.getAll({
|
|
313
325
|
table: 'addon_admin_menu',
|
|
314
326
|
fields: ['id', 'pid', 'name', 'path', 'sort'],
|
|
315
327
|
orderBy: ['sort#ASC', 'id#ASC']
|
|
316
328
|
});
|
|
317
329
|
|
|
318
|
-
//
|
|
330
|
+
// 8. 缓存菜单数据到 Redis
|
|
319
331
|
try {
|
|
320
332
|
const redisHelper = new RedisHelper();
|
|
321
333
|
await redisHelper.setObject('menus:all', allMenusData);
|
|
@@ -326,6 +338,6 @@ export async function syncMenuCommand(config: BeflyOptions, options: SyncMenuOpt
|
|
|
326
338
|
Logger.error('菜单同步失败', error);
|
|
327
339
|
throw error;
|
|
328
340
|
} finally {
|
|
329
|
-
await
|
|
341
|
+
await Connect.disconnect();
|
|
330
342
|
}
|
|
331
343
|
}
|
|
@@ -3,29 +3,24 @@
|
|
|
3
3
|
* 测试 Redis 操作功能
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe,
|
|
7
|
-
import {
|
|
8
|
-
|
|
6
|
+
import { describe, expect, it, test, beforeAll, afterAll } from 'bun:test';
|
|
7
|
+
import { RedisClient } from 'bun';
|
|
8
|
+
|
|
9
9
|
import { defaultOptions } from '../config.js';
|
|
10
|
+
import { Connect } from '../lib/connect.js';
|
|
11
|
+
import { RedisHelper } from '../lib/redisHelper.js';
|
|
10
12
|
|
|
11
13
|
let redis: RedisHelper;
|
|
12
14
|
|
|
13
15
|
beforeAll(async () => {
|
|
14
|
-
//
|
|
15
|
-
await
|
|
16
|
-
|
|
17
|
-
redis = new RedisHelper(defaultOptions.redis.prefix);
|
|
16
|
+
// 连接 Redis
|
|
17
|
+
await Connect.connectRedis(defaultOptions.redis);
|
|
18
|
+
redis = new RedisHelper();
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
afterAll(async () => {
|
|
21
|
-
//
|
|
22
|
-
await
|
|
23
|
-
await redis.del('test:object');
|
|
24
|
-
await redis.del('test:set');
|
|
25
|
-
await redis.del('test:ttl');
|
|
26
|
-
await redis.del('test:expire');
|
|
27
|
-
|
|
28
|
-
await Database.disconnectRedis();
|
|
22
|
+
// 断开 Redis 连接
|
|
23
|
+
await Connect.disconnectRedis();
|
|
29
24
|
});
|
|
30
25
|
|
|
31
26
|
describe('RedisHelper - 字符串操作', () => {
|
|
@@ -102,11 +97,15 @@ describe('RedisHelper - 对象操作', () => {
|
|
|
102
97
|
|
|
103
98
|
describe('RedisHelper - Set 操作', () => {
|
|
104
99
|
test('sadd - 添加成员到 Set', async () => {
|
|
105
|
-
|
|
100
|
+
// 先清除,确保测试隔离
|
|
101
|
+
await redis.del('test:set:sadd');
|
|
102
|
+
const count = await redis.sadd('test:set:sadd', ['member1', 'member2', 'member3']);
|
|
106
103
|
expect(count).toBeGreaterThan(0);
|
|
104
|
+
await redis.del('test:set:sadd');
|
|
107
105
|
});
|
|
108
106
|
|
|
109
107
|
test('sismember - 检查成员是否存在', async () => {
|
|
108
|
+
await redis.del('test:set');
|
|
110
109
|
await redis.sadd('test:set', ['member1']);
|
|
111
110
|
|
|
112
111
|
const exists = await redis.sismember('test:set', 'member1');
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sync 模块连接管理集成测试
|
|
3
|
+
* 验证数据库连接的正确关闭
|
|
4
|
+
*/
|
|
5
|
+
import { describe, test, expect, afterEach } from 'bun:test';
|
|
6
|
+
|
|
7
|
+
import { Connect } from '../lib/connect.js';
|
|
8
|
+
|
|
9
|
+
describe('sync 模块连接管理', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
// 每个测试后重置连接状态
|
|
12
|
+
Connect.__reset();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('Connect.isConnected', () => {
|
|
16
|
+
test('初始状态应该都是未连接', () => {
|
|
17
|
+
const status = Connect.isConnected();
|
|
18
|
+
expect(status.sql).toBe(false);
|
|
19
|
+
expect(status.redis).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Connect.disconnect', () => {
|
|
24
|
+
test('disconnect 应该能安全地关闭未连接的状态', async () => {
|
|
25
|
+
// 即使没有连接,disconnect 也不应该抛出错误
|
|
26
|
+
await expect(Connect.disconnect()).resolves.toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('disconnectSql 应该能安全地关闭未连接的状态', async () => {
|
|
30
|
+
await expect(Connect.disconnectSql()).resolves.toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('disconnectRedis 应该能安全地关闭未连接的状态', async () => {
|
|
34
|
+
await expect(Connect.disconnectRedis()).resolves.toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('Connect.getSql', () => {
|
|
39
|
+
test('未连接时应该抛出错误', () => {
|
|
40
|
+
expect(() => Connect.getSql()).toThrow('SQL 客户端未连接');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('Connect.getRedis', () => {
|
|
45
|
+
test('未连接时应该抛出错误', () => {
|
|
46
|
+
expect(() => Connect.getRedis()).toThrow('Redis 客户端未连接');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Connect.getDbHelper', () => {
|
|
51
|
+
test('未连接时应该抛出错误', () => {
|
|
52
|
+
expect(() => Connect.getDbHelper()).toThrow('SQL 客户端未连接');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('Mock 连接测试', () => {
|
|
57
|
+
test('__setMockSql 应该设置 mock SQL 客户端', () => {
|
|
58
|
+
const mockSql = { close: async () => {} } as any;
|
|
59
|
+
Connect.__setMockSql(mockSql);
|
|
60
|
+
|
|
61
|
+
const status = Connect.isConnected();
|
|
62
|
+
expect(status.sql).toBe(true);
|
|
63
|
+
expect(status.redis).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('__setMockRedis 应该设置 mock Redis 客户端', () => {
|
|
67
|
+
const mockRedis = { close: () => {} } as any;
|
|
68
|
+
Connect.__setMockRedis(mockRedis);
|
|
69
|
+
|
|
70
|
+
const status = Connect.isConnected();
|
|
71
|
+
expect(status.sql).toBe(false);
|
|
72
|
+
expect(status.redis).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('__reset 应该重置所有连接', () => {
|
|
76
|
+
const mockSql = { close: async () => {} } as any;
|
|
77
|
+
const mockRedis = { close: () => {} } as any;
|
|
78
|
+
Connect.__setMockSql(mockSql);
|
|
79
|
+
Connect.__setMockRedis(mockRedis);
|
|
80
|
+
|
|
81
|
+
expect(Connect.isConnected().sql).toBe(true);
|
|
82
|
+
expect(Connect.isConnected().redis).toBe(true);
|
|
83
|
+
|
|
84
|
+
Connect.__reset();
|
|
85
|
+
|
|
86
|
+
expect(Connect.isConnected().sql).toBe(false);
|
|
87
|
+
expect(Connect.isConnected().redis).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('disconnect 应该正确关闭 mock 连接', async () => {
|
|
91
|
+
let sqlClosed = false;
|
|
92
|
+
let redisClosed = false;
|
|
93
|
+
|
|
94
|
+
const mockSql = {
|
|
95
|
+
close: async () => {
|
|
96
|
+
sqlClosed = true;
|
|
97
|
+
}
|
|
98
|
+
} as any;
|
|
99
|
+
const mockRedis = {
|
|
100
|
+
close: () => {
|
|
101
|
+
redisClosed = true;
|
|
102
|
+
}
|
|
103
|
+
} as any;
|
|
104
|
+
|
|
105
|
+
Connect.__setMockSql(mockSql);
|
|
106
|
+
Connect.__setMockRedis(mockRedis);
|
|
107
|
+
|
|
108
|
+
await Connect.disconnect();
|
|
109
|
+
|
|
110
|
+
expect(sqlClosed).toBe(true);
|
|
111
|
+
expect(redisClosed).toBe(true);
|
|
112
|
+
expect(Connect.isConnected().sql).toBe(false);
|
|
113
|
+
expect(Connect.isConnected().redis).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('连接异常处理', () => {
|
|
118
|
+
test('disconnectSql 应该处理关闭时的错误', async () => {
|
|
119
|
+
const mockSql = {
|
|
120
|
+
close: async () => {
|
|
121
|
+
throw new Error('Close error');
|
|
122
|
+
}
|
|
123
|
+
} as any;
|
|
124
|
+
|
|
125
|
+
Connect.__setMockSql(mockSql);
|
|
126
|
+
|
|
127
|
+
// 不应该抛出错误,只是记录日志
|
|
128
|
+
await expect(Connect.disconnectSql()).resolves.toBeUndefined();
|
|
129
|
+
expect(Connect.isConnected().sql).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('disconnectRedis 应该处理关闭时的错误', async () => {
|
|
133
|
+
const mockRedis = {
|
|
134
|
+
close: () => {
|
|
135
|
+
throw new Error('Close error');
|
|
136
|
+
}
|
|
137
|
+
} as any;
|
|
138
|
+
|
|
139
|
+
Connect.__setMockRedis(mockRedis);
|
|
140
|
+
|
|
141
|
+
// 不应该抛出错误,只是记录日志
|
|
142
|
+
await expect(Connect.disconnectRedis()).resolves.toBeUndefined();
|
|
143
|
+
expect(Connect.isConnected().redis).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('Connect.getStatus', () => {
|
|
148
|
+
test('未连接时返回正确的状态', () => {
|
|
149
|
+
const status = Connect.getStatus();
|
|
150
|
+
|
|
151
|
+
expect(status.sql.connected).toBe(false);
|
|
152
|
+
expect(status.sql.connectedAt).toBeNull();
|
|
153
|
+
expect(status.sql.uptime).toBeNull();
|
|
154
|
+
expect(status.sql.poolMax).toBe(1);
|
|
155
|
+
|
|
156
|
+
expect(status.redis.connected).toBe(false);
|
|
157
|
+
expect(status.redis.connectedAt).toBeNull();
|
|
158
|
+
expect(status.redis.uptime).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('Mock 连接后返回正确的状态', () => {
|
|
162
|
+
const mockSql = { close: async () => {} } as any;
|
|
163
|
+
const mockRedis = { close: () => {} } as any;
|
|
164
|
+
|
|
165
|
+
Connect.__setMockSql(mockSql);
|
|
166
|
+
Connect.__setMockRedis(mockRedis);
|
|
167
|
+
|
|
168
|
+
const status = Connect.getStatus();
|
|
169
|
+
|
|
170
|
+
// Mock 不会设置连接时间,但连接状态应该为 true
|
|
171
|
+
expect(status.sql.connected).toBe(true);
|
|
172
|
+
expect(status.redis.connected).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('__reset 应该重置所有状态包括连接时间', () => {
|
|
176
|
+
const mockSql = { close: async () => {} } as any;
|
|
177
|
+
Connect.__setMockSql(mockSql);
|
|
178
|
+
|
|
179
|
+
expect(Connect.getStatus().sql.connected).toBe(true);
|
|
180
|
+
|
|
181
|
+
Connect.__reset();
|
|
182
|
+
|
|
183
|
+
const status = Connect.getStatus();
|
|
184
|
+
expect(status.sql.connected).toBe(false);
|
|
185
|
+
expect(status.sql.connectedAt).toBeNull();
|
|
186
|
+
expect(status.sql.poolMax).toBe(1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|