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