sql-love 1.0.6 → 1.1.0

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,15 +1,15 @@
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
+ [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=1.1.0)](https://github.com/kensnyder/sql-love/actions)
8
+ [![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)
9
+ ![GzippedSize](https://deno.bundlejs.com/?q=asql-love&badge&v=1.1.0)
10
+ ![Dependencies](https://badgen.net/static/dependencies/0/green)
11
+ [![Tree shakeable](https://badgen.net/bundlephobia/tree-shaking/sql-love?v=1.1.0)](https://www.npmjs.com/package/sql-love)
12
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=1.1.0)](https://opensource.org/licenses/ISC)
13
13
 
14
14
  Classes for parsing and building SQL select queries in Node/Bun/Deno.
15
15
 
@@ -190,15 +190,16 @@ Use the following methods to build queries. And see
190
190
  - `query.having(expression)` - Having an arbitrary expression
191
191
  - `query.having(columnValueRecord)` - Add multiple conditions
192
192
  - `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
193
+ - `query.orderBy(column, direction)` - Add ORDER BY clause
194
+ - `query.sortField(column, mapNames, modifier)` - Add ORDER BY clause with mapNames
195
195
  - `query.limit(num)` - Limit by the given number
196
196
  - `query.offset(num)` - Specify an offset
197
197
  - `query.page(num)` - Automatically calculate offset based on limit and page
198
198
  - `query.columns(columnNames)` - Add column names to fetch (alias `select()`)
199
199
  - `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
200
+ - `query.table(tableName)` - Add a table to add to the FROM clause
201
+ - `query.tables(tableNames)` - Add an array of tables to add to the FROM clause
202
+ - `query.from(tableName)` - Alias for `table()`
202
203
  - `query.join(expression)` - Add a JOIN expression
203
204
  - `query.innerJoin(expression)` - Add an INNER JOIN expression
204
205
  - `query.leftJoin(expression)` - Add a LEFT JOIN expression
@@ -213,7 +214,13 @@ Use the following methods to build queries. And see
213
214
  ### More examples
214
215
 
215
216
  ```js
216
- // The following are equivalent
217
+ // building the FROM clause
218
+ query.column('id');
219
+ query.column('"Media"."postId"');
220
+ query.column('CONCAT(first_name, " ", last_name) AS full_name');
221
+ query.columns(['id', 'first_name', 'last_name']);
222
+
223
+ // Ways to test against NULL
217
224
  query.where('deleted_at', '=', null);
218
225
  query.where('deleted_at', '=', undefined);
219
226
  query.where('deleted_at', null);
@@ -222,15 +229,16 @@ query.where('deleted_at IS NULL');
222
229
  query.where({ deleted_at: null });
223
230
  query.where({ deleted_at: undefined });
224
231
 
225
- // the following demonstrate ways to use LIKE
232
+ // Using LIKE
226
233
  query.where('name', 'LIKE', 'son');
227
234
  query.where('name', 'LIKE ?', 'son');
228
235
  query.where('name', 'LIKE ?%', 'son');
229
236
  query.where('name', 'LIKE %?', 'son');
230
237
  query.where('name', 'LIKE %?%', 'son');
231
238
  query.where('name', 'LIKE', '%son%');
239
+ query.where('name', 'ILIKE %?', 'son');
232
240
 
233
- // The following demonstrate ways to use various operators
241
+ // Various operators
234
242
  query.where('price', '=', 100);
235
243
  query.where('price', '!=', 100);
236
244
  query.where('price', '<>', 100);
@@ -243,7 +251,7 @@ query.whereBetween('price', [100, null]); // price > 100
243
251
  query.whereBetween('price', [null, 200]); // price < 200
244
252
  query.whereBetween('price', [null, null]); // clause is ignored
245
253
 
246
- // The following demonstrate ways to use IN and NOT IN
254
+ // Using IN and NOT IN
247
255
  query.where('status', 'IN', ['pending', 'approved']);
248
256
  query.where('status', ['pending', 'approved']);
249
257
  query.where('status', '=', ['pending', 'approved']);
@@ -254,7 +262,7 @@ query.where({ 'status NOT IN': ['pending', 'approved'] });
254
262
  query.where({ 'status !=': ['pending', 'approved'] });
255
263
  query.where({ 'status <>': ['pending', 'approved'] });
256
264
 
257
- // the following demonstrates how to use question marks for binding
265
+ // Question marks for binding
258
266
  query.where(
259
267
  'users.id IN (SELECT user_id FROM roles WHERE customer_id IN (?, ?))',
260
268
  [1, 2]
@@ -265,7 +273,7 @@ query.where(
265
273
  [1]
266
274
  );
267
275
 
268
- // The following demonstrates using objects to specify multiple conditions
276
+ // Multiple conditions
269
277
  query.where({
270
278
  deleted_at: null,
271
279
  'price >': 100,
@@ -277,7 +285,7 @@ query.where({
277
285
 
278
286
  // Note: .having() supports the same signatures as .where()
279
287
 
280
- // The following demonstrate equivalent ways to use OR
288
+ // Ways to use OR
281
289
  query.orWhere([{ approved_at: null }, { denied_at: null }]);
282
290
  query.orWhere({ approved_at: null, denied_at: null });
283
291
  query.orWhere(['approved_at IS NULL', 'denied_at IS NULL']);
@@ -289,7 +297,7 @@ query.where('(approved_at IS NULL OR denied_at IS NULL)');
289
297
 
290
298
  // Note: .orHaving() supports the same signatures as .orWhere()
291
299
 
292
- // The following demonstrate joins
300
+ // Joins
293
301
  query.innerJoin('phone_numbers p ON p.user_id = u.id');
294
302
  query.leftJoin('phone_numbers p ON p.user_id = u.id AND p.type = ?', ['main']);
295
303
  query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
@@ -297,10 +305,40 @@ query.outerJoin('phone_numbers p ON p.user_id = u.id AND p.type NOT IN(?, ?)', [
297
305
  'cell',
298
306
  ]);
299
307
 
300
- // The following demonstrates pagination
301
- query.limit(10).page(3); // LIMIT 10 OFFSET 20
302
-
303
- // The following demonstrates cloning
308
+ // Grouping
309
+
310
+ // Sorting
311
+ query.orderBy('modified_at'); // ORDER BY modified_at
312
+ query.orderBy('modified_at', 'DESC'); // ORDER BY modified_at DESC
313
+ query.orderBy('-modified_at'); // ORDER BY modified_at DESC
314
+ query.orderBy('COUNT(*)'); // ORDER BY COUNT(*)
315
+ query.orderBy('COUNT(*)', 'DESC'); // ORDER BY COUNT(*) DESC
316
+ query.orderBy('COUNT(*) DESC'); // ORDER BY COUNT(*) DESC
317
+ query.orderBy('modified_at DESC NULLS LAST'); // ORDER BY modified_at DESC NULLS LAST
318
+ query.orderBy('modified_at USING >'); // ORDER BY modified_at USING >
319
+
320
+ // Sorting with support for hyphens and mapping
321
+ query.sortField('-modified_at'); // ORDER BY modified_at DESC
322
+ query.sortField('-created', { created: 'created_at' }); // ORDER BY created_at DESC
323
+ query.sortField(
324
+ '-created_at',
325
+ {
326
+ created_at: 'posts.created_timestamp',
327
+ },
328
+ 'NULLS LAST'
329
+ ); // ORDER BY posts.created_timestamp DESC NULLS LAST
330
+ query.sortField('-created', {
331
+ created: 'created_at NULLS LAST',
332
+ '-created': 'created_at DESC NULLS LAST',
333
+ }); // ORDER BY created_at DESC NULLS LAST
334
+ query.sortField('-name', {
335
+ name: 'name COLLATE "jp_JP"',
336
+ }); // ORDER BY name COLLATE "jp_JP" DESC
337
+
338
+ // Pagination
339
+ query.limit(10).page(3); // "LIMIT 10 OFFSET 20" (or for mssql, "OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY")
340
+
341
+ // Cloning for creating similar queries
304
342
  const query1 = new SelectBuilder();
305
343
  query1.where('email', 'john@example.com');
306
344
  const query2 = query1.getClone();
@@ -398,10 +436,10 @@ If you need to use SQL keywords in strings, use bindings.
398
436
 
399
437
  ```js
400
438
  // ❌ WILL NOT WORK
401
- new SelectBuilder(`SELECT id, CONCAT('where ', expr) FROM users`);
439
+ new SelectBuilder(`SELECT id, CONCAT('where ', column) FROM users`);
402
440
 
403
441
  // ✅ WORKING EQUIVALENT
404
- new SelectBuilder(`SELECT id, CONCAT(:prefix, expr) FROM users`, {
442
+ new SelectBuilder(`SELECT id, CONCAT(:prefix, column) FROM users`, {
405
443
  prefix: 'where ',
406
444
  });
407
445
  ```
@@ -498,7 +536,9 @@ const grouped = extractGrouped('dept', records);
498
536
  { id: 1, name: 'John', dept: 'Marketing' },
499
537
  { id: 3, name: 'Tim', dept: 'Marketing' },
500
538
  ],
501
- Finance: [{ id: 2, name: 'Jane', dept: 'Finance' }],
539
+ Finance: [
540
+ { id: 2, name: 'Jane', dept: 'Finance' }
541
+ ],
502
542
  }
503
543
  */
504
544
  ```
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.0",
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",
54
+ "@types/bun": "latest",
55
+ "@types/node": "latest",
56
+ "prettier": "3.6.2",
57
+ "prettier-plugin-organize-imports": "4.2.0",
57
58
  "typescript": "5.8.3"
58
59
  }
59
60
  }