befly 3.9.38 → 3.9.40
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 +8 -8
- 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
package/lib/dbHelper.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
2
|
* 数据库助手 - TypeScript 版本
|
|
3
3
|
* 提供数据库 CRUD 操作的封装
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
6
|
+
import type { BeflyContext } from "../types/befly.js";
|
|
7
|
+
import type { WhereConditions, JoinOption } from "../types/common.js";
|
|
8
|
+
import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, AllResult, TransactionCallback } from "../types/database.js";
|
|
9
|
+
|
|
10
|
+
import { snakeCase } from "es-toolkit/string";
|
|
11
|
+
|
|
12
|
+
import { arrayKeysToCamel } from "../utils/arrayKeysToCamel.js";
|
|
13
|
+
import { fieldClear } from "../utils/fieldClear.js";
|
|
14
|
+
import { keysToCamel } from "../utils/keysToCamel.js";
|
|
15
|
+
import { keysToSnake } from "../utils/keysToSnake.js";
|
|
16
|
+
import { CacheKeys } from "./cacheKeys.js";
|
|
17
|
+
import { Logger } from "./logger.js";
|
|
18
|
+
import { SqlBuilder } from "./sqlBuilder.js";
|
|
19
|
+
|
|
20
|
+
const TABLE_COLUMNS_CACHE_TTL_SECONDS = 3600;
|
|
17
21
|
|
|
18
22
|
/**
|
|
19
23
|
* 数据库助手类
|
|
@@ -40,38 +44,38 @@ export class DbHelper {
|
|
|
40
44
|
* @throws 如果 fields 格式非法
|
|
41
45
|
*/
|
|
42
46
|
private validateAndClassifyFields(fields?: string[]): {
|
|
43
|
-
type:
|
|
47
|
+
type: "all" | "include" | "exclude";
|
|
44
48
|
fields: string[];
|
|
45
49
|
} {
|
|
46
50
|
// 情况1:空数组或 undefined,表示查询所有
|
|
47
51
|
if (!fields || fields.length === 0) {
|
|
48
|
-
return { type:
|
|
52
|
+
return { type: "all", fields: [] };
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// 检测是否有星号(禁止)
|
|
52
|
-
if (fields.some((f) => f ===
|
|
53
|
-
throw new Error(
|
|
56
|
+
if (fields.some((f) => f === "*")) {
|
|
57
|
+
throw new Error("fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段");
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
// 检测是否有空字符串或无效值
|
|
57
|
-
if (fields.some((f) => !f || typeof f !==
|
|
58
|
-
throw new Error(
|
|
61
|
+
if (fields.some((f) => !f || typeof f !== "string" || f.trim() === "")) {
|
|
62
|
+
throw new Error("fields 不能包含空字符串或无效值");
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// 统计包含字段和排除字段
|
|
62
|
-
const includeFields = fields.filter((f) => !f.startsWith(
|
|
63
|
-
const excludeFields = fields.filter((f) => f.startsWith(
|
|
66
|
+
const includeFields = fields.filter((f) => !f.startsWith("!"));
|
|
67
|
+
const excludeFields = fields.filter((f) => f.startsWith("!"));
|
|
64
68
|
|
|
65
69
|
// 情况2:全部是包含字段
|
|
66
70
|
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
67
|
-
return { type:
|
|
71
|
+
return { type: "include", fields: includeFields };
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// 情况3:全部是排除字段
|
|
71
75
|
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
72
76
|
// 去掉感叹号前缀
|
|
73
77
|
const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
|
|
74
|
-
return { type:
|
|
78
|
+
return { type: "exclude", fields: cleanExcludeFields };
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
// 混用情况:报错
|
|
@@ -85,7 +89,7 @@ export class DbHelper {
|
|
|
85
89
|
*/
|
|
86
90
|
private async getTableColumns(table: string): Promise<string[]> {
|
|
87
91
|
// 1. 先查 Redis 缓存
|
|
88
|
-
const cacheKey =
|
|
92
|
+
const cacheKey = CacheKeys.tableColumns(table);
|
|
89
93
|
const columns = await this.befly.redis.getObject<string[]>(cacheKey);
|
|
90
94
|
|
|
91
95
|
if (columns && columns.length > 0) {
|
|
@@ -103,7 +107,7 @@ export class DbHelper {
|
|
|
103
107
|
const columnNames = result.map((row: any) => row.Field) as string[];
|
|
104
108
|
|
|
105
109
|
// 3. 写入 Redis 缓存
|
|
106
|
-
await this.befly.redis.setObject(cacheKey, columnNames,
|
|
110
|
+
await this.befly.redis.setObject(cacheKey, columnNames, TABLE_COLUMNS_CACHE_TTL_SECONDS);
|
|
107
111
|
|
|
108
112
|
return columnNames;
|
|
109
113
|
}
|
|
@@ -113,21 +117,21 @@ export class DbHelper {
|
|
|
113
117
|
* 支持排除字段语法
|
|
114
118
|
*/
|
|
115
119
|
private async fieldsToSnake(table: string, fields: string[]): Promise<string[]> {
|
|
116
|
-
if (!fields || !Array.isArray(fields)) return [
|
|
120
|
+
if (!fields || !Array.isArray(fields)) return ["*"];
|
|
117
121
|
|
|
118
122
|
// 验证并分类字段
|
|
119
123
|
const { type, fields: classifiedFields } = this.validateAndClassifyFields(fields);
|
|
120
124
|
|
|
121
125
|
// 情况1:查询所有字段
|
|
122
|
-
if (type ===
|
|
123
|
-
return [
|
|
126
|
+
if (type === "all") {
|
|
127
|
+
return ["*"];
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
// 情况2:指定包含字段
|
|
127
|
-
if (type ===
|
|
131
|
+
if (type === "include") {
|
|
128
132
|
return classifiedFields.map((field) => {
|
|
129
133
|
// 保留函数和特殊字段
|
|
130
|
-
if (field.includes(
|
|
134
|
+
if (field.includes("(") || field.includes(" ")) {
|
|
131
135
|
return field;
|
|
132
136
|
}
|
|
133
137
|
return snakeCase(field);
|
|
@@ -135,7 +139,7 @@ export class DbHelper {
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
// 情况3:排除字段
|
|
138
|
-
if (type ===
|
|
142
|
+
if (type === "exclude") {
|
|
139
143
|
// 获取表的所有字段
|
|
140
144
|
const allColumns = await this.getTableColumns(table);
|
|
141
145
|
|
|
@@ -146,13 +150,13 @@ export class DbHelper {
|
|
|
146
150
|
const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
|
|
147
151
|
|
|
148
152
|
if (resultFields.length === 0) {
|
|
149
|
-
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(
|
|
153
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(", ")}`);
|
|
150
154
|
}
|
|
151
155
|
|
|
152
156
|
return resultFields;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
return [
|
|
159
|
+
return ["*"];
|
|
156
160
|
}
|
|
157
161
|
|
|
158
162
|
/**
|
|
@@ -161,8 +165,8 @@ export class DbHelper {
|
|
|
161
165
|
private orderByToSnake(orderBy: string[]): string[] {
|
|
162
166
|
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
163
167
|
return orderBy.map((item) => {
|
|
164
|
-
if (typeof item !==
|
|
165
|
-
const [field, direction] = item.split(
|
|
168
|
+
if (typeof item !== "string" || !item.includes("#")) return item;
|
|
169
|
+
const [field, direction] = item.split("#");
|
|
166
170
|
return `${snakeCase(field.trim())}#${direction.trim()}`;
|
|
167
171
|
});
|
|
168
172
|
}
|
|
@@ -182,19 +186,19 @@ export class DbHelper {
|
|
|
182
186
|
*/
|
|
183
187
|
private processJoinField(field: string): string {
|
|
184
188
|
// 跳过函数、星号、已处理的字段
|
|
185
|
-
if (field.includes(
|
|
189
|
+
if (field.includes("(") || field === "*" || field.startsWith("`")) {
|
|
186
190
|
return field;
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
// 处理别名 AS
|
|
190
|
-
if (field.toUpperCase().includes(
|
|
194
|
+
if (field.toUpperCase().includes(" AS ")) {
|
|
191
195
|
const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
|
|
192
196
|
return `${this.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
// 处理表名.字段名
|
|
196
|
-
if (field.includes(
|
|
197
|
-
const [tableName, fieldName] = field.split(
|
|
200
|
+
if (field.includes(".")) {
|
|
201
|
+
const [tableName, fieldName] = field.split(".");
|
|
198
202
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
199
203
|
}
|
|
200
204
|
|
|
@@ -209,26 +213,26 @@ export class DbHelper {
|
|
|
209
213
|
*/
|
|
210
214
|
private processJoinWhereKey(key: string): string {
|
|
211
215
|
// 保留逻辑操作符
|
|
212
|
-
if (key ===
|
|
216
|
+
if (key === "$or" || key === "$and") {
|
|
213
217
|
return key;
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
// 处理带操作符的字段名(如 user.userId$gt)
|
|
217
|
-
if (key.includes(
|
|
218
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
221
|
+
if (key.includes("$")) {
|
|
222
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
219
223
|
const fieldPart = key.substring(0, lastDollarIndex);
|
|
220
224
|
const operator = key.substring(lastDollarIndex);
|
|
221
225
|
|
|
222
|
-
if (fieldPart.includes(
|
|
223
|
-
const [tableName, fieldName] = fieldPart.split(
|
|
226
|
+
if (fieldPart.includes(".")) {
|
|
227
|
+
const [tableName, fieldName] = fieldPart.split(".");
|
|
224
228
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
|
|
225
229
|
}
|
|
226
230
|
return `${snakeCase(fieldPart)}${operator}`;
|
|
227
231
|
}
|
|
228
232
|
|
|
229
233
|
// 处理表名.字段名
|
|
230
|
-
if (key.includes(
|
|
231
|
-
const [tableName, fieldName] = key.split(
|
|
234
|
+
if (key.includes(".")) {
|
|
235
|
+
const [tableName, fieldName] = key.split(".");
|
|
232
236
|
return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -240,7 +244,7 @@ export class DbHelper {
|
|
|
240
244
|
* 递归处理联查的 where 条件
|
|
241
245
|
*/
|
|
242
246
|
private processJoinWhere(where: any): any {
|
|
243
|
-
if (!where || typeof where !==
|
|
247
|
+
if (!where || typeof where !== "object") return where;
|
|
244
248
|
|
|
245
249
|
if (Array.isArray(where)) {
|
|
246
250
|
return where.map((item) => this.processJoinWhere(item));
|
|
@@ -250,9 +254,9 @@ export class DbHelper {
|
|
|
250
254
|
for (const [key, value] of Object.entries(where)) {
|
|
251
255
|
const newKey = this.processJoinWhereKey(key);
|
|
252
256
|
|
|
253
|
-
if (key ===
|
|
257
|
+
if (key === "$or" || key === "$and") {
|
|
254
258
|
result[newKey] = (value as any[]).map((item) => this.processJoinWhere(item));
|
|
255
|
-
} else if (typeof value ===
|
|
259
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
256
260
|
result[newKey] = this.processJoinWhere(value);
|
|
257
261
|
} else {
|
|
258
262
|
result[newKey] = value;
|
|
@@ -268,8 +272,8 @@ export class DbHelper {
|
|
|
268
272
|
private processJoinOrderBy(orderBy: string[]): string[] {
|
|
269
273
|
if (!orderBy || !Array.isArray(orderBy)) return orderBy;
|
|
270
274
|
return orderBy.map((item) => {
|
|
271
|
-
if (typeof item !==
|
|
272
|
-
const [field, direction] = item.split(
|
|
275
|
+
if (typeof item !== "string" || !item.includes("#")) return item;
|
|
276
|
+
const [field, direction] = item.split("#");
|
|
273
277
|
return `${this.processJoinField(field.trim())}#${direction.trim()}`;
|
|
274
278
|
});
|
|
275
279
|
}
|
|
@@ -288,7 +292,7 @@ export class DbHelper {
|
|
|
288
292
|
|
|
289
293
|
return {
|
|
290
294
|
table: this.processTableName(options.table),
|
|
291
|
-
fields: processedFields.length > 0 ? processedFields : [
|
|
295
|
+
fields: processedFields.length > 0 ? processedFields : ["*"],
|
|
292
296
|
where: this.processJoinWhere(cleanWhere),
|
|
293
297
|
joins: options.joins,
|
|
294
298
|
orderBy: this.processJoinOrderBy(options.orderBy || []),
|
|
@@ -319,16 +323,16 @@ export class DbHelper {
|
|
|
319
323
|
|
|
320
324
|
for (const join of joins) {
|
|
321
325
|
const processedTable = this.processTableName(join.table);
|
|
322
|
-
const type = join.type ||
|
|
326
|
+
const type = join.type || "left";
|
|
323
327
|
|
|
324
328
|
switch (type) {
|
|
325
|
-
case
|
|
329
|
+
case "inner":
|
|
326
330
|
builder.innerJoin(processedTable, join.on);
|
|
327
331
|
break;
|
|
328
|
-
case
|
|
332
|
+
case "right":
|
|
329
333
|
builder.rightJoin(processedTable, join.on);
|
|
330
334
|
break;
|
|
331
|
-
case
|
|
335
|
+
case "left":
|
|
332
336
|
default:
|
|
333
337
|
builder.leftJoin(processedTable, join.on);
|
|
334
338
|
break;
|
|
@@ -345,7 +349,7 @@ export class DbHelper {
|
|
|
345
349
|
*/
|
|
346
350
|
private addDefaultStateFilter(where: WhereConditions = {}, table?: string, hasJoins: boolean = false): WhereConditions {
|
|
347
351
|
// 如果用户已经指定了 state 条件,优先使用用户的条件
|
|
348
|
-
const hasStateCondition = Object.keys(where).some((key) => key.startsWith(
|
|
352
|
+
const hasStateCondition = Object.keys(where).some((key) => key.startsWith("state") || key.includes(".state"));
|
|
349
353
|
|
|
350
354
|
if (hasStateCondition) {
|
|
351
355
|
return where;
|
|
@@ -383,7 +387,7 @@ export class DbHelper {
|
|
|
383
387
|
* 3. 所有以 'At' 或 '_at' 结尾的字段会被自动转换(时间戳字段)
|
|
384
388
|
* 4. 其他字段保持不变
|
|
385
389
|
*/
|
|
386
|
-
private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = [
|
|
390
|
+
private convertBigIntFields<T = any>(arr: Record<string, any>[], fields: string[] = ["id", "pid", "sort"]): T[] {
|
|
387
391
|
if (!arr || !Array.isArray(arr)) return arr as T[];
|
|
388
392
|
|
|
389
393
|
return arr.map((item) => {
|
|
@@ -402,9 +406,9 @@ export class DbHelper {
|
|
|
402
406
|
// 3. 以 '_id' 结尾(如 user_id, role_id)
|
|
403
407
|
// 4. 以 'At' 结尾(如 createdAt, updatedAt)
|
|
404
408
|
// 5. 以 '_at' 结尾(如 created_at, updated_at)
|
|
405
|
-
const shouldConvert = fields.includes(key) || key.endsWith(
|
|
409
|
+
const shouldConvert = fields.includes(key) || key.endsWith("Id") || key.endsWith("_id") || key.endsWith("At") || key.endsWith("_at");
|
|
406
410
|
|
|
407
|
-
if (shouldConvert && typeof value ===
|
|
411
|
+
if (shouldConvert && typeof value === "string") {
|
|
408
412
|
const num = Number(value);
|
|
409
413
|
if (!isNaN(num)) {
|
|
410
414
|
converted[key] = num;
|
|
@@ -417,12 +421,62 @@ export class DbHelper {
|
|
|
417
421
|
}) as T[];
|
|
418
422
|
}
|
|
419
423
|
|
|
424
|
+
/**
|
|
425
|
+
* 序列化数组字段(写入数据库前)
|
|
426
|
+
* 将数组类型的字段转换为 JSON 字符串
|
|
427
|
+
*/
|
|
428
|
+
private serializeArrayFields(data: Record<string, any>): Record<string, any> {
|
|
429
|
+
const serialized = { ...data };
|
|
430
|
+
|
|
431
|
+
for (const [key, value] of Object.entries(serialized)) {
|
|
432
|
+
// 跳过 null 和 undefined
|
|
433
|
+
if (value === null || value === undefined) continue;
|
|
434
|
+
|
|
435
|
+
// 数组类型序列化为 JSON 字符串
|
|
436
|
+
if (Array.isArray(value)) {
|
|
437
|
+
serialized[key] = JSON.stringify(value);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return serialized;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 反序列化数组字段(从数据库读取后)
|
|
446
|
+
* 将 JSON 字符串转换回数组
|
|
447
|
+
*/
|
|
448
|
+
private deserializeArrayFields<T = any>(data: Record<string, any> | null): T | null {
|
|
449
|
+
if (!data) return null;
|
|
450
|
+
|
|
451
|
+
const deserialized = { ...data };
|
|
452
|
+
|
|
453
|
+
for (const [key, value] of Object.entries(deserialized)) {
|
|
454
|
+
// 跳过非字符串值
|
|
455
|
+
if (typeof value !== "string") continue;
|
|
456
|
+
|
|
457
|
+
// 尝试解析 JSON 数组字符串
|
|
458
|
+
// 只解析符合 JSON 数组格式的字符串(以 [ 开头,以 ] 结尾)
|
|
459
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
460
|
+
try {
|
|
461
|
+
const parsed = JSON.parse(value);
|
|
462
|
+
if (Array.isArray(parsed)) {
|
|
463
|
+
deserialized[key] = parsed;
|
|
464
|
+
}
|
|
465
|
+
} catch {
|
|
466
|
+
// 解析失败则保持原值
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return deserialized as T;
|
|
472
|
+
}
|
|
473
|
+
|
|
420
474
|
/**
|
|
421
475
|
* Where 条件键名转下划线格式(递归处理嵌套)(私有方法)
|
|
422
476
|
* 支持操作符字段(如 userId$gt)和逻辑操作符($or, $and)
|
|
423
477
|
*/
|
|
424
478
|
private whereKeysToSnake(where: any): any {
|
|
425
|
-
if (!where || typeof where !==
|
|
479
|
+
if (!where || typeof where !== "object") return where;
|
|
426
480
|
|
|
427
481
|
// 处理数组($or, $and 等)
|
|
428
482
|
if (Array.isArray(where)) {
|
|
@@ -432,14 +486,14 @@ export class DbHelper {
|
|
|
432
486
|
const result: any = {};
|
|
433
487
|
for (const [key, value] of Object.entries(where)) {
|
|
434
488
|
// 保留 $or, $and 等逻辑操作符
|
|
435
|
-
if (key ===
|
|
489
|
+
if (key === "$or" || key === "$and") {
|
|
436
490
|
result[key] = (value as any[]).map((item) => this.whereKeysToSnake(item));
|
|
437
491
|
continue;
|
|
438
492
|
}
|
|
439
493
|
|
|
440
494
|
// 处理带操作符的字段名(如 userId$gt)
|
|
441
|
-
if (key.includes(
|
|
442
|
-
const lastDollarIndex = key.lastIndexOf(
|
|
495
|
+
if (key.includes("$")) {
|
|
496
|
+
const lastDollarIndex = key.lastIndexOf("$");
|
|
443
497
|
const fieldName = key.substring(0, lastDollarIndex);
|
|
444
498
|
const operator = key.substring(lastDollarIndex);
|
|
445
499
|
const snakeKey = snakeCase(fieldName) + operator;
|
|
@@ -449,7 +503,7 @@ export class DbHelper {
|
|
|
449
503
|
|
|
450
504
|
// 普通字段:转换键名,递归处理值(支持嵌套对象)
|
|
451
505
|
const snakeKey = snakeCase(key);
|
|
452
|
-
if (typeof value ===
|
|
506
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
453
507
|
result[snakeKey] = this.whereKeysToSnake(value);
|
|
454
508
|
} else {
|
|
455
509
|
result[snakeKey] = value;
|
|
@@ -464,11 +518,11 @@ export class DbHelper {
|
|
|
464
518
|
*/
|
|
465
519
|
private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
|
|
466
520
|
if (!this.sql) {
|
|
467
|
-
throw new Error(
|
|
521
|
+
throw new Error("数据库连接未初始化");
|
|
468
522
|
}
|
|
469
523
|
|
|
470
524
|
// 强制类型检查:只接受字符串类型的 SQL
|
|
471
|
-
if (typeof sqlStr !==
|
|
525
|
+
if (typeof sqlStr !== "string") {
|
|
472
526
|
throw new Error(`executeWithConn 只接受字符串类型的 SQL,收到类型: ${typeof sqlStr},值: ${JSON.stringify(sqlStr)}`);
|
|
473
527
|
}
|
|
474
528
|
|
|
@@ -487,27 +541,35 @@ export class DbHelper {
|
|
|
487
541
|
// 计算执行时间
|
|
488
542
|
const duration = Date.now() - startTime;
|
|
489
543
|
|
|
490
|
-
// 慢查询警告(超过
|
|
491
|
-
if (duration >
|
|
492
|
-
|
|
493
|
-
|
|
544
|
+
// 慢查询警告(超过 5000ms)
|
|
545
|
+
if (duration > 5000) {
|
|
546
|
+
Logger.warn(
|
|
547
|
+
{
|
|
548
|
+
subsystem: "db",
|
|
549
|
+
event: "slow",
|
|
550
|
+
duration: duration,
|
|
551
|
+
sqlPreview: sqlStr,
|
|
552
|
+
params: params || [],
|
|
553
|
+
paramsCount: (params || []).length
|
|
554
|
+
},
|
|
555
|
+
"🐌 检测到慢查询"
|
|
556
|
+
);
|
|
494
557
|
}
|
|
495
558
|
|
|
496
559
|
return result;
|
|
497
560
|
} catch (error: any) {
|
|
498
561
|
const duration = Date.now() - startTime;
|
|
499
562
|
|
|
500
|
-
|
|
501
|
-
Logger.error(
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
563
|
+
const sqlPreview = sqlStr.length > 200 ? sqlStr.substring(0, 200) + "..." : sqlStr;
|
|
564
|
+
Logger.error(
|
|
565
|
+
{
|
|
566
|
+
err: error,
|
|
567
|
+
sqlPreview: sqlPreview,
|
|
568
|
+
params: params || [],
|
|
569
|
+
duration: duration
|
|
570
|
+
},
|
|
571
|
+
"SQL 执行错误"
|
|
572
|
+
);
|
|
511
573
|
|
|
512
574
|
const enhancedError: any = new Error(`SQL执行失败: ${error.message}`);
|
|
513
575
|
enhancedError.originalError = error;
|
|
@@ -527,7 +589,7 @@ export class DbHelper {
|
|
|
527
589
|
// 将表名转换为下划线格式
|
|
528
590
|
const snakeTableName = snakeCase(tableName);
|
|
529
591
|
|
|
530
|
-
const result = await this.executeWithConn(
|
|
592
|
+
const result = await this.executeWithConn("SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?", [snakeTableName]);
|
|
531
593
|
|
|
532
594
|
return result?.[0]?.count > 0;
|
|
533
595
|
}
|
|
@@ -549,11 +611,11 @@ export class DbHelper {
|
|
|
549
611
|
* where: { 'o.state': 1 }
|
|
550
612
|
* });
|
|
551
613
|
*/
|
|
552
|
-
async getCount(options: Omit<QueryOptions,
|
|
614
|
+
async getCount(options: Omit<QueryOptions, "fields" | "page" | "limit" | "orderBy">): Promise<number> {
|
|
553
615
|
const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
|
|
554
616
|
|
|
555
617
|
const builder = new SqlBuilder()
|
|
556
|
-
.select([
|
|
618
|
+
.select(["COUNT(*) as count"])
|
|
557
619
|
.from(table)
|
|
558
620
|
.where(this.addDefaultStateFilter(where, table, !!joins));
|
|
559
621
|
|
|
@@ -602,8 +664,12 @@ export class DbHelper {
|
|
|
602
664
|
|
|
603
665
|
const camelRow = keysToCamel<T>(row);
|
|
604
666
|
|
|
667
|
+
// 反序列化数组字段(JSON 字符串 → 数组)
|
|
668
|
+
const deserialized = this.deserializeArrayFields<T>(camelRow);
|
|
669
|
+
if (!deserialized) return null;
|
|
670
|
+
|
|
605
671
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
606
|
-
return this.convertBigIntFields<T>([
|
|
672
|
+
return this.convertBigIntFields<T>([deserialized])[0];
|
|
607
673
|
}
|
|
608
674
|
|
|
609
675
|
/**
|
|
@@ -643,7 +709,7 @@ export class DbHelper {
|
|
|
643
709
|
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
644
710
|
|
|
645
711
|
// 查询总数
|
|
646
|
-
const countBuilder = new SqlBuilder().select([
|
|
712
|
+
const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
|
|
647
713
|
|
|
648
714
|
// 添加 JOIN(计数也需要)
|
|
649
715
|
this.applyJoins(countBuilder, prepared.joins);
|
|
@@ -681,9 +747,12 @@ export class DbHelper {
|
|
|
681
747
|
// 字段名转换:下划线 → 小驼峰
|
|
682
748
|
const camelList = arrayKeysToCamel<T>(list);
|
|
683
749
|
|
|
750
|
+
// 反序列化数组字段
|
|
751
|
+
const deserializedList = camelList.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
|
|
752
|
+
|
|
684
753
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
685
754
|
return {
|
|
686
|
-
lists: this.convertBigIntFields<T>(
|
|
755
|
+
lists: this.convertBigIntFields<T>(deserializedList),
|
|
687
756
|
total: total,
|
|
688
757
|
page: prepared.page,
|
|
689
758
|
limit: prepared.limit,
|
|
@@ -708,7 +777,7 @@ export class DbHelper {
|
|
|
708
777
|
* where: { 'o.state': 1 }
|
|
709
778
|
* })
|
|
710
779
|
*/
|
|
711
|
-
async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions,
|
|
780
|
+
async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, "page" | "limit">): Promise<AllResult<T>> {
|
|
712
781
|
// 添加硬性上限保护,防止内存溢出
|
|
713
782
|
const MAX_LIMIT = 10000;
|
|
714
783
|
const WARNING_LIMIT = 1000;
|
|
@@ -718,7 +787,7 @@ export class DbHelper {
|
|
|
718
787
|
const whereFiltered = this.addDefaultStateFilter(prepared.where, prepared.table, !!prepared.joins);
|
|
719
788
|
|
|
720
789
|
// 查询真实总数
|
|
721
|
-
const countBuilder = new SqlBuilder().select([
|
|
790
|
+
const countBuilder = new SqlBuilder().select(["COUNT(*) as total"]).from(prepared.table).where(whereFiltered);
|
|
722
791
|
|
|
723
792
|
// 添加 JOIN(计数也需要)
|
|
724
793
|
this.applyJoins(countBuilder, prepared.joins);
|
|
@@ -750,7 +819,7 @@ export class DbHelper {
|
|
|
750
819
|
|
|
751
820
|
// 警告日志:返回数据超过警告阈值
|
|
752
821
|
if (result.length >= WARNING_LIMIT) {
|
|
753
|
-
Logger.warn({ table: options.table, count: result.length, total: total },
|
|
822
|
+
Logger.warn({ table: options.table, count: result.length, total: total }, "getAll 返回数据过多,建议使用 getList 分页查询");
|
|
754
823
|
}
|
|
755
824
|
|
|
756
825
|
// 如果达到上限,额外警告
|
|
@@ -761,8 +830,11 @@ export class DbHelper {
|
|
|
761
830
|
// 字段名转换:下划线 → 小驼峰
|
|
762
831
|
const camelResult = arrayKeysToCamel<T>(result);
|
|
763
832
|
|
|
833
|
+
// 反序列化数组字段
|
|
834
|
+
const deserializedList = camelResult.map((item) => this.deserializeArrayFields<T>(item)).filter((item): item is T => item !== null);
|
|
835
|
+
|
|
764
836
|
// 转换 BIGINT 字段(id, pid 等)为数字类型
|
|
765
|
-
const lists = this.convertBigIntFields<T>(
|
|
837
|
+
const lists = this.convertBigIntFields<T>(deserializedList);
|
|
766
838
|
|
|
767
839
|
return {
|
|
768
840
|
lists: lists,
|
|
@@ -787,8 +859,11 @@ export class DbHelper {
|
|
|
787
859
|
// 字段名转换:小驼峰 → 下划线
|
|
788
860
|
const snakeData = keysToSnake(cleanData);
|
|
789
861
|
|
|
862
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
863
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
864
|
+
|
|
790
865
|
// 复制用户数据,但移除系统字段(防止用户尝试覆盖)
|
|
791
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } =
|
|
866
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
|
|
792
867
|
|
|
793
868
|
const processed: Record<string, any> = { ...userData };
|
|
794
869
|
|
|
@@ -854,8 +929,11 @@ export class DbHelper {
|
|
|
854
929
|
// 字段名转换:小驼峰 → 下划线
|
|
855
930
|
const snakeData = keysToSnake(cleanData);
|
|
856
931
|
|
|
932
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
933
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
934
|
+
|
|
857
935
|
// 移除系统字段(防止用户尝试覆盖)
|
|
858
|
-
const { id, created_at, updated_at, deleted_at, state, ...userData } =
|
|
936
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, state: _state, ...userData } = serializedData;
|
|
859
937
|
|
|
860
938
|
// 强制生成系统字段(不可被用户覆盖)
|
|
861
939
|
return {
|
|
@@ -876,7 +954,7 @@ export class DbHelper {
|
|
|
876
954
|
await this.executeWithConn(sql, params);
|
|
877
955
|
return ids;
|
|
878
956
|
} catch (error: any) {
|
|
879
|
-
Logger.error({ err: error, table: table },
|
|
957
|
+
Logger.error({ err: error, table: table }, "批量插入失败");
|
|
880
958
|
throw error;
|
|
881
959
|
}
|
|
882
960
|
}
|
|
@@ -899,9 +977,12 @@ export class DbHelper {
|
|
|
899
977
|
const snakeData = keysToSnake(cleanData);
|
|
900
978
|
const snakeWhere = this.whereKeysToSnake(cleanWhere);
|
|
901
979
|
|
|
980
|
+
// 序列化数组字段(数组 → JSON 字符串)
|
|
981
|
+
const serializedData = this.serializeArrayFields(snakeData);
|
|
982
|
+
|
|
902
983
|
// 移除系统字段(防止用户尝试修改)
|
|
903
984
|
// 注意:state 允许用户修改(用于设置禁用状态 state=2)
|
|
904
|
-
const { id, created_at, updated_at, deleted_at, ...userData } =
|
|
985
|
+
const { id: _id, created_at: _created_at, updated_at: _updated_at, deleted_at: _deleted_at, ...userData } = serializedData;
|
|
905
986
|
|
|
906
987
|
// 强制更新时间戳(不可被用户覆盖)
|
|
907
988
|
const processed: Record<string, any> = {
|
|
@@ -937,7 +1018,7 @@ export class DbHelper {
|
|
|
937
1018
|
* 硬删除数据(物理删除,不可恢复)
|
|
938
1019
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
939
1020
|
*/
|
|
940
|
-
async delForce(options: Omit<DeleteOptions,
|
|
1021
|
+
async delForce(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
941
1022
|
const { table, where } = options;
|
|
942
1023
|
|
|
943
1024
|
// 转换表名:小驼峰 → 下划线
|
|
@@ -959,7 +1040,7 @@ export class DbHelper {
|
|
|
959
1040
|
* 禁用数据(设置 state=2)
|
|
960
1041
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
961
1042
|
*/
|
|
962
|
-
async disableData(options: Omit<DeleteOptions,
|
|
1043
|
+
async disableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
963
1044
|
const { table, where } = options;
|
|
964
1045
|
|
|
965
1046
|
return await this.updData({
|
|
@@ -975,7 +1056,7 @@ export class DbHelper {
|
|
|
975
1056
|
* 启用数据(设置 state=1)
|
|
976
1057
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
977
1058
|
*/
|
|
978
|
-
async enableData(options: Omit<DeleteOptions,
|
|
1059
|
+
async enableData(options: Omit<DeleteOptions, "hard">): Promise<number> {
|
|
979
1060
|
const { table, where } = options;
|
|
980
1061
|
|
|
981
1062
|
return await this.updData({
|
|
@@ -1016,12 +1097,12 @@ export class DbHelper {
|
|
|
1016
1097
|
* 检查数据是否存在(优化性能)
|
|
1017
1098
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
1018
1099
|
*/
|
|
1019
|
-
async exists(options: Omit<QueryOptions,
|
|
1100
|
+
async exists(options: Omit<QueryOptions, "fields" | "orderBy" | "page" | "limit">): Promise<boolean> {
|
|
1020
1101
|
const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
1021
1102
|
|
|
1022
1103
|
// 使用 COUNT(1) 性能更好
|
|
1023
1104
|
const builder = new SqlBuilder()
|
|
1024
|
-
.select([
|
|
1105
|
+
.select(["COUNT(1) as cnt"])
|
|
1025
1106
|
.from(table)
|
|
1026
1107
|
.where(this.addDefaultStateFilter(where, table, false))
|
|
1027
1108
|
.limit(1);
|
|
@@ -1036,7 +1117,7 @@ export class DbHelper {
|
|
|
1036
1117
|
* 查询单个字段值(带字段名验证)
|
|
1037
1118
|
* @param field - 字段名(支持小驼峰或下划线格式)
|
|
1038
1119
|
*/
|
|
1039
|
-
async getFieldValue<T = any>(options: Omit<QueryOptions,
|
|
1120
|
+
async getFieldValue<T = any>(options: Omit<QueryOptions, "fields"> & { field: string }): Promise<T | null> {
|
|
1040
1121
|
const { field, ...queryOptions } = options;
|
|
1041
1122
|
|
|
1042
1123
|
// 验证字段名格式(只允许字母、数字、下划线)
|
|
@@ -1094,7 +1175,7 @@ export class DbHelper {
|
|
|
1094
1175
|
}
|
|
1095
1176
|
|
|
1096
1177
|
// 验证 value 必须是数字
|
|
1097
|
-
if (typeof value !==
|
|
1178
|
+
if (typeof value !== "number" || isNaN(value)) {
|
|
1098
1179
|
throw new Error(`自增值必须是有效的数字 (table: ${table}, field: ${field}, value: ${value})`);
|
|
1099
1180
|
}
|
|
1100
1181
|
|