sql-flex-query 1.0.0 → 1.0.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/README.md +471 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,81 +17,512 @@ A lightweight, dialect-aware SQL query builder that enhances base query template
|
|
|
17
17
|
npm install sql-flex-query
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
---
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
## Basic Examples
|
|
23
|
+
|
|
24
|
+
### 1. Simple SELECT with WHERE and Pagination
|
|
23
25
|
|
|
24
26
|
```javascript
|
|
25
27
|
const { buildQueries } = require('sql-flex-query');
|
|
26
28
|
|
|
27
|
-
const BASE =
|
|
29
|
+
const BASE = `
|
|
28
30
|
SELECT /*SELECT_COLUMNS*/
|
|
29
31
|
FROM users u
|
|
30
32
|
/*WHERE_CLAUSE*/
|
|
31
33
|
/*ORDER_BY*/
|
|
32
34
|
/*LIMIT_CLAUSE*/
|
|
33
|
-
|
|
35
|
+
`;
|
|
34
36
|
|
|
35
|
-
// Postgres (default)
|
|
36
37
|
const result = buildQueries(
|
|
37
38
|
BASE,
|
|
38
|
-
[
|
|
39
|
-
|
|
39
|
+
[
|
|
40
|
+
{ key: 'status', operation: 'EQ', value: 'ACTIVE' },
|
|
41
|
+
{ key: 'age', operation: 'GTE', value: 18 },
|
|
42
|
+
],
|
|
43
|
+
[],
|
|
44
|
+
[{ key: 'createdAt', direction: 'DESC' }],
|
|
45
|
+
1, 10,
|
|
46
|
+
{ createdAt: 'u.created_at' },
|
|
47
|
+
['id', 'name', 'email', 'createdAt']
|
|
40
48
|
);
|
|
49
|
+
// searchQuery: SELECT u.id AS "id", ... WHERE "status" = $1 AND u.created_at >= $2 ORDER BY ... LIMIT 10 OFFSET 0
|
|
50
|
+
// params: ['ACTIVE', 18]
|
|
51
|
+
```
|
|
41
52
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
### 2. Text Search (OR) + Filters (AND)
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const result = buildQueries({
|
|
57
|
+
baseQueryTemplate: `
|
|
58
|
+
SELECT /*SELECT_COLUMNS*/
|
|
59
|
+
FROM products p
|
|
60
|
+
/*WHERE_CLAUSE*/
|
|
61
|
+
/*ORDER_BY*/
|
|
62
|
+
/*LIMIT_CLAUSE*/
|
|
63
|
+
`,
|
|
64
|
+
textSearchParams: [
|
|
65
|
+
{ key: 'name', operation: 'LIKE', value: '%laptop%', ignoreCase: true },
|
|
66
|
+
{ key: 'description', operation: 'LIKE', value: '%laptop%', ignoreCase: true },
|
|
67
|
+
],
|
|
68
|
+
whereParams: [
|
|
69
|
+
{ key: 'status', operation: 'EQ', value: 'PUBLISHED' },
|
|
70
|
+
{ key: 'price', operation: 'LTE', value: 2000 },
|
|
71
|
+
{ key: 'deleted_at', operation: 'NULL' },
|
|
72
|
+
],
|
|
73
|
+
sortBy: [{ key: 'price', direction: 'ASC' }],
|
|
74
|
+
page: 1,
|
|
75
|
+
size: 20,
|
|
76
|
+
dialect: 'postgres',
|
|
77
|
+
});
|
|
78
|
+
// WHERE (LOWER("name") LIKE $1 OR LOWER("description") LIKE $2)
|
|
79
|
+
// AND "status" = $3 AND "price" <= $4 AND "deleted_at" IS NULL
|
|
80
|
+
// params: ['%laptop%', '%laptop%', 'PUBLISHED', 2000]
|
|
49
81
|
```
|
|
50
82
|
|
|
51
|
-
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Complex Examples
|
|
86
|
+
|
|
87
|
+
### 3. Multi-table JOIN with Column Mapping
|
|
52
88
|
|
|
53
89
|
```javascript
|
|
90
|
+
const { buildQueries } = require('sql-flex-query');
|
|
91
|
+
|
|
92
|
+
const BASE = `
|
|
93
|
+
SELECT /*SELECT_COLUMNS*/
|
|
94
|
+
FROM orders o
|
|
95
|
+
JOIN customers c ON c.id = o.customer_id
|
|
96
|
+
JOIN order_items oi ON oi.order_id = o.id
|
|
97
|
+
JOIN products p ON p.id = oi.product_id
|
|
98
|
+
/*WHERE_CLAUSE*/
|
|
99
|
+
/*ORDER_BY*/
|
|
100
|
+
/*LIMIT_CLAUSE*/
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const columnMapper = {
|
|
104
|
+
orderId: 'o.id',
|
|
105
|
+
orderDate: 'o.created_at',
|
|
106
|
+
orderStatus: 'o.status',
|
|
107
|
+
customerName: 'c.name',
|
|
108
|
+
customerEmail: 'c.email',
|
|
109
|
+
productName: 'p.name',
|
|
110
|
+
quantity: 'oi.quantity',
|
|
111
|
+
unitPrice: 'oi.unit_price',
|
|
112
|
+
};
|
|
113
|
+
|
|
54
114
|
const result = buildQueries({
|
|
55
115
|
baseQueryTemplate: BASE,
|
|
56
|
-
|
|
57
|
-
|
|
116
|
+
columnMapper,
|
|
117
|
+
selectColumns: [
|
|
118
|
+
'orderId', 'orderDate', 'orderStatus',
|
|
119
|
+
'customerName', 'customerEmail',
|
|
120
|
+
'productName', 'quantity', 'unitPrice',
|
|
121
|
+
],
|
|
122
|
+
whereParams: [
|
|
123
|
+
{ key: 'orderStatus', operation: 'IN', value: ['SHIPPED', 'DELIVERED'] },
|
|
124
|
+
{ key: 'orderDate', operation: 'GTE', value: '2024-01-01' },
|
|
125
|
+
{ key: 'orderDate', operation: 'LTE', value: '2024-12-31' },
|
|
126
|
+
{ key: 'unitPrice', operation: 'GT', value: 0 },
|
|
127
|
+
],
|
|
128
|
+
textSearchParams: [
|
|
129
|
+
{ key: 'customerName', operation: 'LIKE', value: '%john%', ignoreCase: true },
|
|
130
|
+
{ key: 'customerEmail', operation: 'LIKE', value: '%john%', ignoreCase: true },
|
|
131
|
+
{ key: 'productName', operation: 'LIKE', value: '%john%', ignoreCase: true },
|
|
132
|
+
],
|
|
133
|
+
sortBy: [
|
|
134
|
+
{ key: 'orderDate', direction: 'DESC' },
|
|
135
|
+
{ key: 'customerName', direction: 'ASC' },
|
|
136
|
+
],
|
|
137
|
+
page: 1,
|
|
138
|
+
size: 25,
|
|
139
|
+
dialect: 'postgres',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// searchQuery:
|
|
143
|
+
// SELECT o.id AS "orderId", o.created_at AS "orderDate", o.status AS "orderStatus",
|
|
144
|
+
// c.name AS "customerName", c.email AS "customerEmail",
|
|
145
|
+
// p.name AS "productName", oi.quantity AS "quantity", oi.unit_price AS "unitPrice"
|
|
146
|
+
// FROM orders o
|
|
147
|
+
// JOIN customers c ON c.id = o.customer_id
|
|
148
|
+
// JOIN order_items oi ON oi.order_id = o.id
|
|
149
|
+
// JOIN products p ON p.id = oi.product_id
|
|
150
|
+
// WHERE (LOWER(c.name) LIKE $1 OR LOWER(c.email) LIKE $2 OR LOWER(p.name) LIKE $3)
|
|
151
|
+
// AND o.status IN ($4, $5)
|
|
152
|
+
// AND o.created_at >= $6 AND o.created_at <= $7
|
|
153
|
+
// AND oi.unit_price > $8
|
|
154
|
+
// ORDER BY o.created_at DESC, c.name ASC
|
|
155
|
+
// LIMIT 25 OFFSET 0
|
|
156
|
+
//
|
|
157
|
+
// params: ['%john%', '%john%', '%john%', 'SHIPPED', 'DELIVERED', '2024-01-01', '2024-12-31', 0]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 4. GROUP BY + HAVING (Aggregation Reports)
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const { buildQueries } = require('sql-flex-query');
|
|
164
|
+
|
|
165
|
+
// GROUP BY is part of the template.
|
|
166
|
+
// The builder handles WHERE (before GROUP BY) and HAVING (after GROUP BY) automatically.
|
|
167
|
+
const BASE = `
|
|
168
|
+
SELECT /*SELECT_COLUMNS*/
|
|
169
|
+
FROM orders o
|
|
170
|
+
JOIN customers c ON c.id = o.customer_id
|
|
171
|
+
JOIN order_items oi ON oi.order_id = o.id
|
|
172
|
+
/*WHERE_CLAUSE*/
|
|
173
|
+
GROUP BY c.id, c.name, c.email
|
|
174
|
+
/*HAVING_CLAUSE*/
|
|
175
|
+
/*ORDER_BY*/
|
|
176
|
+
/*LIMIT_CLAUSE*/
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
const columnMapper = {
|
|
180
|
+
customerName: 'c.name',
|
|
181
|
+
customerEmail: 'c.email',
|
|
182
|
+
orderCount: 'COUNT(DISTINCT o.id)',
|
|
183
|
+
totalSpent: 'SUM(oi.quantity * oi.unit_price)',
|
|
184
|
+
avgOrderValue: 'AVG(oi.quantity * oi.unit_price)',
|
|
185
|
+
orderDate: 'o.created_at',
|
|
186
|
+
orderStatus: 'o.status',
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = buildQueries({
|
|
190
|
+
baseQueryTemplate: BASE,
|
|
191
|
+
columnMapper,
|
|
192
|
+
selectColumns: ['customerName', 'customerEmail', 'orderCount', 'totalSpent', 'avgOrderValue'],
|
|
193
|
+
whereParams: [
|
|
194
|
+
// WHERE filters — applied BEFORE GROUP BY
|
|
195
|
+
{ key: 'orderStatus', operation: 'IN', value: ['COMPLETED', 'DELIVERED'] },
|
|
196
|
+
{ key: 'orderDate', operation: 'GTE', value: '2024-01-01' },
|
|
197
|
+
{ key: 'orderDate', operation: 'LTE', value: '2024-12-31' },
|
|
198
|
+
|
|
199
|
+
// HAVING filters — set having: true — applied AFTER GROUP BY
|
|
200
|
+
{ key: 'orderCount', operation: 'GTE', value: 5, having: true },
|
|
201
|
+
{ key: 'totalSpent', operation: 'GTE', value: 1000, having: true },
|
|
202
|
+
],
|
|
203
|
+
sortBy: [{ key: 'totalSpent', direction: 'DESC' }],
|
|
58
204
|
page: 1,
|
|
59
205
|
size: 10,
|
|
60
|
-
dialect: '
|
|
206
|
+
dialect: 'postgres',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// searchQuery:
|
|
210
|
+
// SELECT c.name AS "customerName", c.email AS "customerEmail",
|
|
211
|
+
// COUNT(DISTINCT o.id) AS "orderCount",
|
|
212
|
+
// SUM(oi.quantity * oi.unit_price) AS "totalSpent",
|
|
213
|
+
// AVG(oi.quantity * oi.unit_price) AS "avgOrderValue"
|
|
214
|
+
// FROM orders o
|
|
215
|
+
// JOIN customers c ON c.id = o.customer_id
|
|
216
|
+
// JOIN order_items oi ON oi.order_id = o.id
|
|
217
|
+
// WHERE o.status IN ($1, $2) AND o.created_at >= $3 AND o.created_at <= $4
|
|
218
|
+
// GROUP BY c.id, c.name, c.email
|
|
219
|
+
// HAVING COUNT(DISTINCT o.id) >= $5 AND SUM(oi.quantity * oi.unit_price) >= $6
|
|
220
|
+
// ORDER BY SUM(oi.quantity * oi.unit_price) DESC
|
|
221
|
+
// LIMIT 10 OFFSET 0
|
|
222
|
+
//
|
|
223
|
+
// params: ['COMPLETED', 'DELIVERED', '2024-01-01', '2024-12-31', 5, 1000]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### 5. GROUP BY with modifyCountQuery
|
|
227
|
+
|
|
228
|
+
When using GROUP BY, the default COUNT gives wrong results. Use modifyCountQuery to wrap the count:
|
|
229
|
+
|
|
230
|
+
```javascript
|
|
231
|
+
const BASE_WITH_GROUP = `
|
|
232
|
+
SELECT /*SELECT_COLUMNS*/
|
|
233
|
+
FROM r2r_emails em
|
|
234
|
+
LEFT JOIN r2r_regions r ON em.region_id = r.id
|
|
235
|
+
JOIN r2r_work_queue wq ON wq.id = em.wq_id
|
|
236
|
+
JOIN r2r_work_queue_case_mapping wqcm ON wqcm.wq_id = wq.id
|
|
237
|
+
JOIN (
|
|
238
|
+
SELECT DISTINCT wq_id, case_id, company_code
|
|
239
|
+
FROM r2r_process_output_model WHERE is_latest = true
|
|
240
|
+
) pom ON pom.wq_id = wqcm.wq_id AND pom.case_id = wqcm.case_id
|
|
241
|
+
/*WHERE_CLAUSE*/
|
|
242
|
+
GROUP BY DATE(wq.created_date), pom.company_code, em.structured
|
|
243
|
+
/*HAVING_CLAUSE*/
|
|
244
|
+
/*ORDER_BY*/ /*LIMIT_CLAUSE*/
|
|
245
|
+
`;
|
|
246
|
+
|
|
247
|
+
const columnMapper = {
|
|
248
|
+
createdDate: 'DATE(wq.created_date)',
|
|
249
|
+
companyCode: 'pom.company_code',
|
|
250
|
+
structured: 'em.structured',
|
|
251
|
+
emailCount: 'COUNT(em.id)',
|
|
252
|
+
regionName: 'r.name',
|
|
253
|
+
status: 'em.status',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = buildQueries({
|
|
257
|
+
baseQueryTemplate: BASE_WITH_GROUP,
|
|
258
|
+
columnMapper,
|
|
259
|
+
selectColumns: ['createdDate', 'companyCode', 'structured', 'emailCount'],
|
|
260
|
+
whereParams: [
|
|
261
|
+
{ key: 'status', operation: 'EQ', value: 'PROCESSED' },
|
|
262
|
+
{ key: 'regionName', operation: 'NOT_NULL' },
|
|
263
|
+
{ key: 'emailCount', operation: 'GT', value: 10, having: true },
|
|
264
|
+
],
|
|
265
|
+
sortBy: [
|
|
266
|
+
{ key: 'createdDate', direction: 'DESC' },
|
|
267
|
+
{ key: 'emailCount', direction: 'DESC' },
|
|
268
|
+
],
|
|
269
|
+
page: 1,
|
|
270
|
+
size: 20,
|
|
271
|
+
dialect: 'postgres',
|
|
272
|
+
// Wrap count query to count groups, not rows within groups
|
|
273
|
+
modifyCountQuery: (query) => `SELECT COUNT(*) AS count FROM (${query}) AS grouped_count`,
|
|
61
274
|
});
|
|
275
|
+
|
|
276
|
+
// countQuery:
|
|
277
|
+
// SELECT COUNT(*) AS count FROM (
|
|
278
|
+
// SELECT 1 FROM r2r_emails em ...
|
|
279
|
+
// WHERE em.status = $1 AND r.name IS NOT NULL
|
|
280
|
+
// GROUP BY DATE(wq.created_date), pom.company_code, em.structured
|
|
281
|
+
// HAVING COUNT(em.id) > $2
|
|
282
|
+
// ) AS grouped_count
|
|
62
283
|
```
|
|
63
284
|
|
|
64
|
-
### Fluent API
|
|
285
|
+
### 6. LEFT JOIN + DISTINCT (Fluent API)
|
|
65
286
|
|
|
66
287
|
```javascript
|
|
67
288
|
const { QueryBuilder } = require('sql-flex-query');
|
|
68
289
|
|
|
290
|
+
const BASE = `
|
|
291
|
+
SELECT /*SELECT_COLUMNS*/
|
|
292
|
+
FROM employees e
|
|
293
|
+
LEFT JOIN departments d ON d.id = e.department_id
|
|
294
|
+
LEFT JOIN employee_skills es ON es.employee_id = e.id
|
|
295
|
+
LEFT JOIN skills s ON s.id = es.skill_id
|
|
296
|
+
/*WHERE_CLAUSE*/
|
|
297
|
+
/*ORDER_BY*/
|
|
298
|
+
/*LIMIT_CLAUSE*/
|
|
299
|
+
`;
|
|
300
|
+
|
|
301
|
+
const result = new QueryBuilder('postgres')
|
|
302
|
+
.baseQuery(BASE)
|
|
303
|
+
.columnMapper({
|
|
304
|
+
employeeId: 'e.id',
|
|
305
|
+
employeeName: 'e.name',
|
|
306
|
+
employeeEmail: 'e.email',
|
|
307
|
+
departmentName: 'd.name',
|
|
308
|
+
hireDate: 'e.hire_date',
|
|
309
|
+
salary: 'e.salary',
|
|
310
|
+
skillName: 's.name',
|
|
311
|
+
})
|
|
312
|
+
.select(['employeeId', 'employeeName', 'employeeEmail', 'departmentName'])
|
|
313
|
+
.distinct()
|
|
314
|
+
.where([
|
|
315
|
+
{ key: 'departmentName', operation: 'IN', value: ['Engineering', 'Product', 'Design'] },
|
|
316
|
+
{ key: 'hireDate', operation: 'GTE', value: '2023-01-01' },
|
|
317
|
+
{ key: 'salary', operation: 'GTE', value: 50000 },
|
|
318
|
+
{ key: 'salary', operation: 'LTE', value: 150000 },
|
|
319
|
+
])
|
|
320
|
+
.textSearch([
|
|
321
|
+
{ key: 'employeeName', operation: 'LIKE', value: '%sarah%', ignoreCase: true },
|
|
322
|
+
{ key: 'employeeEmail', operation: 'LIKE', value: '%sarah%', ignoreCase: true },
|
|
323
|
+
{ key: 'skillName', operation: 'LIKE', value: '%sarah%', ignoreCase: true },
|
|
324
|
+
])
|
|
325
|
+
.orderBy([
|
|
326
|
+
{ key: 'departmentName', direction: 'ASC' },
|
|
327
|
+
{ key: 'employeeName', direction: 'ASC' },
|
|
328
|
+
])
|
|
329
|
+
.paginate(2, 15)
|
|
330
|
+
.build();
|
|
331
|
+
|
|
332
|
+
// searchQuery:
|
|
333
|
+
// SELECT DISTINCT e.id AS "employeeId", e.name AS "employeeName",
|
|
334
|
+
// e.email AS "employeeEmail", d.name AS "departmentName"
|
|
335
|
+
// FROM employees e
|
|
336
|
+
// LEFT JOIN departments d ON d.id = e.department_id
|
|
337
|
+
// LEFT JOIN employee_skills es ON es.employee_id = e.id
|
|
338
|
+
// LEFT JOIN skills s ON s.id = es.skill_id
|
|
339
|
+
// WHERE (LOWER(e.name) LIKE $1 OR LOWER(e.email) LIKE $2 OR LOWER(s.name) LIKE $3)
|
|
340
|
+
// AND d.name IN ($4, $5, $6)
|
|
341
|
+
// AND e.hire_date >= $7 AND e.salary >= $8 AND e.salary <= $9
|
|
342
|
+
// ORDER BY d.name ASC, e.name ASC
|
|
343
|
+
// LIMIT 15 OFFSET 15
|
|
344
|
+
//
|
|
345
|
+
// params: ['%sarah%', '%sarah%', '%sarah%', 'Engineering', 'Product', 'Design',
|
|
346
|
+
// '2023-01-01', 50000, 150000]
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### 7. Sales Dashboard (Fluent API + GROUP BY + HAVING + MSSQL)
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
const { QueryBuilder } = require('sql-flex-query');
|
|
353
|
+
|
|
354
|
+
const BASE = `
|
|
355
|
+
SELECT /*SELECT_COLUMNS*/
|
|
356
|
+
FROM sales s
|
|
357
|
+
JOIN sales_reps sr ON sr.id = s.rep_id
|
|
358
|
+
JOIN regions r ON r.id = sr.region_id
|
|
359
|
+
JOIN products p ON p.id = s.product_id
|
|
360
|
+
JOIN product_categories pc ON pc.id = p.category_id
|
|
361
|
+
/*WHERE_CLAUSE*/
|
|
362
|
+
GROUP BY r.name, sr.name, pc.name, YEAR(s.sale_date), MONTH(s.sale_date)
|
|
363
|
+
/*HAVING_CLAUSE*/
|
|
364
|
+
/*ORDER_BY*/ /*LIMIT_CLAUSE*/
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
const result = new QueryBuilder('mssql')
|
|
368
|
+
.baseQuery(BASE)
|
|
369
|
+
.columnMapper({
|
|
370
|
+
regionName: 'r.name',
|
|
371
|
+
repName: 'sr.name',
|
|
372
|
+
categoryName: 'pc.name',
|
|
373
|
+
saleYear: 'YEAR(s.sale_date)',
|
|
374
|
+
saleMonth: 'MONTH(s.sale_date)',
|
|
375
|
+
totalRevenue: 'SUM(s.amount)',
|
|
376
|
+
totalUnits: 'SUM(s.quantity)',
|
|
377
|
+
dealCount: 'COUNT(s.id)',
|
|
378
|
+
avgDealSize: 'AVG(s.amount)',
|
|
379
|
+
saleDate: 's.sale_date',
|
|
380
|
+
saleStatus: 's.status',
|
|
381
|
+
})
|
|
382
|
+
.select([
|
|
383
|
+
'regionName', 'repName', 'categoryName',
|
|
384
|
+
'saleYear', 'saleMonth',
|
|
385
|
+
'totalRevenue', 'totalUnits', 'dealCount', 'avgDealSize',
|
|
386
|
+
])
|
|
387
|
+
.where([
|
|
388
|
+
{ key: 'saleDate', operation: 'GTE', value: '2024-01-01' },
|
|
389
|
+
{ key: 'saleDate', operation: 'LTE', value: '2024-12-31' },
|
|
390
|
+
{ key: 'saleStatus', operation: 'EQ', value: 'CLOSED_WON' },
|
|
391
|
+
{ key: 'regionName', operation: 'IN', value: ['North America', 'Europe', 'APAC'] },
|
|
392
|
+
])
|
|
393
|
+
.having([
|
|
394
|
+
{ key: 'totalRevenue', operation: 'GTE', value: 50000 },
|
|
395
|
+
{ key: 'dealCount', operation: 'GTE', value: 3 },
|
|
396
|
+
])
|
|
397
|
+
.orderBy([
|
|
398
|
+
{ key: 'totalRevenue', direction: 'DESC' },
|
|
399
|
+
{ key: 'regionName', direction: 'ASC' },
|
|
400
|
+
])
|
|
401
|
+
.paginate(1, 20)
|
|
402
|
+
.modifyCountQuery((query) => `SELECT COUNT(*) AS count FROM (${query}) AS t`)
|
|
403
|
+
.build();
|
|
404
|
+
|
|
405
|
+
// searchQuery (SQL Server):
|
|
406
|
+
// SELECT r.name AS [regionName], sr.name AS [repName], pc.name AS [categoryName],
|
|
407
|
+
// YEAR(s.sale_date) AS [saleYear], MONTH(s.sale_date) AS [saleMonth],
|
|
408
|
+
// SUM(s.amount) AS [totalRevenue], SUM(s.quantity) AS [totalUnits],
|
|
409
|
+
// COUNT(s.id) AS [dealCount], AVG(s.amount) AS [avgDealSize]
|
|
410
|
+
// FROM sales s
|
|
411
|
+
// JOIN sales_reps sr ON sr.id = s.rep_id
|
|
412
|
+
// JOIN regions r ON r.id = sr.region_id
|
|
413
|
+
// JOIN products p ON p.id = s.product_id
|
|
414
|
+
// JOIN product_categories pc ON pc.id = p.category_id
|
|
415
|
+
// WHERE s.sale_date >= @p1 AND s.sale_date <= @p2
|
|
416
|
+
// AND s.status = @p3 AND r.name IN (@p4, @p5, @p6)
|
|
417
|
+
// GROUP BY r.name, sr.name, pc.name, YEAR(s.sale_date), MONTH(s.sale_date)
|
|
418
|
+
// HAVING SUM(s.amount) >= @p7 AND COUNT(s.id) >= @p8
|
|
419
|
+
// ORDER BY SUM(s.amount) DESC, r.name ASC OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY
|
|
420
|
+
//
|
|
421
|
+
// params: ['2024-01-01', '2024-12-31', 'CLOSED_WON',
|
|
422
|
+
// 'North America', 'Europe', 'APAC', 50000, 3]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### 8. Subquery JOIN with MySQL (Fluent API)
|
|
426
|
+
|
|
427
|
+
```javascript
|
|
428
|
+
const { QueryBuilder } = require('sql-flex-query');
|
|
429
|
+
|
|
430
|
+
const BASE = `
|
|
431
|
+
SELECT /*SELECT_COLUMNS*/
|
|
432
|
+
FROM users u
|
|
433
|
+
JOIN (
|
|
434
|
+
SELECT user_id,
|
|
435
|
+
COUNT(*) as login_count,
|
|
436
|
+
MAX(login_at) as last_login
|
|
437
|
+
FROM login_history
|
|
438
|
+
WHERE login_at >= '2024-01-01'
|
|
439
|
+
GROUP BY user_id
|
|
440
|
+
) lh ON lh.user_id = u.id
|
|
441
|
+
LEFT JOIN user_subscriptions us ON us.user_id = u.id AND us.is_active = true
|
|
442
|
+
LEFT JOIN plans p ON p.id = us.plan_id
|
|
443
|
+
/*WHERE_CLAUSE*/
|
|
444
|
+
/*ORDER_BY*/
|
|
445
|
+
/*LIMIT_CLAUSE*/
|
|
446
|
+
`;
|
|
447
|
+
|
|
69
448
|
const result = new QueryBuilder('mysql')
|
|
70
449
|
.baseQuery(BASE)
|
|
71
|
-
.columnMapper({
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
450
|
+
.columnMapper({
|
|
451
|
+
userId: 'u.id',
|
|
452
|
+
userName: 'u.name',
|
|
453
|
+
userEmail: 'u.email',
|
|
454
|
+
loginCount: 'lh.login_count',
|
|
455
|
+
lastLogin: 'lh.last_login',
|
|
456
|
+
planName: 'p.name',
|
|
457
|
+
userStatus: 'u.status',
|
|
458
|
+
userRole: 'u.role',
|
|
459
|
+
})
|
|
460
|
+
.select(['userId', 'userName', 'userEmail', 'loginCount', 'lastLogin', 'planName'])
|
|
461
|
+
.where([
|
|
462
|
+
{ key: 'userStatus', operation: 'EQ', value: 'ACTIVE' },
|
|
463
|
+
{ key: 'loginCount', operation: 'GTE', value: 10 },
|
|
464
|
+
{ key: 'userRole', operation: 'IN', value: ['ADMIN', 'PREMIUM', 'ENTERPRISE'] },
|
|
465
|
+
{ key: 'planName', operation: 'NOT_NULL' },
|
|
466
|
+
])
|
|
467
|
+
.textSearch([
|
|
468
|
+
{ key: 'userName', operation: 'LIKE', value: '%search_term%', ignoreCase: true },
|
|
469
|
+
{ key: 'userEmail', operation: 'LIKE', value: '%search_term%', ignoreCase: true },
|
|
470
|
+
])
|
|
471
|
+
.orderBy([
|
|
472
|
+
{ key: 'loginCount', direction: 'DESC' },
|
|
473
|
+
{ key: 'lastLogin', direction: 'DESC' },
|
|
474
|
+
])
|
|
475
|
+
.paginate(1, 50)
|
|
77
476
|
.build();
|
|
477
|
+
|
|
478
|
+
// searchQuery (MySQL):
|
|
479
|
+
// SELECT u.id AS \`userId\`, u.name AS \`userName\`, u.email AS \`userEmail\`,
|
|
480
|
+
// lh.login_count AS \`loginCount\`, lh.last_login AS \`lastLogin\`,
|
|
481
|
+
// p.name AS \`planName\`
|
|
482
|
+
// FROM users u
|
|
483
|
+
// JOIN (...) lh ON lh.user_id = u.id
|
|
484
|
+
// LEFT JOIN user_subscriptions us ON us.user_id = u.id AND us.is_active = true
|
|
485
|
+
// LEFT JOIN plans p ON p.id = us.plan_id
|
|
486
|
+
// WHERE (\`userName\` LIKE ? OR \`userEmail\` LIKE ?)
|
|
487
|
+
// AND u.status = ? AND lh.login_count >= ?
|
|
488
|
+
// AND u.role IN (?, ?, ?) AND p.name IS NOT NULL
|
|
489
|
+
// ORDER BY lh.login_count DESC, lh.last_login DESC
|
|
490
|
+
// LIMIT 50 OFFSET 0
|
|
491
|
+
//
|
|
492
|
+
// params: ['%search_term%', '%search_term%', 'ACTIVE', 10, 'ADMIN', 'PREMIUM', 'ENTERPRISE']
|
|
78
493
|
```
|
|
79
494
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## Template Placeholders Reference
|
|
498
|
+
|
|
499
|
+
| Placeholder | Required | Description |
|
|
500
|
+
|-------------|----------|-------------|
|
|
501
|
+
| `/*SELECT_COLUMNS*/` | Yes | Replaced with column list or `*` |
|
|
502
|
+
| `/*WHERE_CLAUSE*/` | Yes | Replaced with `WHERE ...` or empty string |
|
|
503
|
+
| `/*ORDER_BY*/` | Yes | Replaced with `ORDER BY ...` or empty string |
|
|
504
|
+
| `/*LIMIT_CLAUSE*/` | Yes | Replaced with pagination clause or empty string |
|
|
505
|
+
| `/*HAVING_CLAUSE*/` | Optional | Replaced with `HAVING ...` (use with GROUP BY) |
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## Operations Reference
|
|
510
|
+
|
|
511
|
+
| Operation | SQL | Needs Value | Example |
|
|
512
|
+
|-----------|-----|-------------|---------|
|
|
513
|
+
| `EQ` | `col = ?` | Yes | `{ key: 'status', operation: 'EQ', value: 'ACTIVE' }` |
|
|
514
|
+
| `NEQ` | `col <> ?` | Yes | `{ key: 'role', operation: 'NEQ', value: 'GUEST' }` |
|
|
515
|
+
| `LIKE` | `col LIKE ?` | Yes | `{ key: 'name', operation: 'LIKE', value: '%john%' }` |
|
|
516
|
+
| `NOT_LIKE` | `col NOT LIKE ?` | Yes | `{ key: 'email', operation: 'NOT_LIKE', value: '%spam%' }` |
|
|
517
|
+
| `GT` | `col > ?` | Yes | `{ key: 'age', operation: 'GT', value: 18 }` |
|
|
518
|
+
| `LT` | `col < ?` | Yes | `{ key: 'price', operation: 'LT', value: 100 }` |
|
|
519
|
+
| `GTE` | `col >= ?` | Yes | `{ key: 'score', operation: 'GTE', value: 90 }` |
|
|
520
|
+
| `LTE` | `col <= ?` | Yes | `{ key: 'weight', operation: 'LTE', value: 80 }` |
|
|
521
|
+
| `IN` | `col IN (?, ?)` | Yes (array) | `{ key: 'status', operation: 'IN', value: ['A', 'B'] }` |
|
|
522
|
+
| `NULL` | `col IS NULL` | No | `{ key: 'deleted_at', operation: 'NULL' }` |
|
|
523
|
+
| `NOT_NULL` | `col IS NOT NULL` | No | `{ key: 'verified_at', operation: 'NOT_NULL' }` |
|
|
524
|
+
|
|
525
|
+
---
|
|
95
526
|
|
|
96
527
|
## License
|
|
97
528
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sql-flex-query",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A lightweight, dialect-aware SQL query builder that enhances base query templates with dynamic WHERE, HAVING, ORDER BY, pagination, and more.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|