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