befly 3.7.2 → 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.
Files changed (3) hide show
  1. package/env.ts +6 -6
  2. package/lib/dbHelper.ts +125 -15
  3. package/package.json +2 -2
package/env.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import type { EnvConfig } from './types/env.js';
9
9
 
10
- const isDev = process.env.NODE_ENV === 'development';
10
+ const isProd = process.env.NODE_ENV === 'production';
11
11
 
12
12
  /**
13
13
  * 核心默认配置
@@ -16,16 +16,16 @@ const isDev = process.env.NODE_ENV === 'development';
16
16
  const coreEnv: EnvConfig = {
17
17
  // ========== 项目配置 ==========
18
18
  NODE_ENV: process.env.NODE_ENV || 'development',
19
- APP_NAME: isDev ? '野蜂飞舞开发环境' : '野蜂飞舞正式环境',
19
+ APP_NAME: isProd ? '野蜂飞舞正式环境' : '野蜂飞舞开发环境',
20
20
  APP_PORT: 3000,
21
- APP_HOST: isDev ? '0.0.0.0' : '127.0.0.1',
21
+ APP_HOST: isProd ? '127.0.0.1' : '0.0.0.0',
22
22
  DEV_EMAIL: 'dev@qq.com',
23
- DEV_PASSWORD: isDev ? '123456' : '123456',
23
+ DEV_PASSWORD: '123456',
24
24
  BODY_LIMIT: 10485760, // 10MB
25
25
  PARAMS_CHECK: false,
26
26
 
27
27
  // ========== 日志配置 ==========
28
- LOG_DEBUG: isDev ? 1 : 0,
28
+ LOG_DEBUG: isProd ? 0 : 1,
29
29
  LOG_EXCLUDE_FIELDS: 'password,token,secret',
30
30
  LOG_DIR: './logs',
31
31
  LOG_TO_CONSOLE: 1,
@@ -55,7 +55,7 @@ const coreEnv: EnvConfig = {
55
55
  REDIS_KEY_PREFIX: 'befly',
56
56
 
57
57
  // ========== JWT 配置 ==========
58
- JWT_SECRET: isDev ? 'befly-dev-secret' : 'befly-prod-secret',
58
+ JWT_SECRET: isProd ? 'befly-dev-secret' : 'befly-prod-secret',
59
59
  JWT_EXPIRES_IN: '7d',
60
60
  JWT_ALGORITHM: 'HS256',
61
61
 
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 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,
@@ -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.2",
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": "4e67637bf6481e2455901b0dba798bfa52236898"
72
+ "gitHead": "18ec6863b4c71b8267f535a0b2b6a15c19e31322"
73
73
  }