sql-love 1.0.6 → 1.1.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  ISC License (ISC)
2
- Copyright 2023-2025 Ken Snyder
2
+ Copyright 2023-2026 Ken Snyder
3
3
 
4
4
  Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
5
5
 
package/README.md CHANGED
@@ -1,15 +1,13 @@
1
- <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=1.0.6" 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.0" width="250" height="208" />
2
2
 
3
3
  # sql-love
4
4
 
5
- [![NPM Link](https://badgen.net/npm/v/sql-love?v=1.0.6)](https://npmjs.com/package/sql-love)
6
- [![Language](https://badgen.net/static/language/TS?v=1.0.6)](https://github.com/search?q=repo:kensnyder/sql-love++language:TypeScript&type=code)
7
- [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=1.0.6)](https://github.com/kensnyder/sql-love/actions)
8
- [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=1.0.6)](https://codecov.io/gh/kensnyder/sql-love)
9
- [![Gzipped Size](https://badgen.net/bundlephobia/minzip/sql-love?label=minzipped&v=1.0.6)](https://bundlephobia.com/package/sql-love@1.0.6)
10
- [![Dependency details](https://badgen.net/bundlephobia/dependency-count/sql-love?v=1.0.6)](https://www.npmjs.com/package/sql-love?activeTab=dependencies)
11
- [![Tree shakeable](https://badgen.net/bundlephobia/tree-shaking/sql-love?v=1.0.6)](https://www.npmjs.com/package/sql-love)
12
- [![ISC License](https://badgen.net/static/license/ISC/green?v=1.0.6)](https://opensource.org/licenses/ISC)
5
+ [![NPM Link](https://badgen.net/npm/v/sql-love?v=1.1.0)](https://npmjs.com/package/sql-love)
6
+ [![Language](https://badgen.net/static/language/TS?v=1.1.0)](https://github.com/search?q=repo:kensnyder/sql-love++language:TypeScript&type=code)
7
+ [![Code Coverage](https://codecov.io/gh/kensnyder/sql-love/branch/main/graph/badge.svg?v=1.1.0)](https://codecov.io/gh/kensnyder/sql-love)
8
+ ![Dependencies](https://badgen.net/static/dependencies/0/green)
9
+ ![Tree shakeable](https://badgen.net/static/tree%20shakeable/Yes/green)
10
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=1.1.0)](https://opensource.org/licenses/ISC)
13
11
 
14
12
  Classes for parsing and building SQL select queries in Node/Bun/Deno.
15
13
 
@@ -68,7 +66,7 @@ if (areaCode) {
68
66
  }
69
67
  query.sort(sortField);
70
68
  query.limit(limitTo);
71
- const { sql, bindings } = query.compile();
69
+ const { sql, bindings } = query.compile({ engine: 'pg' });
72
70
 
73
71
  //
74
72
  // Then execute the SQL in your preferred client:
@@ -190,15 +188,16 @@ Use the following methods to build queries. And see
190
188
  - `query.having(expression)` - Having an arbitrary expression
191
189
  - `query.having(columnValueRecord)` - Add multiple conditions
192
190
  - `query.orHaving(expressions)` - Multiple `having()`s joined by OR
193
- - `query.orderBy(column)` - Add ORDER BY clause
194
- - `query.sortField(column, mapNames)` - Add ORDER BY clause with mapNames
191
+ - `query.orderBy(column, direction)` - Add ORDER BY clause
192
+ - `query.sortField(column, mapNames, modifier)` - Add ORDER BY clause with mapNames
195
193
  - `query.limit(num)` - Limit by the given number
196
194
  - `query.offset(num)` - Specify an offset
197
195
  - `query.page(num)` - Automatically calculate offset based on limit and page
198
196
  - `query.columns(columnNames)` - Add column names to fetch (alias `select()`)
199
197
  - `query.column(columnName)` - Add a column name to fetch
200
- - `query.table(tableName)` - Specify the table in the FROM clause
201
- - `query.from(tableName)` - Same as above
198
+ - `query.table(tableName)` - Add a table to add to the FROM clause
199
+ - `query.tables(tableNames)` - Add an array of tables to add to the FROM clause
200
+ - `query.from(tableName)` - Alias for `table()`
202
201
  - `query.join(expression)` - Add a JOIN expression
203
202
  - `query.innerJoin(expression)` - Add an INNER JOIN expression
204
203
  - `query.leftJoin(expression)` - Add a LEFT JOIN expression
@@ -213,7 +212,13 @@ Use the following methods to build queries. And see
213
212
  ### More examples
214
213
 
215
214
  ```js
216
- // The following are equivalent
215
+ // building the FROM clause
216
+ query.column('id');
217
+ query.column('"Media"."postId"');
218
+ query.column('CONCAT(first_name, " ", last_name) AS full_name');
219
+ query.columns(['id', 'first_name', 'last_name']);
220
+
221
+ // Ways to test against NULL
217
222
  query.where('deleted_at', '=', null);
218
223
  query.where('deleted_at', '=', undefined);
219
224
  query.where('deleted_at', null);
@@ -222,15 +227,16 @@ query.where('deleted_at IS NULL');
222
227
  query.where({ deleted_at: null });
223
228
  query.where({ deleted_at: undefined });
224
229
 
225
- // the following demonstrate ways to use LIKE
230
+ // Using LIKE
226
231
  query.where('name', 'LIKE', 'son');
227
232
  query.where('name', 'LIKE ?', 'son');
228
233
  query.where('name', 'LIKE ?%', 'son');
229
234
  query.where('name', 'LIKE %?', 'son');
230
235
  query.where('name', 'LIKE %?%', 'son');
231
236
  query.where('name', 'LIKE', '%son%');
237
+ query.where('name', 'ILIKE %?', 'son');
232
238
 
233
- // The following demonstrate ways to use various operators
239
+ // Various operators
234
240
  query.where('price', '=', 100);
235
241
  query.where('price', '!=', 100);
236
242
  query.where('price', '<>', 100);
@@ -243,7 +249,7 @@ query.whereBetween('price', [100, null]); // price > 100
243
249
  query.whereBetween('price', [null, 200]); // price < 200
244
250
  query.whereBetween('price', [null, null]); // clause is ignored
245
251
 
246
- // The following demonstrate ways to use IN and NOT IN
252
+ // Using IN and NOT IN
247
253
  query.where('status', 'IN', ['pending', 'approved']);
248
254
  query.where('status', ['pending', 'approved']);
249
255
  query.where('status', '=', ['pending', 'approved']);
@@ -254,7 +260,7 @@ query.where({ 'status NOT IN': ['pending', 'approved'] });
254
260
  query.where({ 'status !=': ['pending', 'approved'] });
255
261
  query.where({ 'status <>': ['pending', 'approved'] });
256
262
 
257
- // the following demonstrates how to use question marks for binding
263
+ // Question marks for binding
258
264
  query.where(
259
265
  'users.id IN (SELECT user_id FROM roles WHERE customer_id IN (?, ?))',
260
266
  [1, 2]
@@ -265,7 +271,7 @@ query.where(
265
271
  [1]
266
272
  );
267
273
 
268
- // The following demonstrates using objects to specify multiple conditions
274
+ // Multiple conditions
269
275
  query.where({
270
276
  deleted_at: null,
271
277
  'price >': 100,
@@ -277,7 +283,7 @@ query.where({
277
283
 
278
284
  // Note: .having() supports the same signatures as .where()
279
285
 
280
- // The following demonstrate equivalent ways to use OR
286
+ // Ways to use OR
281
287
  query.orWhere([{ approved_at: null }, { denied_at: null }]);
282
288
  query.orWhere({ approved_at: null, denied_at: null });
283
289
  query.orWhere(['approved_at IS NULL', 'denied_at IS NULL']);
@@ -289,7 +295,7 @@ query.where('(approved_at IS NULL OR denied_at IS NULL)');
289
295
 
290
296
  // Note: .orHaving() supports the same signatures as .orWhere()
291
297
 
292
- // The following demonstrate joins
298
+ // Joins
293
299
  query.innerJoin('phone_numbers p ON p.user_id = u.id');
294
300
  query.leftJoin('phone_numbers p ON p.user_id = u.id AND p.type = ?', ['main']);
295
301
  query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
@@ -297,10 +303,40 @@ query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
297
303
  'cell',
298
304
  ]);
299
305
 
300
- // The following demonstrates pagination
301
- query.limit(10).page(3); // LIMIT 10 OFFSET 20
302
-
303
- // The following demonstrates cloning
306
+ // Grouping
307
+
308
+ // Sorting
309
+ query.orderBy('modified_at'); // ORDER BY modified_at
310
+ query.orderBy('modified_at', 'DESC'); // ORDER BY modified_at DESC
311
+ query.orderBy('-modified_at'); // ORDER BY modified_at DESC
312
+ query.orderBy('COUNT(*)'); // ORDER BY COUNT(*)
313
+ query.orderBy('COUNT(*)', 'DESC'); // ORDER BY COUNT(*) DESC
314
+ query.orderBy('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
315
+ query.orderBy('modified_at DESC NULLS LAST'); // ORDER BY modified_at DESC NULLS LAST
316
+ query.orderBy('modified_at USING >'); // ORDER BY modified_at USING >
317
+
318
+ // Sorting with support for hyphens and mapping
319
+ query.sortField('-modified_at'); // ORDER BY modified_at DESC
320
+ query.sortField('-created', { created: 'created_at' }); // ORDER BY created_at DESC
321
+ query.sortField(
322
+ '-created_at',
323
+ {
324
+ created_at: 'posts.created_timestamp',
325
+ },
326
+ 'NULLS LAST'
327
+ ); // ORDER BY posts.created_timestamp DESC NULLS LAST
328
+ query.sortField('-created', {
329
+ created: 'created_at NULLS LAST',
330
+ '-created': 'created_at DESC NULLS LAST',
331
+ }); // ORDER BY created_at DESC NULLS LAST
332
+ query.sortField('-name', {
333
+ name: 'name COLLATE "jp_JP"',
334
+ }); // ORDER BY name COLLATE "jp_JP" DESC
335
+
336
+ // Pagination
337
+ query.limit(10).page(3); // "LIMIT 10 OFFSET 20" (or for mssql, "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY")
338
+
339
+ // Cloning for creating similar queries
304
340
  const query1 = new SelectBuilder();
305
341
  query1.where('email', 'john@example.com');
306
342
  const query2 = query1.getClone();
@@ -398,10 +434,10 @@ If you need to use SQL keywords in strings, use bindings.
398
434
 
399
435
  ```js
400
436
  // ❌ WILL NOT WORK
401
- new SelectBuilder(`SELECT id, CONCAT('where ', expr) FROM users`);
437
+ new SelectBuilder(`SELECT id, CONCAT('where ', column) FROM users`);
402
438
 
403
439
  // ✅ WORKING EQUIVALENT
404
- new SelectBuilder(`SELECT id, CONCAT(:prefix, expr) FROM users`, {
440
+ new SelectBuilder(`SELECT id, CONCAT(:prefix, column) FROM users`, {
405
441
  prefix: 'where ',
406
442
  });
407
443
  ```
@@ -498,7 +534,9 @@ const grouped = extractGrouped('dept', records);
498
534
  { id: 1, name: 'John', dept: 'Marketing' },
499
535
  { id: 3, name: 'Tim', dept: 'Marketing' },
500
536
  ],
501
- Finance: [{ id: 2, name: 'Jane', dept: 'Finance' }],
537
+ Finance: [
538
+ { id: 2, name: 'Jane', dept: 'Finance' }
539
+ ],
502
540
  }
503
541
  */
504
542
  ```
package/dist/index.cjs CHANGED
@@ -650,13 +650,24 @@ UNION
650
650
  }
651
651
  return this;
652
652
  }
653
- sortField(fieldName, mapNames = {}) {
653
+ sortField(fieldName, mapNames = {}, modifier = "") {
654
+ if (!fieldName) {
655
+ return this;
656
+ }
654
657
  let direction = "ASC";
655
658
  if (fieldName.startsWith("-")) {
659
+ if (mapNames[fieldName]) {
660
+ const maybeModifier = modifier ? ` ${modifier}` : "";
661
+ this.orderBy(mapNames[fieldName] + maybeModifier);
662
+ return this;
663
+ }
656
664
  direction = "DESC";
657
665
  fieldName = fieldName.slice(1);
658
666
  }
659
667
  const column = mapNames[fieldName] || fieldName;
668
+ if (modifier) {
669
+ direction += ` ${modifier}`;
670
+ }
660
671
  this.orderBy(column, direction);
661
672
  return this;
662
673
  }
package/dist/index.d.ts CHANGED
@@ -320,7 +320,7 @@ export declare class SelectBuilder {
320
320
  /**
321
321
  * Add a WHERE clause with a BETWEEN condition
322
322
  * @param column The column name
323
- * @param twoValueArray The two values to be between
323
+ * @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
324
324
  */
325
325
  whereBetween(column: string, twoValueArray: [
326
326
  any,
@@ -350,21 +350,31 @@ export declare class SelectBuilder {
350
350
  * @param column The column name or expression to sort by. Include DESC or prefix with - to sort descending
351
351
  * @param [direction] The direction to sort by (ASC or DESC)
352
352
  * @example
353
- * query.sortField('modified_at'); // ORDER BY modified_at
354
- * query.sortField('-modified_at'); // ORDER BY modified_at DESC
355
- * query.sortField('COUNT(*)'); // ORDER BY COUNT(*)
356
- * query.sortField('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
353
+ * query.orderBy('modified_at'); // ORDER BY modified_at
354
+ * query.orderBy('-modified_at'); // ORDER BY modified_at DESC
355
+ * query.orderBy('COUNT(*)'); // ORDER BY COUNT(*)
356
+ * query.orderBy('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
357
+ * query.orderBy('modified_at DESC NULLS LAST'); // ORDER BY modified_at DESC NULLS LAST
358
+ * query.orderBy('modified_at USING >'); // ORDER BY modified_at USING >
357
359
  */
358
360
  orderBy(column: string, direction?: "ASC" | "DESC"): this;
359
361
  /**
360
362
  * Sort by the given column, with a map of columns to translate
361
- * @param fieldName The file name such as "created_at" or "-created_at" for descending
363
+ * @param fieldName The file name such as 'created_at' or '-created_at' for descending
362
364
  * @param [mapNames] Column names to translate from field to column expression
365
+ * @param [modifier] Additional clause to append such as 'NULLS LAST' or 'COLLATE "en_US"'
363
366
  * @example
364
367
  * query.sortField('-modified_at'); // ORDER BY modified_at DESC
365
368
  * query.sortField('-created', {'created': 'created_at'}); // ORDER BY created_at DESC
366
- */
367
- sortField(fieldName: string, mapNames?: Record<string, string>): this;
369
+ * query.sortField('-created', {
370
+ * 'created': 'created_at NULLS LAST',
371
+ * '-created': 'created_at DESC NULLS LAST'
372
+ * }); // ORDER BY created_at DESC NULLS LAST
373
+ * query.sortField('-name', {
374
+ * 'name': 'name COLLATE "jp_JP"'
375
+ * }); // ORDER BY name COLLATE "jp_JP" DESC
376
+ */
377
+ sortField(fieldName: string, mapNames?: Record<string, string>, modifier?: string): this;
368
378
  /**
369
379
  * Check to see if the given string is 0 or all digits not starting with 0
370
380
  * @param string The input string
package/dist/index.mjs CHANGED
@@ -600,13 +600,24 @@ UNION
600
600
  }
601
601
  return this;
602
602
  }
603
- sortField(fieldName, mapNames = {}) {
603
+ sortField(fieldName, mapNames = {}, modifier = "") {
604
+ if (!fieldName) {
605
+ return this;
606
+ }
604
607
  let direction = "ASC";
605
608
  if (fieldName.startsWith("-")) {
609
+ if (mapNames[fieldName]) {
610
+ const maybeModifier = modifier ? ` ${modifier}` : "";
611
+ this.orderBy(mapNames[fieldName] + maybeModifier);
612
+ return this;
613
+ }
606
614
  direction = "DESC";
607
615
  fieldName = fieldName.slice(1);
608
616
  }
609
617
  const column = mapNames[fieldName] || fieldName;
618
+ if (modifier) {
619
+ direction += ` ${modifier}`;
620
+ }
610
621
  this.orderBy(column, direction);
611
622
  return this;
612
623
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sql-love",
3
- "version": "1.0.6",
3
+ "version": "1.1.1",
4
4
  "description": "Build SQL queries with JavaScript",
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,7 +11,7 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "exports": {
13
13
  ".": {
14
- "tyxpes": "./dist/index.d.ts",
14
+ "types": "./dist/index.d.ts",
15
15
  "require": "./dist/index.cjs",
16
16
  "import": "./dist/index.mjs"
17
17
  }
@@ -19,7 +19,8 @@
19
19
  "scripts": {
20
20
  "test": "bun test",
21
21
  "test-watch": "bun test --watch",
22
- "coverage": "bun test --coverage --coverage-reporter=lcov",
22
+ "coverage": "bun test --coverage",
23
+ "coverage-lcov": "bun test --coverage --coverage-reporter=lcov",
23
24
  "build": "bun run build:clean && bun run build:dts && bun run build:esm && bun run build:cjs",
24
25
  "build:clean": "rm -Rf dist/ && mkdir dist && touch dist/.gitkeep",
25
26
  "build:dts": "yes | bunx dts-bundle-generator -o dist/index.d.ts index.ts",
@@ -50,10 +51,10 @@
50
51
  },
51
52
  "homepage": "https://github.com/kensnyder/sql-love#readme",
52
53
  "devDependencies": {
53
- "@types/bun": "^1.2.8",
54
- "@types/node": "22.14.0",
55
- "prettier": "3.5.3",
56
- "prettier-plugin-organize-imports": "4.1.0",
57
- "typescript": "5.8.3"
54
+ "@types/bun": "latest",
55
+ "@types/node": "latest",
56
+ "prettier": "3.9.6",
57
+ "prettier-plugin-organize-imports": "4.3.0",
58
+ "typescript": "7.0.2"
58
59
  }
59
60
  }