befly 3.9.5 → 3.9.7

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 CHANGED
@@ -11,7 +11,7 @@ import { keysToSnake } from 'befly-shared/keysToSnake';
11
11
  import { fieldClear } from 'befly-shared/fieldClear';
12
12
  import { RedisTTL, RedisKeys } from 'befly-shared/redisKeys';
13
13
  import { Logger } from './logger.js';
14
- import type { WhereConditions } from '../types/common.js';
14
+ import type { WhereConditions, JoinOption } from '../types/common.js';
15
15
  import type { BeflyContext } from '../types/befly.js';
16
16
  import type { QueryOptions, InsertOptions, UpdateOptions, DeleteOptions, ListResult, TransactionCallback } from '../types/database.js';
17
17
 
@@ -167,25 +167,175 @@ export class DbHelper {
167
167
  });
168
168
  }
169
169
 
170
+ /**
171
+ * 处理表名(转下划线格式)
172
+ * 'userProfile' -> 'user_profile'
173
+ */
174
+ private processTableName(table: string): string {
175
+ return snakeCase(table.trim());
176
+ }
177
+
178
+ /**
179
+ * 处理联查字段(支持表名.字段名格式)
180
+ * 'user.userId' -> 'user.user_id'
181
+ * 'username' -> 'user_name'
182
+ */
183
+ private processJoinField(field: string): string {
184
+ // 跳过函数、星号、已处理的字段
185
+ if (field.includes('(') || field === '*' || field.startsWith('`')) {
186
+ return field;
187
+ }
188
+
189
+ // 处理别名 AS
190
+ if (field.toUpperCase().includes(' AS ')) {
191
+ const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
192
+ return `${this.processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
193
+ }
194
+
195
+ // 处理表名.字段名
196
+ if (field.includes('.')) {
197
+ const [tableName, fieldName] = field.split('.');
198
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
199
+ }
200
+
201
+ // 普通字段
202
+ return snakeCase(field);
203
+ }
204
+
205
+ /**
206
+ * 处理联查的 where 条件键名
207
+ * 'user.userId': 1 -> 'user.user_id': 1
208
+ * 'user.status$in': [...] -> 'user.status$in': [...]
209
+ */
210
+ private processJoinWhereKey(key: string): string {
211
+ // 保留逻辑操作符
212
+ if (key === '$or' || key === '$and') {
213
+ return key;
214
+ }
215
+
216
+ // 处理带操作符的字段名(如 user.userId$gt)
217
+ if (key.includes('$')) {
218
+ const lastDollarIndex = key.lastIndexOf('$');
219
+ const fieldPart = key.substring(0, lastDollarIndex);
220
+ const operator = key.substring(lastDollarIndex);
221
+
222
+ if (fieldPart.includes('.')) {
223
+ const [tableName, fieldName] = fieldPart.split('.');
224
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
225
+ }
226
+ return `${snakeCase(fieldPart)}${operator}`;
227
+ }
228
+
229
+ // 处理表名.字段名
230
+ if (key.includes('.')) {
231
+ const [tableName, fieldName] = key.split('.');
232
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
233
+ }
234
+
235
+ // 普通字段
236
+ return snakeCase(key);
237
+ }
238
+
239
+ /**
240
+ * 递归处理联查的 where 条件
241
+ */
242
+ private processJoinWhere(where: any): any {
243
+ if (!where || typeof where !== 'object') return where;
244
+
245
+ if (Array.isArray(where)) {
246
+ return where.map((item) => this.processJoinWhere(item));
247
+ }
248
+
249
+ const result: any = {};
250
+ for (const [key, value] of Object.entries(where)) {
251
+ const newKey = this.processJoinWhereKey(key);
252
+
253
+ if (key === '$or' || key === '$and') {
254
+ result[newKey] = (value as any[]).map((item) => this.processJoinWhere(item));
255
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
256
+ result[newKey] = this.processJoinWhere(value);
257
+ } else {
258
+ result[newKey] = value;
259
+ }
260
+ }
261
+ return result;
262
+ }
263
+
264
+ /**
265
+ * 处理联查的 orderBy
266
+ * 'o.createdAt#DESC' -> 'o.created_at#DESC'
267
+ */
268
+ private processJoinOrderBy(orderBy: string[]): string[] {
269
+ if (!orderBy || !Array.isArray(orderBy)) return orderBy;
270
+ return orderBy.map((item) => {
271
+ if (typeof item !== 'string' || !item.includes('#')) return item;
272
+ const [field, direction] = item.split('#');
273
+ return `${this.processJoinField(field.trim())}#${direction.trim()}`;
274
+ });
275
+ }
276
+
170
277
  /**
171
278
  * 统一的查询参数预处理方法
172
279
  */
