sql-love 1.1.1 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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;
@@ -477,7 +819,7 @@ UNION
477
819
  if (numArgs === 1) {
478
820
  collection.push(column);
479
821
  return this;
480
- } else if (numArgs === 2 && Array.isArray(operator) && operator.length > 0 && substrCount(column, "?") === operator.length) {
822
+ } else if (numArgs === 2 && Array.isArray(operator) && substrCount(column, "?") === operator.length) {
481
823
  const values = operator;
482
824
  const sql = column.replace(/\?/g, () => {
483
825
  this._bindings.push(values.shift());
@@ -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,27 @@ UNION
600
994
  }
601
995
  return this;
602
996
  }
997
+ /**
998
+ * Sort by the given field, with a map of allowed fields to column expressions.
999
+ * Only field names present in mapNames are allowed; anything else emits a
1000
+ * console.warn and adds no ORDER BY clause. This prevents SQL injection when
1001
+ * fieldName comes from user input.
1002
+ * @param fieldName The field name such as 'created' or '-created' for descending
1003
+ * @param mapNames Allowed field names mapped to column expressions
1004
+ * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
1005
+ * @example
1006
+ * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
1007
+ * query.sortField('-foo', {'created': 'created_at'}); // console.warn; no ORDER BY added
1008
+ * query.sortField('-created', {
1009
+ * 'created': 'created_at NULLS LAST',
1010
+ * '-created': 'created_at DESC NULLS LAST'
1011
+ * }); // ORDER BY created_at DESC NULLS LAST
1012
+ * query.sortField('-name', {
1013
+ * 'name': 'name COLLATE "jp_JP"'
1014
+ * }); // ORDER BY name COLLATE "jp_JP" DESC
1015
+ */
603
1016
  sortField(fieldName, mapNames = {}, modifier = "") {
604
- if (!fieldName) {
1017
+ if (typeof fieldName !== "string" || fieldName === "") {
605
1018
  return this;
606
1019
  }
607
1020
  let direction = "ASC";
@@ -614,19 +1027,39 @@ UNION
614
1027
  direction = "DESC";
615
1028
  fieldName = fieldName.slice(1);
616
1029
  }
617
- const column = mapNames[fieldName] || fieldName;
1030
+ if (!Object.prototype.hasOwnProperty.call(mapNames, fieldName)) {
1031
+ console.warn(
1032
+ `SelectBuilder: sortField() ignoring unknown field "${fieldName.slice(0, 100)}"`
1033
+ );
1034
+ return this;
1035
+ }
1036
+ const column = mapNames[fieldName];
618
1037
  if (modifier) {
619
1038
  direction += ` ${modifier}`;
620
1039
  }
621
1040
  this.orderBy(column, direction);
622
1041
  return this;
623
1042
  }
1043
+ /**
1044
+ * Check to see if the given string is 0 or all digits not starting with 0
1045
+ * @param string The input string
1046
+ * @private
1047
+ */
624
1048
  _isEntirelyDigits(string) {
625
1049
  return /^(0|[1-9]\d*)$/.test(string);
626
1050
  }
1051
+ /**
1052
+ * Check to see if the given string is all digits
1053
+ * @param string The input string
1054
+ * @private
1055
+ */
627
1056
  _isEntirelyDigitsNoZeros(string) {
628
1057
  return /^[1-9]\d*$/.test(string);
629
1058
  }
1059
+ /**
1060
+ * Limit results to the given number
1061
+ * @param max The number to limit by (placeholder string or integer greater than 0)
1062
+ */
630
1063
  limit(max) {
631
1064
  if (typeof max === "string") {
632
1065
  max = max.trim();
@@ -636,38 +1069,55 @@ UNION
636
1069
  } else if (typeof max === "number" && Number.isInteger(max) && max >= 1) {
637
1070
  this._limit = max;
638
1071
  } else {
639
- throw new Error(`SelectBuilder: "limit(n)" must receive a positive integer (got "${max}")`);
1072
+ throw new Error(
1073
+ `SelectBuilder: "limit(n)" must receive a positive integer (got "${max}")`
1074
+ );
640
1075
  }
641
1076
  return this;
642
1077
  }
1078
+ /**
1079
+ * Fetch results from the given offset
1080
+ * @param number The offset (placeholder string or integer greater than or equal to 0)
1081
+ */
643
1082
  offset(number) {
644
1083
  if (typeof number === "string" && this._isEntirelyDigits(number.trim())) {
645
1084
  this._offset = parseInt(number, 10);
646
1085
  } else if (typeof number === "number" && Number.isInteger(number) && number >= 0) {
647
1086
  this._offset = number;
648
1087
  } else {
649
- throw new Error(`SelectBuilder: "offset(n)" must receive a positive integer (got "${number}")`);
1088
+ throw new Error(
1089
+ `SelectBuilder: "offset(n)" must receive a positive integer (got "${number}")`
1090
+ );
650
1091
  }
651
1092
  return this;
652
1093
  }
1094
+ /**
1095
+ * Set the offset based on the limit with the given page number
1096
+ * @param number The page number (integer greater than 0)
1097
+ */
653
1098
  page(number) {
654
1099
  if (typeof number === "string" && this._isEntirelyDigitsNoZeros(number.trim())) {
655
1100
  this._page = parseInt(number, 10);
656
1101
  } else if (typeof number === "number" && Number.isInteger(number) && number >= 1) {
657
1102
  this._page = number;
658
1103
  } else {
659
- throw new Error('SelectBuilder: "page(n)" must receive a positive integer');
1104
+ throw new Error(
1105
+ 'SelectBuilder: "page(n)" must receive a positive integer'
1106
+ );
660
1107
  }
661
1108
  return this;
662
1109
  }
663
- }
1110
+ };
1111
+
664
1112
  // src/extractGrouped/extractGrouped.ts
