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.
Files changed (2) hide show
  1. package/lib/dbHelper.ts +162 -31
  2. 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 fields;
38
- return fields.map((field) => {
39
- // 保留通配符和特殊字段
40
- if (field === '*' || field.includes('(') || field.includes(' ')) {
41
- return field;
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
- return snakeCase(field);
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: this.fieldsToSnake(options.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
- // 使用 sql.unsafe 执行查询
199
- let result;
200
- if (params && params.length > 0) {
201
- result = await this.sql.unsafe(sqlStr, params);
202
- } else {
203
- result = await this.sql.unsafe(sqlStr);
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
- const duration = Date.now() - startTime;
317
+ // 计算执行时间
318
+ const duration = Date.now() - startTime;
208
319
 
209
- // 慢查询警告(超过 1000ms)
210
- if (duration > 1000) {
211
- const sqlPreview = sqlStr.length > 100 ? sqlStr.substring(0, 100) + '...' : sqlStr;
212
- Logger.warn(`🐌 检测到慢查询 (${duration}ms): ${sqlPreview}`);
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
- return result;
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",
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": "fe8261f577a1f8ba995673b68a67daa87f9d49c3"
72
+ "gitHead": "389f7e3620a48a4fc7730519c05ab5b85b2c4179"
73
73
  }