173
280
  private async prepareQueryOptions(options: QueryOptions) {
174
281
  const cleanWhere = this.cleanFields(options.where || {});
282
+ const hasJoins = options.joins && options.joins.length > 0;
175
283
 
176
- // 处理 fields(支持排除语法)
284
+ // 联查时使用特殊处理逻辑
285
+ if (hasJoins) {
286
+ // 联查时字段直接处理(支持表别名)
287
+ const processedFields = (options.fields || []).map((f) => this.processJoinField(f));
288
+
289
+ return {
290
+ table: this.processTableName(options.table),
291
+ fields: processedFields.length > 0 ? processedFields : ['*'],
292
+ where: this.processJoinWhere(cleanWhere),
293
+ joins: options.joins,
294
+ orderBy: this.processJoinOrderBy(options.orderBy || []),
295
+ page: options.page || 1,
296
+ limit: options.limit || 10
297
+ };
298
+ }
299
+
300
+ // 单表查询使用原有逻辑
177
301
  const processedFields = await this.fieldsToSnake(snakeCase(options.table), options.fields || []);
178
302
 
179
303
  return {
180
304
  table: snakeCase(options.table),
181
305
  fields: processedFields,
182
306
  where: this.whereKeysToSnake(cleanWhere),
307
+ joins: undefined,
183
308
  orderBy: this.orderByToSnake(options.orderBy || []),
184
309
  page: options.page || 1,
185
310
  limit: options.limit || 10
186
311
  };
187
312
  }
188
313
 
314
+ /**
315
+ * 为 builder 添加 JOIN
316
+ */
317
+ private applyJoins(builder: SqlBuilder, joins?: JoinOption[]): void {
318
+ if (!joins || joins.length === 0) return;
319
+
320
+ for (const join of joins) {
321
+ const processedTable = this.processTableName(join.table);
322
+ const type = join.type || 'left';
323
+
324
+ switch (type) {
325
+ case 'inner':
326
+ builder.innerJoin(processedTable, join.on);
327
+ break;
328
+ case 'right':
329
+ builder.rightJoin(processedTable, join.on);
330
+ break;
331
+ case 'left':
332
+ default:
333
+ builder.leftJoin(processedTable, join.on);
334
+ break;
335
+ }
336
+ }
337
+ }
338
+
189
339
  /**
190
340
  * 添加默认的 state 过滤条件
191
341
  * 默认查询 state > 0 的数据(排除已删除和特殊状态)
@@ -375,17 +525,27 @@ export class DbHelper {
375
525
  * 查询记录数
376
526
  * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
377
527
  * @param options.where - 查询条件
528
+ * @param options.joins - 多表联查选项
378
529
  * @example
379
530
  * // 查询总数
380
531
  * const count = await db.getCount({ table: 'user' });
381
532
  * // 查询指定条件的记录数
382
533
  * const activeCount = await db.getCount({ table: 'user', where: { state: 1 } });
534
+ * // 联查计数
535
+ * const count = await db.getCount({
536
+ * table: 'order o',
537
+ * joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
538
+ * where: { 'o.state': 1 }
539
+ * });
383
540
  */
