sql-love 0.9.3 → 0.9.5
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/CHANGELOG.md +7 -1
- package/README.md +253 -27
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/SelectBuilder/SelectBuilder.d.ts +30 -7
- package/dist/src/SelectBuilder/SelectBuilder.js +51 -22
- package/dist/src/extractGrouped/extractGrouped.d.ts +17 -0
- package/dist/src/extractGrouped/extractGrouped.js +31 -0
- package/dist/src/extractIndexed/extractIndexed.d.ts +12 -0
- package/dist/src/extractIndexed/extractIndexed.js +23 -0
- package/dist/src/getPagination/getPagination.d.ts +16 -0
- package/dist/src/getPagination/getPagination.js +25 -0
- package/dist/src/runMysql/runMysql.d.ts +46 -0
- package/dist/src/runMysql/runMysql.js +49 -0
- package/dist/src/runPrisma/runPrisma.d.ts +26 -0
- package/dist/src/runPrisma/runPrisma.js +18 -0
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/SelectBuilder/SelectBuilder.spec.ts +47 -3
- package/src/SelectBuilder/SelectBuilder.ts +85 -26
- package/src/extractGrouped/extractGrouped.spec.ts +43 -0
- package/src/extractGrouped/extractGrouped.ts +36 -0
- package/src/extractIndexed/extractIndexed.spec.ts +40 -0
- package/src/extractIndexed/extractIndexed.ts +28 -0
- package/src/getPagination/getPagination.spec.ts +61 -0
- package/src/getPagination/getPagination.ts +30 -0
- package/src/runMysql/runMysql.spec.ts +164 -0
- package/src/runMysql/runMysql.ts +94 -0
- package/src/runPrisma/runPrisma.spec.ts +67 -0
- package/src/runPrisma/runPrisma.ts +39 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
## Change Log
|
|
2
2
|
|
|
3
|
-
### v0.9.
|
|
3
|
+
### v0.9.5 - 5 Dec 2023
|
|
4
|
+
|
|
5
|
+
- Utility functions (extractGrouped, extractIndexed, getPagination, runPrisma, runMysql)
|
|
6
|
+
- Added `SelectBuilder.setDefaultEngine(engine)`
|
|
7
|
+
- More documentation
|
|
8
|
+
|
|
9
|
+
### v0.9.4 - 30 Nov 2023
|
|
4
10
|
|
|
5
11
|
- Support parsing and building "ILIKE" and "NOT ILIKE"
|
|
6
12
|
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
<img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=0.9.
|
|
1
|
+
<img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=0.9.5" width="250" height="208" />
|
|
2
2
|
|
|
3
3
|
# sql-love
|
|
4
4
|
|
|
5
|
-
[](https://npmjs.com/package/sql-love)
|
|
6
|
+
[](https://npmjs.com/package/sql-love)
|
|
7
|
+
[](https://github.com/kensnyder/sql-love/actions)
|
|
8
|
+
[](https://codecov.io/gh/kensnyder/sql-love)
|
|
9
|
+
[](https://opensource.org/licenses/ISC)
|
|
10
10
|
|
|
11
11
|
Classes for parsing and building SQL select queries in Node
|
|
12
12
|
|
|
@@ -20,12 +20,21 @@ npm install sql-love
|
|
|
20
20
|
|
|
21
21
|
- [SelectBuilder](#selectbuilder)
|
|
22
22
|
- [Parsing base SQL](#parsing-base-sql)
|
|
23
|
+
- [SQL injection](#sql-injection)
|
|
23
24
|
- [Building the Query](#building-the-query)
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
25
|
+
- [More examples](#where-examples)
|
|
26
|
+
- [Counting results](#counting-results)
|
|
27
|
+
- [Other methods](#other-methods)
|
|
28
|
+
- [new SelectBuilder() limitations](#selectparse-limitations)
|
|
29
|
+
- [Utility functions](#utility-functions)
|
|
30
|
+
- [getPagination](#getpagination)
|
|
31
|
+
- [extractGrouped](#extractGrouped)
|
|
32
|
+
- [extractIndexed](#extractIndexed)
|
|
33
|
+
- [runMysql and runPrismaWithCount](#runPrisma-and-runPrismaWithCount)
|
|
34
|
+
- [runMysql and runMysqlWithCount](#runMysql-and-runMysqlWithCount)
|
|
35
|
+
- [runPg and runPgWithCount](#runPg-and-runPgWithCount)
|
|
36
|
+
- [How to contribute](./CONTRIBUTING.md)
|
|
37
|
+
- [ISC license](./LICENSE.md)
|
|
29
38
|
|
|
30
39
|
## SelectBuilder
|
|
31
40
|
|
|
@@ -37,7 +46,8 @@ clauses including JOIN, WHERE, ORDER BY, LIMIT, OFFSET.
|
|
|
37
46
|
You can define a base query and pass it to the SelectBuilder constructor.
|
|
38
47
|
|
|
39
48
|
```js
|
|
40
|
-
|
|
49
|
+
import { SelectBuilder } from 'sql-love';
|
|
50
|
+
|
|
41
51
|
const query = new SelectBuilder(`
|
|
42
52
|
SELECT u.id, u.fname, u.lname, u.email, p.phone
|
|
43
53
|
FROM users
|
|
@@ -60,11 +70,16 @@ const { sql, bindings } = query.compile();
|
|
|
60
70
|
connection.query(sql, bindings, (err, results, fields) => {});
|
|
61
71
|
// or Prisma
|
|
62
72
|
const result = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
73
|
+
|
|
74
|
+
// Note that these are prepared statements so the values in the "bindings"
|
|
75
|
+
// array are safe from SQL injection.
|
|
63
76
|
```
|
|
64
77
|
|
|
65
78
|
It is possible to add placeholders to the base query.
|
|
66
79
|
|
|
67
80
|
```js
|
|
81
|
+
import { SelectBuilder } from 'sql-love';
|
|
82
|
+
|
|
68
83
|
const query = new SelectBuilder(
|
|
69
84
|
`
|
|
70
85
|
SELECT u.id, u.fname, u.lname, u.email, a.city, a.zip
|
|
@@ -95,27 +110,42 @@ And "bindings" equals:
|
|
|
95
110
|
You may specify a compiler engine to use. The default is `"mysql"`.
|
|
96
111
|
|
|
97
112
|
```js
|
|
113
|
+
import { SelectBuilder } from 'sql-love';
|
|
114
|
+
|
|
98
115
|
const query = new SelectBuilder('SELECT * FROM users')
|
|
99
116
|
.limit(10)
|
|
100
117
|
.page(3)
|
|
101
118
|
.where('id', 1);
|
|
102
119
|
|
|
103
|
-
query.compile('mysql').sql;
|
|
120
|
+
query.compile({ engine: 'mysql' }).sql;
|
|
104
121
|
// SELECT * FROM users WHERE id = ? OFFSET 20 LIMIT 10
|
|
105
122
|
|
|
106
|
-
query.compile('pg').sql;
|
|
123
|
+
query.compile({ engine: 'pg' }).sql;
|
|
107
124
|
// SELECT * FROM users WHERE id = $1 OFFSET 20 LIMIT 10
|
|
108
125
|
|
|
109
|
-
query.compile('mssql').sql;
|
|
126
|
+
query.compile({ engine: 'mssql' }).sql;
|
|
110
127
|
// SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
|
|
111
128
|
|
|
112
|
-
query.compile('oracle').sql;
|
|
129
|
+
query.compile({ engine: 'oracle' }).sql;
|
|
113
130
|
// SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
|
|
131
|
+
|
|
132
|
+
// Or you can specify the default engine to use in the compile function
|
|
133
|
+
SelectBuilder.setDefaultEngine('pg');
|
|
114
134
|
```
|
|
115
135
|
|
|
116
|
-
###
|
|
136
|
+
### SQL injection
|
|
137
|
+
|
|
138
|
+
You should use prepared statements to run the sql and bindings returned by
|
|
139
|
+
`SelectBuilder.compile()`. This will protect you from SQL injection.
|
|
140
|
+
|
|
141
|
+
Even though `prisma.$queryRawUnsafe()` implies that the query is unsafe,
|
|
142
|
+
execute with the "sql", and "bindings" is actually safe because it uses prepared
|
|
143
|
+
statements.
|
|
144
|
+
|
|
145
|
+
### Building the query
|
|
117
146
|
|
|
118
|
-
Use the following methods to build queries.
|
|
147
|
+
Use the following methods to build queries. And see
|
|
148
|
+
[more examples below](#where-examples).
|
|
119
149
|
|
|
120
150
|
- `query.where(column, operator, value)` - Require column satisfy operator
|
|
121
151
|
- `query.where(column, value)` - Require column equal a value
|
|
@@ -148,7 +178,94 @@ Use the following methods to build queries.
|
|
|
148
178
|
- `query.rightOuterJoin(expression)` - Add a RIGHT OUTER JOIN expression
|
|
149
179
|
- `query.groupBy(column)` - Group by a column or expression
|
|
150
180
|
|
|
151
|
-
###
|
|
181
|
+
### More examples
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// The following are equivalent
|
|
185
|
+
query.where('deleted_at', '=', null);
|
|
186
|
+
query.where('deleted_at', '=', undefined);
|
|
187
|
+
query.where('deleted_at', null);
|
|
188
|
+
query.where('deleted_at', undefined);
|
|
189
|
+
query.where('deleted_at IS NULL');
|
|
190
|
+
query.where({ deleted_at: null });
|
|
191
|
+
query.where({ deleted_at: undefined });
|
|
192
|
+
|
|
193
|
+
// the following demonstrate ways to use LIKE
|
|
194
|
+
query.where('name', 'LIKE', 'son');
|
|
195
|
+
query.where('name', 'LIKE ?', 'son');
|
|
196
|
+
query.where('name', 'LIKE ?%', 'son');
|
|
197
|
+
query.where('name', 'LIKE %?', 'son');
|
|
198
|
+
query.where('name', 'LIKE %?%', 'son');
|
|
199
|
+
query.where('name', 'LIKE', '%son%');
|
|
200
|
+
|
|
201
|
+
// The following demonstrate ways to use various operators
|
|
202
|
+
query.where('price', '=', 100);
|
|
203
|
+
query.where('price', '!=', 100);
|
|
204
|
+
query.where('price', '<>', 100);
|
|
205
|
+
query.where('price', '>', 100);
|
|
206
|
+
query.where({ 'price >': 100 });
|
|
207
|
+
query.where('price', 'BETWEEN', [100, 200]);
|
|
208
|
+
query.where('price', 'NOT BETWEEN', [100, 200]);
|
|
209
|
+
query.whereBetween('price', [100, 200]); // price BETWEEN 100 AND 200
|
|
210
|
+
query.whereBetween('price', [100, null]); // price > 100
|
|
211
|
+
query.whereBetween('price', [null, 200]); // price < 200
|
|
212
|
+
query.whereBetween('price', [null, null]); // clause is ignored
|
|
213
|
+
|
|
214
|
+
// The following demonstrate ways to use IN and NOT IN
|
|
215
|
+
query.where('status', 'IN', ['pending', 'approved']);
|
|
216
|
+
query.where('status', ['pending', 'approved']);
|
|
217
|
+
query.where('status', '=', ['pending', 'approved']);
|
|
218
|
+
query.where('status', '!=', ['pending', 'approved']);
|
|
219
|
+
query.where('status', 'NOT IN', ['pending', 'approved']);
|
|
220
|
+
query.where({ status: ['pending', 'approved'] });
|
|
221
|
+
query.where({ 'status NOT IN': ['pending', 'approved'] });
|
|
222
|
+
query.where({ 'status !=': ['pending', 'approved'] });
|
|
223
|
+
query.where({ 'status <>': ['pending', 'approved'] });
|
|
224
|
+
// Note: .having() supports the same signatures as .where()
|
|
225
|
+
|
|
226
|
+
// The following demonstrates using objects to specify multiple conditions
|
|
227
|
+
query.where({
|
|
228
|
+
deleted_at: null,
|
|
229
|
+
'price >': 100,
|
|
230
|
+
'price <': 200,
|
|
231
|
+
color: ['blue', 'black'],
|
|
232
|
+
'stars BETWEEN': [4, 5],
|
|
233
|
+
conditions: ['new', 'used'],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// The following demonstrate equivalent ways to use OR
|
|
237
|
+
query.orWhere([{ approved_at: null }, { denied_at: null }]);
|
|
238
|
+
query.orWhere({ approved_at: null, denied_at: null });
|
|
239
|
+
query.orWhere(['approved_at IS NULL', 'denied_at IS NULL']);
|
|
240
|
+
query.orWhere([
|
|
241
|
+
['approved_at', null],
|
|
242
|
+
['denied_at =', null],
|
|
243
|
+
]);
|
|
244
|
+
query.where('(approved_at IS NULL OR denied_at IS NULL)');
|
|
245
|
+
// Note: .orHaving() supports the same signatures as .orWhere()
|
|
246
|
+
|
|
247
|
+
// The following demonstrate joins
|
|
248
|
+
query.innerJoin('phone_numbers p ON p.user_id = u.id');
|
|
249
|
+
query.leftJoin('phone_numbers p ON p.user_id = u.id AND p.type = ?', ['main']);
|
|
250
|
+
query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
|
|
251
|
+
'home',
|
|
252
|
+
'cell',
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
// The following demonstrates pagination
|
|
256
|
+
query.limit(10).page(3); // LIMIT 10 OFFSET 20
|
|
257
|
+
|
|
258
|
+
// The following demonstrates cloning
|
|
259
|
+
const query1 = new SelectBuilder();
|
|
260
|
+
query1.where('email', 'john@example.com');
|
|
261
|
+
const query2 = query1.getClone();
|
|
262
|
+
query1.table('users');
|
|
263
|
+
query2.table('contacts');
|
|
264
|
+
query1.compile().sql; // SELECT * FROM users WHERE email = ?
|
|
265
|
+
query2.compile().sql; // SELECT * FROM contacts WHERE email = ?
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Counting results
|
|
152
269
|
|
|
153
270
|
One powerful feature of SelectBuilder is that it can construct a count query
|
|
154
271
|
to determine the number of results that would have been returned if there were
|
|
@@ -159,6 +276,9 @@ const query = new SelectBuilder('SELECT id, name FROM users LIMIT 5');
|
|
|
159
276
|
|
|
160
277
|
const { sql } = query.compileCount();
|
|
161
278
|
// SELECT COUNT(*) AS found_rows FROM users
|
|
279
|
+
|
|
280
|
+
const { sql } = query.compileCount({ countExpr: 'DISTINCT externalId' });
|
|
281
|
+
// SELECT COUNT(DISTINCT externalId) AS found_rows FROM users
|
|
162
282
|
```
|
|
163
283
|
|
|
164
284
|
With queries that have a "HAVING" clause, the main query will be wrapped in a
|
|
@@ -193,14 +313,14 @@ SelectBuilder has a few other useful methods.
|
|
|
193
313
|
- `query.reset(fields)` - Reset a few particular aspects of the query (e.g. \['where', 'having'\])
|
|
194
314
|
- `query.reset()` - Reset query to an empty state
|
|
195
315
|
|
|
196
|
-
### Parser
|
|
316
|
+
### Parser limitations
|
|
197
317
|
|
|
198
318
|
`new SelectBuilder(sql)` uses regular expressions and is not a true parser.
|
|
199
319
|
The goal is to be fast and useful for 99% of situations.
|
|
200
320
|
|
|
201
321
|
Below are some limitations illustrated by example.
|
|
202
322
|
|
|
203
|
-
#### Nested
|
|
323
|
+
#### Nested subqueries
|
|
204
324
|
|
|
205
325
|
Most subqueries can be parsed but sub-subqueries don't work.
|
|
206
326
|
|
|
@@ -215,14 +335,13 @@ SELECT * FROM categories_posts WHERE category_id IN(
|
|
|
215
335
|
|
|
216
336
|
// ✅ WORKING EQUIVALENT
|
|
217
337
|
const query = new SelectBuilder(`SELECT * FROM categories_posts`);
|
|
218
|
-
const subquery =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)`);
|
|
338
|
+
const subquery = `SELECT id FROM categories WHERE client_id IN(
|
|
339
|
+
SELECT client_id FROM affiliations WHERE name LIKE 'test'
|
|
340
|
+
)`;
|
|
222
341
|
query.where(`category_id IN(${subquery})`);
|
|
223
342
|
```
|
|
224
343
|
|
|
225
|
-
#### Keywords in
|
|
344
|
+
#### Keywords in strings
|
|
226
345
|
|
|
227
346
|
If you need to use SQL keywords in strings, use bindings.
|
|
228
347
|
|
|
@@ -236,7 +355,7 @@ new SelectBuilder(`SELECT id, CONCAT(:prefix, expr) FROM users`, {
|
|
|
236
355
|
});
|
|
237
356
|
```
|
|
238
357
|
|
|
239
|
-
#### Nested OR and AND
|
|
358
|
+
#### Nested OR and AND clauses
|
|
240
359
|
|
|
241
360
|
Nested logic can't be parsed properly.
|
|
242
361
|
|
|
@@ -260,3 +379,110 @@ query.orWhere([
|
|
|
260
379
|
'id > 0 AND is_active IS NOT NULL',
|
|
261
380
|
]);
|
|
262
381
|
```
|
|
382
|
+
|
|
383
|
+
## Utility functions
|
|
384
|
+
|
|
385
|
+
There are several utility functions bundled with this library.
|
|
386
|
+
|
|
387
|
+
### getPagination
|
|
388
|
+
|
|
389
|
+
You can get detailed page and limit information from a query.
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
import { SelectBuilder, getPagination } from 'sql-love';
|
|
393
|
+
|
|
394
|
+
const query = new SelectBuilder('SELECT * FROM users').limit(10).page(1);
|
|
395
|
+
// ...
|
|
396
|
+
// run a count query to determine that there are 42 results
|
|
397
|
+
const pagination = getPagination(query, 42);
|
|
398
|
+
expect(pagination).toEqual({
|
|
399
|
+
page: 1,
|
|
400
|
+
prevPage: null,
|
|
401
|
+
nextPage: 2,
|
|
402
|
+
perPage: 10,
|
|
403
|
+
numPages: 5,
|
|
404
|
+
total: 42,
|
|
405
|
+
isFirst: true,
|
|
406
|
+
isLast: false,
|
|
407
|
+
});
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### extractIndexed
|
|
411
|
+
|
|
412
|
+
You can index a results set by a particular field.
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
import { extractIndexed } from 'sql-love';
|
|
416
|
+
|
|
417
|
+
const records = [
|
|
418
|
+
{ id: 1, name: 'John' },
|
|
419
|
+
{ id: 2, name: 'Jane' },
|
|
420
|
+
];
|
|
421
|
+
const indexed = extractIndexed('id', records);
|
|
422
|
+
/*
|
|
423
|
+
{
|
|
424
|
+
1: { id: 1, name: 'John' },
|
|
425
|
+
2: { id: 2, name: 'Jane' },
|
|
426
|
+
});
|
|
427
|
+
*/
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### extractGrouped
|
|
431
|
+
|
|
432
|
+
You can divide a results set into groups based on a particular field.
|
|
433
|
+
|
|
434
|
+
```js
|
|
435
|
+
import { extractGrouped } from 'sql-love';
|
|
436
|
+
|
|
437
|
+
const records = [
|
|
438
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
439
|
+
{ id: 2, name: 'Jane', dept: 'Finance' },
|
|
440
|
+
{ id: 3, name: 'Tim', dept: 'Marketing' },
|
|
441
|
+
];
|
|
442
|
+
const grouped = extractGrouped('dept', records);
|
|
443
|
+
/*
|
|
444
|
+
{
|
|
445
|
+
Marketing: [
|
|
446
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
447
|
+
{ id: 3, name: 'Tim', dept: 'Marketing' },
|
|
448
|
+
],
|
|
449
|
+
Finance: [{ id: 2, name: 'Jane', dept: 'Finance' }],
|
|
450
|
+
}
|
|
451
|
+
*/
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### runPrisma and runPrismaWithCount
|
|
455
|
+
|
|
456
|
+
`runPrisma()` will run a query using Prisma and return the results.
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
import { SelectBuilder, runPrisma, runPrismaWithCount } from 'sql-love';
|
|
460
|
+
import { prisma } from '~/db.server';
|
|
461
|
+
|
|
462
|
+
const query = new SelectBuilder('SELECT * FROM users');
|
|
463
|
+
query.where('dept', 'Marketing');
|
|
464
|
+
|
|
465
|
+
const rows = runPrisma(prisma, query, { engine: 'pg' });
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`runPrismaWithCount()` will run a query using Prisma and return the results,
|
|
469
|
+
found rows and pagination.
|
|
470
|
+
|
|
471
|
+
```js
|
|
472
|
+
import { SelectBuilder, runPrisma, runPrismaWithCount } from 'sql-love';
|
|
473
|
+
import { prisma } from '~/db.server';
|
|
474
|
+
|
|
475
|
+
const query = new SelectBuilder('SELECT * FROM users')
|
|
476
|
+
.where('dept', 'Marketing')
|
|
477
|
+
.limit(2)
|
|
478
|
+
.page(5);
|
|
479
|
+
const { records, total, pagination } = await runPrismaWithCount(prisma, query, {
|
|
480
|
+
engine: 'pg',
|
|
481
|
+
});
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
See [getPagination](#getpagination) for details on the pagination object.
|
|
485
|
+
|
|
486
|
+
### runMysql and runMysqlWithCount
|
|
487
|
+
|
|
488
|
+
### runPg and runPgWithCount
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as Parser } from './src/Parser/Parser';
|
|
2
2
|
export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
|
|
3
|
+
export { default as extractGrouped } from './src/extractGrouped/extractGrouped';
|
|
4
|
+
export { default as extractIndexed } from './src/extractIndexed/extractIndexed';
|
|
5
|
+
export { default as getPagination } from './src/getPagination/getPagination';
|
|
6
|
+
export { runPrisma, runPrismaWithCount } from './src/runPrisma/runPrisma';
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
1
|
export { default as Parser } from './src/Parser/Parser';
|
|
2
2
|
export { default as SelectBuilder } from './src/SelectBuilder/SelectBuilder';
|
|
3
|
+
export { default as extractGrouped } from './src/extractGrouped/extractGrouped';
|
|
4
|
+
export { default as extractIndexed } from './src/extractIndexed/extractIndexed';
|
|
5
|
+
export { default as getPagination } from './src/getPagination/getPagination';
|
|
6
|
+
export { runPrisma, runPrismaWithCount } from './src/runPrisma/runPrisma';
|
|
@@ -73,12 +73,20 @@ export default class SelectBuilder {
|
|
|
73
73
|
* @param [bindings] Placeholders to bind to the query
|
|
74
74
|
*/
|
|
75
75
|
constructor(sql?: string, bindings?: Record<string, any>);
|
|
76
|
+
/**
|
|
77
|
+
* Set the default database engine style (mssql, mysql, oracle, pg)
|
|
78
|
+
* @param engine
|
|
79
|
+
*/
|
|
80
|
+
static setDefaultEngine(engine: EngineStyle): typeof SelectBuilder;
|
|
76
81
|
/**
|
|
77
82
|
* Get the SQL as a pretty-printed string alongside an array of bindings
|
|
78
|
-
* @param
|
|
83
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
79
84
|
* @return The SQL query as a string
|
|
80
85
|
*/
|
|
81
|
-
compile(
|
|
86
|
+
compile({ engine, bindingIndex, }?: {
|
|
87
|
+
engine?: EngineStyle;
|
|
88
|
+
bindingIndex?: number;
|
|
89
|
+
}): {
|
|
82
90
|
sql: string;
|
|
83
91
|
bindings: any;
|
|
84
92
|
};
|
|
@@ -89,10 +97,26 @@ export default class SelectBuilder {
|
|
|
89
97
|
* @param engineStyle The database engine style (mssql, mysql, oracle, pg)
|
|
90
98
|
* @param countExpr The expression to use inside the COUNT()
|
|
91
99
|
*/
|
|
92
|
-
compileCount(
|
|
100
|
+
compileCount({ countExpr, engine, bindingIndex, }?: {
|
|
101
|
+
countExpr?: string;
|
|
102
|
+
engine?: EngineStyle;
|
|
103
|
+
bindingIndex?: number;
|
|
104
|
+
}): {
|
|
93
105
|
sql: string;
|
|
94
106
|
bindings: any;
|
|
95
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* Get SQL needed to union this query with another
|
|
110
|
+
* @param query The query to union
|
|
111
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
112
|
+
*/
|
|
113
|
+
compileUnion(query: SelectBuilder, { engine, bindingIndex, }?: {
|
|
114
|
+
engine?: EngineStyle;
|
|
115
|
+
bindingIndex?: number;
|
|
116
|
+
}): {
|
|
117
|
+
sql: string;
|
|
118
|
+
bindings: any[];
|
|
119
|
+
};
|
|
96
120
|
/**
|
|
97
121
|
* @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
|
|
98
122
|
* Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
|
|
@@ -290,17 +314,17 @@ export default class SelectBuilder {
|
|
|
290
314
|
* Add a series of WHERE clauses joined by OR (See _conditions for usage)
|
|
291
315
|
* @param conditions
|
|
292
316
|
*/
|
|
293
|
-
_orConditions(conditions: WhereCriteria[] | Record<string, any>[]): string;
|
|
317
|
+
_orConditions(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): string;
|
|
294
318
|
/**
|
|
295
319
|
* Add WHERE conditions to place inside an OR block (See _conditions for usage)
|
|
296
320
|
* @param conditions A list or object where each item is an array with parameters that would be taken by where()
|
|
297
321
|
*/
|
|
298
|
-
orWhere(conditions: WhereCriteria[] | Record<string, any>[]): this;
|
|
322
|
+
orWhere(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): this;
|
|
299
323
|
/**
|
|
300
324
|
* Add an OR with conditions under the HAVING clause
|
|
301
325
|
* @param conditions A list or object where each item is an array with parameters that would be taken by having()
|
|
302
326
|
*/
|
|
303
|
-
orHaving(conditions: WhereCriteria[] | Record<string, any>[]): this;
|
|
327
|
+
orHaving(conditions: WhereCriteria[] | Record<string, any>[] | Record<string, any>): this;
|
|
304
328
|
/**
|
|
305
329
|
* Add a HAVING condition (See _conditions for usage)
|
|
306
330
|
*/
|
|
@@ -352,5 +376,4 @@ export default class SelectBuilder {
|
|
|
352
376
|
* @param number The page number (integer greater than 0)
|
|
353
377
|
*/
|
|
354
378
|
page(number: number | string): this;
|
|
355
|
-
fetch(engine: any): void;
|
|
356
379
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import Parser from '../Parser/Parser';
|
|
2
2
|
import substrCount from '../substrCount/substrCount';
|
|
3
3
|
const isNullish = v => v === undefined || v === null || v === false || isNaN(v);
|
|
4
|
+
let defaultEngine = 'mysql';
|
|
4
5
|
/**
|
|
5
6
|
* Build a select query
|
|
6
7
|
* Class Select
|
|
@@ -85,12 +86,20 @@ export default class SelectBuilder {
|
|
|
85
86
|
new Parser(this).parse(sql);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Set the default database engine style (mssql, mysql, oracle, pg)
|
|
91
|
+
* @param engine
|
|
92
|
+
*/
|
|
93
|
+
static setDefaultEngine(engine) {
|
|
94
|
+
defaultEngine = engine;
|
|
95
|
+
return SelectBuilder;
|
|
96
|
+
}
|
|
88
97
|
/**
|
|
89
98
|
* Get the SQL as a pretty-printed string alongside an array of bindings
|
|
90
|
-
* @param
|
|
99
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
91
100
|
* @return The SQL query as a string
|
|
92
101
|
*/
|
|
93
|
-
compile(
|
|
102
|
+
compile({ engine = defaultEngine, bindingIndex = 1, } = {}) {
|
|
94
103
|
// build all the lines
|
|
95
104
|
const lines = [
|
|
96
105
|
'SELECT',
|
|
@@ -105,29 +114,29 @@ export default class SelectBuilder {
|
|
|
105
114
|
];
|
|
106
115
|
if (this._page > 0) {
|
|
107
116
|
const offset = (this._page - 1) * this._limit;
|
|
108
|
-
lines.push(this._compileOFFSET(
|
|
109
|
-
lines.push(this._compileLIMIT(
|
|
117
|
+
lines.push(this._compileOFFSET(engine, offset));
|
|
118
|
+
lines.push(this._compileLIMIT(engine, this._limit));
|
|
110
119
|
}
|
|
111
120
|
else {
|
|
112
121
|
if (this._offset) {
|
|
113
|
-
lines.push(this._compileOFFSET(
|
|
122
|
+
lines.push(this._compileOFFSET(engine, this._offset));
|
|
114
123
|
}
|
|
115
124
|
if (this._limit) {
|
|
116
|
-
lines.push(this._compileLIMIT(
|
|
125
|
+
lines.push(this._compileLIMIT(engine, this._limit));
|
|
117
126
|
}
|
|
118
127
|
}
|
|
119
128
|
const pretty = lines.filter(Boolean).join('\n').trim();
|
|
120
129
|
let replacer;
|
|
121
130
|
const bindings = [];
|
|
122
|
-
if (
|
|
123
|
-
let idx =
|
|
124
|
-
replacer = (
|
|
131
|
+
if (engine === 'pg') {
|
|
132
|
+
let idx = bindingIndex;
|
|
133
|
+
replacer = (_, $1) => {
|
|
125
134
|
bindings.push(this._bindings[$1]);
|
|
126
135
|
return '$' + idx++;
|
|
127
136
|
};
|
|
128
137
|
}
|
|
129
138
|
else {
|
|
130
|
-
replacer = (
|
|
139
|
+
replacer = (_, $1) => {
|
|
131
140
|
bindings.push(this._bindings[$1]);
|
|
132
141
|
return '?';
|
|
133
142
|
};
|
|
@@ -156,9 +165,9 @@ export default class SelectBuilder {
|
|
|
156
165
|
* @param engineStyle The database engine style (mssql, mysql, oracle, pg)
|
|
157
166
|
* @param countExpr The expression to use inside the COUNT()
|
|
158
167
|
*/
|
|
159
|
-
compileCount(
|
|
168
|
+
compileCount({ countExpr = '*', engine = defaultEngine, bindingIndex = 1, } = {}) {
|
|
160
169
|
const query = this.getFoundRowsQuery(countExpr);
|
|
161
|
-
const { sql, bindings } = query.compile(
|
|
170
|
+
const { sql, bindings } = query.compile({ engine, bindingIndex });
|
|
162
171
|
if (this._havings.length === 0) {
|
|
163
172
|
return { sql, bindings };
|
|
164
173
|
}
|
|
@@ -170,6 +179,22 @@ export default class SelectBuilder {
|
|
|
170
179
|
};
|
|
171
180
|
}
|
|
172
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Get SQL needed to union this query with another
|
|
184
|
+
* @param query The query to union
|
|
185
|
+
* @param engine The database engine style (mssql, mysql, oracle, pg)
|
|
186
|
+
*/
|
|
187
|
+
compileUnion(query, { engine = defaultEngine, bindingIndex = 1, } = {}) {
|
|
188
|
+
const first = this.compile({ engine, bindingIndex });
|
|
189
|
+
const second = query.compile({
|
|
190
|
+
engine,
|
|
191
|
+
bindingIndex: first.bindings.length + bindingIndex,
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
sql: `(${first.sql})\nUNION\n(${second.sql})`,
|
|
195
|
+
bindings: [...first.bindings, ...second.bindings],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
173
198
|
/**
|
|
174
199
|
* @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
|
|
175
200
|
* Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
|
|
@@ -618,12 +643,21 @@ export default class SelectBuilder {
|
|
|
618
643
|
*/
|
|
619
644
|
_orConditions(conditions) {
|
|
620
645
|
const items = [];
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
646
|
+
if (Array.isArray(conditions)) {
|
|
647
|
+
for (const condition of conditions) {
|
|
648
|
+
if (Array.isArray(condition)) {
|
|
649
|
+
this._conditions(items, condition);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
this._conditions(items, [condition]);
|
|
653
|
+
}
|
|
624
654
|
}
|
|
625
|
-
|
|
626
|
-
|
|
655
|
+
}
|
|
656
|
+
else {
|
|
657
|
+
for (const [key, value] of Object.entries(conditions)) {
|
|
658
|
+
const one = [];
|
|
659
|
+
this._conditions(one, [key, value]);
|
|
660
|
+
items.push(one[0]);
|
|
627
661
|
}
|
|
628
662
|
}
|
|
629
663
|
const joined = items.join(' OR ');
|
|
@@ -769,9 +803,4 @@ export default class SelectBuilder {
|
|
|
769
803
|
}
|
|
770
804
|
return this;
|
|
771
805
|
}
|
|
772
|
-
fetch(engine) {
|
|
773
|
-
if (engine.$queryRawUnsafe) {
|
|
774
|
-
//
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
806
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group database rows by a column value
|
|
3
|
+
* @param field The database column to group by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractGrouped('org', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "Marketing": [
|
|
9
|
+
* { id: 1, name: "John", org: "Marketing" },
|
|
10
|
+
* { id: 2, name: "Jane", org: "Marketing" },
|
|
11
|
+
* ],
|
|
12
|
+
* "Finance": [
|
|
13
|
+
* { id: 3, name: "Jose", org: "Finance" },
|
|
14
|
+
* ],
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export default function extractGrouped<DbRecord extends Record<string, any>>(field: keyof DbRecord, records: DbRecord[]): Record<string, DbRecord[]>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group database rows by a column value
|
|
3
|
+
* @param field The database column to group by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractGrouped('org', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "Marketing": [
|
|
9
|
+
* { id: 1, name: "John", org: "Marketing" },
|
|
10
|
+
* { id: 2, name: "Jane", org: "Marketing" },
|
|
11
|
+
* ],
|
|
12
|
+
* "Finance": [
|
|
13
|
+
* { id: 3, name: "Jose", org: "Finance" },
|
|
14
|
+
* ],
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export default function extractGrouped(field, records) {
|
|
18
|
+
let idx = 0;
|
|
19
|
+
const grouped = {};
|
|
20
|
+
for (const record of records) {
|
|
21
|
+
if (!(field in record)) {
|
|
22
|
+
throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
|
|
23
|
+
}
|
|
24
|
+
if (!grouped[record[field]]) {
|
|
25
|
+
grouped[record[field]] = [];
|
|
26
|
+
}
|
|
27
|
+
grouped[record[field]].push(record);
|
|
28
|
+
idx++;
|
|
29
|
+
}
|
|
30
|
+
return grouped;
|
|
31
|
+
}
|