befly 3.10.0 → 3.10.2
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/.gitignore +0 -0
- package/README.md +10 -13
- package/configs/presetFields.ts +10 -0
- package/configs/presetRegexp.ts +225 -0
- package/docs/README.md +17 -11
- package/docs/api/api.md +15 -1
- package/docs/guide/quickstart.md +19 -5
- package/docs/infra/redis.md +23 -11
- package/docs/quickstart.md +5 -335
- package/docs/reference/addon.md +0 -15
- package/docs/reference/config.md +1 -1
- package/docs/reference/logger.md +3 -3
- package/docs/reference/sync.md +99 -73
- package/docs/reference/table.md +1 -1
- package/package.json +15 -16
- package/docs/cipher.md +0 -582
- package/docs/database.md +0 -1176
- package/tests/_mocks/mockSqliteDb.ts +0 -204
- package/tests/addonHelper-cache.test.ts +0 -32
- package/tests/api-integration-array-number.test.ts +0 -282
- package/tests/apiHandler-routePath-only.test.ts +0 -32
- package/tests/befly-config-env.test.ts +0 -78
- package/tests/cacheHelper.test.ts +0 -323
- package/tests/cacheKeys.test.ts +0 -41
- package/tests/checkApi-routePath-strict.test.ts +0 -166
- package/tests/checkMenu.test.ts +0 -346
- package/tests/checkTable-smoke.test.ts +0 -157
- package/tests/cipher.test.ts +0 -249
- package/tests/dbDialect-cache.test.ts +0 -23
- package/tests/dbDialect.test.ts +0 -46
- package/tests/dbHelper-advanced.test.ts +0 -723
- package/tests/dbHelper-all-array-types.test.ts +0 -316
- package/tests/dbHelper-array-serialization.test.ts +0 -258
- package/tests/dbHelper-batch-write.test.ts +0 -90
- package/tests/dbHelper-columns.test.ts +0 -234
- package/tests/dbHelper-execute.test.ts +0 -187
- package/tests/dbHelper-joins.test.ts +0 -221
- package/tests/fields-redis-cache.test.ts +0 -127
- package/tests/fields-validate.test.ts +0 -99
- package/tests/fixtures/scanFilesAddon/node_modules/@befly-addon/demo/apis/sub/b.ts +0 -3
- package/tests/fixtures/scanFilesApis/a.ts +0 -3
- package/tests/fixtures/scanFilesApis/sub/b.ts +0 -3
- package/tests/getClientIp.test.ts +0 -54
- package/tests/integration.test.ts +0 -189
- package/tests/jwt.test.ts +0 -65
- package/tests/loadPlugins-order-smoke.test.ts +0 -75
- package/tests/logger.test.ts +0 -325
- package/tests/redisHelper.test.ts +0 -495
- package/tests/redisKeys.test.ts +0 -9
- package/tests/scanConfig.test.ts +0 -144
- package/tests/scanFiles-routePath.test.ts +0 -46
- package/tests/smoke-sql.test.ts +0 -24
- package/tests/sqlBuilder-advanced.test.ts +0 -608
- package/tests/sqlBuilder.test.ts +0 -209
- package/tests/sync-connection.test.ts +0 -183
- package/tests/sync-init-guard.test.ts +0 -105
- package/tests/syncApi-insBatch-fields-consistent.test.ts +0 -61
- package/tests/syncApi-obsolete-records.test.ts +0 -69
- package/tests/syncApi-type-compat.test.ts +0 -72
- package/tests/syncDev-permissions.test.ts +0 -81
- package/tests/syncMenu-disableMenus-hard-delete.test.ts +0 -88
- package/tests/syncMenu-duplicate-path.test.ts +0 -122
- package/tests/syncMenu-obsolete-records.test.ts +0 -161
- package/tests/syncMenu-parentPath-from-tree.test.ts +0 -75
- package/tests/syncMenu-paths.test.ts +0 -59
- package/tests/syncTable-apply.test.ts +0 -279
- package/tests/syncTable-array-number.test.ts +0 -160
- package/tests/syncTable-constants.test.ts +0 -101
- package/tests/syncTable-db-integration.test.ts +0 -237
- package/tests/syncTable-ddl.test.ts +0 -245
- package/tests/syncTable-helpers.test.ts +0 -99
- package/tests/syncTable-schema.test.ts +0 -99
- package/tests/syncTable-testkit.test.ts +0 -25
- package/tests/syncTable-types.test.ts +0 -122
- package/tests/tableRef-and-deserialize.test.ts +0 -67
- package/tests/util.test.ts +0 -100
- package/tests/validator-array-number.test.ts +0 -310
- package/tests/validator-default.test.ts +0 -373
- package/tests/validator.test.ts +0 -679
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DbHelper getTableColumns 方法单元测试
|
|
3
|
-
* 测试表字段查询、Redis 缓存、SQL 语法修复等功能
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { test, expect, mock } from "bun:test";
|
|
7
|
-
|
|
8
|
-
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
9
|
-
import { MySqlDialect } from "../lib/dbDialect.js";
|
|
10
|
-
import { DbHelper } from "../lib/dbHelper.js";
|
|
11
|
-
|
|
12
|
-
function createRedisMock(options?: { getObject?: any; setObject?: any; del?: any; genTimeID?: any }) {
|
|
13
|
-
const getObject = options?.getObject ? options.getObject : mock(async () => null);
|
|
14
|
-
const setObject = options?.setObject ? options.setObject : mock(async () => true);
|
|
15
|
-
const del = options?.del ? options.del : mock(async () => 1);
|
|
16
|
-
const genTimeID = options?.genTimeID ? options.genTimeID : mock(async () => 1);
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
getObject: getObject,
|
|
20
|
-
setObject: setObject,
|
|
21
|
-
del: del,
|
|
22
|
-
genTimeID: genTimeID
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
test("getTableColumns - 正常查询表字段", async () => {
|
|
27
|
-
const mockColumns = [{ Field: "id" }, { Field: "username" }, { Field: "email" }, { Field: "created_at" }];
|
|
28
|
-
|
|
29
|
-
const sqlMock = {
|
|
30
|
-
unsafe: mock(async (sql: string) => {
|
|
31
|
-
// 验证 SQL 语法正确(使用反引号)
|
|
32
|
-
expect(sql).toBe("SHOW COLUMNS FROM `users`");
|
|
33
|
-
return mockColumns;
|
|
34
|
-
})
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const redisMock = createRedisMock({
|
|
38
|
-
getObject: mock(async () => null) // 缓存未命中
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
42
|
-
|
|
43
|
-
const columns = await (dbHelper as any).getTableColumns("users");
|
|
44
|
-
|
|
45
|
-
expect(columns).toEqual(["id", "username", "email", "created_at"]);
|
|
46
|
-
expect(sqlMock.unsafe).toHaveBeenCalledTimes(1);
|
|
47
|
-
expect(redisMock.getObject).toHaveBeenCalledWith(CacheKeys.tableColumns("users"));
|
|
48
|
-
expect(redisMock.setObject).toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("getTableColumns - Redis 缓存命中", async () => {
|
|
52
|
-
const cachedColumns = ["id", "name", "email"];
|
|
53
|
-
const redisMock = createRedisMock({
|
|
54
|
-
getObject: mock(async () => cachedColumns)
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const sqlMock = {
|
|
58
|
-
unsafe: mock(async () => {
|
|
59
|
-
throw new Error("不应该执行 SQL 查询");
|
|
60
|
-
})
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
64
|
-
|
|
65
|
-
const columns = await (dbHelper as any).getTableColumns("users");
|
|
66
|
-
|
|
67
|
-
expect(columns).toEqual(cachedColumns);
|
|
68
|
-
expect(redisMock.getObject).toHaveBeenCalledWith(CacheKeys.tableColumns("users"));
|
|
69
|
-
expect(sqlMock.unsafe).not.toHaveBeenCalled(); // SQL 不应该被调用
|
|
70
|
-
expect(redisMock.setObject).not.toHaveBeenCalled(); // 不需要写缓存
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("getTableColumns - 表不存在错误", async () => {
|
|
74
|
-
const sqlMock = {
|
|
75
|
-
unsafe: mock(async () => []) // 返回空结果
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const redisMock = createRedisMock();
|
|
79
|
-
|
|
80
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
await (dbHelper as any).getTableColumns("non_existent_table");
|
|
84
|
-
expect(true).toBe(false); // 不应该执行到这里
|
|
85
|
-
} catch (error: any) {
|
|
86
|
-
expect(error.message).toContain("表 non_existent_table 不存在或没有字段");
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("getTableColumns - SQL 语法使用反引号", async () => {
|
|
91
|
-
const mockColumns = [{ Field: "id" }];
|
|
92
|
-
let capturedSql = "";
|
|
93
|
-
|
|
94
|
-
const sqlMock = {
|
|
95
|
-
unsafe: mock(async (sql: string) => {
|
|
96
|
-
capturedSql = sql;
|
|
97
|
-
return mockColumns;
|
|
98
|
-
})
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const dbHelper = new DbHelper({ redis: createRedisMock() as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
102
|
-
|
|
103
|
-
await (dbHelper as any).getTableColumns("addon_admin_user");
|
|
104
|
-
|
|
105
|
-
// 验证 SQL 语法
|
|
106
|
-
expect(capturedSql).toBe("SHOW COLUMNS FROM `addon_admin_user`");
|
|
107
|
-
expect(capturedSql).not.toContain("??"); // 不应该包含占位符
|
|
108
|
-
expect(capturedSql).toContain("`"); // 应该使用反引号
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("getTableColumns - 表名特殊字符处理", async () => {
|
|
112
|
-
const mockColumns = [{ Field: "id" }];
|
|
113
|
-
const sqlMock = {
|
|
114
|
-
unsafe: mock(async () => mockColumns)
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const dbHelper = new DbHelper({ redis: createRedisMock() as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
118
|
-
|
|
119
|
-
// 测试下划线表名
|
|
120
|
-
await (dbHelper as any).getTableColumns("addon_admin_user");
|
|
121
|
-
expect(sqlMock.unsafe).toHaveBeenLastCalledWith("SHOW COLUMNS FROM `addon_admin_user`");
|
|
122
|
-
|
|
123
|
-
// 测试普通表名
|
|
124
|
-
await (dbHelper as any).getTableColumns("users");
|
|
125
|
-
expect(sqlMock.unsafe).toHaveBeenLastCalledWith("SHOW COLUMNS FROM `users`");
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("getTableColumns - 缓存键格式正确", async () => {
|
|
129
|
-
const mockColumns = [{ Field: "id" }];
|
|
130
|
-
const sqlMock = {
|
|
131
|
-
unsafe: mock(async () => mockColumns)
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
const redisMock = createRedisMock({
|
|
135
|
-
getObject: mock(async () => null),
|
|
136
|
-
setObject: mock(async (key: string, value: any, seconds: number) => {
|
|
137
|
-
// 验证缓存键格式
|
|
138
|
-
expect(key).toBe(CacheKeys.tableColumns("test_table"));
|
|
139
|
-
// 验证缓存值格式
|
|
140
|
-
expect(Array.isArray(value)).toBe(true);
|
|
141
|
-
// 验证过期时间
|
|
142
|
-
expect(seconds).toBe(3600); // 1 小时
|
|
143
|
-
return true;
|
|
144
|
-
})
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
148
|
-
|
|
149
|
-
await (dbHelper as any).getTableColumns("test_table");
|
|
150
|
-
|
|
151
|
-
expect(redisMock.setObject).toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("getTableColumns - 多次调用相同表(缓存效果)", async () => {
|
|
155
|
-
const mockColumns = [{ Field: "id" }, { Field: "name" }];
|
|
156
|
-
let sqlCallCount = 0;
|
|
157
|
-
|
|
158
|
-
const sqlMock = {
|
|
159
|
-
unsafe: mock(async () => {
|
|
160
|
-
sqlCallCount++;
|
|
161
|
-
return mockColumns;
|
|
162
|
-
})
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const cache: Record<string, any> = {};
|
|
166
|
-
const redisMock = createRedisMock({
|
|
167
|
-
getObject: mock(async (key: string) => cache[key] || null),
|
|
168
|
-
setObject: mock(async (key: string, value: any) => {
|
|
169
|
-
cache[key] = value;
|
|
170
|
-
return true;
|
|
171
|
-
})
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
175
|
-
|
|
176
|
-
// 第一次调用 - 应该查询数据库
|
|
177
|
-
const columns1 = await (dbHelper as any).getTableColumns("users");
|
|
178
|
-
expect(columns1).toEqual(["id", "name"]);
|
|
179
|
-
expect(sqlCallCount).toBe(1);
|
|
180
|
-
|
|
181
|
-
// 第二次调用 - 应该从缓存读取
|
|
182
|
-
const columns2 = await (dbHelper as any).getTableColumns("users");
|
|
183
|
-
expect(columns2).toEqual(["id", "name"]);
|
|
184
|
-
expect(sqlCallCount).toBe(1); // SQL 调用次数不变
|
|
185
|
-
|
|
186
|
-
// 第三次调用 - 仍然从缓存读取
|
|
187
|
-
const columns3 = await (dbHelper as any).getTableColumns("users");
|
|
188
|
-
expect(columns3).toEqual(["id", "name"]);
|
|
189
|
-
expect(sqlCallCount).toBe(1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("getTableColumns - Redis 错误处理", async () => {
|
|
193
|
-
const mockColumns = [{ Field: "id" }];
|
|
194
|
-
const sqlMock = {
|
|
195
|
-
unsafe: mock(async () => mockColumns)
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const redisMock = createRedisMock({
|
|
199
|
-
getObject: mock(async () => {
|
|
200
|
-
throw new Error("Redis 连接失败");
|
|
201
|
-
})
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
const dbHelper = new DbHelper({ redis: redisMock as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
await (dbHelper as any).getTableColumns("users");
|
|
208
|
-
expect(true).toBe(false); // 不应该执行到这里
|
|
209
|
-
} catch (error: any) {
|
|
210
|
-
expect(error.message).toContain("Redis 连接失败");
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
test("getTableColumns - 字段映射正确性", async () => {
|
|
215
|
-
const mockColumns = [
|
|
216
|
-
{ Field: "id", Type: "int" },
|
|
217
|
-
{ Field: "user_name", Type: "varchar" },
|
|
218
|
-
{ Field: "created_at", Type: "timestamp" },
|
|
219
|
-
{ Field: "is_active", Type: "tinyint" }
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
const sqlMock = {
|
|
223
|
-
unsafe: mock(async () => mockColumns)
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const dbHelper = new DbHelper({ redis: createRedisMock() as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
227
|
-
|
|
228
|
-
const columns = await (dbHelper as any).getTableColumns("users");
|
|
229
|
-
|
|
230
|
-
// 验证只提取 Field 字段
|
|
231
|
-
expect(columns).toEqual(["id", "user_name", "created_at", "is_active"]);
|
|
232
|
-
expect(columns.length).toBe(4);
|
|
233
|
-
expect(columns.every((col: string) => typeof col === "string")).toBe(true);
|
|
234
|
-
});
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DbHelper executeWithConn 方法单元测试
|
|
3
|
-
* 测试 SQL 执行、错误处理等功能
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { test, expect, mock } from "bun:test";
|
|
7
|
-
|
|
8
|
-
import { MySqlDialect } from "../lib/dbDialect.js";
|
|
9
|
-
import { DbHelper } from "../lib/dbHelper.js";
|
|
10
|
-
|
|
11
|
-
function createMockRedis() {
|
|
12
|
-
return {
|
|
13
|
-
get: mock(async () => null),
|
|
14
|
-
set: mock(async () => true),
|
|
15
|
-
del: mock(async () => 1),
|
|
16
|
-
getObject: mock(async () => null),
|
|
17
|
-
setObject: mock(async () => true),
|
|
18
|
-
genTimeID: mock(async () => 1)
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
test("executeWithConn - 正常执行(无参数)", async () => {
|
|
23
|
-
const mockResult = [{ id: 1, name: "test" }];
|
|
24
|
-
const sqlMock = {
|
|
25
|
-
unsafe: mock(async () => mockResult)
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const redis = createMockRedis();
|
|
29
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
30
|
-
|
|
31
|
-
// 使用反射访问私有方法
|
|
32
|
-
const result = await (dbHelper as any).executeWithConn("SELECT * FROM users");
|
|
33
|
-
|
|
34
|
-
expect(result).toEqual(mockResult);
|
|
35
|
-
expect(sqlMock.unsafe).toHaveBeenCalledWith("SELECT * FROM users");
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test("executeWithConn - 正常执行(带参数)", async () => {
|
|
39
|
-
const mockResult = [{ id: 1, email: "test@example.com" }];
|
|
40
|
-
const sqlMock = {
|
|
41
|
-
unsafe: mock(async () => mockResult)
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const redis = createMockRedis();
|
|
45
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
46
|
-
|
|
47
|
-
const result = await (dbHelper as any).executeWithConn("SELECT * FROM users WHERE id = ?", [1]);
|
|
48
|
-
|
|
49
|
-
expect(result).toEqual(mockResult);
|
|
50
|
-
expect(sqlMock.unsafe).toHaveBeenCalledWith("SELECT * FROM users WHERE id = ?", [1]);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("executeWithConn - SQL 错误捕获", async () => {
|
|
54
|
-
const sqlError = new Error("You have an error in your SQL syntax");
|
|
55
|
-
const sqlMock = {
|
|
56
|
-
unsafe: mock(async () => {
|
|
57
|
-
throw sqlError;
|
|
58
|
-
})
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const redis = createMockRedis();
|
|
62
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
await (dbHelper as any).executeWithConn("SELECT * FROM invalid_table");
|
|
66
|
-
expect(true).toBe(false); // 不应该执行到这里
|
|
67
|
-
} catch (error: any) {
|
|
68
|
-
// 验证错误信息
|
|
69
|
-
expect(error.message).toContain("SQL执行失败");
|
|
70
|
-
expect(error.originalError).toBe(sqlError);
|
|
71
|
-
expect(error.sql).toBe("SELECT * FROM invalid_table");
|
|
72
|
-
expect(error.params).toEqual([]);
|
|
73
|
-
expect(error.duration).toBeGreaterThanOrEqual(0);
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test("executeWithConn - 错误信息包含完整信息", async () => {
|
|
78
|
-
const sqlMock = {
|
|
79
|
-
unsafe: mock(async () => {
|
|
80
|
-
throw new Error('Syntax error near "??"');
|
|
81
|
-
})
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const redis = createMockRedis();
|
|
85
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
86
|
-
|
|
87
|
-
const testSql = "SHOW COLUMNS FROM ??";
|
|
88
|
-
const testParams = ["users"];
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
await (dbHelper as any).executeWithConn(testSql, testParams);
|
|
92
|
-
} catch (error: any) {
|
|
93
|
-
// 验证增强的错误对象
|
|
94
|
-
expect(error.sql).toBe(testSql);
|
|
95
|
-
expect(error.params).toEqual(testParams);
|
|
96
|
-
expect(typeof error.duration).toBe("number");
|
|
97
|
-
expect(error.originalError.message).toBe('Syntax error near "??"');
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("executeWithConn - 超长 SQL 保留在错误对象中", async () => {
|
|
102
|
-
const longSql = "SELECT * FROM users WHERE " + "id = ? AND ".repeat(50) + "name = ?";
|
|
103
|
-
const sqlMock = {
|
|
104
|
-
unsafe: mock(async () => {
|
|
105
|
-
throw new Error("Test error");
|
|
106
|
-
})
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const redis = createMockRedis();
|
|
110
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
await (dbHelper as any).executeWithConn(longSql);
|
|
114
|
-
} catch (error: any) {
|
|
115
|
-
// SQL 完整保存在错误对象中
|
|
116
|
-
expect(error.sql).toBe(longSql);
|
|
117
|
-
expect(error.params).toEqual([]);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test("executeWithConn - 慢查询检测(>1000ms)", async () => {
|
|
122
|
-
const mockResult = [{ id: 1 }];
|
|
123
|
-
const sqlMock = {
|
|
124
|
-
unsafe: mock(async () => {
|
|
125
|
-
// 模拟慢查询
|
|
126
|
-
await new Promise((resolve) => setTimeout(resolve, 1100));
|
|
127
|
-
return mockResult;
|
|
128
|
-
})
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
const redis = createMockRedis();
|
|
132
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
133
|
-
|
|
134
|
-
const result = await (dbHelper as any).executeWithConn("SELECT SLEEP(1)");
|
|
135
|
-
|
|
136
|
-
// 功能仍正常返回结果
|
|
137
|
-
expect(result).toEqual(mockResult);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("executeWithConn - 数据库未连接错误", async () => {
|
|
141
|
-
const redis = createMockRedis();
|
|
142
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: null, dialect: new MySqlDialect() }); // 没有 sql 实例
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
await (dbHelper as any).executeWithConn("SELECT * FROM users");
|
|
146
|
-
expect(true).toBe(false); // 不应该执行到这里
|
|
147
|
-
} catch (error: any) {
|
|
148
|
-
expect(error.message).toBe("数据库连接未初始化");
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("executeWithConn - 空参数数组", async () => {
|
|
153
|
-
const mockResult = [{ count: 10 }];
|
|
154
|
-
const sqlMock = {
|
|
155
|
-
unsafe: mock(async () => mockResult)
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const redis = createMockRedis();
|
|
159
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
160
|
-
|
|
161
|
-
const result = await (dbHelper as any).executeWithConn("SELECT COUNT(*) as count FROM users", []);
|
|
162
|
-
|
|
163
|
-
expect(result).toEqual(mockResult);
|
|
164
|
-
// 空数组应该走 else 分支(不传参数)
|
|
165
|
-
expect(sqlMock.unsafe).toHaveBeenCalledWith("SELECT COUNT(*) as count FROM users");
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test("executeWithConn - 复杂参数处理", async () => {
|
|
169
|
-
const sqlMock = {
|
|
170
|
-
unsafe: mock(async () => {
|
|
171
|
-
throw new Error("Test error");
|
|
172
|
-
})
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const redis = createMockRedis();
|
|
176
|
-
const dbHelper = new DbHelper({ redis: redis as any, sql: sqlMock, dialect: new MySqlDialect() });
|
|
177
|
-
|
|
178
|
-
const complexParams = [1, "test", { nested: "object" }, [1, 2, 3], null, undefined];
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
await (dbHelper as any).executeWithConn("SELECT ?", complexParams);
|
|
182
|
-
} catch (error: any) {
|
|
183
|
-
// 验证参数被正确保存
|
|
184
|
-
expect(error.params).toEqual(complexParams);
|
|
185
|
-
expect(error.sql).toBe("SELECT ?");
|
|
186
|
-
}
|
|
187
|
-
});
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DbHelper JOIN 功能测试
|
|
3
|
-
* 测试多表联查相关功能(推荐使用表别名)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, test, expect } from "bun:test";
|
|
7
|
-
|
|
8
|
-
import { DbUtils } from "../lib/dbUtils.js";
|
|
9
|
-
|
|
10
|
-
describe("DbUtils tableRef - normalizeTableRef/getJoinMainQualifier", () => {
|
|
11
|
-
test("普通表名转下划线(无 alias)", () => {
|
|
12
|
-
expect(DbUtils.normalizeTableRef("userProfile")).toBe("user_profile");
|
|
13
|
-
expect(DbUtils.normalizeTableRef("orderDetail")).toBe("order_detail");
|
|
14
|
-
expect(DbUtils.normalizeTableRef("user")).toBe("user");
|
|
15
|
-
expect(DbUtils.normalizeTableRef("order")).toBe("order");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test("带 alias 的 tableRef:只转换 schema/table,不改 alias", () => {
|
|
19
|
-
expect(DbUtils.normalizeTableRef("UserProfile up")).toBe("user_profile up");
|
|
20
|
-
expect(DbUtils.normalizeTableRef("myDb.UserProfile up")).toBe("my_db.user_profile up");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("JOIN 主限定符:优先 alias", () => {
|
|
24
|
-
expect(DbUtils.getJoinMainQualifier("order o")).toBe("o");
|
|
25
|
-
expect(DbUtils.getJoinMainQualifier("userProfile")).toBe("user_profile");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("DbHelper JOIN - processJoinField", () => {
|
|
30
|
-
test("带表名的字段", () => {
|
|
31
|
-
expect(DbUtils.processJoinField("o.userId")).toBe("o.user_id");
|
|
32
|
-
expect(DbUtils.processJoinField("u.userName")).toBe("u.user_name");
|
|
33
|
-
expect(DbUtils.processJoinField("o.createdAt")).toBe("o.created_at");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("表/别名部分保持原样(JOIN 场景点号前通常是别名)", () => {
|
|
37
|
-
expect(DbUtils.processJoinField("orderDetail.productId")).toBe("orderDetail.product_id");
|
|
38
|
-
expect(DbUtils.processJoinField("userProfile.avatarUrl")).toBe("userProfile.avatar_url");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("普通字段(无表名)", () => {
|
|
42
|
-
expect(DbUtils.processJoinField("userName")).toBe("user_name");
|
|
43
|
-
expect(DbUtils.processJoinField("createdAt")).toBe("created_at");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("带 AS 别名的字段", () => {
|
|
47
|
-
expect(DbUtils.processJoinField("o.totalAmount AS total")).toBe("o.total_amount AS total");
|
|
48
|
-
expect(DbUtils.processJoinField("u.userName AS name")).toBe("u.user_name AS name");
|
|
49
|
-
expect(DbUtils.processJoinField("p.name AS productName")).toBe("p.name AS productName");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("函数字段保持原样", () => {
|
|
53
|
-
expect(DbUtils.processJoinField("COUNT(*)")).toBe("COUNT(*)");
|
|
54
|
-
expect(DbUtils.processJoinField("SUM(o.amount)")).toBe("SUM(o.amount)");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("星号保持原样", () => {
|
|
58
|
-
expect(DbUtils.processJoinField("*")).toBe("*");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("已转义字段保持原样", () => {
|
|
62
|
-
expect(DbUtils.processJoinField("`order`")).toBe("`order`");
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
describe("DbHelper JOIN - processJoinWhereKey", () => {
|
|
67
|
-
test("带表名的字段名", () => {
|
|
68
|
-
expect(DbUtils.processJoinWhereKey("o.userId")).toBe("o.user_id");
|
|
69
|
-
expect(DbUtils.processJoinWhereKey("u.userName")).toBe("u.user_name");
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("带表名和操作符的字段名", () => {
|
|
73
|
-
expect(DbUtils.processJoinWhereKey("o.createdAt$gt")).toBe("o.created_at$gt");
|
|
74
|
-
expect(DbUtils.processJoinWhereKey("u.status$in")).toBe("u.status$in");
|
|
75
|
-
expect(DbUtils.processJoinWhereKey("o.amount$gte")).toBe("o.amount$gte");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("普通字段带操作符", () => {
|
|
79
|
-
expect(DbUtils.processJoinWhereKey("createdAt$gt")).toBe("created_at$gt");
|
|
80
|
-
expect(DbUtils.processJoinWhereKey("userId$ne")).toBe("user_id$ne");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("逻辑操作符保持原样", () => {
|
|
84
|
-
expect(DbUtils.processJoinWhereKey("$or")).toBe("$or");
|
|
85
|
-
expect(DbUtils.processJoinWhereKey("$and")).toBe("$and");
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe("DbHelper JOIN - processJoinWhere", () => {
|
|
90
|
-
test("简单条件", () => {
|
|
91
|
-
const where = { "o.userId": 1, "o.state": 1 };
|
|
92
|
-
const result = DbUtils.processJoinWhere(where);
|
|
93
|
-
expect(result).toEqual({ "o.user_id": 1, "o.state": 1 });
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("带操作符的条件", () => {
|
|
97
|
-
const where = { "o.createdAt$gt": 1000, "u.state$ne": 0 };
|
|
98
|
-
const result = DbUtils.processJoinWhere(where);
|
|
99
|
-
expect(result).toEqual({ "o.created_at$gt": 1000, "u.state$ne": 0 });
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("$or 条件", () => {
|
|
103
|
-
const where = {
|
|
104
|
-
$or: [{ "u.userName$like": "%test%" }, { "u.email$like": "%test%" }]
|
|
105
|
-
};
|
|
106
|
-
const result = DbUtils.processJoinWhere(where);
|
|
107
|
-
expect(result).toEqual({
|
|
108
|
-
$or: [{ "u.user_name$like": "%test%" }, { "u.email$like": "%test%" }]
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("复杂嵌套条件", () => {
|
|
113
|
-
const where = {
|
|
114
|
-
"o.state": 1,
|
|
115
|
-
"u.state": 1,
|
|
116
|
-
$or: [{ "u.userName$like": "%test%" }, { "p.name$like": "%test%" }],
|
|
117
|
-
"o.createdAt$gte": 1000
|
|
118
|
-
};
|
|
119
|
-
const result = DbUtils.processJoinWhere(where);
|
|
120
|
-
expect(result).toEqual({
|
|
121
|
-
"o.state": 1,
|
|
122
|
-
"u.state": 1,
|
|
123
|
-
$or: [{ "u.user_name$like": "%test%" }, { "p.name$like": "%test%" }],
|
|
124
|
-
"o.created_at$gte": 1000
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("DbHelper JOIN - processJoinOrderBy", () => {
|
|
130
|
-
test("带表名的排序", () => {
|
|
131
|
-
const orderBy = ["o.createdAt#DESC", "u.userName#ASC"];
|
|
132
|
-
const result = DbUtils.processJoinOrderBy(orderBy);
|
|
133
|
-
expect(result).toEqual(["o.created_at#DESC", "u.user_name#ASC"]);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("普通排序", () => {
|
|
137
|
-
const orderBy = ["createdAt#DESC"];
|
|
138
|
-
const result = DbUtils.processJoinOrderBy(orderBy);
|
|
139
|
-
expect(result).toEqual(["created_at#DESC"]);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("无排序方向的保持原样", () => {
|
|
143
|
-
const orderBy = ["id"];
|
|
144
|
-
const result = DbUtils.processJoinOrderBy(orderBy);
|
|
145
|
-
expect(result).toEqual(["id"]);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
describe("DbHelper JOIN - JoinOption 类型验证", () => {
|
|
150
|
-
test("LEFT JOIN(默认)", () => {
|
|
151
|
-
const join = { table: "user", on: "order.user_id = user.id" };
|
|
152
|
-
expect(join.type).toBeUndefined();
|
|
153
|
-
expect(join.table).toBe("user");
|
|
154
|
-
expect(join.on).toBe("order.user_id = user.id");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("INNER JOIN", () => {
|
|
158
|
-
const join = { type: "inner" as const, table: "product", on: "order.product_id = product.id" };
|
|
159
|
-
expect(join.type).toBe("inner");
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
test("RIGHT JOIN", () => {
|
|
163
|
-
const join = {
|
|
164
|
-
type: "right" as const,
|
|
165
|
-
table: "category",
|
|
166
|
-
on: "product.category_id = category.id"
|
|
167
|
-
};
|
|
168
|
-
expect(join.type).toBe("right");
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("DbHelper JOIN - 完整场景模拟", () => {
|
|
173
|
-
test("订单列表联查参数处理", () => {
|
|
174
|
-
// 模拟输入
|
|
175
|
-
const options = {
|
|
176
|
-
table: "order o",
|
|
177
|
-
joins: [
|
|
178
|
-
{ table: "user u", on: "o.user_id = u.id" },
|
|
179
|
-
{ table: "product p", on: "o.product_id = p.id" }
|
|
180
|
-
],
|
|
181
|
-
fields: ["o.id", "o.totalAmount", "u.userName", "p.name AS productName"],
|
|
182
|
-
where: {
|
|
183
|
-
"o.state": 1,
|
|
184
|
-
"u.state": 1,
|
|
185
|
-
"o.createdAt$gte": 1701388800000
|
|
186
|
-
},
|
|
187
|
-
orderBy: ["o.createdAt#DESC"]
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
// 处理表名(tableRef 规范化)
|
|
191
|
-
const processedTable = DbUtils.normalizeTableRef(options.table);
|
|
192
|
-
expect(processedTable).toBe("order o");
|
|
193
|
-
|
|
194
|
-
// 处理字段
|
|
195
|
-
const processedFields = options.fields.map((f) => DbUtils.processJoinField(f));
|
|
196
|
-
expect(processedFields).toEqual(["o.id", "o.total_amount", "u.user_name", "p.name AS productName"]);
|
|
197
|
-
|
|
198
|
-
// 处理 where
|
|
199
|
-
const processedWhere = DbUtils.processJoinWhere(options.where);
|
|
200
|
-
expect(processedWhere).toEqual({
|
|
201
|
-
"o.state": 1,
|
|
202
|
-
"u.state": 1,
|
|
203
|
-
"o.created_at$gte": 1701388800000
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// 处理 orderBy
|
|
207
|
-
const processedOrderBy = DbUtils.processJoinOrderBy(options.orderBy);
|
|
208
|
-
expect(processedOrderBy).toEqual(["o.created_at#DESC"]);
|
|
209
|
-
|
|
210
|
-
// 处理 joins
|
|
211
|
-
const processedJoins = options.joins.map((j) => ({
|
|
212
|
-
type: (j as any).type || "left",
|
|
213
|
-
table: DbUtils.normalizeTableRef(j.table),
|
|
214
|
-
on: j.on
|
|
215
|
-
}));
|
|
216
|
-
expect(processedJoins).toEqual([
|
|
217
|
-
{ type: "left", table: "user u", on: "o.user_id = u.id" },
|
|
218
|
-
{ type: "left", table: "product p", on: "o.product_id = p.id" }
|
|
219
|
-
]);
|
|
220
|
-
});
|
|
221
|
-
});
|