384
541
  async getCount(options: Omit<QueryOptions, 'fields' | 'page' | 'limit' | 'orderBy'>): Promise<number> {
385
- const { table, where } = await this.prepareQueryOptions(options as QueryOptions);
542
+ const { table, where, joins } = await this.prepareQueryOptions(options as QueryOptions);
386
543
 
387
544
  const builder = new SqlBuilder().select(['COUNT(*) as count']).from(table).where(this.addDefaultStateFilter(where));
388
545
 
546
+ // 添加 JOIN
547
+ this.applyJoins(builder, joins);
548
+
389
549
  const { sql, params } = builder.toSelectSql();
390
550
  const result = await this.executeWithConn(sql, params);
391
551
 
@@ -394,18 +554,28 @@ export class DbHelper {
394
554
 
395
555
  /**
396
556
  * 查询单条数据
397
- * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
398
- * @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
557
+ * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名如 'order o')
558
+ * @param options.fields - 字段列表(联查时需带表别名,如 'o.id', 'u.username')
559
+ * @param options.joins - 多表联查选项
399
560
  * @example
400
- * // 以下两种写法等效:
561
+ * // 单表查询
401
562
  * getOne({ table: 'userProfile', fields: ['userId', 'userName'] })
402
- * getOne({ table: 'user_profile', fields: ['user_id', 'user_name'] })
563
+ * // 联查
564
+ * getOne({
565
+ * table: 'order o',
566
+ * joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
567
+ * fields: ['o.id', 'o.totalAmount', 'u.username'],
568
+ * where: { 'o.id': 1 }
569
+ * })
403
570
  */
404
571
  async getOne<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<T | null> {
405
- const { table, fields, where } = await this.prepareQueryOptions(options);
572
+ const { table, fields, where, joins } = await this.prepareQueryOptions(options);
406
573
 
407
574
  const builder = new SqlBuilder().select(fields).from(table).where(this.addDefaultStateFilter(where));
408
575
 
576
+ // 添加 JOIN
577
+ this.applyJoins(builder, joins);
578
+
409
579
  const { sql, params } = builder.toSelectSql();
410
580
  const result = await this.executeWithConn(sql, params);
411
581
 
@@ -421,11 +591,25 @@ export class DbHelper {
421
591
 
422
592
  /**
423
593
  * 查询列表(带分页)
424
- * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
425
- * @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
594
+ * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
595
+ * @param options.fields - 字段列表(联查时需带表别名)
596
+ * @param options.joins - 多表联查选项
426
597
  * @example
427
- * // 使用小驼峰格式(推荐)
598
+ * // 单表分页
428
599
  * getList({ table: 'userProfile', fields: ['userId', 'userName', 'createdAt'] })
600
+ * // 联查分页
601
+ * getList({
602
+ * table: 'order o',
603
+ * joins: [
604
+ * { table: 'user u', on: 'o.user_id = u.id' },
605
+ * { table: 'product p', on: 'o.product_id = p.id' }
606
+ * ],
607
+ * fields: ['o.id', 'o.totalAmount', 'u.username', 'p.name AS productName'],
608
+ * where: { 'o.status': 'paid' },
609
+ * orderBy: ['o.createdAt#DESC'],
610
+ * page: 1,
611
+ * limit: 10
612
+ * })
429
613
  */
430
614
  async getList<T extends Record<string, any> = Record<string, any>>(options: QueryOptions): Promise<ListResult<T>> {
431
615
  const prepared = await this.prepareQueryOptions(options);
@@ -444,6 +628,9 @@ export class DbHelper {
444
628
  // 查询总数
445
629
  const countBuilder = new SqlBuilder().select(['COUNT(*) as total']).from(prepared.table).where(whereFiltered);
446
630
 
631
+ // 添加 JOIN(计数也需要)
632
+ this.applyJoins(countBuilder, prepared.joins);
633
+
447
634
  const { sql: countSql, params: countParams } = countBuilder.toSelectSql();
448
635
  const countResult = await this.executeWithConn(countSql, countParams);
449
636
  const total = countResult?.[0]?.total || 0;
@@ -463,6 +650,9 @@ export class DbHelper {
463
650
  const offset = (prepared.page - 1) * prepared.limit;
464
651
  const dataBuilder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(whereFiltered).limit(prepared.limit).offset(offset);
465
652
 
653
+ // 添加 JOIN
654
+ this.applyJoins(dataBuilder, prepared.joins);
655
+
466
656
  // 只有用户明确指定了 orderBy 才添加排序
467
657
  if (prepared.orderBy && prepared.orderBy.length > 0) {
468
658
  dataBuilder.orderBy(prepared.orderBy);
@@ -486,12 +676,20 @@ export class DbHelper {
486
676
 
487
677
  /**
488
678
  * 查询所有数据(不分页,有上限保护)
489
- * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换)
490
- * @param options.fields - 字段列表(支持小驼峰或下划线格式,会自动转换为数据库字段名)
679
+ * @param options.table - 表名(支持小驼峰或下划线格式,会自动转换;联查时可带别名)
680
+ * @param options.fields - 字段列表(联查时需带表别名)
681
+ * @param options.joins - 多表联查选项
491
682
  * ⚠️ 警告:此方法会查询大量数据,建议使用 getList 分页查询
492
683
  * @example
493
- * // 使用小驼峰格式(推荐)
684
+ * // 单表查询
494
685
  * getAll({ table: 'userProfile', fields: ['userId', 'userName'] })
686
+ * // 联查
687
+ * getAll({
688
+ * table: 'order o',
689
+ * joins: [{ table: 'user u', on: 'o.user_id = u.id' }],
690
+ * fields: ['o.id', 'u.username'],
691
+ * where: { 'o.state': 1 }
692
+ * })
495
693
  */
496
694
  async getAll<T extends Record<string, any> = Record<string, any>>(options: Omit<QueryOptions, 'page' | 'limit'>): Promise<T[]> {
497
695
  // 添加硬性上限保护,防止内存溢出
@@ -502,6 +700,9 @@ export class DbHelper {
502
700
 
503
701
  const builder = new SqlBuilder().select(prepared.fields).from(prepared.table).where(this.addDefaultStateFilter(prepared.where)).limit(MAX_LIMIT);
504
702
 
703
+ // 添加 JOIN
704
+ this.applyJoins(builder, prepared.joins);
705
+
505
706
  if (prepared.orderBy && prepared.orderBy.length > 0) {
506
707
  builder.orderBy(prepared.orderBy);
507
708
  }
package/lib/sqlBuilder.ts CHANGED
@@ -355,6 +355,30 @@ export class SqlBuilder {
355
355
  return this;
356
356
  }
357
357
 
358
+ /**
359
+ * RIGHT JOIN
360
+ */
361
+ rightJoin(table: string, on: string): this {
362
+ if (typeof table !== 'string' || typeof on !== 'string') {
363
+ throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
364
+ }
365
+ const escapedTable = this._escapeTable(table);
366
+ this._joins.push(`RIGHT JOIN ${escapedTable} ON ${on}`);
367
+ return this;
368
+ }
369
+
370
+ /**
371
+ * INNER JOIN
372
+ */
373
+ innerJoin(table: string, on: string): this {
374
+ if (typeof table !== 'string' || typeof on !== 'string') {
375
+ throw new Error(`JOIN 表名和条件必须是字符串 (table: ${table}, on: ${on})`);
376
+ }
377
+ const escapedTable = this._escapeTable(table);
378
+ this._joins.push(`INNER JOIN ${escapedTable} ON ${on}`);
379
+ return this;
380
+ }
381
+
358
382
  /**
359
383
  * ORDER BY
360
384
  * @param fields - 格式为 ["field#ASC", "field2#DESC"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.9.5",
3
+ "version": "3.9.7",
4
4
  "description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
5
5
  "type": "module",
6
6
  "private": false,
@@ -38,6 +38,7 @@
38
38
  "license": "Apache-2.0",
39
39
  "files": [
40
40
  "checks",
41
+ "docs",
41
42
  "dist",
42
43
  "hooks",
43
44
  "lib",
@@ -64,7 +65,7 @@
64
65
  "bun": ">=1.3.0"
65
66
  },
66
67
  "dependencies": {
67
- "befly-shared": "^1.2.1",
68
+ "befly-shared": "^1.2.2",
68
69
  "chalk": "^5.6.2",
69
70
  "es-toolkit": "^1.42.0",
70
71
  "fast-jwt": "^6.1.0",
@@ -73,7 +74,7 @@
73
74
  "pino": "^10.1.0",
74
75
  "pino-roll": "^4.0.0"
75
76
  },
76
- "gitHead": "762170d80f1649d216fcf702752b1375d335ccc0",
77
+ "gitHead": "2a0ccabca4ae72fbf490d41a6c3e5aeb63fb08c6",
77
78
  "devDependencies": {
78
79
  "typescript": "^5.9.3"
79
80
  }