sql-love 1.1.0 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,15 +1,31 @@
1
1
  // src/Parser/Parser.ts
2
- class Parser {
2
+ var Parser = class {
3
3
  query;
4
+ /**
5
+ * Create a new instance
6
+ * @param query A Select object on which to build parsed conditions
7
+ */
4
8
  constructor(query) {
5
9
  this.query = query;
6
10
  }
11
+ /**
12
+ * Strip single-line and multi-line comment blocks
13
+ * @param sql The SQL string
14
+ * @return The SQL string without comments
15
+ * @private
16
+ */
7
17
  _stripComments(sql) {
8
18
  sql = sql.replace(/\/\*[\s\S]*?\*\//g, "");
9
19
  sql = sql.replace(/--([\r\n]|\s+[^\r\n]+[\r\n])/g, "");
10
20
  sql = sql.replace(/#[^\r\n]+[\r\n]/g, "");
11
21
  return sql;
12
22
  }
23
+ /**
24
+ * Before splitting into SQL clauses, extract some regex-able subQueries
25
+ * @param sql The unparsed sql string
26
+ * @return An array with new sql and subQueries
27
+ * @private
28
+ */
13
29
  _extractSubQueries(sql) {
14
30
  const subQueries = {};
15
31
  let i = 0;
@@ -23,6 +39,11 @@ class Parser {
23
39
  sql = sql.replace(/\bIN\s*\(\s*SELECT\s.+?\)/, extractor);
24
40
  return { sql, subQueries };
25
41
  }
42
+ /**
43
+ * Inject column subQueries back into this object
44
+ * @param subQueries The lookup of extracted subQueries
45
+ * @private
46
+ */
26
47
  _injectSubQueries(subQueries) {
27
48
  const replacer = ($0) => {
28
49
  return subQueries[$0] || $0;
@@ -35,10 +56,21 @@ class Parser {
35
56
  this.query._wheres = this.query._wheres.map(mapper);
36
57
  this.query._havings = this.query._havings.map(mapper);
37
58
  }
59
+ /**
60
+ * Split SQL into clauses (used by parse())
61
+ * @param sql The SQL to split
62
+ * @return
63
+ * @private
64
+ */
38
65
  _split(sql) {
39
66
  const splitter = /(?:^|\s)(SELECT|FROM|(?:INNER|LEFT\s+OUTER|RIGHT\s+OUTER|LEFT|RIGHT|CROSS|FULL|FULL\s+OUTER)\s+JOIN|WHERE|GROUP\s+BY|HAVING|ORDER\s+BY|LIMIT|OFFSET|FETCH\s+NEXT)\b/i;
40
67
  return sql.split(splitter);
41
68
  }
69
+ /**
70
+ * Get a QuickSelect object representing the given SQL SELECT statement
71
+ * @param rawSql The raw SQL for the SELECT statement
72
+ * @return True if the SQL was parsed successfully
73
+ */
42
74
  parse(rawSql) {
43
75
  const stripped = this._stripComments(rawSql);
44
76
  const { sql, subQueries } = this._extractSubQueries(stripped);
@@ -49,11 +81,16 @@ class Parser {
49
81
  const keyword = rawKeyword.toUpperCase().replace(/\s+/g, "_");
50
82
  const clause = expressions[i++].trim();
51
83
  const handler = `_handle${keyword}`;
52
- this[handler](clause);
84
+ this[handler]?.(clause);
53
85
  }
54
86
  this._injectSubQueries(subQueries);
55
87
  return true;
56
88
  }
89
+ /**
90
+ * Handle SQL_CALC_FOUND_ROWS and column names
91
+ * @param clause The clause after the SELECT
92
+ * @private
93
+ */
57
94
  _handleSELECT(clause) {
58
95
  let columns = clause.split(/s*,s*/);
59
96
  let buffer = "";
@@ -81,34 +118,84 @@ class Parser {
81
118
  }
82
119
  });
83
120
  }
121
+ /**
122
+ * Handle table names
123
+ * @param clause The clause after the FROM
124
+ * @private
125
+ */
84
126
  _handleFROM(clause) {
85
127
  const tables = clause.split(/\s*,\s*/);
86
128
  tables.forEach((table) => this.query.table(table));
87
129
  }
130
+ /**
131
+ * Handle INNER JOIN statements
132
+ * @param clause The clause after the INNER JOIN
133
+ * @private
134
+ */
88
135
  _handleINNER_JOIN(clause) {
89
136
  this.query.innerJoin(clause);
90
137
  }
138
+ /**
139
+ * Handle LEFT JOIN statements
140
+ * @param clause The clause after the LEFT JOIN
141
+ * @private
142
+ */
91
143
  _handleLEFT_JOIN(clause) {
92
144
  this.query.leftJoin(clause);
93
145
  }
146
+ /**
147
+ * Handle LEFT OUTER JOIN statements
148
+ * @param clause The clause after the LEFT OUTER JOIN
149
+ * @private
150
+ */
94
151
  _handleLEFT_OUTER_JOIN(clause) {
95
152
  this.query.leftOuterJoin(clause);
96
153
  }
154
+ /**
155
+ * Handle RIGHT JOIN statements
156
+ * @param clause The clause after the RIGHT JOIN
157
+ * @private
158
+ */
97
159
  _handleRIGHT_JOIN(clause) {
98
160
  this.query.rightJoin(clause);
99
161
  }
162
+ /**
163
+ * Handle RIGHT OUTER JOIN statements
164
+ * @param clause The clause after the RIGHT OUTER JOIN
165
+ * @private
166
+ */
100
167
  _handleRIGHT_OUTER_JOIN(clause) {
101
168
  this.query.rightOuterJoin(clause);
102
169
  }
170
+ /**
171
+ * Handle CROSS JOIN statements
172
+ * @param clause The clause after the CROSS JOIN
173
+ * @private
174
+ */
103
175
  _handleCROSS_JOIN(clause) {
104
176
  this.query.crossJoin(clause);
105
177
  }
178
+ /**
179
+ * Handle FULL JOIN statements
180
+ * @param clause The clause after the FULL JOIN
181
+ * @private
182
+ */
106
183
  _handleFULL_JOIN(clause) {
107
184
  this.query.fullJoin(clause);
108
185
  }
186
+ /**
187
+ * Handle FULL OUTER JOIN statements
188
+ * @param clause The clause after the FULL OUTER JOIN
189
+ * @private
190
+ */
109
191
  _handleFULL_OUTER_JOIN(clause) {
110
192
  this.query.fullOuterJoin(clause);
111
193
  }
194
+ /**
195
+ * Handle WHERE conditions
196
+ * @param clause All the conditions after WHERE
197
+ * @private
198
+ */
112
199
  _handleWHERE(clause) {
113
200
  if (/^(1|'1'|true)$/i.test(clause)) {
114
201
  this.query._wheres.push(clause);
@@ -116,9 +203,20 @@ class Parser {
116
203
  this._handleConditions("where", clause);
117
204
  }
118
205
  }
206
+ /**
207
+ * Handle HAVING statements
208
+ * @param clause All the conditions after HAVING
209
+ * @private
210
+ */
119
211
  _handleHAVING(clause) {
120
212
  this._handleConditions("having", clause);
121
213
  }
214
+ /**
215
+ * Build a conditions list
216
+ * @param type Either where or having
217
+ * @param clause The expressions following the type keyword
218
+ * @private
219
+ */
122
220
  _handleConditions(type, clause) {
123
221
  const andGroups = clause.split(/\bAND\b/i);
124
222
  andGroups.forEach((andGroup) => {
@@ -132,14 +230,29 @@ class Parser {
132
230
  }
133
231
  });
134
232
  }
233
+ /**
234
+ * Handle GROUP BY statements
235
+ * @param clause The clauses after the GROUP BY
236
+ * @private
237
+ */
135
238
  _handleGROUP_BY(clause) {
136
239
  const columns = clause.split(/\s*,\s*/);
137
240
  columns.forEach((column) => this.query.groupBy(column));
138
241
  }
242
+ /**
243
+ * Handle ORDER BY statements
244
+ * @param clause The clause after the ORDER BY
245
+ * @private
246
+ */
139
247
  _handleORDER_BY(clause) {
140
248
  const columns = clause.split(/\s*,\s*/);
141
249
  columns.forEach((column) => this.query.orderBy(column));
142
250
  }
251
+ /**
252
+ * Handle LIMIT statements including "LIMIT #" and "LIMIT #, #"
253
+ * @param clause The clause after the LIMIT
254
+ * @private
255
+ */
143
256
  _handleLIMIT(clause) {
144
257
  const offsetLimit = clause.match(/^(\d+|\?|:\w+)\s*,\s*(\d+|\?|:\w+)$/);
145
258
  if (offsetLimit) {
@@ -150,15 +263,26 @@ class Parser {
150
263
  this.query.limit(clause);
151
264
  }
152
265
  }
266
+ /**
267
+ * Handle OFFSET statements
268
+ * @param clause The number after the OFFSET
269
+ * @private
270
+ */
153
271
  _handleOFFSET(clause) {
154
272
  clause = clause.replace(/^(\d+) ROWS$/i, "$1");
155
273
  this.query.offset(clause);
156
274
  }
275
+ /**
276
+ * Handle OFFSET statements
277
+ * @param clause The number after the OFFSET
278
+ * @private
279
+ */
157
280
  _handleFETCH_NEXT(clause) {
158
281
  clause = clause.replace(/^(\d+) ROWS ONLY$/i, "$1");
159
282
  this.query.limit(clause);
160
283
  }
161
- }
284
+ };
285
+
162
286
  // src/substrCount/substrCount.ts
163
287
  function substrCount(haystack, needle) {
164
288
  let count = 0;
@@ -171,23 +295,76 @@ function substrCount(haystack, needle) {
171
295
  }
172
296
 
173
297
  // src/SelectBuilder/SelectBuilder.ts
174
- var isNullish = (v) => v === undefined || v === null || v === false || isNaN(v);
298
+ var isNullish = (v) => v === void 0 || v === null || v === false || Object.is(NaN, v);
175
299
  var defaultEngine = "mysql";
176
-
177
- class SelectBuilder {
300
+ var SelectBuilder = class _SelectBuilder {
301
+ /**
302
+ * The list of values to bind to the query
303
+ * @private
304
+ */
178
305
  _bindings = [];
306
+ /**
307
+ * The list of strings to come immediately after "SELECT"
308
+ * and before column names
309
+ * @private
310
+ */
179
311
  _options = [];
312
+ /**
313
+ * The list of column names to select
314
+ * @private
315
+ */
180
316
  _tables = [];
317
+ /**
318
+ * The list of tables in the FROM clause
319
+ * @private
320
+ */
181
321
  _columns = [];
322
+ /**
323
+ * The list of JOIN strings to add
324
+ * @private
325
+ */
182
326
  _joins = [];
327
+ /**
328
+ * The list of WHERE clauses to join with AND
329
+ * @private
330
+ */
183
331
  _wheres = [];
332
+ /**
333
+ * The list of GROUP BY clauses to join with commas
334
+ * @private
335
+ */
184
336
  _groupBys = [];
337
+ /**
338
+ * The list of HAVING clauses to join with AND
339
+ * @private
340
+ */
185
341
  _havings = [];
342
+ /**
343
+ * The list of ORDER BY clauses to join with commas
344
+ * @private
345
+ */
186
346
  _orderBys = [];
347
+ /**
348
+ * The LIMIT to use
349
+ * @private
350
+ */
187
351
  _limit;
352
+ /**
353
+ * The OFFSET to use
354
+ * @private
355
+ */
188
356
  _offset;
357
+ /**
358
+ * The page used to construct an OFFSET based on the LIMIT
359
+ * @private
360
+ */
189
361
  _page;
190
- constructor(sql = null, bindings = null) {
362
+ /**
363
+ * Select constructor
364
+ * @param [sql] A base SQL query to parse
365
+ * @param [bindings] Placeholders to bind to the query
366
+ */
367
+ constructor(sql, bindings = {}) {
191
368
  if (sql) {
192
369
  if (bindings) {
193
370
  const keys = Object.keys(bindings);
@@ -200,58 +377,58 @@ class SelectBuilder {
200
377
  new Parser(this).parse(sql);
201
378
  }
202
379
  }
380
+ /**
381
+ * Set the default database engine style (mssql, mysql, oracle, pg)
382
+ * @param engine
383
+ */
203
384
  static setDefaultEngine(engine) {
204
385
  defaultEngine = engine;
205
- return SelectBuilder;
386
+ return _SelectBuilder;
206
387
  }
388
+ /**
389
+ * Get the SQL as a pretty-printed string alongside an array of bindings
390
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
391
+ * @return The SQL query as a string
392
+ */
207
393
  compile({
208
394
  engine = defaultEngine,
209
395
  bindingIndex = 1
210
396
  } = {}) {
211
397
  const lines = [
212
398
  "SELECT",
213
- this._options.length ? ` ${this._options.join(`
214
- `)}` : null,
215
- this._columns.length ? ` ${this._columns.join(`,
216
- `)}` : ` *
217
- `,
399
+ this._options.length ? ` ${this._options.join("\n ")}` : null,
400
+ this._columns.length ? ` ${this._columns.join(",\n ")}` : " *\n",
218
401
  `FROM ${this._tables.join(", ")}`,
219
- this._joins.length ? this._joins.join(`
220
- `) : null,
221
- this._wheres.length ? `WHERE ${this._wheres.join(`
222
- AND `)}` : null,
223
- this._groupBys.length ? `GROUP BY ${this._groupBys.join(`,
224
- `)}` : null,
225
- this._havings.length ? `HAVING ${this._havings.join(`
226
- AND `)}` : null,
227
- this._orderBys.length ? `ORDER BY ${this._orderBys.join(`,
228
- `)}` : null
402
+ this._joins.length ? this._joins.join("\n") : null,
403
+ this._wheres.length ? `WHERE ${this._wheres.join("\n AND ")}` : null,
404
+ this._groupBys.length ? `GROUP BY ${this._groupBys.join(",\n ")}` : null,
405
+ this._havings.length ? `HAVING ${this._havings.join("\n AND ")}` : null,
406
+ this._orderBys.length ? `ORDER BY ${this._orderBys.join(",\n ")}` : null
229
407
  ];
230
- if (this._page > 0) {
408
+ if (this._page && this._page > 0 && this._limit) {
231
409
  const offset = (this._page - 1) * this._limit;
232
- lines.push(this._compileOFFSET(engine, offset));
233
- lines.push(this._compileLIMIT(engine, this._limit));
410
+ lines.push(this._compileOFFSET(engine, offset) ?? "");
411
+ lines.push(this._compileLIMIT(engine, this._limit) ?? "");
234
412
  } else {
235
413
  if (this._offset) {
236
- lines.push(this._compileOFFSET(engine, this._offset));
414
+ lines.push(this._compileOFFSET(engine, this._offset) ?? "");
237
415
  }
238
416
  if (this._limit) {
239
- lines.push(this._compileLIMIT(engine, this._limit));
417
+ lines.push(this._compileLIMIT(engine, this._limit) ?? "");
240
418
  }
241
419
  }
242
- const pretty = lines.filter(Boolean).join(`
243
- `).trim();
420
+ const pretty = lines.filter(Boolean).join("\n").trim();
244
421
  let replacer;
245
422
  const bindings = [];
246
423
  if (engine === "pg") {
247
424
  let idx = bindingIndex;
248
425
  replacer = (_, $1) => {
249
- bindings.push(this._bindings[$1]);
426
+ bindings.push(this._bindings[Number($1)]);
250
427
  return "$" + idx++;
251
428
  };
252
429
  } else {
253
430
  replacer = (_, $1) => {
254
- bindings.push(this._bindings[$1]);
431
+ bindings.push(this._bindings[Number($1)]);
255
432
  return "?";
256
433
  };
257
434
  }
@@ -272,6 +449,11 @@ class SelectBuilder {
272
449
  return `OFFSET ${offset} ROWS`;
273
450
  }
274
451
  }
452
+ /**
453
+ * Get SQL needed to return the found rows of this query
454
+ * @param engineStyle The database engine style (mssql, mysql, oracle, pg)
455
+ * @param countExpr The expression to use inside the COUNT()
456
+ */
275
457
  compileCount({
276
458
  countExpr = "*",
277
459
  engine = defaultEngine,
@@ -279,11 +461,10 @@ class SelectBuilder {
279
461
  } = {}) {
280
462
  const query = this.getFoundRowsQuery(countExpr);
281
463
  const { sql, bindings } = query.compile({ engine, bindingIndex });
282
- if (this._havings.length === 0) {
464
+ if (this._havings.length === 0 && this._groupBys.length === 0) {
283
465
  return { sql, bindings };
284
466
  } else {
285
- const subquerySql = sql.replace(/\n/g, `
286
- `);
467
+ const subquerySql = sql.replace(/\n/g, "\n ");
287
468
  return {
288
469
  sql: `SELECT COUNT(*) AS found_rows
289
470
  FROM (
@@ -293,6 +474,11 @@ ${subquerySql}
293
474
  };
294
475
  }
295
476
  }
477
+ /**
478
+ * Get SQL needed to union this query with another
479
+ * @param query The query to union
480
+ * @param engine The database engine style (mssql, mysql, oracle, pg)
481
+ */
296
482
  compileUnion(query, {
297
483
  engine = defaultEngine,
298
484
  bindingIndex = 1
@@ -309,7 +495,12 @@ UNION
309
495
  bindings: [...first.bindings, ...second.bindings]
310
496
  };
311
497
  }
312
- reset(fieldOrFields = null) {
498
+ /**
499
+ * @param [fieldOrFields] If given, reset the given component(s), otherwise reset all query components
500
+ * Valid components: option, column, table, where, orWhere, having, groupBy, orderBy, limit, offset, page
501
+ * @return This object
502
+ */
503
+ reset(fieldOrFields) {
313
504
  if (Array.isArray(fieldOrFields)) {
314
505
  fieldOrFields.forEach((name) => this.reset(name));
315
506
  return this;
@@ -339,15 +530,18 @@ UNION
339
530
  this._groupBys = [];
340
531
  this._havings = [];
341
532
  this._orderBys = [];
342
- this._limit = null;
343
- this._offset = null;
344
- this._page = null;
533
+ this._limit = void 0;
534
+ this._offset = void 0;
535
+ this._page = void 0;
345
536
  this._bindings = [];
346
537
  }
347
538
  return this;
348
539
  }
540
+ /**
541
+ * Clone this builder object
542
+ */
349
543
  getClone() {
350
- const copy = new SelectBuilder;
544
+ const copy = new _SelectBuilder();
351
545
  copy._options = [...this._options];
352
546
  copy._columns = [...this._columns];
353
547
  copy._tables = [...this._tables];
@@ -362,6 +556,10 @@ UNION
362
556
  copy._bindings = [...this._bindings];
363
557
  return copy;
364
558
  }
559
+ /**
560
+ * Build a version of this query that simply returns COUNT(*)
561
+ * @param [countExpr="*"] Use to specify `DISTINCT colname` if needed
562
+ */
365
563
  getFoundRowsQuery(countExpr = "*") {
366
564
  if (this._havings.length === 0) {
367
565
  const clone = this.getClone();
@@ -369,46 +567,86 @@ UNION
369
567
  clone._options = [];
370
568
  clone._groupBys = [];
371
569
  clone._orderBys = [];
372
- clone._limit = null;
373
- clone._offset = null;
374
- clone._page = null;
570
+ clone._limit = void 0;
571
+ clone._offset = void 0;
572
+ clone._page = void 0;
375
573
  return clone;
376
574
  } else {
377
575
  const subquery = this.getClone();
378
- subquery._limit = null;
379
- subquery._offset = null;
380
- subquery._page = null;
576
+ subquery._limit = void 0;
577
+ subquery._offset = void 0;
578
+ subquery._page = void 0;
381
579
  return subquery;
382
580
  }
383
581
  }
582
+ /**
583
+ * Add an array of column names to fetch
584
+ * @param columnNames The names of columns
585
+ */
384
586
  select(columnNames) {
385
587
  this._columns = this._columns.concat(columnNames);
386
588
  return this;
387
589
  }
590
+ /**
591
+ * Add an array of column names to fetch
592
+ * @param columnNames The names of columns
593
+ */
388
594
  columns(columnNames) {
389
595
  this._columns = this._columns.concat(columnNames);
390
596
  return this;
391
597
  }
598
+ /**
599
+ * Add a column name to fetch
600
+ * @param columnName The name of the column
601
+ */
392
602
  column(columnName) {
393
603
  this._columns.push(columnName);
394
604
  return this;
395
605
  }
606
+ /**
607
+ * Add an option expression such as "TOP 10" or "SQL_CALC_FOUND_ROWS"
608
+ * @param optionExpression Expression to go after "SELECT" and before column list
609
+ */
396
610
  option(optionExpression) {
397
611
  this._options.push(optionExpression);
398
612
  return this;
399
613
  }
614
+ /**
615
+ * Add a table to the "FROM" clause (same as .from())
616
+ * @param {String} tableName The name of the table to query
617
+ * @return {Select}
618
+ */
400
619
  table(tableName) {
401
620
  this._tables.push(tableName);
402
621
  return this;
403
622
  }
623
+ /**
624
+ * Add multiple table to the "FROM" clause
625
+ * @param {Array} tableNames The names of the tables to query
626
+ * @return {Select}
627
+ */
404
628
  tables(tableNames) {
405
629
  this._tables.push(...tableNames);
406
630
  return this;
407
631
  }
632
+ /**
633
+ * Add a table to the "FROM" clause (same as .table())
634
+ * @param {String} tableName The name of the table to query
635
+ * @return {Select}
636
+ */
408
637
  from(tableName) {
409
638
  this._tables.push(tableName);
410
639
  return this;
411
640
  }
641
+ /**
642
+ * Add the given join type expression
643
+ * @param joinType The type of join (e.g. "INNER JOIN", "LEFT JOIN", etc.)
644
+ * @param expression The expression following the INNER JOIN keyword
645
+ * @param bindings A list of values to bind to ? placeholders in the expression
646
+ * @example
647
+ * query.join('posts p ON p.id = c.post_id');
648
+ * query.join('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
649
+ */
412
650
  _addJoin(joinType, expression, bindings = []) {
413
651
  if (bindings.length > 0) {
414
652
  expression = expression.replace(/\?/g, () => {
@@ -418,42 +656,118 @@ UNION
418
656
  }
419
657
  this._joins.push(`${joinType} ${expression}`);
420
658
  }
659
+ /**
660
+ * Add a JOIN expression
661
+ * @param expression The expression following the INNER JOIN keyword
662
+ * @param bindings A list of values to bind to ? placeholders in the expression
663
+ * @example
664
+ * query.join('posts p ON p.id = c.post_id');
665
+ * query.join('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
666
+ */
421
667
  join(expression, bindings = []) {
422
668
  this._addJoin("JOIN", expression, bindings);
423
669
  return this;
424
670
  }
671
+ /**
672
+ * Add a LEFT JOIN expression
673
+ * @param expression The expression following the LEFT JOIN keyword
674
+ * @param bindings A list of values to bind to ? placeholders in the expression
675
+ * @example
676
+ * query.leftJoin('posts p ON p.id = c.post_id');
677
+ * query.leftJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
678
+ */
425
679
  leftJoin(expression, bindings = []) {
426
680
  this._addJoin("LEFT JOIN", expression, bindings);
427
681
  return this;
428
682
  }
683
+ /**
684
+ * Add a FULL JOIN expression
685
+ * @param expression The expression following the FULL JOIN keyword
686
+ * @param bindings A list of values to bind to ? placeholders in the expression
687
+ * @example
688
+ * query.fullJoin('posts p ON p.id = c.post_id');
689
+ * query.fullJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
690
+ */
429
691
  fullJoin(expression, bindings = []) {
430
692
  this._addJoin("FULL JOIN", expression, bindings);
431
693
  return this;
432
694
  }
695
+ /**
696
+ * Add a RIGHT JOIN expression
697
+ * @param expression The expression following the RIGHT JOIN keyword
698
+ * @param bindings A list of values to bind to ? placeholders in the expression
699
+ * @example
700
+ * query.rightJoin('posts p ON p.id = c.post_id');
701
+ * query.rightJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
702
+ */
433
703
  rightJoin(expression, bindings = []) {
434
704
  this._addJoin("RIGHT JOIN", expression, bindings);
435
705
  return this;
436
706
  }
707
+ /**
708
+ * Add a CROSS JOIN expression
709
+ * @param expression The expression following the CROSS JOIN keyword
710
+ * @param bindings A list of values to bind to ? placeholders in the expression
711
+ * @example
712
+ * query.crossJoin('posts p ON p.id = c.post_id');
713
+ * query.crossJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
714
+ */
437
715
  crossJoin(expression, bindings = []) {
438
716
  this._addJoin("CROSS JOIN", expression, bindings);
439
717
  return this;
440
718
  }
719
+ /**
720
+ * Add an INNER JOIN expression (same as ->join())
721
+ * @param expression The expression following the INNER JOIN keyword
722
+ * @param bindings A list of values to bind to ? placeholders in the expression
723
+ * @example
724
+ * query.innerJoin('posts p ON p.id = c.post_id');
725
+ * query.innerJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
726
+ */
441
727
  innerJoin(expression, bindings = []) {
442
728
  this._addJoin("INNER JOIN", expression, bindings);
443
729
  return this;
444
730
  }
731
+ /**
732
+ * Add a LEFT OUTER JOIN expression
733
+ * @param expression The expression following the LEFT OUTER JOIN keyword
734
+ * @param bindings A list of values to bind to ? placeholders in the expression
735
+ * @example
736
+ * query.leftOuterJoin('posts p ON p.id = c.post_id');
737
+ * query.leftOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
738
+ */
445
739
  leftOuterJoin(expression, bindings = []) {
446
740
  this._addJoin("LEFT OUTER JOIN", expression, bindings);
447
741
  return this;
448
742
  }
743
+ /**
744
+ * Add a FULL OUTER JOIN expression
745
+ * @param expression The expression following the FULL OUTER JOIN keyword
746
+ * @param bindings A list of values to bind to ? placeholders in the expression
747
+ * @example
748
+ * query.fullOuterJoin('posts p ON p.id = c.post_id');
749
+ * query.fullOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
750
+ */
449
751
  fullOuterJoin(expression, bindings = []) {
450
752
  this._addJoin("FULL OUTER JOIN", expression, bindings);
451
753
  return this;
452
754
  }
755
+ /**
756
+ * Add a RIGHT OUTER JOIN expression
757
+ * @param expression The expression following the RIGHT OUTER JOIN keyword
758
+ * @param bindings A list of values to bind to ? placeholders in the expression
759
+ * @example
760
+ * query.rightOuterJoin('posts p ON p.id = c.post_id');
761
+ * query.rightOuterJoin('posts p ON p.id = c.post_id AND p.deleted_at < ?', [myDate]);
762
+ */
453
763
  rightOuterJoin(expression, bindings = []) {
454
764
  this._addJoin("RIGHT OUTER JOIN", expression, bindings);
455
765
  return this;
456
766
  }
767
+ /**
768
+ * Remove a join condition with the specified table
769
+ * @param table The name of the table or tables in the first part of the join statement
770
+ */
457
771
  unjoin(table) {
458
772
  if (Array.isArray(table)) {
459
773
  table.forEach((t) => this.unjoin(t));
@@ -465,6 +779,34 @@ UNION
465
779
  });
466
780
  return this;
467
781
  }
782
+ /**
783
+ * Utility function to add conditions for a clause (WHERE, HAVING)
784
+ * @param collection The collection to add the clauses to (e.g. this._wheres or this._havings)
785
+ * @param criteria Criteria adhering to WhereCriteria type
786
+ * @example The following are equivalent
787
+ * this._conditions(this._wheres, ['deleted_at IS NULL']);
788
+ * this._conditions(this._wheres, ['deleted_at', null]);
789
+ * this._conditions(this._wheres, ['deleted_at', '=', null]);
790
+ * @example More examples
791
+ * this._conditions(this._wheres, ['fname', 'LIKE', 'joe']); // `fname` LIKE 'joe'
792
+ * this._conditions(this._wheres, ['fname', 'LIKE ?', 'joe']); // `fname` LIKE 'joe'
793
+ * this._conditions(this._wheres, ['fname LIKE %?%', 'joe']); // `fname` LIKE '%joe%'
794
+ * this._conditions(this._wheres, ['fname LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
795
+ * this._conditions(this._wheres, ['fname', 'LIKE ?%', 'joe']); // `fname` LIKE 'joe%'
796
+ * this._conditions(this._wheres, ['price >', 10]); // `price` > 10
797
+ * this._conditions(this._wheres, ['price', '>', 10]); // `price` > 10
798
+ * this._conditions(this._wheres, ['price =', 10]); // `price` = 10
799
+ * this._conditions(this._wheres, ['price !=', 10]); // `price` != 10
800
+ * this._conditions(this._wheres, ['price', 10]); // `price` = 10
801
+ * this._conditions(this._wheres, ['price', '=', 10]); // `price` = 10
802
+ * this._conditions(this._wheres, ['price', '!=', 10]); // `price` != 10
803
+ * this._conditions(this._wheres, ['price', 'BETWEEN', [10,20]]); // `price` BETWEEN 10 AND 20
804
+ * this._conditions(this._wheres, ['price', 'NOT BETWEEN', [10,20]]); // `price` NOT BETWEEN 10 AND 20
805
+ * this._conditions(this._wheres, ['price', [10,20]]); // `price` IN(10,20)
806
+ * this._conditions(this._wheres, ['price', '=', [10,20]]); // `price` IN(10,20)
807
+ * this._conditions(this._wheres, ['price', 'IN', [10,20]]); // `price` IN(10,20)
808
+ * this._conditions(this._wheres, ['price', 'NOT IN', [10,20]]); // `price` NOT IN(10,20)
809
+ */
468
810
  _conditions(collection, criteria) {
469
811
  const numArgs = criteria.length;
470
812
  let [column, operator, value] = criteria;
@@ -488,18 +830,22 @@ UNION
488
830
  } else if (numArgs === 2) {
489
831
  value = operator;
490
832
  const parts = column.split(" ");
491
- column = parts.shift();
833
+ column = parts.shift() ?? "";
492
834
  operator = parts.join(" ");
493
835
  }
494
836
  if (typeof operator !== "string" || operator === "") {
495
837
  operator = "=";
496
838
  }
497
839
  operator = operator.toLocaleUpperCase();
498
- const likeMatch = operator.match(/^(LIKE|NOT LIKE|ILIKE|NOT ILIKE)(?: (\?|\?%|%\?|%\?%))?$/i);
840
+ const likeMatch = operator.match(
841
+ /^(LIKE|NOT LIKE|ILIKE|NOT ILIKE)(?: (\?|\?%|%\?|%\?%))?$/i
842
+ );
499
843
  if (operator === "NOT BETWEEN" || operator === "BETWEEN") {
500
844
  const one = this._bindings.length;
501
845
  const two = this._bindings.length + 1;
502
- collection.push(`${column} ${operator} __BOUND_${one}__ AND __BOUND_${two}__`);
846
+ collection.push(
847
+ `${column} ${operator} __BOUND_${one}__ AND __BOUND_${two}__`
848
+ );
503
849
  this._bindings.push(value[0], value[1]);
504
850
  } else if (likeMatch) {
505
851
  const like = likeMatch[1].toUpperCase();
@@ -517,32 +863,52 @@ UNION
517
863
  this._bindings.push(infix.replace("?", value || ""));
518
864
  }
519
865
  } else if (value === null) {
520
- collection.push(operator === "=" ? `${column} IS NULL` : `${column} IS NOT NULL`);
866
+ collection.push(
867
+ operator === "=" ? `${column} IS NULL` : `${column} IS NOT NULL`
868
+ );
521
869
  } else if (Array.isArray(value)) {
522
870
  const inVals = [];
523
- for (let i = 0;i < value.length; i++) {
871
+ for (let i = 0; i < value.length; i++) {
524
872
  inVals.push(`__BOUND_${this._bindings.length}__`);
525
873
  this._bindings.push(value[i]);
526
874
  }
527
875
  const joined = inVals.join(", ");
528
- collection.push(operator === "=" || operator === "IN" ? `${column} IN(${joined})` : `${column} NOT IN(${joined})`);
876
+ collection.push(
877
+ operator === "=" || operator === "IN" ? `${column} IN(${joined})` : `${column} NOT IN(${joined})`
878
+ );
529
879
  } else if (operator === "IN" || operator === "NOT IN") {
530
- collection.push(`${column} ${operator}(__BOUND_${this._bindings.length}__)`);
880
+ collection.push(
881
+ `${column} ${operator}(__BOUND_${this._bindings.length}__)`
882
+ );
531
883
  this._bindings.push(value);
532
884
  } else {
533
- collection.push(`${column} ${operator} __BOUND_${this._bindings.length}__`);
885
+ collection.push(
886
+ `${column} ${operator} __BOUND_${this._bindings.length}__`
887
+ );
534
888
  this._bindings.push(value);
535
889
  }
536
890
  return this;
537
891
  }
892
+ /**
893
+ * Add a group by column or expression
894
+ * @param column The name of a column (or expression) to group by
895
+ */
538
896
  groupBy(column) {
539
897
  this._groupBys.push(column);
540
898
  return this;
541
899
  }
900
+ /**
901
+ * Add WHERE clauses to conditions (See _conditions for usage)
902
+ */
542
903
  where(...args) {
543
904
  this._conditions(this._wheres, args);
544
905
  return this;
545
906
  }
907
+ /**
908
+ * Add a WHERE clause with a BETWEEN condition
909
+ * @param column The column name
910
+ * @param twoValueArray The two values to be between. If the first is null, it will use >= instead. If the second is null, it will use <= instead. If both are null, no criteria will be added
911
+ */
546
912
  whereBetween(column, twoValueArray) {
547
913
  if (!isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
548
914
  this.where(column, "BETWEEN", twoValueArray);
@@ -550,9 +916,14 @@ UNION
550
916
  this.where(column, ">=", twoValueArray[0]);
551
917
  } else if (isNullish(twoValueArray[0]) && !isNullish(twoValueArray[1])) {
552
918
  this.where(column, "<=", twoValueArray[1]);
553
- } else {}
919
+ } else {
920
+ }
554
921
  return this;
555
922
  }
923
+ /**
924
+ * Add a series of WHERE clauses joined by OR (See _conditions for usage)
925
+ * @param conditions
926
+ */
556
927
  _orConditions(conditions) {
557
928
  const items = [];
558
929
  if (Array.isArray(conditions)) {
@@ -577,21 +948,44 @@ UNION
577
948
  return joined;
578
949
  }
579
950
  }
951
+ /**
952
+ * Add WHERE conditions to place inside an OR block (See _conditions for usage)
953
+ * @param conditions A list or object where each item is an array with parameters that would be taken by where()
954
+ */
580
955
  orWhere(conditions) {
581
956
  const joined = this._orConditions(conditions);
582
957
  this.where(`(${joined})`);
583
958
  return this;
584
959
  }
960
+ /**
961
+ * Add an OR with conditions under the HAVING clause
962
+ * @param conditions A list or object where each item is an array with parameters that would be taken by having()
963
+ */
585
964
  orHaving(conditions) {
586
965
  const joined = this._orConditions(conditions);
587
966
  this.having(`(${joined})`);
588
967
  return this;
589
968
  }
969
+ /**
970
+ * Add a HAVING condition (See _conditions for usage)
971
+ */
590
972
  having(...args) {
591
973
  this._conditions(this._havings, args);
592
974
  return this;
593
975
  }
594
- orderBy(column, direction = null) {
976
+ /**
977
+ * Add a column or expression to order by
978
+ * @param column The column name or expression to sort by. Include DESC or prefix with - to sort descending
979
+ * @param [direction] The direction to sort by (ASC or DESC)
980
+ * @example
981
+ * query.orderBy('modified_at'); // ORDER BY modified_at
982
+ * query.orderBy('-modified_at'); // ORDER BY modified_at DESC
983
+ * query.orderBy('COUNT(*)'); // ORDER BY COUNT(*)
984
+ * query.orderBy('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
985
+ * query.orderBy('modified_at DESC NULLS LAST'); // ORDER BY modified_at DESC NULLS LAST
986
+ * query.orderBy('modified_at USING >'); // ORDER BY modified_at USING >
987
+ */
988
+ orderBy(column, direction) {
595
989
  if (direction) {
596
990
  this._orderBys.push(`${column} ${direction}`);
597
991
  } else {
@@ -600,8 +994,24 @@ UNION
600
994
  }
601
995
  return this;
602
996
  }
997
+ /**
998
+ * Sort by the given column, with a map of columns to translate
999
+ * @param fieldName The file name such as 'created_at' or '-created_at' for descending
1000
+ * @param [mapNames] Column names to translate from field to column expression
1001
+ * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
1002
+ * @example
1003
+ * query.sortField('-modified_at'); // ORDER BY modified_at DESC
1004
+ * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
1005
+ * query.sortField('-created', {
1006
+ * 'created': 'created_at NULLS LAST',
1007
+ * '-created': 'created_at DESC NULLS LAST'
1008
+ * }); // ORDER BY created_at DESC NULLS LAST
1009
+ * query.sortField('-name', {
1010
+ * 'name': 'name COLLATE "jp_JP"'
1011
+ * }); // ORDER BY name COLLATE "jp_JP" DESC
1012
+ */
603
1013
  sortField(fieldName, mapNames = {}, modifier = "") {
604
- if (!fieldName) {
1014
+ if (typeof fieldName !== "string" || fieldName === "") {
605
1015
  return this;
606
1016
  }
607
1017
  let direction = "ASC";
@@ -621,12 +1031,26 @@ UNION
621
1031
  this.orderBy(column, direction);
622
1032
  return this;
623
1033
  }
1034
+ /**
1035
+ * Check to see if the given string is 0 or all digits not starting with 0
1036
+ * @param string The input string
1037
+ * @private
1038
+ */
624
1039
  _isEntirelyDigits(string) {
625
1040
  return /^(0|[1-9]\d*)$/.test(string);
626
1041
  }
1042
+ /**
1043
+ * Check to see if the given string is all digits
1044
+ * @param string The input string
1045
+ * @private
1046
+ */
627
1047
  _isEntirelyDigitsNoZeros(string) {
628
1048
  return /^[1-9]\d*$/.test(string);
629
1049
  }
1050
+ /**
1051
+ * Limit results to the given number
1052
+ * @param max The number to limit by (placeholder string or integer greater than 0)
1053
+ */
630
1054
  limit(max) {
631
1055
  if (typeof max === "string") {
632
1056
  max = max.trim();
@@ -636,38 +1060,55 @@ UNION
636
1060
  } else if (typeof max === "number" && Number.isInteger(max) && max >= 1) {
637
1061
  this._limit = max;
638
1062
  } else {
639
- throw new Error(`SelectBuilder: "limit(n)" must receive a positive integer (got "${max}")`);
1063
+ throw new Error(
1064
+ `SelectBuilder: "limit(n)" must receive a positive integer (got "${max}")`
1065
+ );
640
1066
  }
641
1067
  return this;
642
1068
  }
1069
+ /**
1070
+ * Fetch results from the given offset
1071
+ * @param number The offset (placeholder string or integer greater than or equal to 0)
1072
+ */
643
1073
  offset(number) {
644
1074
  if (typeof number === "string" && this._isEntirelyDigits(number.trim())) {
645
1075
  this._offset = parseInt(number, 10);
646
1076
  } else if (typeof number === "number" && Number.isInteger(number) && number >= 0) {
647
1077
  this._offset = number;
648
1078
  } else {
649
- throw new Error(`SelectBuilder: "offset(n)" must receive a positive integer (got "${number}")`);
1079
+ throw new Error(
1080
+ `SelectBuilder: "offset(n)" must receive a positive integer (got "${number}")`
1081
+ );
650
1082
  }
651
1083
  return this;
652
1084
  }
1085
+ /**
1086
+ * Set the offset based on the limit with the given page number
1087
+ * @param number The page number (integer greater than 0)
1088
+ */
653
1089
  page(number) {
654
1090
  if (typeof number === "string" && this._isEntirelyDigitsNoZeros(number.trim())) {
655
1091
  this._page = parseInt(number, 10);
656
1092
  } else if (typeof number === "number" && Number.isInteger(number) && number >= 1) {
657
1093
  this._page = number;
658
1094
  } else {
659
- throw new Error('SelectBuilder: "page(n)" must receive a positive integer');
1095
+ throw new Error(
1096
+ 'SelectBuilder: "page(n)" must receive a positive integer'
1097
+ );
660
1098
  }
661
1099
  return this;
662
1100
  }
663
- }
1101
+ };
1102
+
664
1103
  // src/extractGrouped/extractGrouped.ts
665
1104
  function extractGrouped(field, records) {
666
1105
  let idx = 0;
667
1106
  const grouped = {};
668
1107
  for (const record of records) {
669
1108
  if (!(field in record)) {
670
- throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
1109
+ throw new Error(
1110
+ `Column "${String(field)}" is not present in record ${idx}`
1111
+ );
671
1112
  }
672
1113
  if (!grouped[record[field]]) {
673
1114
  grouped[record[field]] = [];
@@ -677,35 +1118,44 @@ function extractGrouped(field, records) {
677
1118
  }
678
1119
  return grouped;
679
1120
  }
1121
+
680
1122
  // src/extractIndexed/extractIndexed.ts
681
1123
  function extractIndexed(field, records) {
682
1124
  let idx = 0;
683
1125
  const indexed = {};
684
1126
  for (const record of records) {
685
1127
  if (!(field in record)) {
686
- throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
1128
+ throw new Error(
1129
+ `Column "${String(field)}" is not present in record ${idx}`
1130
+ );
687
1131
  }
688
1132
  indexed[record[field]] = record;
689
1133
  idx++;
690
1134
  }
691
1135
  return indexed;
692
1136
  }
1137
+
693
1138
  // src/extractLookup/extractLookup.ts
694
1139
  function extractLookup(keyField, valueField, records) {
695
1140
  const lookup = {};
696
1141
  let idx = 0;
697
1142
  for (const record of records) {
698
1143
  if (!(keyField in record)) {
699
- throw new Error(`Column "${String(keyField)}" is not present in record ${idx}`);
1144
+ throw new Error(
1145
+ `Column "${String(keyField)}" is not present in record ${idx}`
1146
+ );
700
1147
  }
701
1148
  if (!(valueField in record)) {
702
- throw new Error(`Column "${String(valueField)}" is not present in record ${idx}`);
1149
+ throw new Error(
1150
+ `Column "${String(valueField)}" is not present in record ${idx}`
1151
+ );
703
1152
  }
704
1153
  idx++;
705
- lookup[String(record[keyField])] = record[valueField];
1154
+ lookup[record[keyField]] = record[valueField];
706
1155
  }
707
1156
  return lookup;
708
1157
  }
1158
+
709
1159
  // src/getPagination/getPagination.ts
710
1160
  function getPagination(query, total) {
711
1161
  total = Number(total) || 0;
@@ -725,8 +1175,8 @@ function getPagination(query, total) {
725
1175
  const numPages = total === 0 || perPage === null ? 0 : Math.ceil(total / perPage) || 0;
726
1176
  const isFirst = page === 1;
727
1177
  const isLast = numPages > 0 && page === numPages;
728
- const prevPage = numPages === 0 ? null : page - 1 || null;
729
- const nextPage = numPages > 0 && page + 1 <= numPages ? page + 1 : null;
1178
+ const prevPage = numPages === 0 || page === null ? null : page - 1 || null;
1179
+ const nextPage = numPages > 0 && page !== null && page + 1 <= numPages ? page + 1 : null;
730
1180
  return {
731
1181
  page,
732
1182
  prevPage,
@@ -738,6 +1188,7 @@ function getPagination(query, total) {
738
1188
  isLast
739
1189
  };
740
1190
  }
1191
+
741
1192
  // src/runMysql/runMysql.ts
742
1193
  function _queryToPromise(mysqlClient, sql, bindings) {
743
1194
  return new Promise((resolve, reject) => {
@@ -788,6 +1239,7 @@ async function runMysqlAsyncWithCount(mysqlClient, query, {
788
1239
  return { records, total: 0, pagination: getPagination(query, 0), fields };
789
1240
  }
790
1241
  }
1242
+
791
1243
  // src/toSafeJson/toSafeJson.ts
792
1244
  function toSafeJsonValue(value) {
793
1245
  if (typeof value === "bigint") {
@@ -814,16 +1266,19 @@ function toSafeJsonString(records) {
814
1266
  }
815
1267
 
816
1268
  // src/runPrisma/runPrisma.ts
817
- async function runPrisma(prisma, query, { engine = undefined } = {}) {
1269
+ async function runPrisma(prisma, query, { engine = void 0 } = {}) {
818
1270
  const { sql, bindings } = query.compile({ engine });
819
1271
  return prisma.$queryRawUnsafe(sql, ...bindings);
820
1272
  }
821
1273
  async function runPrismaWithCount(prisma, query, {
822
- engine = undefined,
1274
+ engine = void 0,
823
1275
  countExpr = "*"
824
1276
  } = {}) {
825
1277
  const { sql, bindings } = query.compile({ engine });
826
- const records = await prisma.$queryRawUnsafe(sql, ...bindings);
1278
+ const records = await prisma.$queryRawUnsafe(
1279
+ sql,
1280
+ ...bindings
1281
+ );
827
1282
  if (records.length > 0) {
828
1283
  const { sql: sql2, bindings: bindings2 } = query.compileCount({ engine, countExpr });
829
1284
  const result = await prisma.$queryRawUnsafe(sql2, ...bindings2);
@@ -834,20 +1289,20 @@ async function runPrismaWithCount(prisma, query, {
834
1289
  }
835
1290
  }
836
1291
  export {
837
- toSafeJsonValue,
838
- toSafeJsonString,
839
- toSafeJsonRecords,
840
- toSafeJsonRecord,
841
- runPrismaWithCount,
842
- runPrisma,
843
- runMysqlWithCount,
844
- runMysqlAsyncWithCount,
845
- runMysqlAsync,
846
- runMysql,
847
- getPagination,
848
- extractLookup,
849
- extractIndexed,
850
- extractGrouped,
1292
+ Parser,
851
1293
  SelectBuilder,
852
- Parser
1294
+ extractGrouped,
1295
+ extractIndexed,
1296
+ extractLookup,
1297
+ getPagination,
1298
+ runMysql,
1299
+ runMysqlAsync,
1300
+ runMysqlAsyncWithCount,
1301
+ runMysqlWithCount,
1302
+ runPrisma,
1303
+ runPrismaWithCount,
1304
+ toSafeJsonRecord,
1305
+ toSafeJsonRecords,
1306
+ toSafeJsonString,
1307
+ toSafeJsonValue
853
1308
  };