befly 3.7.3 → 3.7.4
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 +125 -15
- 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 ??';
|
|
93
|
+
const result = await this.executeWithConn(sql, [table]);
|
|
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,
|
|
@@ -240,7 +350,7 @@ export class DbHelper {
|
|
|
240
350
|
* const activeCount = await db.getCount({ table: 'user', where: { state: 1 } });
|
|
241
351
|
*/
|
|
242
352
|
async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
|
|
243
|
-
const { table, where } = this.prepareQueryOptions(options as QueryOptions);
|
|
353
|
+
const { table, where } = await this.prepareQueryOptions(options as QueryOptions);
|
|
244
354
|
|
|
245
355
|
const builder = new SqlBuilder().select(['COUNT(*) as count']).from(table).where(this.addDefaultStateFilter(where));
|
|
246
356
|
|
|
@@ -260,7 +370,7 @@ export class DbHelper {
|
|
|
260
370
|
* getOne({ table: 'user_profile', fields: ['user_id', 'user_name'] })
|
|
261
371
|
*/
|
|
262
372
|
async getOne<T = any>(options: QueryOptions): Promise<T | null> {
|
|
263
|
-
const { table, fields, where } = this.prepareQueryOptions(options);
|
|
373
|
+
const { table, fields, where } = await this.prepareQueryOptions(options);
|
|
264
374
|
|
|
265
375
|
const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
|
|
266
376
|
|
|
@@ -286,7 +396,7 @@ export class DbHelper {
|
|
|
286
396
|
* getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
|
|
287
397
|
*/
|
|
288
398
|
async getList<T = any>(options: QueryOptions): Promise<ListResult<T>> {
|
|
289
|
-
const prepared = this.prepareQueryOptions(options);
|
|
399
|
+
const prepared = await this.prepareQueryOptions(options);
|
|
290
400
|
|
|
291
401
|
// 参数上限校验
|
|
292
402
|
if (prepared.page < 1 || prepared.page > 10000) {
|
|
@@ -356,7 +466,7 @@ export class DbHelper {
|
|
|
356
466
|
const MAX_LIMIT = 10000;
|
|
357
467
|
const WARNING_LIMIT = 1000;
|
|
358
468
|
|
|
359
|
-
const prepared = this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
469
|
+
const prepared = await this.prepareQueryOptions({ ...options, page: 1, limit: 10 });
|
|
360
470
|
|
|
361
471
|
const builder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(this.addDefaultStateFilter(prepared.where)).limit(MAX_LIMIT);
|
|
362
472
|
|
|
@@ -653,7 +763,7 @@ export class DbHelper {
|
|
|
653
763
|
* @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
|
|
654
764
|
*/
|
|
655
765
|
async exists(options: Omit<QueryOptions, 'fields' | 'orderBy' | 'page' | 'limit'>): Promise<boolean> {
|
|
656
|
-
const { table, where } = this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
766
|
+
const { table, where } = await this.prepareQueryOptions({ ...options, page: 1, limit: 1 });
|
|
657
767
|
|
|
658
768
|
// 使用 COUNT(1) 性能更好
|
|
659
769
|
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.4",
|
|
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": "18ec6863b4c71b8267f535a0b2b6a15c19e31322"
|
|
73
73
|
}
|