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