sql-love 1.0.3 → 1.0.4

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.
Files changed (56) hide show
  1. package/README.md +52 -32
  2. package/dist/index.cjs +16 -0
  3. package/dist/index.d.ts +732 -9
  4. package/dist/index.mjs +16 -0
  5. package/package.json +27 -10
  6. package/.github/workflows/node.js.yml +0 -37
  7. package/.prettierrc.json +0 -13
  8. package/CHANGELOG.md +0 -39
  9. package/CONTRIBUTING.md +0 -85
  10. package/assets/sql-love-logo.png +0 -0
  11. package/codecov.yml +0 -7
  12. package/dist/index.js +0 -9
  13. package/dist/src/Parser/Parser.d.ts +0 -157
  14. package/dist/src/Parser/Parser.js +0 -304
  15. package/dist/src/SelectBuilder/SelectBuilder.d.ts +0 -379
  16. package/dist/src/SelectBuilder/SelectBuilder.js +0 -810
  17. package/dist/src/extractGrouped/extractGrouped.d.ts +0 -17
  18. package/dist/src/extractGrouped/extractGrouped.js +0 -31
  19. package/dist/src/extractIndexed/extractIndexed.d.ts +0 -12
  20. package/dist/src/extractIndexed/extractIndexed.js +0 -23
  21. package/dist/src/extractLookup/extractLookup.d.ts +0 -9
  22. package/dist/src/extractLookup/extractLookup.js +0 -21
  23. package/dist/src/getPagination/getPagination.d.ts +0 -16
  24. package/dist/src/getPagination/getPagination.js +0 -40
  25. package/dist/src/runMysql/runMysql.d.ts +0 -58
  26. package/dist/src/runMysql/runMysql.js +0 -61
  27. package/dist/src/runPrisma/runPrisma.d.ts +0 -41
  28. package/dist/src/runPrisma/runPrisma.js +0 -34
  29. package/dist/src/substrCount/substrCount.d.ts +0 -6
  30. package/dist/src/substrCount/substrCount.js +0 -14
  31. package/dist/src/toSafeJson/toSafeJson.d.ts +0 -25
  32. package/dist/src/toSafeJson/toSafeJson.js +0 -45
  33. package/index.ts +0 -19
  34. package/src/Parser/Parser.spec.ts +0 -226
  35. package/src/Parser/Parser.ts +0 -325
  36. package/src/SelectBuilder/SelectBuilder.spec.ts +0 -785
  37. package/src/SelectBuilder/SelectBuilder.ts +0 -914
  38. package/src/extractGrouped/extractGrouped.spec.ts +0 -43
  39. package/src/extractGrouped/extractGrouped.ts +0 -36
  40. package/src/extractIndexed/extractIndexed.spec.ts +0 -40
  41. package/src/extractIndexed/extractIndexed.ts +0 -28
  42. package/src/extractLookup/extractLookup.spec.ts +0 -38
  43. package/src/extractLookup/extractLookup.ts +0 -33
  44. package/src/getPagination/getPagination.spec.ts +0 -75
  45. package/src/getPagination/getPagination.ts +0 -42
  46. package/src/runMysql/runMysql.spec.ts +0 -286
  47. package/src/runMysql/runMysql.ts +0 -106
  48. package/src/runPrisma/runPrisma.spec.ts +0 -99
  49. package/src/runPrisma/runPrisma.ts +0 -75
  50. package/src/substrCount/substrCount.spec.ts +0 -29
  51. package/src/substrCount/substrCount.ts +0 -14
  52. package/src/toSafeJson/toSafeJson.spec.ts +0 -28
  53. package/src/toSafeJson/toSafeJson.ts +0 -56
  54. package/tsconfig.build.json +0 -10
  55. package/tsconfig.json +0 -22
  56. package/vitest.config.ts +0 -11
