befly 3.9.38 → 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 +37 -38
- 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} +223 -231
- package/docs/cipher.md +71 -69
- package/docs/database.md +143 -141
- 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} +1 -1
- 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 +7 -7
- package/lib/asyncContext.ts +43 -0
- package/lib/cacheHelper.ts +212 -77
- package/lib/cacheKeys.ts +38 -0
- package/lib/cipher.ts +30 -30
- package/lib/connect.ts +28 -28
- package/lib/dbHelper.ts +183 -102
- 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 +48 -44
- 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 -52
- 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 -65
- package/sync/syncMenu.ts +190 -55
- 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,117 +3,117 @@
|
|
|
3
3
|
* 测试边界条件、错误处理、复杂场景
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe, test, expect } from
|
|
6
|
+
import { describe, test, expect } from "bun:test";
|
|
7
7
|
|
|
8
|
-
describe(
|
|
9
|
-
test(
|
|
8
|
+
describe("DbHelper - 字段验证逻辑", () => {
|
|
9
|
+
test("validateAndClassifyFields - 空数组应返回 all 类型", () => {
|
|
10
10
|
// 测试目的:验证空数组被正确识别为查询所有字段
|
|
11
11
|
const mockHelper = {
|
|
12
12
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
13
13
|
if (!fields || fields.length === 0) {
|
|
14
|
-
return { type:
|
|
14
|
+
return { type: "all" as const, fields: [] };
|
|
15
15
|
}
|
|
16
|
-
return { type:
|
|
16
|
+
return { type: "include" as const, fields: fields };
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
const result = mockHelper.validateAndClassifyFields([]);
|
|
21
|
-
expect(result.type).toBe(
|
|
21
|
+
expect(result.type).toBe("all");
|
|
22
22
|
expect(result.fields.length).toBe(0);
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
test(
|
|
25
|
+
test("validateAndClassifyFields - undefined 应返回 all 类型", () => {
|
|
26
26
|
const mockHelper = {
|
|
27
27
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
28
28
|
if (!fields || fields.length === 0) {
|
|
29
|
-
return { type:
|
|
29
|
+
return { type: "all" as const, fields: [] };
|
|
30
30
|
}
|
|
31
|
-
return { type:
|
|
31
|
+
return { type: "include" as const, fields: fields };
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
|
|
35
35
|
const result = mockHelper.validateAndClassifyFields(undefined);
|
|
36
|
-
expect(result.type).toBe(
|
|
36
|
+
expect(result.type).toBe("all");
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
test(
|
|
39
|
+
test("validateAndClassifyFields - 星号应抛出错误", () => {
|
|
40
40
|
const mockHelper = {
|
|
41
41
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
42
42
|
if (!fields || fields.length === 0) {
|
|
43
|
-
return { type:
|
|
43
|
+
return { type: "all" as const, fields: [] };
|
|
44
44
|
}
|
|
45
|
-
if (fields.some((f) => f ===
|
|
46
|
-
throw new Error(
|
|
45
|
+
if (fields.some((f) => f === "*")) {
|
|
46
|
+
throw new Error("fields 不支持 * 星号");
|
|
47
47
|
}
|
|
48
|
-
return { type:
|
|
48
|
+
return { type: "include" as const, fields: fields };
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
expect(() => {
|
|
53
|
-
mockHelper.validateAndClassifyFields([
|
|
54
|
-
}).toThrow(
|
|
53
|
+
mockHelper.validateAndClassifyFields(["id", "*", "name"]);
|
|
54
|
+
}).toThrow("fields 不支持 * 星号");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
test(
|
|
57
|
+
test("validateAndClassifyFields - 空字符串应抛出错误", () => {
|
|
58
58
|
const mockHelper = {
|
|
59
59
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
60
|
-
if (fields.some((f) => !f || typeof f !==
|
|
61
|
-
throw new Error(
|
|
60
|
+
if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
|
|
61
|
+
throw new Error("fields 不能包含空字符串或无效值");
|
|
62
62
|
}
|
|
63
|
-
return { type:
|
|
63
|
+
return { type: "include" as const, fields: fields };
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
expect(() => {
|
|
68
|
-
mockHelper.validateAndClassifyFields([
|
|
69
|
-
}).toThrow(
|
|
68
|
+
mockHelper.validateAndClassifyFields(["id", "", "name"]);
|
|
69
|
+
}).toThrow("fields 不能包含空字符串或无效值");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
test(
|
|
72
|
+
test("validateAndClassifyFields - 混用包含和排除字段应抛出错误", () => {
|
|
73
73
|
const mockHelper = {
|
|
74
74
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
75
|
-
const includeFields = fields!.filter((f) => !f.startsWith(
|
|
76
|
-
const excludeFields = fields!.filter((f) => f.startsWith(
|
|
75
|
+
const includeFields = fields!.filter((f) => !f.startsWith("!"));
|
|
76
|
+
const excludeFields = fields!.filter((f) => f.startsWith("!"));
|
|
77
77
|
|
|
78
78
|
if (includeFields.length > 0 && excludeFields.length > 0) {
|
|
79
|
-
throw new Error(
|
|
79
|
+
throw new Error("fields 不能同时包含普通字段和排除字段");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
if (excludeFields.length > 0) {
|
|
83
|
-
return { type:
|
|
83
|
+
return { type: "exclude" as const, fields: excludeFields.map((f) => f.substring(1)) };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
return { type:
|
|
86
|
+
return { type: "include" as const, fields: includeFields };
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
|
|
90
90
|
expect(() => {
|
|
91
|
-
mockHelper.validateAndClassifyFields([
|
|
92
|
-
}).toThrow(
|
|
91
|
+
mockHelper.validateAndClassifyFields(["id", "!password", "name"]);
|
|
92
|
+
}).toThrow("fields 不能同时包含普通字段和排除字段");
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
test(
|
|
95
|
+
test("validateAndClassifyFields - 排除字段应正确去除感叹号", () => {
|
|
96
96
|
const mockHelper = {
|
|
97
97
|
validateAndClassifyFields: (fields?: string[]) => {
|
|
98
|
-
const excludeFields = fields!.filter((f) => f.startsWith(
|
|
99
|
-
return { type:
|
|
98
|
+
const excludeFields = fields!.filter((f) => f.startsWith("!"));
|
|
99
|
+
return { type: "exclude" as const, fields: excludeFields.map((f) => f.substring(1)) };
|
|
100
100
|
}
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
-
const result = mockHelper.validateAndClassifyFields([
|
|
104
|
-
expect(result.type).toBe(
|
|
105
|
-
expect(result.fields).toEqual([
|
|
103
|
+
const result = mockHelper.validateAndClassifyFields(["!password", "!token", "!salt"]);
|
|
104
|
+
expect(result.type).toBe("exclude");
|
|
105
|
+
expect(result.fields).toEqual(["password", "token", "salt"]);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
describe(
|
|
110
|
-
test(
|
|
109
|
+
describe("DbHelper - BIGINT 字段转换逻辑", () => {
|
|
110
|
+
test("convertBigIntFields - 白名单字段应转换", () => {
|
|
111
111
|
const mockConvert = (arr: any[]) => {
|
|
112
112
|
return arr.map((item) => {
|
|
113
113
|
const converted = { ...item };
|
|
114
|
-
const whiteList = [
|
|
114
|
+
const whiteList = ["id", "pid", "sort"];
|
|
115
115
|
for (const key of whiteList) {
|
|
116
|
-
if (key in converted && typeof converted[key] ===
|
|
116
|
+
if (key in converted && typeof converted[key] === "string") {
|
|
117
117
|
converted[key] = Number(converted[key]);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
@@ -121,21 +121,21 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
121
121
|
});
|
|
122
122
|
};
|
|
123
123
|
|
|
124
|
-
const data = [{ id:
|
|
124
|
+
const data = [{ id: "123", pid: "456", sort: "10", name: "test" }];
|
|
125
125
|
const result = mockConvert(data);
|
|
126
126
|
|
|
127
127
|
expect(result[0].id).toBe(123);
|
|
128
128
|
expect(result[0].pid).toBe(456);
|
|
129
129
|
expect(result[0].sort).toBe(10);
|
|
130
|
-
expect(typeof result[0].id).toBe(
|
|
130
|
+
expect(typeof result[0].id).toBe("number");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
test(
|
|
133
|
+
test("convertBigIntFields - Id 后缀字段应转换", () => {
|
|
134
134
|
const mockConvert = (arr: any[]) => {
|
|
135
135
|
return arr.map((item) => {
|
|
136
136
|
const converted = { ...item };
|
|
137
137
|
for (const [key, value] of Object.entries(converted)) {
|
|
138
|
-
if ((key.endsWith(
|
|
138
|
+
if ((key.endsWith("Id") || key.endsWith("_id")) && typeof value === "string") {
|
|
139
139
|
converted[key] = Number(value);
|
|
140
140
|
}
|
|
141
141
|
}
|
|
@@ -143,7 +143,7 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
143
143
|
});
|
|
144
144
|
};
|
|
145
145
|
|
|
146
|
-
const data = [{ userId:
|
|
146
|
+
const data = [{ userId: "100", roleId: "200", category_id: "300", name: "test" }];
|
|
147
147
|
const result = mockConvert(data);
|
|
148
148
|
|
|
149
149
|
expect(result[0].userId).toBe(100);
|
|
@@ -151,12 +151,12 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
151
151
|
expect(result[0].category_id).toBe(300);
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
-
test(
|
|
154
|
+
test("convertBigIntFields - At 后缀字段应转换(时间戳)", () => {
|
|
155
155
|
const mockConvert = (arr: any[]) => {
|
|
156
156
|
return arr.map((item) => {
|
|
157
157
|
const converted = { ...item };
|
|
158
158
|
for (const [key, value] of Object.entries(converted)) {
|
|
159
|
-
if ((key.endsWith(
|
|
159
|
+
if ((key.endsWith("At") || key.endsWith("_at")) && typeof value === "string") {
|
|
160
160
|
converted[key] = Number(value);
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -164,7 +164,7 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
164
164
|
});
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
const data = [{ createdAt:
|
|
167
|
+
const data = [{ createdAt: "1609459200000", updatedAt: "1609459300000", deleted_at: "0" }];
|
|
168
168
|
const result = mockConvert(data);
|
|
169
169
|
|
|
170
170
|
expect(result[0].createdAt).toBe(1609459200000);
|
|
@@ -172,12 +172,12 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
172
172
|
expect(result[0].deleted_at).toBe(0);
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
test(
|
|
175
|
+
test("convertBigIntFields - 非数字字符串不应转换", () => {
|
|
176
176
|
const mockConvert = (arr: any[]) => {
|
|
177
177
|
return arr.map((item) => {
|
|
178
178
|
const converted = { ...item };
|
|
179
179
|
for (const [key, value] of Object.entries(converted)) {
|
|
180
|
-
if (key.endsWith(
|
|
180
|
+
if (key.endsWith("Id") && typeof value === "string") {
|
|
181
181
|
const num = Number(value);
|
|
182
182
|
if (!isNaN(num)) {
|
|
183
183
|
converted[key] = num;
|
|
@@ -188,20 +188,20 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
188
188
|
});
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
const data = [{ userId:
|
|
191
|
+
const data = [{ userId: "invalid", roleId: "123" }];
|
|
192
192
|
const result = mockConvert(data);
|
|
193
193
|
|
|
194
|
-
expect(result[0].userId).toBe(
|
|
194
|
+
expect(result[0].userId).toBe("invalid"); // 保持原值
|
|
195
195
|
expect(result[0].roleId).toBe(123);
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
test(
|
|
198
|
+
test("convertBigIntFields - null 和 undefined 应跳过", () => {
|
|
199
199
|
const mockConvert = (arr: any[]) => {
|
|
200
200
|
return arr.map((item) => {
|
|
201
201
|
const converted = { ...item };
|
|
202
202
|
for (const [key, value] of Object.entries(converted)) {
|
|
203
203
|
if (value === null || value === undefined) continue;
|
|
204
|
-
if (key.endsWith(
|
|
204
|
+
if (key.endsWith("Id") && typeof value === "string") {
|
|
205
205
|
converted[key] = Number(value);
|
|
206
206
|
}
|
|
207
207
|
}
|
|
@@ -209,7 +209,7 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
209
209
|
});
|
|
210
210
|
};
|
|
211
211
|
|
|
212
|
-
const data = [{ userId: null, roleId: undefined, categoryId:
|
|
212
|
+
const data = [{ userId: null, roleId: undefined, categoryId: "123" }];
|
|
213
213
|
const result = mockConvert(data);
|
|
214
214
|
|
|
215
215
|
expect(result[0].userId).toBeNull();
|
|
@@ -217,7 +217,7 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
217
217
|
expect(result[0].categoryId).toBe(123);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
test(
|
|
220
|
+
test("convertBigIntFields - 空数组应返回空数组", () => {
|
|
221
221
|
const mockConvert = (arr: any[]) => {
|
|
222
222
|
if (!arr || !Array.isArray(arr)) return arr;
|
|
223
223
|
return arr;
|
|
@@ -227,7 +227,7 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
227
227
|
expect(result).toEqual([]);
|
|
228
228
|
});
|
|
229
229
|
|
|
230
|
-
test(
|
|
230
|
+
test("convertBigIntFields - 非数组应返回原值", () => {
|
|
231
231
|
const mockConvert = (arr: any) => {
|
|
232
232
|
if (!arr || !Array.isArray(arr)) return arr;
|
|
233
233
|
return arr;
|
|
@@ -238,8 +238,8 @@ describe('DbHelper - BIGINT 字段转换逻辑', () => {
|
|
|
238
238
|
});
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
-
describe(
|
|
242
|
-
test(
|
|
241
|
+
describe("DbHelper - WHERE 条件键名转换", () => {
|
|
242
|
+
test("whereKeysToSnake - 简单字段名应转换为下划线", () => {
|
|
243
243
|
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
244
244
|
|
|
245
245
|
const mockConvert = (where: any): any => {
|
|
@@ -250,21 +250,21 @@ describe('DbHelper - WHERE 条件键名转换', () => {
|
|
|
250
250
|
return result;
|
|
251
251
|
};
|
|
252
252
|
|
|
253
|
-
const where = { userId: 123, userName:
|
|
253
|
+
const where = { userId: 123, userName: "john" };
|
|
254
254
|
const result = mockConvert(where);
|
|
255
255
|
|
|
256
256
|
expect(result.user_id).toBe(123);
|
|
257
|
-
expect(result.user_name).toBe(
|
|
257
|
+
expect(result.user_name).toBe("john");
|
|
258
258
|
});
|
|
259
259
|
|
|
260
|
-
test(
|
|
260
|
+
test("whereKeysToSnake - 带操作符的字段名应正确处理", () => {
|
|
261
261
|
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
262
262
|
|
|
263
263
|
const mockConvert = (where: any): any => {
|
|
264
264
|
const result: any = {};
|
|
265
265
|
for (const [key, value] of Object.entries(where)) {
|
|
266
|
-
if (key.includes(
|
|
267
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
266
|
+
if (key.includes("$")) {
|
|
267
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
268
268
|
const fieldName = key.substring(0, lastDollarIndex);
|
|
269
269
|
const operator = key.substring(lastDollarIndex);
|
|
270
270
|
result[snakeCase(fieldName) + operator] = value;
|
|
@@ -275,25 +275,25 @@ describe('DbHelper - WHERE 条件键名转换', () => {
|
|
|
275
275
|
return result;
|
|
276
276
|
};
|
|
277
277
|
|
|
278
|
-
const where = { userId$gt: 100, userName$like:
|
|
278
|
+
const where = { userId$gt: 100, userName$like: "%john%" };
|
|
279
279
|
const result = mockConvert(where);
|
|
280
280
|
|
|
281
281
|
expect(result.user_id$gt).toBe(100);
|
|
282
|
-
expect(result.user_name$like).toBe(
|
|
282
|
+
expect(result.user_name$like).toBe("%john%");
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
test(
|
|
285
|
+
test("whereKeysToSnake - $or 和 $and 应递归处理", () => {
|
|
286
286
|
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
287
287
|
|
|
288
288
|
const mockConvert = (where: any): any => {
|
|
289
|
-
if (!where || typeof where !==
|
|
289
|
+
if (!where || typeof where !== "object") return where;
|
|
290
290
|
if (Array.isArray(where)) {
|
|
291
291
|
return where.map((item) => mockConvert(item));
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
const result: any = {};
|
|
295
295
|
for (const [key, value] of Object.entries(where)) {
|
|
296
|
-
if (key ===
|
|
296
|
+
if (key === "$or" || key === "$and") {
|
|
297
297
|
result[key] = (value as any[]).map((item) => mockConvert(item));
|
|
298
298
|
} else {
|
|
299
299
|
result[snakeCase(key)] = value;
|
|
@@ -303,24 +303,24 @@ describe('DbHelper - WHERE 条件键名转换', () => {
|
|
|
303
303
|
};
|
|
304
304
|
|
|
305
305
|
const where = {
|
|
306
|
-
$or: [{ userId: 1 }, { userName:
|
|
306
|
+
$or: [{ userId: 1 }, { userName: "john" }]
|
|
307
307
|
};
|
|
308
308
|
const result = mockConvert(where);
|
|
309
309
|
|
|
310
310
|
expect(result.$or[0].user_id).toBe(1);
|
|
311
|
-
expect(result.$or[1].user_name).toBe(
|
|
311
|
+
expect(result.$or[1].user_name).toBe("john");
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
test(
|
|
314
|
+
test("whereKeysToSnake - 嵌套对象应递归转换", () => {
|
|
315
315
|
const snakeCase = (str: string) => str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
316
316
|
|
|
317
317
|
const mockConvert = (where: any): any => {
|
|
318
|
-
if (!where || typeof where !==
|
|
318
|
+
if (!where || typeof where !== "object") return where;
|
|
319
319
|
|
|
320
320
|
const result: any = {};
|
|
321
321
|
for (const [key, value] of Object.entries(where)) {
|
|
322
322
|
const snakeKey = snakeCase(key);
|
|
323
|
-
if (typeof value ===
|
|
323
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
324
324
|
result[snakeKey] = mockConvert(value);
|
|
325
325
|
} else {
|
|
326
326
|
result[snakeKey] = value;
|
|
@@ -331,19 +331,19 @@ describe('DbHelper - WHERE 条件键名转换', () => {
|
|
|
331
331
|
|
|
332
332
|
const where = {
|
|
333
333
|
userProfile: {
|
|
334
|
-
firstName:
|
|
335
|
-
lastName:
|
|
334
|
+
firstName: "John",
|
|
335
|
+
lastName: "Doe"
|
|
336
336
|
}
|
|
337
337
|
};
|
|
338
338
|
const result = mockConvert(where);
|
|
339
339
|
|
|
340
|
-
expect(result.user_profile.first_name).toBe(
|
|
341
|
-
expect(result.user_profile.last_name).toBe(
|
|
340
|
+
expect(result.user_profile.first_name).toBe("John");
|
|
341
|
+
expect(result.user_profile.last_name).toBe("Doe");
|
|
342
342
|
});
|
|
343
343
|
});
|
|
344
344
|
|
|
345
|
-
describe(
|
|
346
|
-
test(
|
|
345
|
+
describe("DbHelper - 默认状态过滤", () => {
|
|
346
|
+
test("addDefaultStateFilter - 应添加 state > 0 条件", () => {
|
|
347
347
|
const mockAddFilter = (where: any) => {
|
|
348
348
|
return {
|
|
349
349
|
...where,
|
|
@@ -358,13 +358,13 @@ describe('DbHelper - 默认状态过滤', () => {
|
|
|
358
358
|
expect(result.state$gt).toBe(0);
|
|
359
359
|
});
|
|
360
360
|
|
|
361
|
-
test(
|
|
361
|
+
test("addDefaultStateFilter - 已有 state 条件不应覆盖", () => {
|
|
362
362
|
// **潜在问题**:如果用户传入 state: 0(查询已删除数据),
|
|
363
363
|
// addDefaultStateFilter 可能会添加 state$gt: 0,导致冲突
|
|
364
364
|
|
|
365
365
|
const mockAddFilter = (where: any) => {
|
|
366
366
|
// 正确做法:检查是否已有 state 相关条件
|
|
367
|
-
if (
|
|
367
|
+
if ("state" in where || "state$gt" in where || "state$gte" in where) {
|
|
368
368
|
return where; // 不添加默认过滤
|
|
369
369
|
}
|
|
370
370
|
return {
|
|
@@ -384,49 +384,49 @@ describe('DbHelper - 默认状态过滤', () => {
|
|
|
384
384
|
});
|
|
385
385
|
});
|
|
386
386
|
|
|
387
|
-
describe(
|
|
388
|
-
test(
|
|
389
|
-
const mockValidate = (page: number,
|
|
387
|
+
describe("DbHelper - 分页参数验证", () => {
|
|
388
|
+
test("getList - page 小于 1 应抛出错误", () => {
|
|
389
|
+
const mockValidate = (page: number, _limit: number) => {
|
|
390
390
|
if (page < 1 || page > 10000) {
|
|
391
391
|
throw new Error(`页码必须在 1 到 10000 之间 (page: ${page})`);
|
|
392
392
|
}
|
|
393
393
|
};
|
|
394
394
|
|
|
395
|
-
expect(() => mockValidate(0, 10)).toThrow(
|
|
396
|
-
expect(() => mockValidate(-5, 10)).toThrow(
|
|
395
|
+
expect(() => mockValidate(0, 10)).toThrow("页码必须在 1 到 10000 之间");
|
|
396
|
+
expect(() => mockValidate(-5, 10)).toThrow("页码必须在 1 到 10000 之间");
|
|
397
397
|
});
|
|
398
398
|
|
|
399
|
-
test(
|
|
399
|
+
test("getList - page 大于 10000 应抛出错误", () => {
|
|
400
400
|
const mockValidate = (page: number) => {
|
|
401
401
|
if (page > 10000) {
|
|
402
402
|
throw new Error(`页码必须在 1 到 10000 之间 (page: ${page})`);
|
|
403
403
|
}
|
|
404
404
|
};
|
|
405
405
|
|
|
406
|
-
expect(() => mockValidate(10001)).toThrow(
|
|
406
|
+
expect(() => mockValidate(10001)).toThrow("页码必须在 1 到 10000 之间");
|
|
407
407
|
});
|
|
408
408
|
|
|
409
|
-
test(
|
|
409
|
+
test("getList - limit 小于 1 应抛出错误", () => {
|
|
410
410
|
const mockValidate = (limit: number) => {
|
|
411
411
|
if (limit < 1 || limit > 1000) {
|
|
412
412
|
throw new Error(`每页数量必须在 1 到 1000 之间 (limit: ${limit})`);
|
|
413
413
|
}
|
|
414
414
|
};
|
|
415
415
|
|
|
416
|
-
expect(() => mockValidate(0)).toThrow(
|
|
416
|
+
expect(() => mockValidate(0)).toThrow("每页数量必须在 1 到 1000 之间");
|
|
417
417
|
});
|
|
418
418
|
|
|
419
|
-
test(
|
|
419
|
+
test("getList - limit 大于 1000 应抛出错误", () => {
|
|
420
420
|
const mockValidate = (limit: number) => {
|
|
421
421
|
if (limit > 1000) {
|
|
422
422
|
throw new Error(`每页数量必须在 1 到 1000 之间 (limit: ${limit})`);
|
|
423
423
|
}
|
|
424
424
|
};
|
|
425
425
|
|
|
426
|
-
expect(() => mockValidate(1001)).toThrow(
|
|
426
|
+
expect(() => mockValidate(1001)).toThrow("每页数量必须在 1 到 1000 之间");
|
|
427
427
|
});
|
|
428
428
|
|
|
429
|
-
test(
|
|
429
|
+
test("getList - 边界值测试", () => {
|
|
430
430
|
const mockValidate = (page: number, limit: number) => {
|
|
431
431
|
if (page < 1 || page > 10000) {
|
|
432
432
|
throw new Error(`页码无效`);
|
|
@@ -443,8 +443,8 @@ describe('DbHelper - 分页参数验证', () => {
|
|
|
443
443
|
});
|
|
444
444
|
});
|
|
445
445
|
|
|
446
|
-
describe(
|
|
447
|
-
test(
|
|
446
|
+
describe("DbHelper - 总数为 0 的优化", () => {
|
|
447
|
+
test("getList - 总数为 0 应直接返回空结果", async () => {
|
|
448
448
|
const mockGetList = async (total: number, page: number, limit: number) => {
|
|
449
449
|
if (total === 0) {
|
|
450
450
|
return {
|
|
@@ -471,7 +471,7 @@ describe('DbHelper - 总数为 0 的优化', () => {
|
|
|
471
471
|
expect(result.pages).toBe(0);
|
|
472
472
|
});
|
|
473
473
|
|
|
474
|
-
test(
|
|
474
|
+
test("getList - pages 计算应正确", () => {
|
|
475
475
|
const calcPages = (total: number, limit: number) => Math.ceil(total / limit);
|
|
476
476
|
|
|
477
477
|
expect(calcPages(100, 10)).toBe(10); // 整除
|
|
@@ -481,8 +481,8 @@ describe('DbHelper - 总数为 0 的优化', () => {
|
|
|
481
481
|
});
|
|
482
482
|
});
|
|
483
483
|
|
|
484
|
-
describe(
|
|
485
|
-
test(
|
|
484
|
+
describe("DbHelper - 字段清理逻辑", () => {
|
|
485
|
+
test("cleanFields - 应排除 null 和 undefined", () => {
|
|
486
486
|
const mockClean = (data: any) => {
|
|
487
487
|
const result: any = {};
|
|
488
488
|
for (const [key, value] of Object.entries(data)) {
|
|
@@ -493,16 +493,16 @@ describe('DbHelper - 字段清理逻辑', () => {
|
|
|
493
493
|
return result;
|
|
494
494
|
};
|
|
495
495
|
|
|
496
|
-
const data = { name:
|
|
496
|
+
const data = { name: "test", age: null, email: undefined, score: 0 };
|
|
497
497
|
const result = mockClean(data);
|
|
498
498
|
|
|
499
|
-
expect(result.name).toBe(
|
|
499
|
+
expect(result.name).toBe("test");
|
|
500
500
|
expect(result.age).toBeUndefined();
|
|
501
501
|
expect(result.email).toBeUndefined();
|
|
502
502
|
expect(result.score).toBe(0); // 0 应保留
|
|
503
503
|
});
|
|
504
504
|
|
|
505
|
-
test(
|
|
505
|
+
test("cleanFields - 空对象应返回空对象", () => {
|
|
506
506
|
const mockClean = (data: any) => {
|
|
507
507
|
if (!data || Object.keys(data).length === 0) return {};
|
|
508
508
|
return data;
|
|
@@ -514,12 +514,12 @@ describe('DbHelper - 字段清理逻辑', () => {
|
|
|
514
514
|
});
|
|
515
515
|
});
|
|
516
516
|
|
|
517
|
-
describe(
|
|
518
|
-
test(
|
|
517
|
+
describe("DbHelper - SQL 执行错误处理", () => {
|
|
518
|
+
test("executeWithConn - SQL 错误应包含完整信息", () => {
|
|
519
519
|
const mockExecute = async (sql: string, params?: any[]) => {
|
|
520
520
|
try {
|
|
521
521
|
// 模拟 SQL 错误
|
|
522
|
-
throw new Error(
|
|
522
|
+
throw new Error("Table not found");
|
|
523
523
|
} catch (error: any) {
|
|
524
524
|
const errorMsg = `SQL 执行失败 - ${error.message} - SQL: ${sql} - 参数: ${JSON.stringify(params)}`;
|
|
525
525
|
throw new Error(errorMsg);
|
|
@@ -527,28 +527,28 @@ describe('DbHelper - SQL 执行错误处理', () => {
|
|
|
527
527
|
};
|
|
528
528
|
|
|
529
529
|
expect(async () => {
|
|
530
|
-
await mockExecute(
|
|
530
|
+
await mockExecute("SELECT * FROM non_existent_table", []);
|
|
531
531
|
}).toThrow();
|
|
532
532
|
});
|
|
533
533
|
|
|
534
|
-
test(
|
|
534
|
+
test("executeWithConn - 超长 SQL 应截断", () => {
|
|
535
535
|
const mockTruncate = (sql: string, maxLength: number = 500) => {
|
|
536
536
|
if (sql.length > maxLength) {
|
|
537
|
-
return sql.substring(0, maxLength) +
|
|
537
|
+
return sql.substring(0, maxLength) + "...";
|
|
538
538
|
}
|
|
539
539
|
return sql;
|
|
540
540
|
};
|
|
541
541
|
|
|
542
|
-
const longSql =
|
|
542
|
+
const longSql = "SELECT * FROM users WHERE " + "name = ? AND ".repeat(100) + "id = ?";
|
|
543
543
|
const truncated = mockTruncate(longSql, 100);
|
|
544
544
|
|
|
545
545
|
expect(truncated.length).toBeLessThanOrEqual(103); // 100 + "..."
|
|
546
|
-
expect(truncated.endsWith(
|
|
546
|
+
expect(truncated.endsWith("...")).toBe(true);
|
|
547
547
|
});
|
|
548
548
|
});
|
|
549
549
|
|
|
550
|
-
describe(
|
|
551
|
-
test(
|
|
550
|
+
describe("DbHelper - 代码逻辑问题分析", () => {
|
|
551
|
+
test("问题1:addDefaultStateFilter 可能覆盖用户的 state 条件", () => {
|
|
552
552
|
// **问题描述**:
|
|
553
553
|
// 当前实现:addDefaultStateFilter 直接添加 state$gt: 0
|
|
554
554
|
// 问题:如果用户想查询 state=0(已删除)或 state=2(已禁用)的数据,
|
|
@@ -565,7 +565,7 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
565
565
|
|
|
566
566
|
const betterImpl = (where: any) => {
|
|
567
567
|
// 检查是否已有 state 条件
|
|
568
|
-
const hasStateCondition = Object.keys(where).some((key) => key ===
|
|
568
|
+
const hasStateCondition = Object.keys(where).some((key) => key === "state" || key.startsWith("state$"));
|
|
569
569
|
|
|
570
570
|
if (hasStateCondition) {
|
|
571
571
|
return where; // 不添加默认过滤
|
|
@@ -588,7 +588,7 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
588
588
|
expect(result2.state$gt).toBeUndefined(); // 正确
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
-
test(
|
|
591
|
+
test("问题2:getTableColumns 缓存键没有区分数据库", () => {
|
|
592
592
|
// **问题描述**:
|
|
593
593
|
// 缓存键格式:table:columns:${table}
|
|
594
594
|
// 问题:如果连接多个数据库,不同数据库的同名表会共享缓存
|
|
@@ -601,15 +601,15 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
601
601
|
const betterCacheKey = (dbName: string, table: string) => `table:columns:${dbName}:${table}`;
|
|
602
602
|
|
|
603
603
|
// 问题示例
|
|
604
|
-
expect(currentCacheKey(
|
|
604
|
+
expect(currentCacheKey("user")).toBe("table:columns:user");
|
|
605
605
|
// db1.user 和 db2.user 会冲突
|
|
606
606
|
|
|
607
607
|
// 改进后
|
|
608
|
-
expect(betterCacheKey(
|
|
609
|
-
expect(betterCacheKey(
|
|
608
|
+
expect(betterCacheKey("db1", "user")).toBe("table:columns:db1:user");
|
|
609
|
+
expect(betterCacheKey("db2", "user")).toBe("table:columns:db2:user");
|
|
610
610
|
});
|
|
611
611
|
|
|
612
|
-
test(
|
|
612
|
+
test("问题3:convertBigIntFields 白名单硬编码", () => {
|
|
613
613
|
// **问题描述**:
|
|
614
614
|
// 白名单字段硬编码为 ['id', 'pid', 'sort']
|
|
615
615
|
// 问题:如果有其他 BIGINT 字段不符合命名规则,需要修改源码
|
|
@@ -619,11 +619,11 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
619
619
|
// 2. 或者从表结构元数据中自动获取 BIGINT 字段
|
|
620
620
|
|
|
621
621
|
const currentImpl = (arr: any[]) => {
|
|
622
|
-
const whiteList = [
|
|
622
|
+
const whiteList = ["id", "pid", "sort"]; // 硬编码
|
|
623
623
|
return arr.map((item) => {
|
|
624
624
|
const converted = { ...item };
|
|
625
625
|
for (const key of whiteList) {
|
|
626
|
-
if (key in converted && typeof converted[key] ===
|
|
626
|
+
if (key in converted && typeof converted[key] === "string") {
|
|
627
627
|
converted[key] = Number(converted[key]);
|
|
628
628
|
}
|
|
629
629
|
}
|
|
@@ -632,13 +632,13 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
632
632
|
};
|
|
633
633
|
|
|
634
634
|
const betterImpl = (arr: any[], customWhiteList?: string[]) => {
|
|
635
|
-
const defaultWhiteList = [
|
|
635
|
+
const defaultWhiteList = ["id", "pid", "sort"];
|
|
636
636
|
const whiteList = customWhiteList || defaultWhiteList;
|
|
637
637
|
|
|
638
638
|
return arr.map((item) => {
|
|
639
639
|
const converted = { ...item };
|
|
640
640
|
for (const key of whiteList) {
|
|
641
|
-
if (key in converted && typeof converted[key] ===
|
|
641
|
+
if (key in converted && typeof converted[key] === "string") {
|
|
642
642
|
converted[key] = Number(converted[key]);
|
|
643
643
|
}
|
|
644
644
|
}
|
|
@@ -647,12 +647,18 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
647
647
|
};
|
|
648
648
|
|
|
649
649
|
// 改进后支持自定义
|
|
650
|
-
const data = [{ id:
|
|
651
|
-
|
|
650
|
+
const data = [{ id: "123", customId: "456" }];
|
|
651
|
+
|
|
652
|
+
// 当前实现:只能转换硬编码白名单字段
|
|
653
|
+
const currentResult = currentImpl(data);
|
|
654
|
+
expect(currentResult[0].id).toBe(123);
|
|
655
|
+
expect(currentResult[0].customId).toBe("456");
|
|
656
|
+
|
|
657
|
+
const result = betterImpl(data, ["id", "customId"]);
|
|
652
658
|
expect(result[0].customId).toBe(456);
|
|
653
659
|
});
|
|
654
660
|
|
|
655
|
-
test(
|
|
661
|
+
test("问题4:executeWithConn 没有超时保护", async () => {
|
|
656
662
|
// **问题描述**:
|
|
657
663
|
// 当前实现没有查询超时限制
|
|
658
664
|
// 问题:慢查询可能导致长时间阻塞
|
|
@@ -662,7 +668,7 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
662
668
|
|
|
663
669
|
const mockExecuteWithTimeout = async (sql: string, params: any[], timeout: number = 30000) => {
|
|
664
670
|
const timeoutPromise = new Promise((_, reject) => {
|
|
665
|
-
setTimeout(() => reject(new Error(
|
|
671
|
+
setTimeout(() => reject(new Error("查询超时")), timeout);
|
|
666
672
|
});
|
|
667
673
|
|
|
668
674
|
const queryPromise = new Promise((resolve) => {
|
|
@@ -673,19 +679,19 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
673
679
|
};
|
|
674
680
|
|
|
675
681
|
// 正常查询
|
|
676
|
-
const result = await mockExecuteWithTimeout(
|
|
682
|
+
const result = await mockExecuteWithTimeout("SELECT * FROM users", [], 1000);
|
|
677
683
|
expect(result).toBeDefined();
|
|
678
684
|
|
|
679
685
|
// 超时查询
|
|
680
686
|
try {
|
|
681
|
-
await mockExecuteWithTimeout(
|
|
687
|
+
await mockExecuteWithTimeout("SELECT * FROM users", [], 10); // 10ms 超时
|
|
682
688
|
expect(true).toBe(false); // 不应执行到这里
|
|
683
689
|
} catch (error: any) {
|
|
684
|
-
expect(error.message).toContain(
|
|
690
|
+
expect(error.message).toContain("查询超时");
|
|
685
691
|
}
|
|
686
692
|
});
|
|
687
693
|
|
|
688
|
-
test(
|
|
694
|
+
test("问题5:getAll 的 MAX_LIMIT 保护不够完善", async () => {
|
|
689
695
|
// **问题描述**:
|
|
690
696
|
// getAll 设置了 MAX_LIMIT = 10000
|
|
691
697
|
// 问题:但没有检测实际查询的数据量,可能超过限制
|
|
@@ -707,11 +713,11 @@ describe('DbHelper - 代码逻辑问题分析', () => {
|
|
|
707
713
|
};
|
|
708
714
|
|
|
709
715
|
try {
|
|
710
|
-
await betterGetAll(
|
|
716
|
+
await betterGetAll("users", 10000);
|
|
711
717
|
expect(true).toBe(false);
|
|
712
718
|
} catch (error: any) {
|
|
713
|
-
expect(error.message).toContain(
|
|
714
|
-
expect(error.message).toContain(
|
|
719
|
+
expect(error.message).toContain("数据量过大");
|
|
720
|
+
expect(error.message).toContain("请使用 getList 分页查询");
|
|
715
721
|
}
|
|
716
722
|
});
|
|
717
723
|
});
|