befly 3.9.37 → 3.9.39
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/README.md +38 -39
- package/befly.config.ts +62 -40
- package/checks/checkApi.ts +16 -16
- package/checks/checkApp.ts +19 -25
- package/checks/checkTable.ts +42 -42
- package/docs/README.md +42 -35
- package/docs/{api.md → api/api.md} +225 -235
- package/docs/cipher.md +71 -69
- package/docs/database.md +155 -153
- package/docs/{examples.md → guide/examples.md} +181 -181
- package/docs/guide/quickstart.md +331 -0
- package/docs/hooks/auth.md +38 -0
- package/docs/hooks/cors.md +28 -0
- package/docs/{hook.md → hooks/hook.md} +140 -57
- package/docs/hooks/parser.md +19 -0
- package/docs/hooks/rateLimit.md +47 -0
- package/docs/{redis.md → infra/redis.md} +84 -93
- package/docs/plugins/cipher.md +61 -0
- package/docs/plugins/database.md +128 -0
- package/docs/{plugin.md → plugins/plugin.md} +83 -81
- package/docs/quickstart.md +26 -26
- package/docs/{addon.md → reference/addon.md} +46 -46
- package/docs/{config.md → reference/config.md} +32 -80
- package/docs/{logger.md → reference/logger.md} +52 -52
- package/docs/{sync.md → reference/sync.md} +32 -35
- package/docs/{table.md → reference/table.md} +7 -7
- package/docs/{validator.md → reference/validator.md} +57 -57
- package/hooks/auth.ts +8 -4
- package/hooks/cors.ts +13 -13
- package/hooks/parser.ts +37 -17
- package/hooks/permission.ts +26 -14
- package/hooks/rateLimit.ts +276 -0
- package/hooks/validator.ts +15 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -81
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +211 -109
- package/lib/jwt.ts +16 -16
- package/lib/logger.ts +610 -19
- package/lib/redisHelper.ts +185 -44
- package/lib/sqlBuilder.ts +90 -91
- package/lib/validator.ts +59 -39
- package/loader/loadApis.ts +53 -47
- package/loader/loadHooks.ts +40 -14
- package/loader/loadPlugins.ts +16 -17
- package/main.ts +57 -47
- package/package.json +47 -45
- package/paths.ts +15 -14
- package/plugins/cache.ts +5 -4
- package/plugins/cipher.ts +3 -3
- package/plugins/config.ts +2 -2
- package/plugins/db.ts +9 -9
- package/plugins/jwt.ts +3 -3
- package/plugins/logger.ts +8 -12
- package/plugins/redis.ts +8 -8
- package/plugins/tool.ts +6 -6
- package/router/api.ts +85 -56
- package/router/static.ts +12 -12
- package/sync/syncAll.ts +12 -12
- package/sync/syncApi.ts +55 -54
- package/sync/syncDb/apply.ts +20 -19
- package/sync/syncDb/constants.ts +25 -23
- package/sync/syncDb/ddl.ts +35 -36
- package/sync/syncDb/helpers.ts +6 -9
- package/sync/syncDb/schema.ts +10 -9
- package/sync/syncDb/sqlite.ts +7 -8
- package/sync/syncDb/table.ts +37 -35
- package/sync/syncDb/tableCreate.ts +21 -20
- package/sync/syncDb/types.ts +23 -20
- package/sync/syncDb/version.ts +10 -10
- package/sync/syncDb.ts +43 -36
- package/sync/syncDev.ts +74 -66
- package/sync/syncMenu.ts +190 -57
- package/tests/api-integration-array-number.test.ts +282 -0
- package/tests/befly-config-env.test.ts +78 -0
- package/tests/cacheHelper.test.ts +135 -104
- package/tests/cacheKeys.test.ts +41 -0
- package/tests/cipher.test.ts +90 -89
- package/tests/dbHelper-advanced.test.ts +140 -134
- package/tests/dbHelper-all-array-types.test.ts +316 -0
- package/tests/dbHelper-array-serialization.test.ts +258 -0
- package/tests/dbHelper-columns.test.ts +56 -55
- package/tests/dbHelper-execute.test.ts +45 -44
- package/tests/dbHelper-joins.test.ts +124 -119
- package/tests/fields-redis-cache.test.ts +29 -27
- package/tests/fields-validate.test.ts +38 -38
- package/tests/getClientIp.test.ts +54 -0
- package/tests/integration.test.ts +69 -67
- package/tests/jwt.test.ts +27 -26
- package/tests/logger.test.ts +267 -34
- package/tests/rateLimit-hook.test.ts +477 -0
- package/tests/redisHelper.test.ts +187 -188
- package/tests/redisKeys.test.ts +6 -73
- package/tests/scanConfig.test.ts +144 -0
- package/tests/sqlBuilder-advanced.test.ts +217 -215
- package/tests/sqlBuilder.test.ts +92 -91
- package/tests/sync-connection.test.ts +29 -29
- package/tests/syncDb-apply.test.ts +97 -96
- package/tests/syncDb-array-number.test.ts +160 -0
- package/tests/syncDb-constants.test.ts +48 -47
- package/tests/syncDb-ddl.test.ts +99 -98
- package/tests/syncDb-helpers.test.ts +29 -28
- package/tests/syncDb-schema.test.ts +61 -60
- package/tests/syncDb-types.test.ts +60 -59
- package/tests/syncMenu-paths.test.ts +68 -0
- package/tests/util.test.ts +42 -41
- package/tests/validator-array-number.test.ts +310 -0
- package/tests/validator-default.test.ts +373 -0
- package/tests/validator.test.ts +271 -266
- package/tsconfig.json +4 -5
- package/types/api.d.ts +7 -12
- package/types/befly.d.ts +60 -13
- package/types/cache.d.ts +8 -4
- package/types/common.d.ts +17 -9
- package/types/context.d.ts +2 -2
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +19 -19
- package/types/hook.d.ts +2 -2
- package/types/jwt.d.ts +118 -0
- package/types/logger.d.ts +30 -0
- package/types/plugin.d.ts +4 -4
- package/types/redis.d.ts +7 -3
- package/types/roleApisCache.ts +23 -0
- package/types/sync.d.ts +10 -10
- package/types/table.d.ts +50 -9
- package/types/validate.d.ts +69 -0
- package/utils/addonHelper.ts +90 -0
- package/utils/arrayKeysToCamel.ts +18 -0
- package/utils/calcPerfTime.ts +13 -0
- package/utils/configTypes.ts +3 -0
- package/utils/cors.ts +19 -0
- package/utils/fieldClear.ts +75 -0
- package/utils/genShortId.ts +12 -0
- package/utils/getClientIp.ts +45 -0
- package/utils/keysToCamel.ts +22 -0
- package/utils/keysToSnake.ts +22 -0
- package/utils/modules.ts +98 -0
- package/utils/pickFields.ts +19 -0
- package/utils/process.ts +56 -0
- package/utils/regex.ts +225 -0
- package/utils/response.ts +115 -0
- package/utils/route.ts +23 -0
- package/utils/scanConfig.ts +142 -0
- package/utils/scanFiles.ts +48 -0
- package/.prettierignore +0 -2
- package/.prettierrc +0 -12
- package/docs/1-/345/237/272/346/234/254/344/273/213/347/273/215.md +0 -35
- package/docs/2-/345/210/235/346/255/245/344/275/223/351/252/214.md +0 -64
- package/docs/3-/347/254/254/344/270/200/344/270/252/346/216/245/345/217/243.md +0 -46
- package/docs/4-/346/223/215/344/275/234/346/225/260/346/215/256/345/272/223.md +0 -172
- package/hooks/requestLogger.ts +0 -84
- package/types/index.ts +0 -24
- package/util.ts +0 -283
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* 测试多表联查相关功能(不支持表别名)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe, test, expect } from
|
|
7
|
-
|
|
6
|
+
import { describe, test, expect } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import { snakeCase } from "es-toolkit/string";
|
|
8
9
|
|
|
9
10
|
// ============================================
|
|
10
11
|
// 辅助函数单元测试(模拟 DbHelper 私有方法)
|
|
@@ -21,17 +22,17 @@ function processTableName(table: string): string {
|
|
|
21
22
|
* 处理联查字段(支持表名.字段名格式)
|
|
22
23
|
*/
|
|
23
24
|
function processJoinField(field: string): string {
|
|
24
|
-
if (field.includes(
|
|
25
|
+
if (field.includes("(") || field === "*" || field.startsWith("`")) {
|
|
25
26
|
return field;
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
if (field.toUpperCase().includes(
|
|
29
|
+
if (field.toUpperCase().includes(" AS ")) {
|
|
29
30
|
const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
|
|
30
31
|
return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
if (field.includes(
|
|
34
|
-
const [tableName, fieldName] = field.split(
|
|
34
|
+
if (field.includes(".")) {
|
|
35
|
+
const [tableName, fieldName] = field.split(".");
|
|
35
36
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
36
37
|
}
|
|
37
38
|
|
|
@@ -42,24 +43,24 @@ function processJoinField(field: string): string {
|
|
|
42
43
|
* 处理联查 where 条件键名
|
|
43
44
|
*/
|
|
44
45
|
function processJoinWhereKey(key: string): string {
|
|
45
|
-
if (key ===
|
|
46
|
+
if (key === "$or" || key === "$and") {
|
|
46
47
|
return key;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
if (key.includes(
|
|
50
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
50
|
+
if (key.includes("$")) {
|
|
51
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
51
52
|
const fieldPart = key.substring(0, lastDollarIndex);
|
|
52
53
|
const operator = key.substring(lastDollarIndex);
|
|
53
54
|
|
|
54
|
-
if (fieldPart.includes(
|
|
55
|
-
const [tableName, fieldName] = fieldPart.split(
|
|
55
|
+
if (fieldPart.includes(".")) {
|
|
56
|
+
const [tableName, fieldName] = fieldPart.split(".");
|
|
56
57
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
|
|
57
58
|
}
|
|
58
59
|
return `${snakeCase(fieldPart)}${operator}`;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
if (key.includes(
|
|
62
|
-
const [tableName, fieldName] = key.split(
|
|
62
|
+
if (key.includes(".")) {
|
|
63
|
+
const [tableName, fieldName] = key.split(".");
|
|
63
64
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -70,7 +71,7 @@ function processJoinWhereKey(key: string): string {
|
|
|
70
71
|
* 递归处理联查 where 条件
|
|
71
72
|
*/
|
|
72
73
|
function processJoinWhere(where: any): any {
|
|
73
|
-
if (!where || typeof where !==
|
|
74
|
+
if (!where || typeof where !== "object") return where;
|
|
74
75
|
|
|
75
76
|
if (Array.isArray(where)) {
|
|
76
77
|
return where.map((item) => processJoinWhere(item));
|
|
@@ -80,9 +81,9 @@ function processJoinWhere(where: any): any {
|
|
|
80
81
|
for (const [key, value] of Object.entries(where)) {
|
|
81
82
|
const newKey = processJoinWhereKey(key);
|
|
82
83
|
|
|
83
|
-
if (key ===
|
|
84
|
+
if (key === "$or" || key === "$and") {
|
|
84
85
|
result[newKey] = (value as any[]).map((item) => processJoinWhere(item));
|
|
85
|
-
} else if (typeof value ===
|
|
86
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
86
87
|
result[newKey] = processJoinWhere(value);
|
|
87
88
|
} else {
|
|
88
89
|
result[newKey] = value;
|
|
@@ -97,8 +98,8 @@ function processJoinWhere(where: any): any {
|
|
|
97
98
|
function processJoinOrderBy(orderBy: string[]): string[] {
|
|
98
99
|
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
99
100
|
return orderBy.map((item) => {
|
|
100
|
-
if (typeof item !==
|
|
101
|
-
const [field, direction] = item.split(
|
|
101
|
+
if (typeof item !== "string" || !item.includes("#")) return item;
|
|
102
|
+
const [field, direction] = item.split("#");
|
|
102
103
|
return `${processJoinField(field.trim())}#${direction.trim()}`;
|
|
103
104
|
});
|
|
104
105
|
}
|
|
@@ -107,201 +108,205 @@ function processJoinOrderBy(orderBy: string[]): string[] {
|
|
|
107
108
|
// 测试用例
|
|
108
109
|
// ============================================
|
|
109
110
|
|
|
110
|
-
describe(
|
|
111
|
-
test(
|
|
112
|
-
expect(processTableName(
|
|
113
|
-
expect(processTableName(
|
|
114
|
-
expect(processTableName(
|
|
115
|
-
expect(processTableName(
|
|
111
|
+
describe("DbHelper JOIN - processTableName", () => {
|
|
112
|
+
test("普通表名转下划线", () => {
|
|
113
|
+
expect(processTableName("userProfile")).toBe("user_profile");
|
|
114
|
+
expect(processTableName("orderDetail")).toBe("order_detail");
|
|
115
|
+
expect(processTableName("user")).toBe("user");
|
|
116
|
+
expect(processTableName("order")).toBe("order");
|
|
116
117
|
});
|
|
117
118
|
});
|
|
118
119
|
|
|
119
|
-
describe(
|
|
120
|
-
test(
|
|
121
|
-
expect(processJoinField(
|
|
122
|
-
expect(processJoinField(
|
|
123
|
-
expect(processJoinField(
|
|
120
|
+
describe("DbHelper JOIN - processJoinField", () => {
|
|
121
|
+
test("带表名的字段", () => {
|
|
122
|
+
expect(processJoinField("order.userId")).toBe("order.user_id");
|
|
123
|
+
expect(processJoinField("user.userName")).toBe("user.user_name");
|
|
124
|
+
expect(processJoinField("order.createdAt")).toBe("order.created_at");
|
|
124
125
|
});
|
|
125
126
|
|
|
126
|
-
test(
|
|
127
|
-
expect(processJoinField(
|
|
128
|
-
expect(processJoinField(
|
|
127
|
+
test("表名也转下划线", () => {
|
|
128
|
+
expect(processJoinField("orderDetail.productId")).toBe("order_detail.product_id");
|
|
129
|
+
expect(processJoinField("userProfile.avatarUrl")).toBe("user_profile.avatar_url");
|
|
129
130
|
});
|
|
130
131
|
|
|
131
|
-
test(
|
|
132
|
-
expect(processJoinField(
|
|
133
|
-
expect(processJoinField(
|
|
132
|
+
test("普通字段(无表名)", () => {
|
|
133
|
+
expect(processJoinField("userName")).toBe("user_name");
|
|
134
|
+
expect(processJoinField("createdAt")).toBe("created_at");
|
|
134
135
|
});
|
|
135
136
|
|
|
136
|
-
test(
|
|
137
|
-
expect(processJoinField(
|
|
138
|
-
expect(processJoinField(
|
|
139
|
-
expect(processJoinField(
|
|
137
|
+
test("带 AS 别名的字段", () => {
|
|
138
|
+
expect(processJoinField("order.totalAmount AS total")).toBe("order.total_amount AS total");
|
|
139
|
+
expect(processJoinField("user.userName AS name")).toBe("user.user_name AS name");
|
|
140
|
+
expect(processJoinField("product.name AS productName")).toBe("product.name AS productName");
|
|
140
141
|
});
|
|
141
142
|
|
|
142
|
-
test(
|
|
143
|
-
expect(processJoinField(
|
|
144
|
-
expect(processJoinField(
|
|
143
|
+
test("函数字段保持原样", () => {
|
|
144
|
+
expect(processJoinField("COUNT(*)")).toBe("COUNT(*)");
|
|
145
|
+
expect(processJoinField("SUM(order.amount)")).toBe("SUM(order.amount)");
|
|
145
146
|
});
|
|
146
147
|
|
|
147
|
-
test(
|
|
148
|
-
expect(processJoinField(
|
|
148
|
+
test("星号保持原样", () => {
|
|
149
|
+
expect(processJoinField("*")).toBe("*");
|
|
149
150
|
});
|
|
150
151
|
|
|
151
|
-
test(
|
|
152
|
-
expect(processJoinField(
|
|
152
|
+
test("已转义字段保持原样", () => {
|
|
153
|
+
expect(processJoinField("`order`")).toBe("`order`");
|
|
153
154
|
});
|
|
154
155
|
});
|
|
155
156
|
|
|
156
|
-
describe(
|
|
157
|
-
test(
|
|
158
|
-
expect(processJoinWhereKey(
|
|
159
|
-
expect(processJoinWhereKey(
|
|
157
|
+
describe("DbHelper JOIN - processJoinWhereKey", () => {
|
|
158
|
+
test("带表名的字段名", () => {
|
|
159
|
+
expect(processJoinWhereKey("order.userId")).toBe("order.user_id");
|
|
160
|
+
expect(processJoinWhereKey("user.userName")).toBe("user.user_name");
|
|
160
161
|
});
|
|
161
162
|
|
|
162
|
-
test(
|
|
163
|
-
expect(processJoinWhereKey(
|
|
164
|
-
expect(processJoinWhereKey(
|
|
165
|
-
expect(processJoinWhereKey(
|
|
163
|
+
test("带表名和操作符的字段名", () => {
|
|
164
|
+
expect(processJoinWhereKey("order.createdAt$gt")).toBe("order.created_at$gt");
|
|
165
|
+
expect(processJoinWhereKey("user.status$in")).toBe("user.status$in");
|
|
166
|
+
expect(processJoinWhereKey("order.amount$gte")).toBe("order.amount$gte");
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
test(
|
|
169
|
-
expect(processJoinWhereKey(
|
|
170
|
-
expect(processJoinWhereKey(
|
|
169
|
+
test("普通字段带操作符", () => {
|
|
170
|
+
expect(processJoinWhereKey("createdAt$gt")).toBe("created_at$gt");
|
|
171
|
+
expect(processJoinWhereKey("userId$ne")).toBe("user_id$ne");
|
|
171
172
|
});
|
|
172
173
|
|
|
173
|
-
test(
|
|
174
|
-
expect(processJoinWhereKey(
|
|
175
|
-
expect(processJoinWhereKey(
|
|
174
|
+
test("逻辑操作符保持原样", () => {
|
|
175
|
+
expect(processJoinWhereKey("$or")).toBe("$or");
|
|
176
|
+
expect(processJoinWhereKey("$and")).toBe("$and");
|
|
176
177
|
});
|
|
177
178
|
});
|
|
178
179
|
|
|
179
|
-
describe(
|
|
180
|
-
test(
|
|
181
|
-
const where = {
|
|
180
|
+
describe("DbHelper JOIN - processJoinWhere", () => {
|
|
181
|
+
test("简单条件", () => {
|
|
182
|
+
const where = { "order.userId": 1, "order.state": 1 };
|
|
182
183
|
const result = processJoinWhere(where);
|
|
183
|
-
expect(result).toEqual({
|
|
184
|
+
expect(result).toEqual({ "order.user_id": 1, "order.state": 1 });
|
|
184
185
|
});
|
|
185
186
|
|
|
186
|
-
test(
|
|
187
|
-
const where = {
|
|
187
|
+
test("带操作符的条件", () => {
|
|
188
|
+
const where = { "order.createdAt$gt": 1000, "user.state$ne": 0 };
|
|
188
189
|
const result = processJoinWhere(where);
|
|
189
|
-
expect(result).toEqual({
|
|
190
|
+
expect(result).toEqual({ "order.created_at$gt": 1000, "user.state$ne": 0 });
|
|
190
191
|
});
|
|
191
192
|
|
|
192
|
-
test(
|
|
193
|
+
test("$or 条件", () => {
|
|
193
194
|
const where = {
|
|
194
|
-
$or: [{
|
|
195
|
+
$or: [{ "user.userName$like": "%test%" }, { "user.email$like": "%test%" }]
|
|
195
196
|
};
|
|
196
197
|
const result = processJoinWhere(where);
|
|
197
198
|
expect(result).toEqual({
|
|
198
|
-
$or: [{
|
|
199
|
+
$or: [{ "user.user_name$like": "%test%" }, { "user.email$like": "%test%" }]
|
|
199
200
|
});
|
|
200
201
|
});
|
|
201
202
|
|
|
202
|
-
test(
|
|
203
|
+
test("复杂嵌套条件", () => {
|
|
203
204
|
const where = {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
$or: [{
|
|
207
|
-
|
|
205
|
+
"order.state": 1,
|
|
206
|
+
"user.state": 1,
|
|
207
|
+
$or: [{ "user.userName$like": "%test%" }, { "product.name$like": "%test%" }],
|
|
208
|
+
"order.createdAt$gte": 1000
|
|
208
209
|
};
|
|
209
210
|
const result = processJoinWhere(where);
|
|
210
211
|
expect(result).toEqual({
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
$or: [{
|
|
214
|
-
|
|
212
|
+
"order.state": 1,
|
|
213
|
+
"user.state": 1,
|
|
214
|
+
$or: [{ "user.user_name$like": "%test%" }, { "product.name$like": "%test%" }],
|
|
215
|
+
"order.created_at$gte": 1000
|
|
215
216
|
});
|
|
216
217
|
});
|
|
217
218
|
});
|
|
218
219
|
|
|
219
|
-
describe(
|
|
220
|
-
test(
|
|
221
|
-
const orderBy = [
|
|
220
|
+
describe("DbHelper JOIN - processJoinOrderBy", () => {
|
|
221
|
+
test("带表名的排序", () => {
|
|
222
|
+
const orderBy = ["order.createdAt#DESC", "user.userName#ASC"];
|
|
222
223
|
const result = processJoinOrderBy(orderBy);
|
|
223
|
-
expect(result).toEqual([
|
|
224
|
+
expect(result).toEqual(["order.created_at#DESC", "user.user_name#ASC"]);
|
|
224
225
|
});
|
|
225
226
|
|
|
226
|
-
test(
|
|
227
|
-
const orderBy = [
|
|
227
|
+
test("普通排序", () => {
|
|
228
|
+
const orderBy = ["createdAt#DESC"];
|
|
228
229
|
const result = processJoinOrderBy(orderBy);
|
|
229
|
-
expect(result).toEqual([
|
|
230
|
+
expect(result).toEqual(["created_at#DESC"]);
|
|
230
231
|
});
|
|
231
232
|
|
|
232
|
-
test(
|
|
233
|
-
const orderBy = [
|
|
233
|
+
test("无排序方向的保持原样", () => {
|
|
234
|
+
const orderBy = ["id"];
|
|
234
235
|
const result = processJoinOrderBy(orderBy);
|
|
235
|
-
expect(result).toEqual([
|
|
236
|
+
expect(result).toEqual(["id"]);
|
|
236
237
|
});
|
|
237
238
|
});
|
|
238
239
|
|
|
239
|
-
describe(
|
|
240
|
-
test(
|
|
241
|
-
const join = { table:
|
|
240
|
+
describe("DbHelper JOIN - JoinOption 类型验证", () => {
|
|
241
|
+
test("LEFT JOIN(默认)", () => {
|
|
242
|
+
const join = { table: "user", on: "order.user_id = user.id" };
|
|
242
243
|
expect(join.type).toBeUndefined();
|
|
243
|
-
expect(join.table).toBe(
|
|
244
|
-
expect(join.on).toBe(
|
|
244
|
+
expect(join.table).toBe("user");
|
|
245
|
+
expect(join.on).toBe("order.user_id = user.id");
|
|
245
246
|
});
|
|
246
247
|
|
|
247
|
-
test(
|
|
248
|
-
const join = { type:
|
|
249
|
-
expect(join.type).toBe(
|
|
248
|
+
test("INNER JOIN", () => {
|
|
249
|
+
const join = { type: "inner" as const, table: "product", on: "order.product_id = product.id" };
|
|
250
|
+
expect(join.type).toBe("inner");
|
|
250
251
|
});
|
|
251
252
|
|
|
252
|
-
test(
|
|
253
|
-
const join = {
|
|
254
|
-
|
|
253
|
+
test("RIGHT JOIN", () => {
|
|
254
|
+
const join = {
|
|
255
|
+
type: "right" as const,
|
|
256
|
+
table: "category",
|
|
257
|
+
on: "product.category_id = category.id"
|
|
258
|
+
};
|
|
259
|
+
expect(join.type).toBe("right");
|
|
255
260
|
});
|
|
256
261
|
});
|
|
257
262
|
|
|
258
|
-
describe(
|
|
259
|
-
test(
|
|
263
|
+
describe("DbHelper JOIN - 完整场景模拟", () => {
|
|
264
|
+
test("订单列表联查参数处理", () => {
|
|
260
265
|
// 模拟输入
|
|
261
266
|
const options = {
|
|
262
|
-
table:
|
|
267
|
+
table: "order",
|
|
263
268
|
joins: [
|
|
264
|
-
{ table:
|
|
265
|
-
{ table:
|
|
269
|
+
{ table: "user", on: "order.userId = user.id" },
|
|
270
|
+
{ table: "product", on: "order.productId = product.id" }
|
|
266
271
|
],
|
|
267
|
-
fields: [
|
|
272
|
+
fields: ["order.id", "order.totalAmount", "user.userName", "product.name AS productName"],
|
|
268
273
|
where: {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
274
|
+
"order.state": 1,
|
|
275
|
+
"user.state": 1,
|
|
276
|
+
"order.createdAt$gte": 1701388800000
|
|
272
277
|
},
|
|
273
|
-
orderBy: [
|
|
278
|
+
orderBy: ["order.createdAt#DESC"]
|
|
274
279
|
};
|
|
275
280
|
|
|
276
281
|
// 处理表名
|
|
277
282
|
const processedTable = processTableName(options.table);
|
|
278
|
-
expect(processedTable).toBe(
|
|
283
|
+
expect(processedTable).toBe("order");
|
|
279
284
|
|
|
280
285
|
// 处理字段
|
|
281
286
|
const processedFields = options.fields.map((f) => processJoinField(f));
|
|
282
|
-
expect(processedFields).toEqual([
|
|
287
|
+
expect(processedFields).toEqual(["order.id", "order.total_amount", "user.user_name", "product.name AS productName"]);
|
|
283
288
|
|
|
284
289
|
// 处理 where
|
|
285
290
|
const processedWhere = processJoinWhere(options.where);
|
|
286
291
|
expect(processedWhere).toEqual({
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
"order.state": 1,
|
|
293
|
+
"user.state": 1,
|
|
294
|
+
"order.created_at$gte": 1701388800000
|
|
290
295
|
});
|
|
291
296
|
|
|
292
297
|
// 处理 orderBy
|
|
293
298
|
const processedOrderBy = processJoinOrderBy(options.orderBy);
|
|
294
|
-
expect(processedOrderBy).toEqual([
|
|
299
|
+
expect(processedOrderBy).toEqual(["order.created_at#DESC"]);
|
|
295
300
|
|
|
296
301
|
// 处理 joins
|
|
297
302
|
const processedJoins = options.joins.map((j) => ({
|
|
298
|
-
type: (j as any).type ||
|
|
303
|
+
type: (j as any).type || "left",
|
|
299
304
|
table: processTableName(j.table),
|
|
300
305
|
on: j.on
|
|
301
306
|
}));
|
|
302
307
|
expect(processedJoins).toEqual([
|
|
303
|
-
{ type:
|
|
304
|
-
{ type:
|
|
308
|
+
{ type: "left", table: "user", on: "order.userId = user.id" },
|
|
309
|
+
{ type: "left", table: "product", on: "order.productId = product.id" }
|
|
305
310
|
]);
|
|
306
311
|
});
|
|
307
312
|
});
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* 验证 Redis 缓存的字段查询功能
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { CacheKeys } from "../lib/cacheKeys.js";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
|
|
8
|
+
|
|
9
|
+
console.log("\n========== Redis 缓存验证 ==========\n");
|
|
8
10
|
|
|
9
11
|
// 模拟 Redis 缓存逻辑
|
|
10
12
|
class MockRedis {
|
|
@@ -34,13 +36,13 @@ async function queryDatabase(table: string): Promise<string[]> {
|
|
|
34
36
|
console.log(`🔍 查询数据库表结构: ${table}`);
|
|
35
37
|
// 模拟数据库延迟
|
|
36
38
|
await new Promise((resolve) => setTimeout(resolve, 3));
|
|
37
|
-
return [
|
|
39
|
+
return ["id", "name", "email", "password", "salt", "created_at"];
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
// 模拟 getTableColumns 方法
|
|
41
43
|
async function getTableColumns(redis: MockRedis, table: string): Promise<string[]> {
|
|
42
44
|
// 1. 先查 Redis 缓存
|
|
43
|
-
const cacheKey =
|
|
45
|
+
const cacheKey = CacheKeys.tableColumns(table);
|
|
44
46
|
let columns = await redis.getObject<string[]>(cacheKey);
|
|
45
47
|
|
|
46
48
|
if (columns && columns.length > 0) {
|
|
@@ -51,7 +53,7 @@ async function getTableColumns(redis: MockRedis, table: string): Promise<string[
|
|
|
51
53
|
columns = await queryDatabase(table);
|
|
52
54
|
|
|
53
55
|
// 3. 写入 Redis 缓存
|
|
54
|
-
await redis.setObject(cacheKey, columns,
|
|
56
|
+
await redis.setObject(cacheKey, columns, TABLE_COLUMNS_CACHE_TTL_SECONDS);
|
|
55
57
|
|
|
56
58
|
return columns;
|
|
57
59
|
}
|
|
@@ -59,63 +61,63 @@ async function getTableColumns(redis: MockRedis, table: string): Promise<string[
|
|
|
59
61
|
async function test() {
|
|
60
62
|
const redis = new MockRedis();
|
|
61
63
|
|
|
62
|
-
console.log(
|
|
64
|
+
console.log("【场景1】单进程多次查询\n");
|
|
63
65
|
|
|
64
66
|
// 第1次查询(缓存未命中)
|
|
65
|
-
console.log(
|
|
67
|
+
console.log("--- 第1次查询 user 表 ---");
|
|
66
68
|
const start1 = Date.now();
|
|
67
|
-
const columns1 = await getTableColumns(redis,
|
|
69
|
+
const columns1 = await getTableColumns(redis, "user");
|
|
68
70
|
const time1 = Date.now() - start1;
|
|
69
|
-
console.log(`结果: ${columns1.join(
|
|
71
|
+
console.log(`结果: ${columns1.join(", ")}`);
|
|
70
72
|
console.log(`耗时: ${time1}ms\n`);
|
|
71
73
|
|
|
72
74
|
// 第2次查询(缓存命中)
|
|
73
|
-
console.log(
|
|
75
|
+
console.log("--- 第2次查询 user 表 ---");
|
|
74
76
|
const start2 = Date.now();
|
|
75
|
-
const columns2 = await getTableColumns(redis,
|
|
77
|
+
const columns2 = await getTableColumns(redis, "user");
|
|
76
78
|
const time2 = Date.now() - start2;
|
|
77
|
-
console.log(`结果: ${columns2.join(
|
|
79
|
+
console.log(`结果: ${columns2.join(", ")}`);
|
|
78
80
|
console.log(`耗时: ${time2}ms\n`);
|
|
79
81
|
|
|
80
82
|
// 第3次查询(缓存命中)
|
|
81
|
-
console.log(
|
|
83
|
+
console.log("--- 第3次查询 user 表 ---");
|
|
82
84
|
const start3 = Date.now();
|
|
83
|
-
const columns3 = await getTableColumns(redis,
|
|
85
|
+
const columns3 = await getTableColumns(redis, "user");
|
|
84
86
|
const time3 = Date.now() - start3;
|
|
85
|
-
console.log(`结果: ${columns3.join(
|
|
87
|
+
console.log(`结果: ${columns3.join(", ")}`);
|
|
86
88
|
console.log(`耗时: ${time3}ms\n`);
|
|
87
89
|
|
|
88
|
-
console.log(
|
|
90
|
+
console.log("【场景2】模拟 PM2 cluster(多进程共享 Redis)\n");
|
|
89
91
|
|
|
90
92
|
// 模拟 Worker 1 查询
|
|
91
|
-
console.log(
|
|
93
|
+
console.log("--- Worker 1 查询 article 表 ---");
|
|
92
94
|
const worker1Start = Date.now();
|
|
93
|
-
const worker1Columns = await getTableColumns(redis,
|
|
95
|
+
const worker1Columns = await getTableColumns(redis, "article");
|
|
94
96
|
const worker1Time = Date.now() - worker1Start;
|
|
95
|
-
console.log(`结果: ${worker1Columns.join(
|
|
97
|
+
console.log(`结果: ${worker1Columns.join(", ")}`);
|
|
96
98
|
console.log(`耗时: ${worker1Time}ms\n`);
|
|
97
99
|
|
|
98
100
|
// 模拟 Worker 2 查询(共享 Redis 缓存)
|
|
99
|
-
console.log(
|
|
101
|
+
console.log("--- Worker 2 查询 article 表 ---");
|
|
100
102
|
const worker2Start = Date.now();
|
|
101
|
-
const worker2Columns = await getTableColumns(redis,
|
|
103
|
+
const worker2Columns = await getTableColumns(redis, "article");
|
|
102
104
|
const worker2Time = Date.now() - worker2Start;
|
|
103
|
-
console.log(`结果: ${worker2Columns.join(
|
|
105
|
+
console.log(`结果: ${worker2Columns.join(", ")}`);
|
|
104
106
|
console.log(`耗时: ${worker2Time}ms`);
|
|
105
107
|
console.log(`✅ Worker 2 直接使用 Worker 1 的缓存,无需再查数据库\n`);
|
|
106
108
|
|
|
107
109
|
// 模拟 Worker 3 查询(共享 Redis 缓存)
|
|
108
|
-
console.log(
|
|
110
|
+
console.log("--- Worker 3 查询 article 表 ---");
|
|
109
111
|
const worker3Start = Date.now();
|
|
110
|
-
const worker3Columns = await getTableColumns(redis,
|
|
112
|
+
const worker3Columns = await getTableColumns(redis, "article");
|
|
111
113
|
const worker3Time = Date.now() - worker3Start;
|
|
112
|
-
console.log(`结果: ${worker3Columns.join(
|
|
114
|
+
console.log(`结果: ${worker3Columns.join(", ")}`);
|
|
113
115
|
console.log(`耗时: ${worker3Time}ms`);
|
|
114
116
|
console.log(`✅ Worker 3 直接使用 Worker 1 的缓存,无需再查数据库\n`);
|
|
115
117
|
|
|
116
|
-
console.log(
|
|
118
|
+
console.log("========== 验证完成 ==========\n");
|
|
117
119
|
|
|
118
|
-
console.log(
|
|
120
|
+
console.log("📊 性能总结:");
|
|
119
121
|
console.log(`- 首次查询(数据库): ${time1}ms`);
|
|
120
122
|
console.log(`- 后续查询(Redis): ${time2}ms`);
|
|
121
123
|
console.log(`- 性能提升: ${(time1 / time2).toFixed(1)}x`);
|