@@ -1,810 +0,0 @@
1
- import Parser from '../Parser/Parser';
2
- import substrCount from '../substrCount/substrCount';
3
- const isNullish = v => v === undefined || v === null || v === false || isNaN(v);
4
- let defaultEngine = 'mysql';
5
- /**
6
- * Build a select query
7
- * Class Select
8
- */
9
- export default class SelectBuilder {
10
- /**
11
- * The list of values to bind to the query
12
- * @private
13
- */
14
- _bindings = [];
15
- /**
16
- * The list of strings to come immediately after "SELECT"
17
- * and before column names
18
- * @private
19
- */
20
- _options = [];
21
- /**
22
- * The list of column names to select
23
- * @private
24
- */
25
- _tables = [];
26
- /**
27
- * The list of tables in the FROM clause
28
- * @private
29
- */
30
- _columns = [];
31
- /**
32
- * The list of JOIN strings to add
33
- * @private
34
- */
35
- _joins = [];
36
- /**
37
- * The list of WHERE clauses to join with AND
38
- * @private
39
- */
40
- _wheres = [];
41
- /**
42
- * The list of GROUP BY clauses to join with commas
43
- * @private
44
- */
45
- _groupBys = [];
46
- /**
47
- * The list of HAVING clauses to join with AND
48
- * @private
49
- */
50
- _havings = [];
51
- /**
52
- * The list of ORDER BY clauses to join with commas
53
- * @private
54
- */
55
- _orderBys = [];
56
- /**
57
- * The LIMIT to use
58
- * @private
59
- */
60
- _limit;
61
- /**
62
- * The OFFSET to use
63
- * @private
64
- */
65
- _offset;
66
- /**
67
- * The page used to construct an OFFSET based on the LIMIT
68
- * @private
69
- */
70
- _page;
71
- /**
72
- * Select constructor
73
- * @param [sql] A base SQL query to parse
74
- * @param [bindings] Placeholders to bind to the query
75
- */
76
- constructor(sql = null, bindings = null) {
77
- if (sql) {
78
- if (bindings) {
79
- const keys = Object.keys(bindings);
80
- const regexp = new RegExp(`:(${keys.join('|')})`, 'g');
81
- sql = sql.replace(regexp, ($0, $1) => {
82
- this._bindings.push(bindings[$1]);
83
- return `__BOUND_${this._bindings.length - 1}__`;
84
- });
85
- }
86
- new Parser(this).parse(sql);
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
- }
97
- /**
98
- * Get the SQL as a pretty-printed string alongside an array of bindings
99
- * @param engine The database engine style (mssql, mysql, oracle, pg)
100
- * @return The SQL query as a string
101
- */
102
- compile({ engine = defaultEngine, bindingIndex = 1, } = {}) {
103
- // build all the lines
104
- const lines = [
105
- 'SELECT',
106
- this._options.length ? ` ${this._options.join('\n ')}` : null,
107
- this._columns.length ? ` ${this._columns.join(',\n ')}` : ' *\n',
108
- `FROM ${this._tables.join(', ')}`,
109
- this._joins.length ? this._joins.join('\n') : null,
110
- this._wheres.length ? `WHERE ${this._wheres.join('\n AND ')}` : null,
111
- this._groupBys.length ? `GROUP BY ${this._groupBys.join(',\n ')}` : null,
112
- this._havings.length ? `HAVING ${this._havings.join('\n AND ')}` : null,
113
- this._orderBys.length ? `ORDER BY ${this._orderBys.join(',\n ')}` : null,
114
- ];
115
- if (this._page > 0) {
116
- const offset = (this._page - 1) * this._limit;
117
- lines.push(this._compileOFFSET(engine, offset));
118
- lines.push(this._compileLIMIT(engine, this._limit));
119
- }
120
- else {
121
- if (this._offset) {
122
- lines.push(this._compileOFFSET(engine, this._offset));
123
- }
124
- if (this._limit) {
125
- lines.push(this._compileLIMIT(engine, this._limit));
126
- }
127
- }
128
- const pretty = lines.filter(Boolean).join('\n').trim();
129
- let replacer;
130
- const bindings = [];
131
- if (engine === 'pg') {
132
- let idx = bindingIndex;
133
- replacer = (_, $1) => {
134
- bindings.push(this._bindings[$1]);
135
- return '$' + idx++;
136
- };
137
- }
138
- else {
139
- replacer = (_, $1) => {
140
- bindings.push(this._bindings[$1]);
141
- return '?';
142
- };
143
- }
144
- const sql = pretty.replace(/__BOUND_(\d+)__/g, replacer);
145
- return { sql, bindings };
146
- }
147
- _compileLIMIT(engineStyle, limit) {
148
- if (engineStyle === 'pg' || engineStyle === 'mysql') {
149
- return `LIMIT ${limit}`;
150
- }
151
- else if (engineStyle === 'mssql') {
152
- return `FETCH NEXT ${limit} ROWS ONLY`;
153
- }
154
- }
155
- _compileOFFSET(engineStyle, offset) {
156
- if (engineStyle === 'pg' || engineStyle === 'mysql') {
157
- return `OFFSET ${offset}`;
158
- }
159
- else if (engineStyle === 'mssql' || engineStyle === 'oracle') {
160
- return `OFFSET ${offset} ROWS`;
161
- }
162
- }
163
- /**
164
- * Get SQL needed to return the found rows of this query
165
- * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
166
- * @param countExpr The expression to use inside the COUNT()
167
- */
168
- compileCount({ countExpr = '*', engine = defaultEngine, bindingIndex = 1, } = {}) {
169
- const query = this.getFoundRowsQuery(countExpr);
170
- const { sql, bindings } = query.compile({ engine, bindingIndex });
171
- if (this._havings.length === 0) {
172
- return { sql, bindings };
173
- }
174
- else {
175
- const subquerySql = sql.replace(/\n/g, '\n\t');
176
- return {
177
- sql: `SELECT COUNT(*) AS found_rows\nFROM (\n${subquerySql}\n) AS subquery_results`,
178
- bindings,
179
- };
180
- }
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
- }
198
- /**
199
- * @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
200
- * Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
201
- * @return This object
202
- */
203
- reset(fieldOrFields = null) {
204
- if (Array.isArray(fieldOrFields)) {
205
- fieldOrFields.forEach(name => this.reset(name));
206
- return this;
207
- }
208
- if (fieldOrFields) {
209
- const pluralizable = [
210
- 'option',
211
- 'column',
212
- 'table',
213
- 'where',
214
- 'having',
215
- 'groupBy',
216
- 'orderBy',
217
- ];
218
- let prop = '_' + fieldOrFields.replace(/s$/, '');
219
- if (pluralizable.indexOf(fieldOrFields) > -1) {
220
- prop += 's';
221
- }
222
- const isScalar = ['limit', 'offset', 'page'].indexOf(fieldOrFields) > -1;
223
- this[prop] = isScalar ? null : [];
224
- }
225
- else {
226
- this._options = [];
227
- this._columns = [];
228
- this._tables = [];
229
- this._joins = [];
230
- this._wheres = [];
231
- this._groupBys = [];
232
- this._havings = [];
233
- this._orderBys = [];
234
- this._limit = null;
235
- this._offset = null;
236
- this._page = null;
237
- this._bindings = [];
238
- }
239
- return this;
240
- }
241
- /**
242
- * Clone this builder object
243
- */
244
- getClone() {
245
- const copy = new SelectBuilder();
246
- copy._options = [...this._options];
247
- copy._columns = [...this._columns];
248
- copy._tables = [...this._tables];
249
- copy._joins = [...this._joins];
250
- copy._wheres = [...this._wheres];
251
- copy._havings = [...this._havings];
252
- copy._groupBys = [...this._groupBys];
253
- copy._orderBys = [...this._orderBys];
254
- copy._limit = this._limit;
255
- copy._offset = this._offset;
256
- copy._page = this._page;
257
- copy._bindings = [...this._bindings];
258
- return copy;
259
- }
260
- /**
261
- * Build a version of this query that simply returns COUNT(*)
262
- * @param [countExpr="*"] Use to specify `DISTINCT colname` if needed
263
- */
264
- getFoundRowsQuery(countExpr = '*') {
265
- if (this._havings.length === 0) {
266
- const clone = this.getClone();
267
- clone._columns = [`COUNT(${countExpr}) AS found_rows`];
268
- clone._options = [];
269
- clone._groupBys = [];
270
- clone._orderBys = [];
271
- clone._limit = null;
272
- clone._offset = null;
273
- clone._page = null;
274
- return clone;
275
- }
276
- else {
277
- const subquery = this.getClone();
278
- subquery._limit = null;
279
- subquery._offset = null;
280
- subquery._page = null;
281
- return subquery;
282
- }
283
- }
284
- /**
285
- * Add an array of column names to fetch
286
- * @param columnNames The names of columns
287
- */
288
- select(columnNames) {
289
- this._columns = this._columns.concat(columnNames);
290
- return this;
291
- }
292
- /**
293
- * Add an array of column names to fetch
294
- * @param columnNames The names of columns
295
- */
296
- columns(columnNames) {
297
- this._columns = this._columns.concat(columnNames);
298
- return this;
299
- }
300
- /**
301
- * Add a column name to fetch
302
- * @param columnName The name of the column
303
- */
304
- column(columnName) {
305
- this._columns.push(columnName);
306
- return this;
307
- }
308
- /**
309
- * Add an option expression such as "TOP 10" or "SQL_CALC_FOUND_ROWS"
310
- * @param optionExpression Expression to go after "SELECT" and before column list
311
- */
312
- option(optionExpression) {
313
- this._options.push(optionExpression);
314
- return this;
315
- }
316
- /**
317
- * Add a table to the "FROM" clause (same as .from())
318
- * @param {String} tableName The name of the table to query
319
- * @return {Select}
320
- */
321
- table(tableName) {
322
- this._tables.push(tableName);
323
- return this;
324
- }
325
- /**
326
- * Add multiple table to the "FROM" clause
327
- * @param {Array} tableNames The names of the tables to query
328
- * @return {Select}
329
- */
330
- tables(tableNames) {
331
- this._tables.push(...tableNames);
332
- return this;
333
- }
334
- /**
335
- * Add a table to the "FROM" clause (same as .table())
336
- * @param {String} tableName The name of the table to query
337
- * @return {Select}
338
- */
339
- from(tableName) {
340
- this._tables.push(tableName);
341
- return this;
342
- }
343
- /**
344
- * Add the given join type expression
345
- * @param joinType The type of join (e.g. "INNER JOIN", "LEFT JOIN", etc.)
346
- * @param expression The expression following the INNER JOIN keyword
347
- * @param bindings A list of values to bind to ? placeholders in the expression
348
- * @example
349
- * query.join('posts p ON p.id = c.post_id');
350
- * query.join('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
351
- */
352
- _addJoin(joinType, expression, bindings = []) {
353
- if (bindings.length > 0) {
354
- expression = expression.replace(/\?/g, () => {
355
- this._bindings.push(bindings.shift());
356
- return `__BOUND_${this._bindings.length - 1}__`;
357
- });
358
- }
359
- this._joins.push(`${joinType} ${expression}`);
360
- }
361
- /**
362
- * Add a JOIN expression
363
- * @param expression The expression following the INNER JOIN keyword
364
- * @param bindings A list of values to bind to ? placeholders in the expression
365
- * @example
366
- * query.join('posts p ON p.id = c.post_id');
367
- * query.join('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
368
- */
369
- join(expression, bindings = []) {
370
- this._addJoin('JOIN', expression, bindings);
371
- return this;
372
- }
373
- /**
374
- * Add a LEFT JOIN expression
375
- * @param expression The expression following the LEFT JOIN keyword
376
- * @param bindings A list of values to bind to ? placeholders in the expression
377
- * @example
378
- * query.leftJoin('posts p ON p.id = c.post_id');
379
- * query.leftJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
380
- */
381
- leftJoin(expression, bindings = []) {
382
- this._addJoin('LEFT JOIN', expression, bindings);
383
- return this;
384
- }
385
- /**
386
- * Add a FULL JOIN expression
387
- * @param expression The expression following the FULL JOIN keyword
388
- * @param bindings A list of values to bind to ? placeholders in the expression
389
- * @example
390
- * query.fullJoin('posts p ON p.id = c.post_id');
391
- * query.fullJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
392
- */
393
- fullJoin(expression, bindings = []) {
394
- this._addJoin('FULL JOIN', expression, bindings);
395
- return this;
396
- }
397
- /**
398
- * Add a RIGHT JOIN expression
399
- * @param expression The expression following the RIGHT JOIN keyword
400
- * @param bindings A list of values to bind to ? placeholders in the expression
401
- * @example
402
- * query.rightJoin('posts p ON p.id = c.post_id');
403
- * query.rightJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
404
- */
405
- rightJoin(expression, bindings = []) {
406
- this._addJoin('RIGHT JOIN', expression, bindings);
407
- return this;
408
- }
409
- /**
410
- * Add a CROSS JOIN expression
411
- * @param expression The expression following the CROSS JOIN keyword
412
- * @param bindings A list of values to bind to ? placeholders in the expression
413
- * @example
414
- * query.crossJoin('posts p ON p.id = c.post_id');
415
- * query.crossJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
416
- */
417
- crossJoin(expression, bindings = []) {
418
- this._addJoin('CROSS JOIN', expression, bindings);
419
- return this;
420
- }
421
- /**
422
- * Add an INNER JOIN expression (same as ->join())
423
- * @param expression The expression following the INNER JOIN keyword
424
- * @param bindings A list of values to bind to ? placeholders in the expression
425
- * @example
426
- * query.innerJoin('posts p ON p.id = c.post_id');
427
- * query.innerJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
428
- */
429
- innerJoin(expression, bindings = []) {
430
- this._addJoin('INNER JOIN', expression, bindings);
431
- return this;
432
- }
433
- /**
434
- * Add a LEFT OUTER JOIN expression
435
- * @param expression The expression following the LEFT OUTER JOIN keyword
436
- * @param bindings A list of values to bind to ? placeholders in the expression
437
- * @example
438
- * query.leftOuterJoin('posts p ON p.id = c.post_id');
439
- * query.leftOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
440
- */
441
- leftOuterJoin(expression, bindings = []) {
442
- this._addJoin('LEFT OUTER JOIN', expression, bindings);
443
- return this;
444
- }
445
- /**
446
- * Add a FULL OUTER JOIN expression
447
- * @param expression The expression following the FULL OUTER JOIN keyword
448
- * @param bindings A list of values to bind to ? placeholders in the expression
449
- * @example
450
- * query.fullOuterJoin('posts p ON p.id = c.post_id');
451
- * query.fullOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
452
- */
453
- fullOuterJoin(expression, bindings = []) {
454
- this._addJoin('FULL OUTER JOIN', expression, bindings);
455
- return this;
456
- }
457
- /**
458
- * Add a RIGHT OUTER JOIN expression
459
- * @param expression The expression following the RIGHT OUTER JOIN keyword
460
- * @param bindings A list of values to bind to ? placeholders in the expression
461
- * @example
462
- * query.rightOuterJoin('posts p ON p.id = c.post_id');
463
- * query.rightOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
464
- */
465
- rightOuterJoin(expression, bindings = []) {
466
- this._addJoin('RIGHT OUTER JOIN', expression, bindings);
467
- return this;
468
- }
469
- /**
470
- * Remove a join condition with the specified table
471
- * @param table The name of the table or tables in the first part of the join statement
472
- */
473
- unjoin(table) {
474
- if (Array.isArray(table)) {
475
- table.forEach(t => this.unjoin(t));
476
- return this;
477
- }
478
- this._joins = this._joins.filter(join => {
479
- const regex = new RegExp(`JOIN ${table}\\b`);
480
- return !regex.test(join);
481
- });
482
- return this;
483
- }
484
- /**
485
- * Utility function to add conditions for a clause (WHERE, HAVING)
486
- * @param collection The collection to add the clauses to (e.g. this._wheres or this._havings)
487
- * @param criteria Criteria adhering to WhereCriteria type
488
- * @example The following are equivalent
489
- * this._conditions(this._wheres, ['deleted_at IS NULL']);
490
- * this._conditions(this._wheres, ['deleted_at', null]);
491
- * this._conditions(this._wheres, ['deleted_at', '=', null]);
492
- * @example More examples
493
- * this._conditions(this._wheres, ['fname', 'LIKE', 'joe']); // `fname` LIKE 'joe'
494
- * this._conditions(this._wheres, ['fname', 'LIKE ?', 'joe']); // `fname` LIKE 'joe'
495
- * this._conditions(this._wheres, ['fname LIKE %?%', 'joe']); // `fname` LIKE '%joe%'
496
- * this._conditions(this._wheres, ['fname LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
497
- * this._conditions(this._wheres, ['fname', 'LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
498
- * this._conditions(this._wheres, ['price >', 10]); // `price` > 10
499
- * this._conditions(this._wheres, ['price', '>', 10]); // `price` > 10
500
- * this._conditions(this._wheres, ['price =', 10]); // `price` = 10
501
- * this._conditions(this._wheres, ['price !=', 10]); // `price` != 10
502
- * this._conditions(this._wheres, ['price', 10]); // `price` = 10
503
- * this._conditions(this._wheres, ['price', '=', 10]); // `price` = 10
504
- * this._conditions(this._wheres, ['price', '!=', 10]); // `price` != 10
505
- * this._conditions(this._wheres, ['price', 'BETWEEN', [10,20]]); // `price` BETWEEN 10 AND 20
506
- * this._conditions(this._wheres, ['price', 'NOT BETWEEN', [10,20]]); // `price` NOT BETWEEN 10 AND 20
507
- * this._conditions(this._wheres, ['price', [10,20]]); // `price` IN(10,20)
508
- * this._conditions(this._wheres, ['price', '=', [10,20]]); // `price` IN(10,20)
509
- * this._conditions(this._wheres, ['price', 'IN', [10,20]]); // `price` IN(10,20)
510
- * this._conditions(this._wheres, ['price', 'NOT IN', [10,20]]); // `price` NOT IN(10,20)
511
- */
512
- _conditions(collection, criteria) {
513
- const numArgs = criteria.length;
514
- let [column, operator, value] = criteria;
515
- if (typeof column === 'object') {
516
- for (const keyValue of Object.entries(column)) {
517
- this._conditions(collection, keyValue);
518
- }
519
- return this;
520
- }
521
- if (numArgs === 1) {
522
- // condition is a stand-alone expression
523
- // e.g. "SUM(price) > 10"
524
- collection.push(column);
525
- return this;
526
- }
527
- else if (numArgs === 2 &&
528
- Array.isArray(operator) &&
529
- operator.length > 0 &&
530
- substrCount(column, '?') === operator.length) {
531
- // column is a string with question marks and operator is an array of replacements
532
- // e.g. query.where('SUBSTR(prefs, ?, ?) = role', [1, 4]);
533
- const values = operator;
534
- const sql = column.replace(/\?/g, () => {
535
- this._bindings.push(values.shift());
536
- return `__BOUND_${this._bindings.length - 1}__`;
537
- });
538
- collection.push(sql);
539
- return this;
540
- }
541
- else if (numArgs === 2) {
542
- // condition has pairs of "column + operator" => "value"
543
- // e.g. ["price >", 10]
544
- // e.g. ["status LIKE ?%", 10]
545
- value = operator;
546
- const parts = column.split(' ');
547
- column = parts.shift();
548
- operator = parts.join(' ');
549
- }
550
- if (typeof operator !== 'string' || operator === '') {
551
- operator = '=';
552
- }
553
- operator = operator.toLocaleUpperCase();
554
- const likeMatch = operator.match(/^(LIKE|NOT LIKE|ILIKE|NOT ILIKE)(?: (\?|\?%|%\?|%\?%))?$/i);
555
- if (operator === 'NOT BETWEEN' || operator === 'BETWEEN') {
556
- // expect a two-item array
557
- const one = this._bindings.length;
558
- const two = this._bindings.length + 1;
559
- collection.push(`${column} ${operator} __BOUND_${one}__ AND __BOUND_${two}__`);
560
- this._bindings.push(value[0], value[1]);
561
- }
562
- else if (likeMatch) {
563
- const like = likeMatch[1].toUpperCase(); // Either "LIKE", "NOT LIKE" etc.
564
- const infix = likeMatch[2] || '?'; // ONE OF ?% or %?% or %? or ?
565
- if (Array.isArray(value)) {
566
- const ors = [];
567
- for (const v of value) {
568
- ors.push(`${column} ${like} __BOUND_${this._bindings.length}__`);
569
- this._bindings.push(infix.replace('?', v));
570
- }
571
- const joined = ors.join(' OR ');
572
- collection.push(`(${joined})`);
573
- }
574
- else {
575
- collection.push(`${column} ${like} __BOUND_${this._bindings.length}__`);
576
- this._bindings.push(infix.replace('?', value || ''));
577
- }
578
- }
579
- else if (value === null) {
580
- collection.push(operator === '=' ? `${column} IS NULL` : `${column} IS NOT NULL`);
581
- }
582
- else if (Array.isArray(value)) {
583
- // an array of values should be IN or NOT IN
584
- const inVals = [];
585
- for (let i = 0; i < value.length; i++) {
586
- inVals.push(`__BOUND_${this._bindings.length}__`);
587
- this._bindings.push(value[i]);
588
- }
589
- const joined = inVals.join(', ');
590
- collection.push(operator === '=' || operator === 'IN'
591
- ? `${column} IN(${joined})`
592
- : `${column} NOT IN(${joined})`);
593
- }
594
- else if (operator === 'IN' || operator === 'NOT IN') {
595
- // in clause that is not array
596
- collection.push(`${column} ${operator}(__BOUND_${this._bindings.length}__)`);
597
- this._bindings.push(value);
598
- }
599
- else {
600
- collection.push(`${column} ${operator} __BOUND_${this._bindings.length}__`);
601
- this._bindings.push(value);
602
- }
603
- return this;
604
- }
605
- /**
606
- * Add a group by column or expression
607
- * @param column The name of a column (or expression) to group by
608
- */
609
- groupBy(column) {
610
- this._groupBys.push(column);
611
- return this;
612
- }
613
- /**
614
- * Add WHERE clauses to conditions (See _conditions for usage)
615
- */
616
- where(...args) {
617
- this._conditions(this._wheres, args);
618
- return this;
619
- }
620
- /**
621
- * Add a WHERE clause with a BETWEEN condition
622
- * @param column The column name
623
- * @param twoValueArray The two values to be between
624
- */
625
- whereBetween(column, twoValueArray) {
626
- if (!isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
627
- this.where(column, 'BETWEEN', twoValueArray);
628
- }
629
- else if (!isNullish(twoValueArray[0]) && isNullish(twoValueArray[1])) {
630
- this.where(column, '>=', twoValueArray[0]);
631
- }
632
- else if (isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
633
- this.where(column, '<=', twoValueArray[1]);
634
- }
635
- else {
636
- // both are nullish! omit expression
637
- }
638
- return this;
639
- }
640
- /**
641
- * Add a series of WHERE clauses joined by OR (See _conditions for usage)
642
- * @param conditions
643
- */
644
- _orConditions(conditions) {
645
- const items = [];
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
- }
654
- }
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]);
661
- }
662
- }
663
- const joined = items.join(' OR ');
664
- if (joined.startsWith('(') && joined.endsWith(')')) {
665
- // start/end paraens will be added by orWhere() or orHaving()
666
- return joined.slice(1, -1);
667
- }
668
- else {
669
- return joined;
670
- }
671
- }
672
- /**
673
- * Add WHERE conditions to place inside an OR block (See _conditions for usage)
674
- * @param conditions A list or object where each item is an array with parameters that would be taken by where()
675
- */
676
- orWhere(conditions) {
677
- const joined = this._orConditions(conditions);
678
- this.where(`(${joined})`);
679
- return this;
680
- }
681
- /**
682
- * Add an OR with conditions under the HAVING clause
683
- * @param conditions A list or object where each item is an array with parameters that would be taken by having()
684
- */
685
- orHaving(conditions) {
686
- const joined = this._orConditions(conditions);
687
- this.having(`(${joined})`);
688
- return this;
689
- }
690
- /**
691
- * Add a HAVING condition (See _conditions for usage)
692
- */
693
- having(...args) {
694
- this._conditions(this._havings, args);
695
- return this;
696
- }
697
- /**
698
- * Add a column or expression to order by
699
- * @param column The column name or expression to sort by. Include DESC or prefix with - to sort descending
700
- * @param [direction] The direction to sort by (ASC or DESC)
701
- * @example
702
- * query.sortField('modified_at'); // ORDER BY modified_at
703
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
704
- * query.sortField('COUNT(*)'); // ORDER BY COUNT(*)
705
- * query.sortField('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
706
- */
707
- orderBy(column, direction = null) {
708
- if (direction) {
709
- this._orderBys.push(`${column} ${direction}`);
710
- }
711
- else {
712
- const expression = column.replace(/^-(.+)/, '$1 DESC');
713
- this._orderBys.push(expression);
714
- }
715
- return this;
716
- }
717
- /**
718
- * Sort by the given column, with a map of columns to translate
719
- * @param fieldName The file name such as "created_at" or "-created_at" for descending
720
- * @param [mapNames] Column names to translate from field to column expression
721
- * @example
722
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
723
- * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
724
- */
725
- sortField(fieldName, mapNames = {}) {
726
- let direction = 'ASC';
727
- if (fieldName.startsWith('-')) {
728
- direction = 'DESC';
729
- fieldName = fieldName.slice(1);
730
- }
731
- const column = mapNames[fieldName] || fieldName;
732
- this.orderBy(column, direction);
733
- return this;
734
- }
735
- /**
736
- * Check to see if the given string is 0 or all digits not starting with 0
737
- * @param string The input string
738
- * @private
739
- */
740
- _isEntirelyDigits(string) {
741
- return /^(0|[1-9]\d*)$/.test(string);
742
- }
743
- /**
744
- * Check to see if the given string is all digits
745
- * @param string The input string
746
- * @private
747
- */
748
- _isEntirelyDigitsNoZeros(string) {
749
- return /^[1-9]\d*$/.test(string);
750
- }
751
- /**
752
- * Limit results to the given number
753
- * @param max The number to limit by (placeholder string or integer greater than 0)
754
- */
755
- limit(max) {
756
- if (typeof max === 'string') {
757
- max = max.trim();
758
- if (this._isEntirelyDigitsNoZeros(max)) {
759
- // is entirely digits (no leading zeros)
760
- this._limit = parseInt(max, 10);
761
- }
762
- }
763
- else if (typeof max === 'number' && Number.isInteger(max) && max >= 1) {
764
- this._limit = max;
765
- }
766
- else {
767
- throw new Error(`SelectBuilder: "limit(n)" must receive a positive integer (got "${max}")`);
768
- }
769
- return this;
770
- }
771
- /**
772
- * Fetch results from the given offset
773
- * @param number The offset (placeholder string or integer greater than or equal to 0)
774
- */
775
- offset(number) {
776
- if (typeof number === 'string' && this._isEntirelyDigits(number.trim())) {
777
- // is entirely digits (or zero)
778
- this._offset = parseInt(number, 10);
779
- }
780
- else if (typeof number === 'number' &&
781
- Number.isInteger(number) &&
782
- number >= 0) {
783
- this._offset = number;
784
- }
785
- else {
786
- throw new Error(`SelectBuilder: "offset(n)" must receive a positive integer (got "${number}")`);
787
- }
788
- return this;
789
- }
790
- /**
791
- * Set the offset based on the limit with the given page number
792
- * @param number The page number (integer greater than 0)
793
- */
794
- page(number) {
795
- if (typeof number === 'string' &&
796
- this._isEntirelyDigitsNoZeros(number.trim())) {
797
- // is entirely digits (or zero)
798
- this._page = parseInt(number, 10);
799
- }
800
- else if (typeof number === 'number' &&
801
- Number.isInteger(number) &&
802
- number >= 1) {
803
- this._page = number;
804
- }
805
- else {
806
- throw new Error('SelectBuilder: "page(n)" must receive a positive integer');
807
- }
808
- return this;
809
- }
810
- }