sql-love 1.0.0 → 1.0.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/README.md CHANGED
@@ -1,12 +1,12 @@
1
- <img alt="SQL Love Logo" src="https://github.com/kensnyder/sql-love/raw/main/assets/sql-love-logo.png?v=1.0.0" 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.0.1" 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.0)](https://npmjs.com/package/sql-love)
6
- [![Dependencies](https://badgen.net/static/dependencies/0/green?v=1.0.0)](https://npmjs.com/package/sql-love)
7
- [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=1.0.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.0.0)](https://codecov.io/gh/kensnyder/sql-love)
9
- [![ISC License](https://badgen.net/static/license/ISC/green?v=1.0.0)](https://opensource.org/licenses/ISC)
5
+ [![NPM Link](https://badgen.net/npm/v/sql-love?v=1.0.1)](https://npmjs.com/package/sql-love)
6
+ [![Dependencies](https://badgen.net/static/dependencies/0/green?v=1.0.1)](https://www.npmjs.com/package/sql-love?activeTab=dependencies)
7
+ [![Build Status](https://github.com/kensnyder/sql-love/actions/workflows/node.js.yml/badge.svg?v=1.0.1)](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.1)](https://codecov.io/gh/kensnyder/sql-love)
9
+ [![ISC License](https://badgen.net/static/license/ISC/green?v=1.0.1)](https://opensource.org/licenses/ISC)
10
10
 
11
11
  Classes for parsing and building SQL select queries in Node
12
12
 
@@ -65,17 +65,29 @@ if (areaCode) {
65
65
  query.sort(sortField);
66
66
  query.limit(limitTo);
67
67
  const { sql, bindings } = query.compile();
68
+ ```
69
+
70
+ Then execute the SQL in your preferred client:
68
71
 
69
- // then execute the SQL in your preferred client:
70
- // like mysql2
72
+ ```js
73
+ // mysql2:
71
74
  connection.query(sql, bindings, (err, results, fields) => {});
72
- // or Prisma
75
+
76
+ // Prisma:
73
77
  const result = await prisma.$queryRawUnsafe(sql, ...bindings);
74
78
 
75
- // Note that these are prepared statements so the values in the "bindings"
76
- // array are safe from SQL injection.
79
+ // Cloudflare d1
80
+ const { results } = await env.DB.prepare(sql)
81
+ .bind(...bindings)
82
+ .all();
77
83
  ```
78
84
 
85
+ _Note that these are prepared statements so the values in the "bindings"_
86
+ _array are safe from SQL injection, with the caveat that you are in charge of_
87
+ _quoting any identifiers. For instance, we don't recommend populating `u.email`_
88
+ _from user input in the example above. But if you do, be sure to use your_
89
+ _client's `quoteIdentifier()` function._
90
+
79
91
  It is possible to add placeholders to the base query.
80
92
 
81
93
  ```js
@@ -111,7 +123,7 @@ And "bindings" equals:
111
123
  You may specify a compiler engine to use. The default is `"mysql"`.
112
124
 
113
125
  ```js
114
- import { SelectBuilder } from 'sql-love';
126
+ import { SelectBuilder, setDefaultEngine } from 'sql-love';
115
127
 
116
128
  const query = new SelectBuilder('SELECT * FROM users')
117
129
  .limit(10)
@@ -119,19 +131,18 @@ const query = new SelectBuilder('SELECT * FROM users')
119
131
  .where('id', 1);
120
132
 
121
133
  query.compile({ engine: 'mysql' }).sql;
134
+ query.compile({ engine: 'sqlite' }).sql;
122
135
  // SELECT * FROM users WHERE id = ? OFFSET 20 LIMIT 10
123
136
 
124
137
  query.compile({ engine: 'pg' }).sql;
125
138
  // SELECT * FROM users WHERE id = $1 OFFSET 20 LIMIT 10
126
139
 
127
140
  query.compile({ engine: 'mssql' }).sql;
128
- // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
129
-
130
141
  query.compile({ engine: 'oracle' }).sql;
131
142
  // SELECT * FROM users WHERE id = ? OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY
132
143
 
133
144
  // Or you can specify the default engine to use in the compile function
134
- SelectBuilder.setDefaultEngine('pg');
145
+ setDefaultEngine('pg');
135
146
  ```
136
147
 
137
148
  ### SQL injection
@@ -231,7 +242,17 @@ query.where({ status: ['pending', 'approved'] });
231
242
  query.where({ 'status NOT IN': ['pending', 'approved'] });
232
243
  query.where({ 'status !=': ['pending', 'approved'] });
233
244
  query.where({ 'status <>': ['pending', 'approved'] });
234
- // Note: .having() supports the same signatures as .where()
245
+
246
+ // the following demonstrates how to use question marks for binding
247
+ query.where(
248
+ 'users.id IN (SELECT user_id FROM roles WHERE customer_id IN (?, ?))',
249
+ [1, 2]
250
+ );
251
+ query.where(
252
+ 'users.id IN (SELECT user_id FROM roles WHERE customer_id = ?)',
253
+ // even if there is only one placeholder, you must use an array
254
+ [1]
255
+ );
235
256
 
236
257
  // The following demonstrates using objects to specify multiple conditions
237
258
  query.where({
@@ -243,6 +264,8 @@ query.where({
243
264
  conditions: ['new', 'used'],
244
265
  });
245
266
 
267
+ // Note: .having() supports the same signatures as .where()
268
+
246
269
  // The following demonstrate equivalent ways to use OR
247
270
  query.orWhere([{ approved_at: null }, { denied_at: null }]);
248
271
  query.orWhere({ approved_at: null, denied_at: null });
@@ -252,6 +275,7 @@ query.orWhere([
252
275
  ['denied_at =', null],
253
276
  ]);
254
277
  query.where('(approved_at IS NULL OR denied_at IS NULL)');
278
+
255
279
  // Note: .orHaving() supports the same signatures as .orWhere()
256
280
 
257
281
  // The following demonstrate joins
@@ -565,22 +589,23 @@ const { records, total, pagination } = await runMysqlAsyncWithCount(
565
589
 
566
590
  ### toSafeJson
567
591
 
568
- With some database clients such as Prisma, your recordsets may contain Date
569
- objects and BigInt objects. These are not safe to pass to JSON.stringify().
570
- The following are examples of the toSafeJson\* utility functions.
592
+ With some database clients such as Prisma, your recordsets may contain BigInt
593
+ objects. These are not safe to pass to JSON.stringify(). The following are
594
+ examples of the toSafeJson\* utility functions.
571
595
 
572
596
  ```js
573
597
  import { toSafeJsonString, toSafeJsonRecords } from 'sql-love';
574
598
 
575
- const records = [{ id: 1, name: 'John', created_at: new Date(), groups: 12n }];
599
+ const records = [{ count: 12n }, { count: 9007199254740993n }];
576
600
 
577
601
  toSafeJsonRecords(records);
578
602
  // [
579
- // { id: 1, name: 'John', created_at: '2023-12-12T17:20:09.471Z', groups: 12 },
603
+ // { count: 12 },
604
+ // { count: "9007199254740993" },
580
605
  // ]
581
606
 
582
607
  toSafeJsonString(records);
583
- // '[{"id":1,"name":"John","created_at":"2023-12-12T17:20:09.471Z","groups":12}]'
608
+ // '[{"count":12},{"count":"9007199254740993"}]'
584
609
 
585
- // Note that BigInt values too big for Number will be converted to strings
610
+ // Note that BigInt values too big for Number are converted to strings
586
611
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sql-love",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Build SQL queries with JavaScript",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -32,11 +32,11 @@
32
32
  },
33
33
  "homepage": "https://github.com/kensnyder/sql-love#readme",
34
34
  "devDependencies": {
35
- "@vitest/coverage-v8": "1.0.4",
36
- "prettier": "3.1.1",
35
+ "@vitest/coverage-v8": "1.3.1",
36
+ "prettier": "3.2.5",
37
37
  "prettier-plugin-organize-imports": "3.2.4",
38
38
  "rimraf": "5.0.5",
39
39
  "typescript": "5.3.3",
40
- "vitest": "1.0.4"
40
+ "vitest": "1.3.1"
41
41
  }
42
42
  }
@@ -17,13 +17,15 @@ interface PrismaClient {
17
17
  * @param options
18
18
  * @param options.engine The engine style to use ("mssql" | "mysql" | "oracle" | "pg")
19
19
  */
20
- export async function runPrisma(
20
+ export async function runPrisma<
21
+ T extends Array<Record<string, any>> = Array<Record<string, any>>,
22
+ >(
21
23
  prisma: PrismaClient,
22
24
  query: SelectBuilder,
23
25
  { engine = undefined }: { engine?: EngineStyle } = {}
24
- ): Promise<Array<Record<string, any>>> {
26
+ ): Promise<T> {
25
27
  const { sql, bindings } = query.compile({ engine });
26
- return prisma.$queryRawUnsafe(sql, ...bindings);
28
+ return prisma.$queryRawUnsafe(sql, ...bindings) as unknown as Promise<T>;
27
29
  }
28
30
 
29
31
  /**
@@ -34,16 +36,34 @@ export async function runPrisma(
34
36
  * @param options.engine The engine style to use ("mssql" | "mysql" | "oracle" | "pg")
35
37
  * @param options.countExpr The SQL expression to use for the count (default "*")
36
38
  */
37
- export async function runPrismaWithCount(
39
+ export async function runPrismaWithCount<
40
+ T extends Array<Record<string, any>> = Array<Record<string, any>>,
41
+ >(
38
42
  prisma: PrismaClient,
39
43
  query: SelectBuilder,
40
44
  {
41
45
  engine = undefined,
42
46
  countExpr = '*',
43
47
  }: { engine?: EngineStyle; countExpr?: string } = {}
44
- ) {
48
+ ): Promise<{
49
+ records: T;
50
+ total: number;
51
+ pagination: {
52
+ page: number;
53
+ prevPage: number;
54
+ nextPage: number;
55
+ perPage: number;
56
+ numPages: number;
57
+ total: number;
58
+ isFirst: boolean;
59
+ isLast: boolean;
60
+ };
61
+ }> {
45
62
  const { sql, bindings } = query.compile({ engine });
46
- const records = await prisma.$queryRawUnsafe(sql, ...bindings);
63
+ const records = (await prisma.$queryRawUnsafe(
64
+ sql,
65
+ ...bindings
66
+ )) as unknown as T;
47
67
  if (records.length > 0) {
48
68
  const { sql, bindings } = query.compileCount({ engine, countExpr });
49
69
  const result = await prisma.$queryRawUnsafe(sql, ...bindings);
@@ -12,12 +12,6 @@ describe('toSafeJsonString()', () => {
12
12
  const json = toSafeJsonString(records);
13
13
  expect(json).toBe(`[{"found_rows":"${myBigInt.toString()}"}]`);
14
14
  });
15
- it('should convert Date to ISO String', () => {
16
- const date = new Date('2023-12-06');
17
- const records = [{ date }];
18
- const json = toSafeJsonString(records);
19
- expect(json).toBe(`[{"date":"${date.toISOString()}"}]`);
20
- });
21
15
  });
22
16
  describe('jsonSafePrisma()', () => {
23
17
  it('should convert small BigInt to Number', () => {
@@ -31,10 +25,4 @@ describe('jsonSafePrisma()', () => {
31
25
  const json = toSafeJsonRecords(records);
32
26
  expect(json).toStrictEqual([{ found_rows: myBigInt.toString() }]);
33
27
  });
34
- it('should convert Date to ISO String', () => {
35
- const date = new Date('2023-12-06');
36
- const records = [{ date }];
37
- const json = toSafeJsonRecords(records);
38
- expect(json).toStrictEqual([{ date: date.toISOString() }]);
39
- });
40
28
  });
@@ -1,8 +1,4 @@
1
- type SafeValue<T> = T extends BigInt
2
- ? number | string
3
- : T extends Date
4
- ? string
5
- : T;
1
+ type SafeValue<T> = T extends BigInt ? number | string : T;
6
2
  type SafeRecord<T> = Record<string, SafeValue<T>>;
7
3
  type SafeRecords<T> = Array<SafeRecord<T>>;
8
4
 
@@ -19,9 +15,6 @@ export function toSafeJsonValue<T>(value: T): SafeValue<T> {
19
15
  // @ts-expect-error
20
16
  return value == asNumber ? asNumber : value.toString();
21
17
  }
22
- if (value instanceof Date) {
23
- return value.toISOString() as SafeValue<T>;
24
- }
25
18
  return value as SafeValue<T>;
26
19
  }
27
20