sql-love 1.1.2 → 1.1.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/README.md CHANGED
@@ -1,13 +1,13 @@
1
- <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=1.1.2" width="250" height="208" />
1
+ <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=1.1.5" width="250" height="208" />
2
2
 
3
3
  # sql-love
4
4
 
5
- [![Language](https://badgen.net/static/language/TS?v=1.1.2)](https://github.com/search?q=repo:kensnyder/sql-love++language:TypeScript&type=code)
6
- [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=1.1.2)](https://codecov.io/gh/kensnyder/sql-love)
5
+ [![Language](https://badgen.net/static/language/TS?v=1.1.5)](https://github.com/search?q=repo:kensnyder/sql-love++language:TypeScript&type=code)
6
+ [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=1.1.5)](https://codecov.io/gh/kensnyder/sql-love)
7
7
  ![Dependencies](https://badgen.net/static/dependencies/0/green)
8
8
  ![Tree shakeable](https://badgen.net/static/tree%20shakeable/Yes/green)
9
9
  ![Gzip Size](https://badgen.net/static/gzip%20size/9kb/blue)
10
- [![ISC License](https://badgen.net/static/license/ISC/green?v=1.1.2)](https://opensource.org/licenses/ISC)
10
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=1.1.5)](https://opensource.org/licenses/ISC)
11
11
 
12
12
  Classes for parsing and building SQL select queries in Node/Bun/Deno.
13
13
 
@@ -189,7 +189,7 @@ Use the following methods to build queries. And see
189
189
  - `query.having(columnValueRecord)` - Add multiple conditions
190
190
  - `query.orHaving(expressions)` - Multiple `having()`s joined by OR
191
191
  - `query.orderBy(column, direction)` - Add ORDER BY clause
192
- - `query.sortField(column, mapNames, modifier)` - Add ORDER BY clause with mapNames
192
+ - `query.sortField(field, mapNames, modifier)` - Add ORDER BY clause for an allowed field in mapNames
193
193
  - `query.limit(num)` - Limit by the given number
194
194
  - `query.offset(num)` - Specify an offset
195
195
  - `query.page(num)` - Automatically calculate offset based on limit and page
@@ -316,8 +316,11 @@ query.orderBy('modified_at DESC NULLS LAST'); // ORDER BY modified_at DESC NULLS
316
316
  query.orderBy('modified_at USING >'); // ORDER BY modified_at USING >
317
317
 
318
318
  // Sorting with support for hyphens and mapping
319
- query.sortField('-modified_at'); // ORDER BY modified_at DESC
319
+ // Only fields present in mapNames are allowed, so field names may safely
320
+ // come from user input (e.g. a "?sort=-created" query string param).
321
+ // Unknown fields emit a console.warn and add no ORDER BY clause.
320
322
  query.sortField('-created', { created: 'created_at' }); // ORDER BY created_at DESC
323
+ query.sortField('-foo', { created: 'created_at' }); // console.warn; no ORDER BY added
321
324
  query.sortField(
322
325
  '-created_at',
323
326
  {
package/dist/index.cjs CHANGED
@@ -406,9 +406,10 @@ var SelectBuilder = class _SelectBuilder {
406
406
  */
407
407
  constructor(sql, bindings = {}) {
408
408
  if (sql) {
409
- if (bindings) {
410
- const keys = Object.keys(bindings);
411
- const regexp = new RegExp(`:(${keys.join("|")})`, "g");
409
+ const keys = bindings ? Object.keys(bindings) : [];
410
+ if (keys.length > 0) {
411
+ const names = keys.sort((a, b) => b.length - a.length).map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
412
+ const regexp = new RegExp(`(?<!:):(${names})\\b`, "g");
412
413
  sql = sql.replace(regexp, ($0, $1) => {
413
414
  this._bindings.push(bindings[$1]);
414
415
  return `__BOUND_${this._bindings.length - 1}__`;
@@ -859,7 +860,7 @@ UNION
859
860
  if (numArgs === 1) {
860
861
  collection.push(column);
861
862
  return this;
862
- } else if (numArgs === 2 && Array.isArray(operator) && operator.length > 0 && substrCount(column, "?") === operator.length) {
863
+ } else if (numArgs === 2 && Array.isArray(operator) && substrCount(column, "?") === operator.length) {
863
864
  const values = operator;
864
865
  const sql = column.replace(/\?/g, () => {
865
866
  this._bindings.push(values.shift());
@@ -1035,13 +1036,16 @@ UNION
1035
1036
  return this;
1036
1037
  }
1037
1038
  /**
1038
- * Sort by the given column, with a map of columns to translate
1039
- * @param fieldName The file name such as 'created_at' or '-created_at' for descending
1040
- * @param [mapNames] Column names to translate from field to column expression
1039
+ * Sort by the given field, with a map of allowed fields to column expressions.
1040
+ * Only field names present in mapNames are allowed; anything else emits a
1041
+ * console.warn and adds no ORDER BY clause. This prevents SQL injection when
1042
+ * fieldName comes from user input.
1043
+ * @param fieldName The field name such as 'created' or '-created' for descending
1044
+ * @param mapNames Allowed field names mapped to column expressions
1041
1045
  * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
1042
1046
  * @example
1043
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
1044
1047
  * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
1048
+ * query.sortField('-foo', {'created': 'created_at'}); // console.warn; no ORDER BY added
1045
1049
  * query.sortField('-created', {
1046
1050
  * 'created': 'created_at NULLS LAST',
1047
1051
  * '-created': 'created_at DESC NULLS LAST'
@@ -1064,7 +1068,13 @@ UNION
1064
1068
  direction = "DESC";
1065
1069
  fieldName = fieldName.slice(1);
1066
1070
  }
1067
- const column = mapNames[fieldName] || fieldName;
1071
+ if (!Object.prototype.hasOwnProperty.call(mapNames, fieldName)) {
1072
+ console.warn(
1073
+ `SelectBuilder: sortField() ignoring unknown field "${fieldName.slice(0, 100)}"`
1074
+ );
1075
+ return this;
1076
+ }
1077
+ const column = mapNames[fieldName];
1068
1078
  if (modifier) {
1069
1079
  direction += ` ${modifier}`;
1070
1080
  }
package/dist/index.d.ts CHANGED
@@ -359,13 +359,16 @@ export declare class SelectBuilder {
359
359
  */
360
360
  orderBy(column: string, direction?: "ASC" | "DESC"): this;
361
361
  /**
362
- * Sort by the given column, with a map of columns to translate
363
- * @param fieldName The file name such as 'created_at' or '-created_at' for descending
364
- * @param [mapNames] Column names to translate from field to column expression
362
+ * Sort by the given field, with a map of allowed fields to column expressions.
363
+ * Only field names present in mapNames are allowed; anything else emits a
364
+ * console.warn and adds no ORDER BY clause. This prevents SQL injection when
365
+ * fieldName comes from user input.
366
+ * @param fieldName The field name such as 'created' or '-created' for descending
367
+ * @param mapNames Allowed field names mapped to column expressions
365
368
  * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
366
369
  * @example
367
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
368
370
  * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
371
+ * query.sortField('-foo', {'created': 'created_at'}); // console.warn; no ORDER BY added
369
372
  * query.sortField('-created', {
370
373
  * 'created': 'created_at NULLS LAST',
371
374
  * '-created': 'created_at DESC NULLS LAST'
package/dist/index.mjs CHANGED
@@ -366,9 +366,10 @@ var SelectBuilder = class _SelectBuilder {
366
366
  */
367
367
  constructor(sql, bindings = {}) {
368
368
  if (sql) {
369
- if (bindings) {
370
- const keys = Object.keys(bindings);
371
- const regexp = new RegExp(`:(${keys.join("|")})`, "g");
369
+ const keys = bindings ? Object.keys(bindings) : [];
370
+ if (keys.length > 0) {
371
+ const names = keys.sort((a, b) => b.length - a.length).map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
372
+ const regexp = new RegExp(`(?<!:):(${names})\\b`, "g");
372
373
  sql = sql.replace(regexp, ($0, $1) => {
373
374
  this._bindings.push(bindings[$1]);
374
375
  return `__BOUND_${this._bindings.length - 1}__`;
@@ -819,7 +820,7 @@ UNION
819
820
  if (numArgs === 1) {
820
821
  collection.push(column);
821
822
  return this;
822
- } else if (numArgs === 2 && Array.isArray(operator) && operator.length > 0 && substrCount(column, "?") === operator.length) {
823
+ } else if (numArgs === 2 && Array.isArray(operator) && substrCount(column, "?") === operator.length) {
823
824
  const values = operator;
824
825
  const sql = column.replace(/\?/g, () => {
825
826
  this._bindings.push(values.shift());
@@ -995,13 +996,16 @@ UNION
995
996
  return this;
996
997
  }
997
998
  /**
998
- * Sort by the given column, with a map of columns to translate
999
- * @param fieldName The file name such as 'created_at' or '-created_at' for descending
1000
- * @param [mapNames] Column names to translate from field to column expression
999
+ * Sort by the given field, with a map of allowed fields to column expressions.
1000
+ * Only field names present in mapNames are allowed; anything else emits a
1001
+ * console.warn and adds no ORDER BY clause. This prevents SQL injection when
1002
+ * fieldName comes from user input.
1003
+ * @param fieldName The field name such as 'created' or '-created' for descending
1004
+ * @param mapNames Allowed field names mapped to column expressions
1001
1005
  * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
1002
1006
  * @example
1003
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
1004
1007
  * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
1008
+ * query.sortField('-foo', {'created': 'created_at'}); // console.warn; no ORDER BY added
1005
1009
  * query.sortField('-created', {
1006
1010
  * 'created': 'created_at NULLS LAST',
1007
1011
  * '-created': 'created_at DESC NULLS LAST'
@@ -1024,7 +1028,13 @@ UNION
1024
1028
  direction = "DESC";
1025
1029
  fieldName = fieldName.slice(1);
1026
1030
  }
1027
- const column = mapNames[fieldName] || fieldName;
1031
+ if (!Object.prototype.hasOwnProperty.call(mapNames, fieldName)) {
1032
+ console.warn(
1033
+ `SelectBuilder: sortField() ignoring unknown field "${fieldName.slice(0, 100)}"`
1034
+ );
1035
+ return this;
1036
+ }
1037
+ const column = mapNames[fieldName];
1028
1038
  if (modifier) {
1029
1039
  direction += ` ${modifier}`;
1030
1040
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sql-love",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
4
4
  "description": "Build SQL queries with JavaScript",
5
5
  "type": "module",
6
6
  "engines": {
@@ -55,7 +55,7 @@
55
55
  "@types/bun": "latest",
56
56
  "@types/node": "latest",
57
57
  "dts-bundle-generator": "9.5.1",
58
- "prettier": "3.9.6",
58
+ "prettier": "3.9.9",
59
59
  "prettier-plugin-organize-imports": "4.3.0",
60
60
  "typescript": "5.9.3"
61
61
  }