@starbemtech/star-db-query-builder 1.0.38 → 1.2.1
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/.github/workflows/publish.yml +118 -0
- package/ARCHITECTURE.md +313 -0
- package/CHANGELOG.md +65 -0
- package/README.md +1309 -129
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/block-navigation.js +1 -1
- package/coverage/lcov-report/index.html +41 -11
- package/coverage/lcov-report/mysqlClient.ts.html +685 -0
- package/coverage/lcov-report/pgClient.ts.html +823 -0
- package/coverage/lcov-report/sorter.js +21 -7
- package/coverage/lcov.info +533 -0
- package/coverage/mysqlClient.ts.html +685 -0
- package/coverage/pgClient.ts.html +823 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/core/repository.d.ts +374 -0
- package/dist/src/core/repository.js +677 -0
- package/dist/src/core/repository.js.map +1 -0
- package/dist/src/{default → core}/types.d.ts +9 -1
- package/dist/src/{default → core}/types.js.map +1 -1
- package/dist/src/core/utils.d.ts +133 -0
- package/dist/src/{default → core}/utils.js +167 -0
- package/dist/src/core/utils.js.map +1 -0
- package/dist/src/db/IDatabaseClient.d.ts +10 -1
- package/dist/src/db/initDb.d.ts +120 -1
- package/dist/src/db/initDb.js +119 -0
- package/dist/src/db/initDb.js.map +1 -1
- package/dist/src/db/mysqlClient.d.ts +23 -1
- package/dist/src/db/mysqlClient.js +115 -0
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.d.ts +22 -1
- package/dist/src/db/pgClient.js +127 -0
- package/dist/src/db/pgClient.js.map +1 -1
- package/dist/src/monitor/monitor.d.ts +6 -1
- package/dist/src/monitor/monitor.js +5 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/setupTests.d.ts +26 -0
- package/dist/src/setupTests.js +43 -0
- package/dist/src/setupTests.js.map +1 -0
- package/docs/INDEX.md +145 -0
- package/docs/methods/findFirst.md +394 -0
- package/docs/methods/findMany.md +587 -0
- package/docs/methods/insert.md +536 -0
- package/docs/methods/insertMany.md +627 -0
- package/docs/methods/joins.md +781 -0
- package/docs/methods/rawQuery.md +284 -0
- package/docs/methods/transactions.md +737 -0
- package/eslint.config.mjs +77 -0
- package/index.ts +5 -4
- package/jest.config.ts +23 -28
- package/package.json +52 -29
- package/scripts/release.sh +123 -0
- package/src/core/repository.ts +865 -0
- package/src/{default → core}/types.ts +11 -2
- package/src/{default → core}/utils.ts +168 -0
- package/src/db/IDatabaseClient.ts +11 -1
- package/src/db/__tests__/mysqlClient.test.ts +262 -0
- package/src/db/__tests__/pgClient.test.ts +260 -0
- package/src/db/initDb.ts +120 -1
- package/src/db/mysqlClient.ts +119 -3
- package/src/db/pgClient.ts +131 -3
- package/src/monitor/monitor.ts +5 -0
- package/src/setupTests.ts +45 -0
- package/tsconfig.test.json +21 -0
- package/.eslintignore +0 -4
- package/.eslintrc.json +0 -32
- package/coverage/clover.xml +0 -6
- package/coverage/coverage-final.json +0 -1
- package/dist/.eslintrc.json +0 -32
- package/dist/src/default/genericRepository.d.ts +0 -20
- package/dist/src/default/genericRepository.js +0 -192
- package/dist/src/default/genericRepository.js.map +0 -1
- package/dist/src/default/utils.d.ts +0 -9
- package/dist/src/default/utils.js.map +0 -1
- package/index.d.ts +0 -60
- package/src/default/genericRepository.ts +0 -320
- /package/dist/src/{default → core}/types.js +0 -0
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
# joins
|
|
2
|
+
|
|
3
|
+
Executes queries with JOIN operations to combine data from multiple tables.
|
|
4
|
+
|
|
5
|
+
## Signature
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
joins<T>({
|
|
9
|
+
tableName: string,
|
|
10
|
+
dbClient: IDatabaseClient,
|
|
11
|
+
select: string[],
|
|
12
|
+
joins: JoinClause[],
|
|
13
|
+
where?: Conditions<T>,
|
|
14
|
+
groupBy?: string[],
|
|
15
|
+
orderBy?: OrderBy,
|
|
16
|
+
limit?: number,
|
|
17
|
+
offset?: number,
|
|
18
|
+
unaccent?: boolean
|
|
19
|
+
}): Promise<T[]>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Parameters
|
|
23
|
+
|
|
24
|
+
| Parameter | Type | Required | Description |
|
|
25
|
+
| ----------- | ----------------- | -------- | -------------------------------------------------- |
|
|
26
|
+
| `tableName` | `string` | ✅ | Name of the main database table |
|
|
27
|
+
| `dbClient` | `IDatabaseClient` | ✅ | Database client instance |
|
|
28
|
+
| `select` | `string[]` | ✅ | Array of field names to select (must be specified) |
|
|
29
|
+
| `joins` | `JoinClause[]` | ✅ | Array of JOIN clauses |
|
|
30
|
+
| `where` | `Conditions<T>` | ❌ | Conditions to filter records |
|
|
31
|
+
| `groupBy` | `string[]` | ❌ | Fields to group by |
|
|
32
|
+
| `orderBy` | `OrderBy` | ❌ | Sort order specification |
|
|
33
|
+
| `limit` | `number` | ❌ | Maximum number of records to return |
|
|
34
|
+
| `offset` | `number` | ❌ | Number of records to skip |
|
|
35
|
+
| `unaccent` | `boolean` | ❌ | Enable unaccent search for PostgreSQL |
|
|
36
|
+
|
|
37
|
+
## JoinClause Interface
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
interface JoinClause {
|
|
41
|
+
type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL'
|
|
42
|
+
table: string
|
|
43
|
+
on: string
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Return Value
|
|
48
|
+
|
|
49
|
+
- **Type**: `Promise<T[]>`
|
|
50
|
+
- **Description**: Returns an array of records from the joined tables
|
|
51
|
+
|
|
52
|
+
## Examples
|
|
53
|
+
|
|
54
|
+
### Basic JOIN
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { joins } from '@starbemtech/star-db-query-builder'
|
|
58
|
+
|
|
59
|
+
// Get users with their orders
|
|
60
|
+
const usersWithOrders = await joins({
|
|
61
|
+
tableName: 'users',
|
|
62
|
+
dbClient,
|
|
63
|
+
select: [
|
|
64
|
+
'users.id',
|
|
65
|
+
'users.name',
|
|
66
|
+
'users.email',
|
|
67
|
+
'orders.total',
|
|
68
|
+
'orders.created_at',
|
|
69
|
+
],
|
|
70
|
+
joins: [
|
|
71
|
+
{
|
|
72
|
+
type: 'LEFT',
|
|
73
|
+
table: 'orders',
|
|
74
|
+
on: 'users.id = orders.user_id',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
where: {
|
|
78
|
+
'users.status': { operator: '=', value: 'active' },
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
console.log(usersWithOrders)
|
|
83
|
+
// [
|
|
84
|
+
// {
|
|
85
|
+
// id: 'user-1',
|
|
86
|
+
// name: 'John Doe',
|
|
87
|
+
// email: 'john@example.com',
|
|
88
|
+
// total: 150.00,
|
|
89
|
+
// created_at: '2023-12-01T10:00:00.000Z'
|
|
90
|
+
// },
|
|
91
|
+
// // ... more results
|
|
92
|
+
// ]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Multiple JOINs
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// Complex report with multiple tables
|
|
99
|
+
const report = await joins({
|
|
100
|
+
tableName: 'users',
|
|
101
|
+
dbClient,
|
|
102
|
+
select: [
|
|
103
|
+
'users.name',
|
|
104
|
+
'users.email',
|
|
105
|
+
'COUNT(orders.id) as order_count',
|
|
106
|
+
'SUM(orders.total) as total_spent',
|
|
107
|
+
'plans.name as plan_name',
|
|
108
|
+
'plans.price as plan_price',
|
|
109
|
+
],
|
|
110
|
+
joins: [
|
|
111
|
+
{
|
|
112
|
+
type: 'LEFT',
|
|
113
|
+
table: 'orders',
|
|
114
|
+
on: 'users.id = orders.user_id',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
type: 'LEFT',
|
|
118
|
+
table: 'user_plans',
|
|
119
|
+
on: 'users.id = user_plans.user_id',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
type: 'LEFT',
|
|
123
|
+
table: 'plans',
|
|
124
|
+
on: 'user_plans.plan_id = plans.id',
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
where: {
|
|
128
|
+
'users.status': { operator: '=', value: 'active' },
|
|
129
|
+
},
|
|
130
|
+
groupBy: [
|
|
131
|
+
'users.id',
|
|
132
|
+
'users.name',
|
|
133
|
+
'users.email',
|
|
134
|
+
'plans.name',
|
|
135
|
+
'plans.price',
|
|
136
|
+
],
|
|
137
|
+
orderBy: [{ field: 'total_spent', direction: 'DESC' }],
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Different JOIN Types
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// INNER JOIN - only users with orders
|
|
145
|
+
const usersWithOrders = await joins({
|
|
146
|
+
tableName: 'users',
|
|
147
|
+
dbClient,
|
|
148
|
+
select: ['users.name', 'orders.total'],
|
|
149
|
+
joins: [
|
|
150
|
+
{
|
|
151
|
+
type: 'INNER',
|
|
152
|
+
table: 'orders',
|
|
153
|
+
on: 'users.id = orders.user_id',
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// LEFT JOIN - all users, with or without orders
|
|
159
|
+
const allUsersWithOrders = await joins({
|
|
160
|
+
tableName: 'users',
|
|
161
|
+
dbClient,
|
|
162
|
+
select: ['users.name', 'orders.total'],
|
|
163
|
+
joins: [
|
|
164
|
+
{
|
|
165
|
+
type: 'LEFT',
|
|
166
|
+
table: 'orders',
|
|
167
|
+
on: 'users.id = orders.user_id',
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
// RIGHT JOIN - all orders, with or without users
|
|
173
|
+
const ordersWithUsers = await joins({
|
|
174
|
+
tableName: 'users',
|
|
175
|
+
dbClient,
|
|
176
|
+
select: ['users.name', 'orders.total'],
|
|
177
|
+
joins: [
|
|
178
|
+
{
|
|
179
|
+
type: 'RIGHT',
|
|
180
|
+
table: 'orders',
|
|
181
|
+
on: 'users.id = orders.user_id',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// FULL OUTER JOIN - all users and all orders
|
|
187
|
+
const allData = await joins({
|
|
188
|
+
tableName: 'users',
|
|
189
|
+
dbClient,
|
|
190
|
+
select: ['users.name', 'orders.total'],
|
|
191
|
+
joins: [
|
|
192
|
+
{
|
|
193
|
+
type: 'FULL',
|
|
194
|
+
table: 'orders',
|
|
195
|
+
on: 'users.id = orders.user_id',
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Complex WHERE Conditions with JOINs
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Users with orders from last month
|
|
205
|
+
const recentUsers = await joins({
|
|
206
|
+
tableName: 'users',
|
|
207
|
+
dbClient,
|
|
208
|
+
select: ['users.name', 'users.email', 'orders.total', 'orders.created_at'],
|
|
209
|
+
joins: [
|
|
210
|
+
{
|
|
211
|
+
type: 'INNER',
|
|
212
|
+
table: 'orders',
|
|
213
|
+
on: 'users.id = orders.user_id',
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
where: {
|
|
217
|
+
AND: [
|
|
218
|
+
{ 'users.status': { operator: '=', value: 'active' } },
|
|
219
|
+
{
|
|
220
|
+
'orders.created_at': { operator: '>=', value: new Date('2023-11-01') },
|
|
221
|
+
},
|
|
222
|
+
{ 'orders.total': { operator: '>', value: 100 } },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
orderBy: [{ field: 'orders.created_at', direction: 'DESC' }],
|
|
226
|
+
})
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Aggregation with JOINs
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// User statistics with order data
|
|
233
|
+
const userStats = await joins({
|
|
234
|
+
tableName: 'users',
|
|
235
|
+
dbClient,
|
|
236
|
+
select: [
|
|
237
|
+
'users.id',
|
|
238
|
+
'users.name',
|
|
239
|
+
'COUNT(orders.id) as order_count',
|
|
240
|
+
'SUM(orders.total) as total_spent',
|
|
241
|
+
'AVG(orders.total) as avg_order_value',
|
|
242
|
+
'MAX(orders.created_at) as last_order_date',
|
|
243
|
+
],
|
|
244
|
+
joins: [
|
|
245
|
+
{
|
|
246
|
+
type: 'LEFT',
|
|
247
|
+
table: 'orders',
|
|
248
|
+
on: 'users.id = orders.user_id',
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
where: {
|
|
252
|
+
'users.created_at': { operator: '>=', value: new Date('2023-01-01') },
|
|
253
|
+
},
|
|
254
|
+
groupBy: ['users.id', 'users.name'],
|
|
255
|
+
having: {
|
|
256
|
+
'COUNT(orders.id)': { operator: '>', value: 0 },
|
|
257
|
+
},
|
|
258
|
+
orderBy: [{ field: 'total_spent', direction: 'DESC' }],
|
|
259
|
+
})
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Pagination with JOINs
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Paginated user orders
|
|
266
|
+
const paginatedOrders = await joins({
|
|
267
|
+
tableName: 'users',
|
|
268
|
+
dbClient,
|
|
269
|
+
select: [
|
|
270
|
+
'users.name',
|
|
271
|
+
'users.email',
|
|
272
|
+
'orders.id',
|
|
273
|
+
'orders.total',
|
|
274
|
+
'orders.status',
|
|
275
|
+
'orders.created_at',
|
|
276
|
+
],
|
|
277
|
+
joins: [
|
|
278
|
+
{
|
|
279
|
+
type: 'INNER',
|
|
280
|
+
table: 'orders',
|
|
281
|
+
on: 'users.id = orders.user_id',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
where: {
|
|
285
|
+
'orders.status': { operator: '=', value: 'completed' },
|
|
286
|
+
},
|
|
287
|
+
limit: 20,
|
|
288
|
+
offset: 0,
|
|
289
|
+
orderBy: [{ field: 'orders.created_at', direction: 'DESC' }],
|
|
290
|
+
})
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### TypeScript Usage
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
interface UserWithOrder {
|
|
297
|
+
user_id: string
|
|
298
|
+
user_name: string
|
|
299
|
+
user_email: string
|
|
300
|
+
order_id: string
|
|
301
|
+
order_total: number
|
|
302
|
+
order_status: string
|
|
303
|
+
order_created_at: Date
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Typed usage
|
|
307
|
+
const usersWithOrders: UserWithOrder[] = await joins<UserWithOrder>({
|
|
308
|
+
tableName: 'users',
|
|
309
|
+
dbClient,
|
|
310
|
+
select: [
|
|
311
|
+
'users.id as user_id',
|
|
312
|
+
'users.name as user_name',
|
|
313
|
+
'users.email as user_email',
|
|
314
|
+
'orders.id as order_id',
|
|
315
|
+
'orders.total as order_total',
|
|
316
|
+
'orders.status as order_status',
|
|
317
|
+
'orders.created_at as order_created_at',
|
|
318
|
+
],
|
|
319
|
+
joins: [
|
|
320
|
+
{
|
|
321
|
+
type: 'LEFT',
|
|
322
|
+
table: 'orders',
|
|
323
|
+
on: 'users.id = orders.user_id',
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
where: {
|
|
327
|
+
'users.status': { operator: '=', value: 'active' },
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Error Handling
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
try {
|
|
336
|
+
const result = await joins({
|
|
337
|
+
tableName: 'users',
|
|
338
|
+
dbClient,
|
|
339
|
+
select: ['users.name', 'orders.total'],
|
|
340
|
+
joins: [
|
|
341
|
+
{
|
|
342
|
+
type: 'LEFT',
|
|
343
|
+
table: 'orders',
|
|
344
|
+
on: 'users.id = orders.user_id',
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
console.log(`Found ${result.length} records`)
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Join query error:', error.message)
|
|
352
|
+
// Handle error appropriately
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## Generated SQL Examples
|
|
357
|
+
|
|
358
|
+
### Simple LEFT JOIN
|
|
359
|
+
|
|
360
|
+
```sql
|
|
361
|
+
SELECT users.id, users.name, users.email, orders.total, orders.created_at
|
|
362
|
+
FROM users
|
|
363
|
+
LEFT JOIN orders ON users.id = orders.user_id
|
|
364
|
+
WHERE users.status = $1
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Multiple JOINs with GROUP BY
|
|
368
|
+
|
|
369
|
+
```sql
|
|
370
|
+
SELECT
|
|
371
|
+
users.name,
|
|
372
|
+
users.email,
|
|
373
|
+
COUNT(orders.id) as order_count,
|
|
374
|
+
SUM(orders.total) as total_spent,
|
|
375
|
+
plans.name as plan_name
|
|
376
|
+
FROM users
|
|
377
|
+
LEFT JOIN orders ON users.id = orders.user_id
|
|
378
|
+
LEFT JOIN user_plans ON users.id = user_plans.user_id
|
|
379
|
+
LEFT JOIN plans ON user_plans.plan_id = plans.id
|
|
380
|
+
WHERE users.status = $1
|
|
381
|
+
GROUP BY users.id, users.name, users.email, plans.name
|
|
382
|
+
ORDER BY total_spent DESC
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### Complex WHERE with JOINs
|
|
386
|
+
|
|
387
|
+
```sql
|
|
388
|
+
SELECT users.name, users.email, orders.total, orders.created_at
|
|
389
|
+
FROM users
|
|
390
|
+
INNER JOIN orders ON users.id = orders.user_id
|
|
391
|
+
WHERE (users.status = $1 AND orders.created_at >= $2 AND orders.total > $3)
|
|
392
|
+
ORDER BY orders.created_at DESC
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Best Practices
|
|
396
|
+
|
|
397
|
+
### 1. Always Specify SELECT Fields
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// Good: Explicit field selection
|
|
401
|
+
const result = await joins({
|
|
402
|
+
tableName: 'users',
|
|
403
|
+
dbClient,
|
|
404
|
+
select: ['users.id', 'users.name', 'orders.total'],
|
|
405
|
+
joins: [
|
|
406
|
+
{
|
|
407
|
+
type: 'LEFT',
|
|
408
|
+
table: 'orders',
|
|
409
|
+
on: 'users.id = orders.user_id',
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// Avoid: Not specifying select fields
|
|
415
|
+
const result = await joins({
|
|
416
|
+
tableName: 'users',
|
|
417
|
+
dbClient,
|
|
418
|
+
select: [], // This will cause issues
|
|
419
|
+
joins: [
|
|
420
|
+
{
|
|
421
|
+
type: 'LEFT',
|
|
422
|
+
table: 'orders',
|
|
423
|
+
on: 'users.id = orders.user_id',
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
})
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 2. Use Table Aliases for Clarity
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
// Good: Use table prefixes for clarity
|
|
433
|
+
const result = await joins({
|
|
434
|
+
tableName: 'users',
|
|
435
|
+
dbClient,
|
|
436
|
+
select: [
|
|
437
|
+
'users.id as user_id',
|
|
438
|
+
'users.name as user_name',
|
|
439
|
+
'orders.id as order_id',
|
|
440
|
+
'orders.total as order_total',
|
|
441
|
+
],
|
|
442
|
+
joins: [
|
|
443
|
+
{
|
|
444
|
+
type: 'LEFT',
|
|
445
|
+
table: 'orders',
|
|
446
|
+
on: 'users.id = orders.user_id',
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
})
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### 3. Choose Appropriate JOIN Types
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Use INNER JOIN when you need matching records from both tables
|
|
456
|
+
const usersWithOrders = await joins({
|
|
457
|
+
tableName: 'users',
|
|
458
|
+
dbClient,
|
|
459
|
+
select: ['users.name', 'orders.total'],
|
|
460
|
+
joins: [
|
|
461
|
+
{
|
|
462
|
+
type: 'INNER', // Only users who have orders
|
|
463
|
+
table: 'orders',
|
|
464
|
+
on: 'users.id = orders.user_id',
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// Use LEFT JOIN when you want all records from the main table
|
|
470
|
+
const allUsersWithOrders = await joins({
|
|
471
|
+
tableName: 'users',
|
|
472
|
+
dbClient,
|
|
473
|
+
select: ['users.name', 'orders.total'],
|
|
474
|
+
joins: [
|
|
475
|
+
{
|
|
476
|
+
type: 'LEFT', // All users, even those without orders
|
|
477
|
+
table: 'orders',
|
|
478
|
+
on: 'users.id = orders.user_id',
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### 4. Use Proper Indexing
|
|
485
|
+
|
|
486
|
+
```sql
|
|
487
|
+
-- Ensure proper indexes exist for JOIN conditions
|
|
488
|
+
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
|
489
|
+
CREATE INDEX idx_user_plans_user_id ON user_plans(user_id);
|
|
490
|
+
CREATE INDEX idx_user_plans_plan_id ON user_plans(plan_id);
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### 5. Handle NULL Values in JOINs
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
// Handle NULL values from LEFT JOINs
|
|
497
|
+
const result = await joins({
|
|
498
|
+
tableName: 'users',
|
|
499
|
+
dbClient,
|
|
500
|
+
select: [
|
|
501
|
+
'users.name',
|
|
502
|
+
'COALESCE(orders.total, 0) as total_spent',
|
|
503
|
+
'CASE WHEN orders.id IS NULL THEN 0 ELSE 1 END as has_orders',
|
|
504
|
+
],
|
|
505
|
+
joins: [
|
|
506
|
+
{
|
|
507
|
+
type: 'LEFT',
|
|
508
|
+
table: 'orders',
|
|
509
|
+
on: 'users.id = orders.user_id',
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
})
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Common Use Cases
|
|
516
|
+
|
|
517
|
+
### 1. User Dashboard Data
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
const getUserDashboardData = async (userId: string) => {
|
|
521
|
+
return joins({
|
|
522
|
+
tableName: 'users',
|
|
523
|
+
dbClient,
|
|
524
|
+
select: [
|
|
525
|
+
'users.name',
|
|
526
|
+
'users.email',
|
|
527
|
+
'COUNT(orders.id) as total_orders',
|
|
528
|
+
'SUM(orders.total) as total_spent',
|
|
529
|
+
'plans.name as current_plan',
|
|
530
|
+
'plans.price as plan_price',
|
|
531
|
+
],
|
|
532
|
+
joins: [
|
|
533
|
+
{
|
|
534
|
+
type: 'LEFT',
|
|
535
|
+
table: 'orders',
|
|
536
|
+
on: 'users.id = orders.user_id',
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
type: 'LEFT',
|
|
540
|
+
table: 'user_plans',
|
|
541
|
+
on: 'users.id = user_plans.user_id AND user_plans.is_active = true',
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
type: 'LEFT',
|
|
545
|
+
table: 'plans',
|
|
546
|
+
on: 'user_plans.plan_id = plans.id',
|
|
547
|
+
},
|
|
548
|
+
],
|
|
549
|
+
where: {
|
|
550
|
+
'users.id': { operator: '=', value: userId },
|
|
551
|
+
},
|
|
552
|
+
groupBy: [
|
|
553
|
+
'users.id',
|
|
554
|
+
'users.name',
|
|
555
|
+
'users.email',
|
|
556
|
+
'plans.name',
|
|
557
|
+
'plans.price',
|
|
558
|
+
],
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
### 2. Sales Report
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
const getSalesReport = async (startDate: Date, endDate: Date) => {
|
|
567
|
+
return joins({
|
|
568
|
+
tableName: 'orders',
|
|
569
|
+
dbClient,
|
|
570
|
+
select: [
|
|
571
|
+
'users.name as customer_name',
|
|
572
|
+
'users.email as customer_email',
|
|
573
|
+
'orders.id as order_id',
|
|
574
|
+
'orders.total as order_total',
|
|
575
|
+
'orders.status as order_status',
|
|
576
|
+
'products.name as product_name',
|
|
577
|
+
'order_items.quantity',
|
|
578
|
+
'order_items.price as item_price',
|
|
579
|
+
],
|
|
580
|
+
joins: [
|
|
581
|
+
{
|
|
582
|
+
type: 'INNER',
|
|
583
|
+
table: 'users',
|
|
584
|
+
on: 'orders.user_id = users.id',
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
type: 'INNER',
|
|
588
|
+
table: 'order_items',
|
|
589
|
+
on: 'orders.id = order_items.order_id',
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
type: 'INNER',
|
|
593
|
+
table: 'products',
|
|
594
|
+
on: 'order_items.product_id = products.id',
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
where: {
|
|
598
|
+
AND: [
|
|
599
|
+
{ 'orders.created_at': { operator: '>=', value: startDate } },
|
|
600
|
+
{ 'orders.created_at': { operator: '<=', value: endDate } },
|
|
601
|
+
{ 'orders.status': { operator: '=', value: 'completed' } },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
orderBy: [{ field: 'orders.created_at', direction: 'DESC' }],
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### 3. Product Analytics
|
|
610
|
+
|
|
611
|
+
```typescript
|
|
612
|
+
const getProductAnalytics = async () => {
|
|
613
|
+
return joins({
|
|
614
|
+
tableName: 'products',
|
|
615
|
+
dbClient,
|
|
616
|
+
select: [
|
|
617
|
+
'products.name as product_name',
|
|
618
|
+
'categories.name as category_name',
|
|
619
|
+
'COUNT(order_items.id) as times_ordered',
|
|
620
|
+
'SUM(order_items.quantity) as total_quantity_sold',
|
|
621
|
+
'SUM(order_items.price * order_items.quantity) as total_revenue',
|
|
622
|
+
'AVG(order_items.price) as avg_price',
|
|
623
|
+
],
|
|
624
|
+
joins: [
|
|
625
|
+
{
|
|
626
|
+
type: 'LEFT',
|
|
627
|
+
table: 'categories',
|
|
628
|
+
on: 'products.category_id = categories.id',
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
type: 'LEFT',
|
|
632
|
+
table: 'order_items',
|
|
633
|
+
on: 'products.id = order_items.product_id',
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
type: 'LEFT',
|
|
637
|
+
table: 'orders',
|
|
638
|
+
on: 'order_items.order_id = orders.id AND orders.status = "completed"',
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
groupBy: ['products.id', 'products.name', 'categories.name'],
|
|
642
|
+
having: {
|
|
643
|
+
'COUNT(order_items.id)': { operator: '>', value: 0 },
|
|
644
|
+
},
|
|
645
|
+
orderBy: [{ field: 'total_revenue', direction: 'DESC' }],
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
### 4. User Activity Feed
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
const getUserActivityFeed = async (userId: string, limit: number = 50) => {
|
|
654
|
+
return joins({
|
|
655
|
+
tableName: 'users',
|
|
656
|
+
dbClient,
|
|
657
|
+
select: [
|
|
658
|
+
'activity_logs.action',
|
|
659
|
+
'activity_logs.description',
|
|
660
|
+
'activity_logs.created_at',
|
|
661
|
+
'orders.id as order_id',
|
|
662
|
+
'orders.total as order_total',
|
|
663
|
+
'products.name as product_name',
|
|
664
|
+
],
|
|
665
|
+
joins: [
|
|
666
|
+
{
|
|
667
|
+
type: 'LEFT',
|
|
668
|
+
table: 'activity_logs',
|
|
669
|
+
on: 'users.id = activity_logs.user_id',
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
type: 'LEFT',
|
|
673
|
+
table: 'orders',
|
|
674
|
+
on: 'activity_logs.entity_id = orders.id AND activity_logs.entity_type = "order"',
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
type: 'LEFT',
|
|
678
|
+
table: 'order_items',
|
|
679
|
+
on: 'orders.id = order_items.order_id',
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
type: 'LEFT',
|
|
683
|
+
table: 'products',
|
|
684
|
+
on: 'order_items.product_id = products.id',
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
where: {
|
|
688
|
+
'users.id': { operator: '=', value: userId },
|
|
689
|
+
},
|
|
690
|
+
limit,
|
|
691
|
+
orderBy: [{ field: 'activity_logs.created_at', direction: 'DESC' }],
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
## Performance Considerations
|
|
697
|
+
|
|
698
|
+
### 1. Index Strategy for JOINs
|
|
699
|
+
|
|
700
|
+
```sql
|
|
701
|
+
-- Create indexes on JOIN columns
|
|
702
|
+
CREATE INDEX idx_orders_user_id ON orders(user_id);
|
|
703
|
+
CREATE INDEX idx_order_items_order_id ON order_items(order_id);
|
|
704
|
+
CREATE INDEX idx_order_items_product_id ON order_items(product_id);
|
|
705
|
+
CREATE INDEX idx_user_plans_user_id ON user_plans(user_id);
|
|
706
|
+
CREATE INDEX idx_user_plans_plan_id ON user_plans(plan_id);
|
|
707
|
+
|
|
708
|
+
-- Composite indexes for common query patterns
|
|
709
|
+
CREATE INDEX idx_orders_user_status_created ON orders(user_id, status, created_at);
|
|
710
|
+
CREATE INDEX idx_order_items_order_product ON order_items(order_id, product_id);
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### 2. Query Optimization
|
|
714
|
+
|
|
715
|
+
```typescript
|
|
716
|
+
// Good: Use indexed fields in WHERE clauses
|
|
717
|
+
const result = await joins({
|
|
718
|
+
tableName: 'users',
|
|
719
|
+
dbClient,
|
|
720
|
+
select: ['users.name', 'orders.total'],
|
|
721
|
+
joins: [
|
|
722
|
+
{
|
|
723
|
+
type: 'INNER',
|
|
724
|
+
table: 'orders',
|
|
725
|
+
on: 'users.id = orders.user_id',
|
|
726
|
+
},
|
|
727
|
+
],
|
|
728
|
+
where: {
|
|
729
|
+
'users.status': { operator: '=', value: 'active' }, // Indexed field
|
|
730
|
+
'orders.created_at': { operator: '>=', value: new Date('2023-01-01') }, // Indexed field
|
|
731
|
+
},
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
// Avoid: Using non-indexed fields in WHERE clauses
|
|
735
|
+
const result = await joins({
|
|
736
|
+
tableName: 'users',
|
|
737
|
+
dbClient,
|
|
738
|
+
select: ['users.name', 'orders.total'],
|
|
739
|
+
joins: [
|
|
740
|
+
{
|
|
741
|
+
type: 'INNER',
|
|
742
|
+
table: 'orders',
|
|
743
|
+
on: 'users.id = orders.user_id',
|
|
744
|
+
},
|
|
745
|
+
],
|
|
746
|
+
where: {
|
|
747
|
+
'users.bio': { operator: 'LIKE', value: '%developer%' }, // Non-indexed field
|
|
748
|
+
},
|
|
749
|
+
})
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
### 3. Limit Result Sets
|
|
753
|
+
|
|
754
|
+
```typescript
|
|
755
|
+
// Always use LIMIT for large result sets
|
|
756
|
+
const result = await joins({
|
|
757
|
+
tableName: 'users',
|
|
758
|
+
dbClient,
|
|
759
|
+
select: ['users.name', 'orders.total'],
|
|
760
|
+
joins: [
|
|
761
|
+
{
|
|
762
|
+
type: 'LEFT',
|
|
763
|
+
table: 'orders',
|
|
764
|
+
on: 'users.id = orders.user_id',
|
|
765
|
+
},
|
|
766
|
+
],
|
|
767
|
+
limit: 1000, // Prevent memory issues
|
|
768
|
+
orderBy: [{ field: 'users.created_at', direction: 'DESC' }],
|
|
769
|
+
})
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
## Error Messages
|
|
773
|
+
|
|
774
|
+
Common error messages you might encounter:
|
|
775
|
+
|
|
776
|
+
- `Table name is required` - The `tableName` parameter is missing
|
|
777
|
+
- `DB client is required` - The `dbClient` parameter is missing
|
|
778
|
+
- `column "field_name" does not exist` - Invalid field name in SELECT or WHERE clause
|
|
779
|
+
- `relation "table_name" does not exist` - Invalid table name in JOIN clause
|
|
780
|
+
- `syntax error at or near "JOIN"` - Invalid JOIN syntax
|
|
781
|
+
- Database-specific errors from the underlying database driver
|