befly 3.9.6 → 3.9.8

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.
@@ -0,0 +1,307 @@
1
+ /**
2
+ * DbHelper JOIN 功能测试
3
+ * 测试多表联查相关功能(不支持表别名)
4
+ */
5
+
6
+ import { describe, test, expect } from 'bun:test';
7
+ import { snakeCase } from 'es-toolkit/string';
8
+
9
+ // ============================================
10
+ // 辅助函数单元测试(模拟 DbHelper 私有方法)
11
+ // ============================================
12
+
13
+ /**
14
+ * 处理表名(转下划线格式)
15
+ */
16
+ function processTableName(table: string): string {
17
+ return snakeCase(table.trim());
18
+ }
19
+
20
+ /**
21
+ * 处理联查字段(支持表名.字段名格式)
22
+ */
23
+ function processJoinField(field: string): string {
24
+ if (field.includes('(') || field === '*' || field.startsWith('`')) {
25
+ return field;
26
+ }
27
+
28
+ if (field.toUpperCase().includes(' AS ')) {
29
+ const [fieldPart, aliasPart] = field.split(/\s+AS\s+/i);
30
+ return `${processJoinField(fieldPart.trim())} AS ${aliasPart.trim()}`;
31
+ }
32
+
33
+ if (field.includes('.')) {
34
+ const [tableName, fieldName] = field.split('.');
35
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
36
+ }
37
+
38
+ return snakeCase(field);
39
+ }
40
+
41
+ /**
42
+ * 处理联查 where 条件键名
43
+ */
44
+ function processJoinWhereKey(key: string): string {
45
+ if (key === '$or' || key === '$and') {
46
+ return key;
47
+ }
48
+
49
+ if (key.includes('$')) {
50
+ const lastDollarIndex = key.lastIndexOf('$');
51
+ const fieldPart = key.substring(0, lastDollarIndex);
52
+ const operator = key.substring(lastDollarIndex);
53
+
54
+ if (fieldPart.includes('.')) {
55
+ const [tableName, fieldName] = fieldPart.split('.');
56
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}${operator}`;
57
+ }
58
+ return `${snakeCase(fieldPart)}${operator}`;
59
+ }
60
+
61
+ if (key.includes('.')) {
62
+ const [tableName, fieldName] = key.split('.');
63
+ return `${snakeCase(tableName)}.${snakeCase(fieldName)}`;
64
+ }
65
+
66
+ return snakeCase(key);
67
+ }
68
+
69
+ /**
70
+ * 递归处理联查 where 条件
71
+ */
72
+ function processJoinWhere(where: any): any {
73
+ if (!where || typeof where !== 'object') return where;
74
+
75
+ if (Array.isArray(where)) {
76
+ return where.map((item) => processJoinWhere(item));
77
+ }
78
+
79
+ const result: any = {};
80
+ for (const [key, value] of Object.entries(where)) {
81
+ const newKey = processJoinWhereKey(key);
82
+
83
+ if (key === '$or' || key === '$and') {
84
+ result[newKey] = (value as any[]).map((item) => processJoinWhere(item));
85
+ } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
86
+ result[newKey] = processJoinWhere(value);
87
+ } else {
88
+ result[newKey] = value;
89
+ }
90
+ }
91
+ return result;
92
+ }
93
+
94
+ /**
95
+ * 处理联查 orderBy
96
+ */
97
+ function processJoinOrderBy(orderBy: string[]): string[] {
98
+ if (!orderBy || !Array.isArray(orderBy)) return orderBy;
99
+ return orderBy.map((item) => {
100
+ if (typeof item !== 'string' || !item.includes('#')) return item;
101
+ const [field, direction] = item.split('#');
102
+ return `${processJoinField(field.trim())}#${direction.trim()}`;
103
+ });
104
+ }
105
+
106
+ // ============================================
107
+ // 测试用例
108
+ // ============================================
109
+
110
+ describe('DbHelper JOIN - processTableName', () => {
111
+ test('普通表名转下划线', () => {
112
+ expect(processTableName('userProfile')).toBe('user_profile');
113
+ expect(processTableName('orderDetail')).toBe('order_detail');
114
+ expect(processTableName('user')).toBe('user');
115
+ expect(processTableName('order')).toBe('order');
116
+ });
117
+ });
118
+
119
+ describe('DbHelper JOIN - processJoinField', () => {
120
+ test('带表名的字段', () => {
121
+ expect(processJoinField('order.userId')).toBe('order.user_id');
122
+ expect(processJoinField('user.userName')).toBe('user.user_name');
123
+ expect(processJoinField('order.createdAt')).toBe('order.created_at');
124
+ });
125
+
126
+ test('表名也转下划线', () => {
127
+ expect(processJoinField('orderDetail.productId')).toBe('order_detail.product_id');
128
+ expect(processJoinField('userProfile.avatarUrl')).toBe('user_profile.avatar_url');
129
+ });
130
+
131
+ test('普通字段(无表名)', () => {
132
+ expect(processJoinField('userName')).toBe('user_name');
133
+ expect(processJoinField('createdAt')).toBe('created_at');
134
+ });
135
+
136
+ test('带 AS 别名的字段', () => {
137
+ expect(processJoinField('order.totalAmount AS total')).toBe('order.total_amount AS total');
138
+ expect(processJoinField('user.userName AS name')).toBe('user.user_name AS name');
139
+ expect(processJoinField('product.name AS productName')).toBe('product.name AS productName');
140
+ });
141
+
142
+ test('函数字段保持原样', () => {
143
+ expect(processJoinField('COUNT(*)')).toBe('COUNT(*)');
144
+ expect(processJoinField('SUM(order.amount)')).toBe('SUM(order.amount)');
145
+ });
146
+
147
+ test('星号保持原样', () => {
148
+ expect(processJoinField('*')).toBe('*');
149
+ });
150
+
151
+ test('已转义字段保持原样', () => {
152
+ expect(processJoinField('`order`')).toBe('`order`');
153
+ });
154
+ });
155
+
156
+ describe('DbHelper JOIN - processJoinWhereKey', () => {
157
+ test('带表名的字段名', () => {
158
+ expect(processJoinWhereKey('order.userId')).toBe('order.user_id');
159
+ expect(processJoinWhereKey('user.userName')).toBe('user.user_name');
160
+ });
161
+
162
+ test('带表名和操作符的字段名', () => {
163
+ expect(processJoinWhereKey('order.createdAt$gt')).toBe('order.created_at$gt');
164
+ expect(processJoinWhereKey('user.status$in')).toBe('user.status$in');
165
+ expect(processJoinWhereKey('order.amount$gte')).toBe('order.amount$gte');
166
+ });
167
+
168
+ test('普通字段带操作符', () => {
169
+ expect(processJoinWhereKey('createdAt$gt')).toBe('created_at$gt');
170
+ expect(processJoinWhereKey('userId$ne')).toBe('user_id$ne');
171
+ });
172
+
173
+ test('逻辑操作符保持原样', () => {
174
+ expect(processJoinWhereKey('$or')).toBe('$or');
175
+ expect(processJoinWhereKey('$and')).toBe('$and');
176
+ });
177
+ });
178
+
179
+ describe('DbHelper JOIN - processJoinWhere', () => {
180
+ test('简单条件', () => {
181
+ const where = { 'order.userId': 1, 'order.state': 1 };
182
+ const result = processJoinWhere(where);
183
+ expect(result).toEqual({ 'order.user_id': 1, 'order.state': 1 });
184
+ });
185
+
186
+ test('带操作符的条件', () => {
187
+ const where = { 'order.createdAt$gt': 1000, 'user.state$ne': 0 };
188
+ const result = processJoinWhere(where);
189
+ expect(result).toEqual({ 'order.created_at$gt': 1000, 'user.state$ne': 0 });
190
+ });
191
+
192
+ test('$or 条件', () => {
193
+ const where = {
194
+ $or: [{ 'user.userName$like': '%test%' }, { 'user.email$like': '%test%' }]
195
+ };
196
+ const result = processJoinWhere(where);
197
+ expect(result).toEqual({
198
+ $or: [{ 'user.user_name$like': '%test%' }, { 'user.email$like': '%test%' }]
199
+ });
200
+ });
201
+
202
+ test('复杂嵌套条件', () => {
203
+ const where = {
204
+ 'order.state': 1,
205
+ 'user.state': 1,
206
+ $or: [{ 'user.userName$like': '%test%' }, { 'product.name$like': '%test%' }],
207
+ 'order.createdAt$gte': 1000
208
+ };
209
+ const result = processJoinWhere(where);
210
+ expect(result).toEqual({
211
+ 'order.state': 1,
212
+ 'user.state': 1,
213
+ $or: [{ 'user.user_name$like': '%test%' }, { 'product.name$like': '%test%' }],
214
+ 'order.created_at$gte': 1000
215
+ });
216
+ });
217
+ });
218
+
219
+ describe('DbHelper JOIN - processJoinOrderBy', () => {
220
+ test('带表名的排序', () => {
221
+ const orderBy = ['order.createdAt#DESC', 'user.userName#ASC'];
222
+ const result = processJoinOrderBy(orderBy);
223
+ expect(result).toEqual(['order.created_at#DESC', 'user.user_name#ASC']);
224
+ });
225
+
226
+ test('普通排序', () => {
227
+ const orderBy = ['createdAt#DESC'];
228
+ const result = processJoinOrderBy(orderBy);
229
+ expect(result).toEqual(['created_at#DESC']);
230
+ });
231
+
232
+ test('无排序方向的保持原样', () => {
233
+ const orderBy = ['id'];
234
+ const result = processJoinOrderBy(orderBy);
235
+ expect(result).toEqual(['id']);
236
+ });
237
+ });
238
+
239
+ describe('DbHelper JOIN - JoinOption 类型验证', () => {
240
+ test('LEFT JOIN(默认)', () => {
241
+ const join = { table: 'user', on: 'order.user_id = user.id' };
242
+ expect(join.type).toBeUndefined();
243
+ expect(join.table).toBe('user');
244
+ expect(join.on).toBe('order.user_id = user.id');
245
+ });
246
+
247
+ test('INNER JOIN', () => {
248
+ const join = { type: 'inner' as const, table: 'product', on: 'order.product_id = product.id' };
249
+ expect(join.type).toBe('inner');
250
+ });
251
+
252
+ test('RIGHT JOIN', () => {
253
+ const join = { type: 'right' as const, table: 'category', on: 'product.category_id = category.id' };
254
+ expect(join.type).toBe('right');
255
+ });
256
+ });
257
+
258
+ describe('DbHelper JOIN - 完整场景模拟', () => {
259
+ test('订单列表联查参数处理', () => {
260
+ // 模拟输入
261
+ const options = {
262
+ table: 'order',
263
+ joins: [
264
+ { table: 'user', on: 'order.userId = user.id' },
265
+ { table: 'product', on: 'order.productId = product.id' }
266
+ ],
267
+ fields: ['order.id', 'order.totalAmount', 'user.userName', 'product.name AS productName'],
268
+ where: {
269
+ 'order.state': 1,
270
+ 'user.state': 1,
271
+ 'order.createdAt$gte': 1701388800000
272
+ },
273
+ orderBy: ['order.createdAt#DESC']
274
+ };
275
+
276
+ // 处理表名
277
+ const processedTable = processTableName(options.table);
278
+ expect(processedTable).toBe('order');
279
+
280
+ // 处理字段
281
+ const processedFields = options.fields.map((f) => processJoinField(f));
282
+ expect(processedFields).toEqual(['order.id', 'order.total_amount', 'user.user_name', 'product.name AS productName']);
283
+
284
+ // 处理 where
285
+ const processedWhere = processJoinWhere(options.where);
286
+ expect(processedWhere).toEqual({
287
+ 'order.state': 1,
288
+ 'user.state': 1,
289
+ 'order.created_at$gte': 1701388800000
290
+ });
291
+
292
+ // 处理 orderBy
293
+ const processedOrderBy = processJoinOrderBy(options.orderBy);
294
+ expect(processedOrderBy).toEqual(['order.created_at#DESC']);
295
+
296
+ // 处理 joins
297
+ const processedJoins = options.joins.map((j) => ({
298
+ type: (j as any).type || 'left',
299
+ table: processTableName(j.table),
300
+ on: j.on
301
+ }));
302
+ expect(processedJoins).toEqual([
303
+ { type: 'left', table: 'user', on: 'order.userId = user.id' },
304
+ { type: 'left', table: 'product', on: 'order.productId = product.id' }
305
+ ]);
306
+ });
307
+ });
package/types/common.d.ts CHANGED
@@ -92,6 +92,19 @@ export type ComparisonOperator = '=' | '>' | '<' | '>=' | '<=' | '!=' | '<>' | '
92
92
  */
