befly 3.7.3 → 3.7.5
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/lib/dbHelper.ts +162 -31
- package/package.json +2 -2
package/lib/dbHelper.ts
CHANGED
|
@@ -30,18 +30,125 @@ export class DbHelper {
|
|
|
30
30
|
this.isTransaction = !!sql;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 验证 fields 格式并分类
|
|
35
|
+
* @returns { type: 'all' | 'include' | 'exclude', fields: string[] }
|
|
36
|
+
* @throws 如果 fields 格式非法
|
|
37
|
+
*/
|
|
38
|
+
private validateAndClassifyFields(fields?: string[]): {
|
|
39
|
+
type: 'all' | 'include' | 'exclude';
|
|
40
|
+
fields: string[];
|
|
41
|
+
} {
|
|
42
|
+
// 情况1:空数组或 undefined,表示查询所有
|
|
43
|
+
if (!fields || fields.length === 0) {
|
|
44
|
+
return { type: 'all', fields: [] };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 检测是否有星号(禁止)
|
|
48
|
+
if (fields.some((f) => f === '*')) {
|
|
49
|
+
throw new Error('fields 不支持 * 星号,请使用空数组 [] 或不传参数表示查询所有字段');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 检测是否有空字符串或无效值
|
|
53
|
+
if (fields.some((f) => !f || typeof f !== 'string' || f.trim() === '')) {
|
|
54
|
+
throw new Error('fields 不能包含空字符串或无效值');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 统计包含字段和排除字段
|
|
58
|
+
const includeFields = fields.filter((f) => !f.startsWith('!'));
|
|
59
|
+
const excludeFields = fields.filter((f) => f.startsWith('!'));
|
|
60
|
+
|
|
61
|
+
// 情况2:全部是包含字段
|
|
62
|
+
if (includeFields.length > 0 && excludeFields.length === 0) {
|
|
63
|
+
return { type: 'include', fields: includeFields };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 情况3:全部是排除字段
|
|
67
|
+
if (excludeFields.length > 0 && includeFields.length === 0) {
|
|
68
|
+
// 去掉感叹号前缀
|
|
69
|
+
const cleanExcludeFields = excludeFields.map((f) => f.substring(1));
|
|
70
|
+
return { type: 'exclude', fields: cleanExcludeFields };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 混用情况:报错
|
|
74
|
+
throw new Error('fields 不能同时包含普通字段和排除字段(! 开头)。只能使用以下3种方式之一:\n1. 空数组 [] 或不传(查询所有)\n2. 全部指定字段 ["id", "name"]\n3. 全部排除字段 ["!password", "!token"]');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 获取表的所有字段名(Redis 缓存)
|
|
79
|
+
* @param table - 表名(下划线格式)
|
|
80
|
+
* @returns 字段名数组(下划线格式)
|
|
81
|
+
*/
|
|
82
|
+
private async getTableColumns(table: string): Promise<string[]> {
|
|
83
|
+
// 1. 先查 Redis 缓存
|
|
84
|
+
const cacheKey = `table:columns:${table}`;
|
|
85
|
+
let columns = await this.befly.redis.getObject<string[]>(cacheKey);
|
|
86
|
+
|
|
87
|
+
if (columns && columns.length > 0) {
|
|
88
|
+
return columns;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. 缓存未命中,查询数据库
|
|
92
|
+
const sql = `SHOW COLUMNS FROM \`${table}\``;
|
|
93
|
+
const result = await this.executeWithConn(sql);
|
|
94
|
+
|
|
95
|
+
if (!result || result.length === 0) {
|
|
96
|
+
throw new Error(`表 ${table} 不存在或没有字段`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
columns = result.map((row: any) => row.Field);
|
|
100
|
+
|
|
101
|
+
// 3. 写入 Redis 缓存(1小时过期)
|
|
102
|
+
await this.befly.redis.setObject(cacheKey, columns, 3600);
|
|
103
|
+
|
|
104
|
+
return columns;
|
|
105
|
+
}
|
|
106
|
+
|
|
33
107
|
/**
|
|
34
108
|
* 字段数组转下划线格式(私有方法)
|
|
109
|
+
* 支持排除字段语法
|
|
35
110
|
*/
|
|
36
|
-
private fieldsToSnake(fields: string[]): string[] {
|
|
37
|
-
if (!fields || !Array.isArray(fields)) return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
111
|
+
private async fieldsToSnake(table: string, fields: string[]): Promise<string[]> {
|
|
112
|
+
if (!fields || !Array.isArray(fields)) return ['*'];
|
|
113
|
+
|
|
114
|
+
// 验证并分类字段
|
|
115
|
+
const { type, fields: classifiedFields } = this.validateAndClassifyFields(fields);
|
|
116
|
+
|
|
117
|
+
// 情况1:查询所有字段
|
|
118
|
+
if (type === 'all') {
|
|
119
|
+
return ['*'];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 情况2:指定包含字段
|
|
123
|
+
if (type === 'include') {
|
|
124
|
+
return classifiedFields.map((field) => {
|
|
125
|
+
// 保留函数和特殊字段
|
|
126
|
+
if (field.includes('(') || field.includes(' ')) {
|
|
127
|
+
return field;
|
|
128
|
+
}
|
|
129
|
+
return snakeCase(field);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 情况3:排除字段
|
|
134
|
+
if (type === 'exclude') {
|
|
135
|
+
// 获取表的所有字段
|
|
136
|
+
const allColumns = await this.getTableColumns(table);
|
|
137
|
+
|
|
138
|
+
// 转换排除字段为下划线格式
|
|
139
|
+
const excludeSnakeFields = classifiedFields.map((f) => snakeCase(f));
|
|
140
|
+
|
|
141
|
+
// 过滤掉排除字段
|
|
142
|
+
const resultFields = allColumns.filter((col) => !excludeSnakeFields.includes(col));
|
|
143
|
+
|
|
144
|
+
if (resultFields.length === 0) {
|
|
145
|
+
throw new Error(`排除字段后没有剩余字段可查询。表: ${table}, 排除: ${excludeSnakeFields.join(', ')}`);
|
|
42
146
|
}
|
|
43
|
-
|
|
44
|
-
|
|
147
|
+
|
|
148
|
+
return resultFields;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ['*'];
|
|
45
152
|
}
|
|
46
153
|
|
|
47
154
|
/**
|
|
@@ -59,12 +166,15 @@ export class DbHelper {
|
|
|
59
166
|
/**
|
|
60
167
|
* 统一的查询参数预处理方法
|
|
61
168
|
*/
|
|
62
|
-
private prepareQueryOptions(options: QueryOptions) {
|
|
169
|
+
private async prepareQueryOptions(options: QueryOptions) {
|
|
63
170
|
const cleanWhere = this.cleanFields(options.where);
|
|
64
171
|
|
|
172
|
+
// 处理 fields(支持排除语法)
|
|
173
|
+
const processedFields = await this.fieldsToSnake(snakeCase(options.table), options.fields || []);
|
|
174
|
+
|
|
65
175
|
return {
|
|
66
176
|
table: snakeCase(options.table),
|
|
67
|
-
fields:
|
|
177
|
+
fields: processedFields,
|
|
68
178
|
where: this.whereKeysToSnake(cleanWhere),
|
|
69
179
|
orderBy: this.orderByToSnake(options.orderBy || []),
|
|
70
180
|
page: options.page || 1,
|
|
@@ -185,7 +295,7 @@ export class DbHelper {
|
|
|
185
295
|
}
|
|
186
296
|
|
|
187
297
|
/**
|
|
188
|
-
* 执行 SQL(使用 sql.unsafe
|
|
298
|
+
* 执行 SQL(使用 sql.unsafe,带慢查询日志和错误处理)
|
|
189
299
|
*/
|
|
190
300
|
private async executeWithConn(sqlStr: string, params?: any[]): Promise<any> {
|
|
191
301
|
if (!this.sql) {
|
|
@@ -195,24 +305,45 @@ export class DbHelper {
|
|
|
195
305
|
// 记录开始时间
|
|
196
306
|
const startTime = Date.now();
|
|
197
307
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
308
|
+
try {
|
|
309
|
+
// 使用 sql.unsafe 执行查询
|
|
310
|
+
let result;
|
|
311
|
+
if (params && params.length > 0) {
|
|
312
|
+
result = await this.sql.unsafe(sqlStr, params);
|
|
313
|
+
} else {
|
|
314
|
+
result = await this.sql.unsafe(sqlStr);
|
|
315
|
+
}
|
|
205
316
|
|
|
206
|
-
|
|
207
|
-
|
|
317
|
+
// 计算执行时间
|
|
318
|
+
const duration = Date.now() - startTime;
|
|
208
319
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
320
|
+
// 慢查询警告(超过 1000ms)
|
|
321
|
+
if (duration > 1000) {
|
|
322
|
+
const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
|
|
323
|
+
Logger.warn(`🐌 检测到慢查询 (${duration}ms): ${sqlPreview}`);
|
|
324
|
+
}
|
|
214
325
|
|
|
215
|
-
|
|
326
|
+
return result;
|
|
327
|
+
} catch (error: any) {
|
|
328
|
+
const duration = Date.now() - startTime;
|
|
329
|
+
const truncatedSql = sqlStr.length > 200 ? sqlStr.substring(0, 200) + '...' : sqlStr;
|
|
330
|
+
|
|
331
|
+
Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
332
|
+
Logger.error('SQL 执行错误');
|
|
333
|
+
Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
334
|
+
Logger.error(`SQL 语句: ${truncatedSql}`);
|
|
335
|
+
Logger.error(`参数列表: ${JSON.stringify(params || [])}`);
|
|
336
|
+
Logger.error(`执行耗时: ${duration}ms`);
|
|
337
|
+
Logger.error(`错误信息: ${error.message}`);
|
|
338
|
+
Logger.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
339
|
+
|
|
340
|
+
const enhancedError: any = new Error(`SQL执行失败: ${error.message}`);
|
|
341
|
+
enhancedError.originalError = error;
|
|
342
|
+
enhancedError.sql = sqlStr;
|
|
343
|
+
enhancedError.params = params || [];
|
|
344
|
+
enhancedError.duration = duration;
|
|
345
|
+
throw enhancedError;
|
|
346
|
+
}
|
|
216
347
|
}
|
|
217
348
|
|
|
218
349
|
/**
|
|
@@ -240,7 +371,7 @@ export class DbHelper {
|
|
|
240
371
|
* const activeCount = await db.getCount({ table: 'user', where: { state: 1 } });
|
|
241
372
|
*/
|
|
242
373
|
async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
|
|
243
|
-
const { table, where } = this.prepareQueryOptions(options as QueryOptions);
|
|
374
|
+
const { table, where } = await this.prepareQueryOptions(options as QueryOptions);
|
|
244
375
|
|
|
245
376
|
const builder = new SqlBuilder().select(['COUNT(*) as count']).from(table).where(this.addDefaultStateFilter(where));
|
|
246
377
|
|
|
@@ -260,7 +391,7 @@ export class DbHelper {
|
|
|
260
391
|
* getOne({ table: 'user_profile', fields: ['user_id', 'user_name'] })
|
|
261
392
|
*/
|
|
262
393
|
async getOne<T = any>(options: QueryOptions): Promise<T | null> {
|
|
263
|
-
const { table, fields, where } = this.prepareQueryOptions(options);
|
|
394
|
+
const { table, fields, where } = await this.prepareQueryOptions(options);
|
|
264
395
|
|
|
265
396
|
const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
|
|
266
397
|
|
|
@@ -286,7 +417,7 @@ export class DbHelper {
|
|
|
286
417
|
* getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
|
|
287
418
|
*/
|
|
288
419
|
async getList<T = any>(options: QueryOptions): Promise<ListResult<T>> {
|
|
289
|
-
const prepared = this.prepareQueryOptions(options);
|
|
420
|
+
const prepared = await this.prepareQueryOptions(options);
|
|
290
421
|
|
|
291
422
|
// 参数上限校验
|
|
292
423
|
if (prepared.page < 1 || prepared.page > 10000) {
|
|
@@ -356,7 +487,7 @@ export class DbHelper {
|
|
|
356
487
|
const MAX_LIMIT = 10000;
|
|
357
488
|
const WARNING_LIMIT = 1000;
|
|
358
489
|
|
|
359
|
-
const prepared = this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
490
|
+
const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
360
491
|
|
|
361
492
|
const builder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(this.addDefaultStateFilter(prepared.where)).limit(MAX_LIMIT);
|
|
362
493
|
|
|
@@ -653,7 +784,7 @@ export class DbHelper {
|
|
|
653
784
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
654
785
|
*/
|
|
655
786
|
async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
|
|
656
|
-
const { table, where } = this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
787
|
+
const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
657
788
|
|
|
658
789
|
// 使用 COUNT(1) 性能更好
|
|
659
790
|
const builder = new SqlBuilder().select(['COUNT(1) as cnt']).from(table).where(this.addDefaultStateFilter(where)).limit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.5",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"es-toolkit": "^1.41.0",
|
|
70
70
|
"pathe": "^2.0.3"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "389f7e3620a48a4fc7730519c05ab5b85b2c4179"
|
|
73
73
|
}
|