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 +48 -23
- package/package.json +4 -4
- package/src/runPrisma/runPrisma.ts +26 -6
- package/src/toSafeJson/toSafeJson.spec.ts +0 -12
- package/src/toSafeJson/toSafeJson.ts +1 -8
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.
|
|
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
|
-
[](https://npmjs.com/package/sql-love)
|
|
6
|
+
[](https://www.npmjs.com/package/sql-love?activeTab=dependencies)
|
|
7
|
+
[](https://github.com/kensnyder/sql-love/actions)
|
|
8
|
+
[](https://codecov.io/gh/kensnyder/sql-love)
|
|
9
|
+
[](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
|
-
|
|
70
|
-
//
|
|
72
|
+
```js
|
|
73
|
+
// mysql2:
|
|
71
74
|
connection.query(sql, bindings, (err, results, fields) => {});
|
|
72
|
-
|
|
75
|
+
|
|
76
|
+
// Prisma:
|
|
73
77
|
const result = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
74
78
|
|
|
75
|
-
//
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
569
|
-
objects
|
|
570
|
-
|
|
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 = [{
|
|
599
|
+
const records = [{ count: 12n }, { count: 9007199254740993n }];
|
|
576
600
|
|
|
577
601
|
toSafeJsonRecords(records);
|
|
578
602
|
// [
|
|
579
|
-
// {
|
|
603
|
+
// { count: 12 },
|
|
604
|
+
// { count: "9007199254740993" },
|
|
580
605
|
// ]
|
|
581
606
|
|
|
582
607
|
toSafeJsonString(records);
|
|
583
|
-
// '[{"
|
|
608
|
+
// '[{"count":12},{"count":"9007199254740993"}]'
|
|
584
609
|
|
|
585
|
-
// Note that BigInt values too big for Number
|
|
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.
|
|
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.
|
|
36
|
-
"prettier": "3.
|
|
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.
|
|
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<
|
|
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(
|
|
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
|
|