93
93
  export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
94
94
 
95
+ /**
96
+ * JOIN 选项
97
+ * 用于简化多表联查的写法
98
+ */
99
+ export interface JoinOption {
100
+ /** JOIN 类型,默认 'left' */
101
+ type?: 'left' | 'right' | 'inner';
102
+ /** 要联接的表名(不支持别名) */
103
+ table: string;
104
+ /** JOIN 条件(如 'order.user_id = user.id') */
105
+ on: string;
106
+ }
107
+
95
108
  /**
96
109
  * 工具函数返回类型
97
110
  */
@@ -4,6 +4,7 @@
4
4
 
5
5
  import type { SqlValue } from 'befly-shared/types';
6
6
  import type { DatabaseTables, TableName, TableType, TableInsertType, TableUpdateType, TypedWhereConditions } from './table';
7
+ import type { JoinOption } from './common';
7
8
 
8
9
  // 重新导出表类型工具
9
10
  export type { DatabaseTables, TableName, TableType, TableInsertType, TableUpdateType, SystemFields, BaseTable, InsertType, UpdateType, SelectType, TypedWhereConditions } from './table';
@@ -98,12 +99,14 @@ export interface TypedDeleteOptions<K extends TableName> {
98
99
  * 查询选项(兼容旧版,不进行类型检查)
99
100
  */
100
101
  export interface QueryOptions {
101
- /** 表名 */
102
+ /** 表名(可带别名,如 'order o') */
102
103
  table: string;
103
- /** 查询字段 */
104
+ /** 查询字段(联查时需带表别名,如 'o.id', 'u.username') */
104
105
  fields?: string[];
105
- /** WHERE 条件 */
106
+ /** WHERE 条件(联查时字段需带表别名,如 { 'o.state': 1 }) */
106
107
  where?: WhereConditions;
108
+ /** 多表联查选项 */
109
+ joins?: JoinOption[];
107
110
  /** 排序(格式:["字段#ASC", "字段#DESC"]) */
108
111
  orderBy?: string[];
109
112
  /** 页码(从 1 开始) */