665
1113
  function extractGrouped(field, records) {
666
1114
  let idx = 0;
667
1115
  const grouped = {};
668
1116
  for (const record of records) {
669
1117
  if (!(field in record)) {
670
- throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
1118
+ throw new Error(
1119
+ `Column "${String(field)}" is not present in record ${idx}`
1120
+ );
671
1121
  }
672
1122
  if (!grouped[record[field]]) {
673
1123
  grouped[record[field]] = [];
@@ -677,35 +1127,44 @@ function extractGrouped(field, records) {
677
1127
  }
678
1128
  return grouped;
679
1129
  }
1130
+
680
1131
  // src/extractIndexed/extractIndexed.ts
681
1132
  function extractIndexed(field, records) {
682
1133
  let idx = 0;
683
1134
  const indexed = {};
684
1135
  for (const record of records) {
685
1136
  if (!(field in record)) {
686
- throw new Error(`Column "${String(field)}" is not present in record ${idx}`);
1137
+ throw new Error(
1138
+ `Column "${String(field)}" is not present in record ${idx}`
1139
+ );
687
1140
  }
688
1141
  indexed[record[field]] = record;
689
1142
  idx++;
690
1143
  }
691
1144
  return indexed;
692
1145
  }
1146
+
693
1147
  // src/extractLookup/extractLookup.ts
694
1148
  function extractLookup(keyField, valueField, records) {
695
1149
  const lookup = {};
696
1150
  let idx = 0;
697
1151
  for (const record of records) {
698
1152
  if (!(keyField in record)) {
699
- throw new Error(`Column "${String(keyField)}" is not present in record ${idx}`);
1153
+ throw new Error(
1154
+ `Column "${String(keyField)}" is not present in record ${idx}`
1155
+ );
700
1156
  }
701
1157
  if (!(valueField in record)) {
702
- throw new Error(`Column "${String(valueField)}" is not present in record ${idx}`);
1158
+ throw new Error(
1159
+ `Column "${String(valueField)}" is not present in record ${idx}`
1160
+ );
703
1161
  }
704
1162
  idx++;
705
- lookup[String(record[keyField])] = record[valueField];
1163
+ lookup[record[keyField]] = record[valueField];
706
1164
  }
707
1165
  return lookup;
708
1166
  }
1167
+
709
1168
  // src/getPagination/getPagination.ts
710
1169
  function getPagination(query, total) {
711
1170
  total = Number(total) || 0;
@@ -725,8 +1184,8 @@ function getPagination(query, total) {
725
1184
  const numPages = total === 0 || perPage === null ? 0 : Math.ceil(total / perPage) || 0;
726
1185
  const isFirst = page === 1;
727
1186
  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;
1187
+ const prevPage = numPages === 0 || page === null ? null : page - 1 || null;
1188
+ const nextPage = numPages > 0 && page !== null && page + 1 <= numPages ? page + 1 : null;
730
1189
  return {
731
1190
  page,
732
1191
  prevPage,
@@ -738,6 +1197,7 @@ function getPagination(query, total) {
738
1197
  isLast
739
1198
  };
740
1199
  }
1200
+
741
1201
  // src/runMysql/runMysql.ts
742
1202
  function _queryToPromise(mysqlClient, sql, bindings) {
743
1203
  return new Promise((resolve, reject) => {
@@ -788,6 +1248,7 @@ async function runMysqlAsyncWithCount(mysqlClient, query, {
788
1248
  return { records, total: 0, pagination: getPagination(query, 0), fields };
789
1249
  }
790
1250
  }
1251
+
791
1252
  // src/toSafeJson/toSafeJson.ts
792
1253
  function toSafeJsonValue(value) {
793
1254
  if (typeof value === "bigint") {
@@ -814,16 +1275,19 @@ function toSafeJsonString(records) {
814
1275
  }
815
1276
 
816
1277
  // src/runPrisma/runPrisma.ts
817
- async function runPrisma(prisma, query, { engine = undefined } = {}) {
1278
+ async function runPrisma(prisma, query, { engine = void 0 } = {}) {
818
1279
  const { sql, bindings } = query.compile({ engine });
819
1280
  return prisma.$queryRawUnsafe(sql, ...bindings);
820
1281
  }
821
1282
  async function runPrismaWithCount(prisma, query, {
822
- engine = undefined,
1283
+ engine = void 0,
823
1284
  countExpr = "*"
824
1285
  } = {}) {
825
1286
  const { sql, bindings } = query.compile({ engine });
826
- const records = await prisma.$queryRawUnsafe(sql, ...bindings);
1287
+ const records = await prisma.$queryRawUnsafe(
1288
+ sql,
1289
+ ...bindings
1290
+ );
827
1291
  if (records.length > 0) {
828
1292
  const { sql: sql2, bindings: bindings2 } = query.compileCount({ engine, countExpr });
829
1293
  const result = await prisma.$queryRawUnsafe(sql2, ...bindings2);
@@ -834,20 +1298,20 @@ async function runPrismaWithCount(prisma, query, {
834
1298
  }
835
1299
  }
836
1300
  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,
1301
+ Parser,
851
1302
  SelectBuilder,
852
- Parser
1303
+ extractGrouped,
1304
+ extractIndexed,
1305
+ extractLookup,
1306
+ getPagination,
1307
+ runMysql,
1308
+ runMysqlAsync,
1309
+ runMysqlAsyncWithCount,
1310
+ runMysqlWithCount,
1311
+ runPrisma,
1312
+ runPrismaWithCount,
1313
+ toSafeJsonRecord,
1314
+ toSafeJsonRecords,
1315
+ toSafeJsonString,
1316
+ toSafeJsonValue
853
1317
  };