sql-love 0.9.4 → 0.9.6
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/CHANGELOG.md +7 -0
- package/README.md +253 -27
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/src/SelectBuilder/SelectBuilder.d.ts +34 -11
- package/dist/src/SelectBuilder/SelectBuilder.js +62 -29
- package/dist/src/extractGrouped/extractGrouped.d.ts +17 -0
- package/dist/src/extractGrouped/extractGrouped.js +31 -0
- package/dist/src/extractIndexed/extractIndexed.d.ts +12 -0
- package/dist/src/extractIndexed/extractIndexed.js +23 -0
- package/dist/src/getPagination/getPagination.d.ts +16 -0
- package/dist/src/getPagination/getPagination.js +25 -0
- package/dist/src/runMysql/runMysql.d.ts +46 -0
- package/dist/src/runMysql/runMysql.js +49 -0
- package/dist/src/runPrisma/runPrisma.d.ts +26 -0
- package/dist/src/runPrisma/runPrisma.js +18 -0
- package/index.ts +4 -0
- package/package.json +1 -1
- package/src/SelectBuilder/SelectBuilder.spec.ts +50 -6
- package/src/SelectBuilder/SelectBuilder.ts +96 -33
- package/src/extractGrouped/extractGrouped.spec.ts +43 -0
- package/src/extractGrouped/extractGrouped.ts +36 -0
- package/src/extractIndexed/extractIndexed.spec.ts +40 -0
- package/src/extractIndexed/extractIndexed.ts +28 -0
- package/src/getPagination/getPagination.spec.ts +61 -0
- package/src/getPagination/getPagination.ts +30 -0
- package/src/runMysql/runMysql.spec.ts +164 -0
- package/src/runMysql/runMysql.ts +94 -0
- package/src/runPrisma/runPrisma.spec.ts +67 -0
- package/src/runPrisma/runPrisma.ts +39 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Group database rows by a column value
|
|
3
|
+
* @param field The database column to group by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractGrouped('org', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "Marketing": [
|
|
9
|
+
* { id: 1, name: "John", org: "Marketing" },
|
|
10
|
+
* { id: 2, name: "Jane", org: "Marketing" },
|
|
11
|
+
* ],
|
|
12
|
+
* "Finance": [
|
|
13
|
+
* { id: 3, name: "Jose", org: "Finance" },
|
|
14
|
+
* ],
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export default function extractGrouped<DbRecord extends Record<string, any>>(
|
|
18
|
+
field: keyof DbRecord,
|
|
19
|
+
records: DbRecord[]
|
|
20
|
+
): Record<string, DbRecord[]> {
|
|
21
|
+
let idx = 0;
|
|
22
|
+
const grouped: Record<string, DbRecord[]> = {};
|
|
23
|
+
for (const record of records) {
|
|
24
|
+
if (!(field in record)) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Column "${String(field)}" is not present in record ${idx}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
if (!grouped[record[field]]) {
|
|
30
|
+
grouped[record[field]] = [];
|
|
31
|
+
}
|
|
32
|
+
grouped[record[field]].push(record);
|
|
33
|
+
idx++;
|
|
34
|
+
}
|
|
35
|
+
return grouped;
|
|
36
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import extractIndexed from './extractIndexed';
|
|
2
|
+
|
|
3
|
+
describe('extractIndexed()', () => {
|
|
4
|
+
it('should handle indexing by string', () => {
|
|
5
|
+
const records = [
|
|
6
|
+
{ uuid: '2d49f286-790d-4aee-b38b-94d0872e9bd8', name: 'John' },
|
|
7
|
+
{ uuid: '7d02664a-d03e-4882-88e2-23c701c314b5', name: 'Jane' },
|
|
8
|
+
];
|
|
9
|
+
const indexed = extractIndexed('uuid', records);
|
|
10
|
+
expect(indexed).toEqual({
|
|
11
|
+
'2d49f286-790d-4aee-b38b-94d0872e9bd8': {
|
|
12
|
+
uuid: '2d49f286-790d-4aee-b38b-94d0872e9bd8',
|
|
13
|
+
name: 'John',
|
|
14
|
+
},
|
|
15
|
+
'7d02664a-d03e-4882-88e2-23c701c314b5': {
|
|
16
|
+
uuid: '7d02664a-d03e-4882-88e2-23c701c314b5',
|
|
17
|
+
name: 'Jane',
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
it('should handle indexing by integer', () => {
|
|
22
|
+
const records = [
|
|
23
|
+
{ id: 1, name: 'John' },
|
|
24
|
+
{ id: 2, name: 'Jane' },
|
|
25
|
+
];
|
|
26
|
+
const indexed = extractIndexed('id', records);
|
|
27
|
+
expect(indexed).toEqual({
|
|
28
|
+
1: { id: 1, name: 'John' },
|
|
29
|
+
2: { id: 2, name: 'Jane' },
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('should throw error if column is not present in every record', () => {
|
|
33
|
+
const records = [
|
|
34
|
+
{ id: 1, name: 'John', employeeId: 'js102' },
|
|
35
|
+
{ id: 2, name: 'Jane' },
|
|
36
|
+
];
|
|
37
|
+
const thrower = () => extractIndexed('employeeId', records);
|
|
38
|
+
expect(thrower).toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index each database row by a column value
|
|
3
|
+
* @param field The database column to index by
|
|
4
|
+
* @param records Records from the database
|
|
5
|
+
* @example
|
|
6
|
+
* extractIndexed('id', users); // result:
|
|
7
|
+
* {
|
|
8
|
+
* "1": { id: 1, name: "John" },
|
|
9
|
+
* "2": { id: 2, name: "Jane" },
|
|
10
|
+
* }
|
|
11
|
+
*/
|
|
12
|
+
export default function extractIndexed<DbRecord extends Record<string, any>>(
|
|
13
|
+
field: keyof DbRecord,
|
|
14
|
+
records: DbRecord[]
|
|
15
|
+
) {
|
|
16
|
+
let idx = 0;
|
|
17
|
+
const indexed: Record<string, DbRecord> = {};
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
if (!(field in record)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Column "${String(field)}" is not present in record ${idx}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
indexed[record[field]] = record;
|
|
25
|
+
idx++;
|
|
26
|
+
}
|
|
27
|
+
return indexed;
|
|
28
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
import getPagination from './getPagination';
|
|
3
|
+
|
|
4
|
+
describe('getPagination()', () => {
|
|
5
|
+
it('should handle 42 results with no offset', () => {
|
|
6
|
+
const query = new SelectBuilder('SELECT * FROM users LIMIT 10');
|
|
7
|
+
const pagination = getPagination(query, 42);
|
|
8
|
+
expect(pagination).toEqual({
|
|
9
|
+
page: 1,
|
|
10
|
+
prevPage: null,
|
|
11
|
+
nextPage: 2,
|
|
12
|
+
perPage: 10,
|
|
13
|
+
numPages: 5,
|
|
14
|
+
total: 42,
|
|
15
|
+
isFirst: true,
|
|
16
|
+
isLast: false,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('should handle 42 results with offset', () => {
|
|
20
|
+
const query = new SelectBuilder('SELECT * FROM users LIMIT 10').page(3);
|
|
21
|
+
const pagination = getPagination(query, 42n);
|
|
22
|
+
expect(pagination).toEqual({
|
|
23
|
+
page: 3,
|
|
24
|
+
prevPage: 2,
|
|
25
|
+
nextPage: 4,
|
|
26
|
+
perPage: 10,
|
|
27
|
+
numPages: 5,
|
|
28
|
+
total: 42,
|
|
29
|
+
isFirst: false,
|
|
30
|
+
isLast: false,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it('should handle 0 results', () => {
|
|
34
|
+
const query = new SelectBuilder('SELECT * FROM users LIMIT 10');
|
|
35
|
+
const pagination = getPagination(query, 0);
|
|
36
|
+
expect(pagination).toEqual({
|
|
37
|
+
page: 1,
|
|
38
|
+
prevPage: null,
|
|
39
|
+
nextPage: null,
|
|
40
|
+
perPage: 10,
|
|
41
|
+
numPages: 0,
|
|
42
|
+
total: 0,
|
|
43
|
+
isFirst: true,
|
|
44
|
+
isLast: false,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('should handle when no limit is present', () => {
|
|
48
|
+
const query = new SelectBuilder('SELECT * FROM users');
|
|
49
|
+
const pagination = getPagination(query, 42);
|
|
50
|
+
expect(pagination).toEqual({
|
|
51
|
+
page: 1,
|
|
52
|
+
prevPage: null,
|
|
53
|
+
nextPage: null,
|
|
54
|
+
perPage: NaN,
|
|
55
|
+
numPages: 0,
|
|
56
|
+
total: 42,
|
|
57
|
+
isFirst: true,
|
|
58
|
+
isLast: false,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Given a foundRows number of records and a limit, return the foundRows number of pages
|
|
5
|
+
* @param query The query that was just run
|
|
6
|
+
* @param total The number of records if there were no limit
|
|
7
|
+
*/
|
|
8
|
+
export default function getPagination(
|
|
9
|
+
query: SelectBuilder,
|
|
10
|
+
total: BigInt | number
|
|
11
|
+
) {
|
|
12
|
+
total = Number(total) || 0;
|
|
13
|
+
const page = Number(query._page) || 1;
|
|
14
|
+
const perPage = Number(query._limit);
|
|
15
|
+
const numPages = Math.ceil(total / perPage) || 0;
|
|
16
|
+
const isFirst = page === 1;
|
|
17
|
+
const isLast = page === numPages;
|
|
18
|
+
const prevPage = page - 1 || null;
|
|
19
|
+
const nextPage = page + 1 <= numPages ? page + 1 : null;
|
|
20
|
+
return {
|
|
21
|
+
page,
|
|
22
|
+
prevPage,
|
|
23
|
+
nextPage,
|
|
24
|
+
perPage,
|
|
25
|
+
numPages,
|
|
26
|
+
total,
|
|
27
|
+
isFirst,
|
|
28
|
+
isLast,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
import {
|
|
4
|
+
runMysql,
|
|
5
|
+
runMysqlAsync,
|
|
6
|
+
runMysqlAsyncWithCount,
|
|
7
|
+
runMysqlWithCount,
|
|
8
|
+
} from './runMysql';
|
|
9
|
+
|
|
10
|
+
describe('runMysql', () => {
|
|
11
|
+
it('should invoke client.query with correct args', async () => {
|
|
12
|
+
const mockResults = [
|
|
13
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
14
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
15
|
+
];
|
|
16
|
+
const mockFields = [{ name: 'id' }, { name: 'name' }, { name: 'dept' }];
|
|
17
|
+
const client = {
|
|
18
|
+
query: vi.fn(
|
|
19
|
+
(
|
|
20
|
+
sql: string,
|
|
21
|
+
bindings: any[],
|
|
22
|
+
callback: (
|
|
23
|
+
err: Error | null,
|
|
24
|
+
rows: Array<Record<string, any>>,
|
|
25
|
+
fields: any[]
|
|
26
|
+
) => void
|
|
27
|
+
) => {
|
|
28
|
+
callback(null, mockResults, mockFields);
|
|
29
|
+
}
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
const query = new SelectBuilder('SELECT * FROM users').where(
|
|
33
|
+
'dept',
|
|
34
|
+
'Marketing'
|
|
35
|
+
);
|
|
36
|
+
const [results, fields] = await runMysql(client, query);
|
|
37
|
+
expect(client.query.mock.calls[0][0]).toBe(
|
|
38
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?'
|
|
39
|
+
);
|
|
40
|
+
expect(client.query.mock.calls[0][1]).toEqual(['Marketing']);
|
|
41
|
+
expect(results).toBe(mockResults);
|
|
42
|
+
expect(fields).toBe(mockFields);
|
|
43
|
+
});
|
|
44
|
+
it('should also run with count', async () => {
|
|
45
|
+
const mockResults1 = [
|
|
46
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
47
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
48
|
+
];
|
|
49
|
+
const mockFields1 = [{ name: 'id' }, { name: 'name' }, { name: 'dept' }];
|
|
50
|
+
const mockResults2 = [{ found_rows: 9 }];
|
|
51
|
+
const mockFields2 = [{ name: 'found_rows' }];
|
|
52
|
+
let callCount = 0;
|
|
53
|
+
const client = {
|
|
54
|
+
query: vi.fn(
|
|
55
|
+
(
|
|
56
|
+
sql: string,
|
|
57
|
+
bindings: any[],
|
|
58
|
+
callback: (
|
|
59
|
+
err: Error | null,
|
|
60
|
+
rows: Array<Record<string, any>>,
|
|
61
|
+
fields: any[]
|
|
62
|
+
) => void
|
|
63
|
+
) => {
|
|
64
|
+
if (++callCount === 1) {
|
|
65
|
+
callback(null, mockResults1, mockFields1);
|
|
66
|
+
}
|
|
67
|
+
callback(null, mockResults2, mockFields2);
|
|
68
|
+
}
|
|
69
|
+
),
|
|
70
|
+
};
|
|
71
|
+
const query = new SelectBuilder('SELECT * FROM users')
|
|
72
|
+
.where('dept', 'Marketing')
|
|
73
|
+
.limit(2)
|
|
74
|
+
.page(5);
|
|
75
|
+
const { records, total, pagination } = await runMysqlWithCount(
|
|
76
|
+
client,
|
|
77
|
+
query
|
|
78
|
+
);
|
|
79
|
+
expect(client.query.mock.calls[0][0]).toBe(
|
|
80
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?\nOFFSET 8\nLIMIT 2'
|
|
81
|
+
);
|
|
82
|
+
expect(client.query.mock.calls[0][1]).toEqual(['Marketing']);
|
|
83
|
+
expect(records).toBe(mockResults1);
|
|
84
|
+
expect(total).toBe(9);
|
|
85
|
+
expect(pagination).toEqual({
|
|
86
|
+
page: 5,
|
|
87
|
+
prevPage: 4,
|
|
88
|
+
nextPage: null,
|
|
89
|
+
perPage: 2,
|
|
90
|
+
numPages: 5,
|
|
91
|
+
total: 9,
|
|
92
|
+
isFirst: false,
|
|
93
|
+
isLast: true,
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
it('should invoke client.query with correct args (async)', async () => {
|
|
97
|
+
const mockResults = [
|
|
98
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
99
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
100
|
+
];
|
|
101
|
+
const mockFields = [{ name: 'id' }, { name: 'name' }, { name: 'dept' }];
|
|
102
|
+
const client = {
|
|
103
|
+
query: vi.fn((sql: string, bindings: any[]) => {
|
|
104
|
+
return Promise.resolve([mockResults, mockFields]);
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
const query = new SelectBuilder('SELECT * FROM users').where(
|
|
108
|
+
'dept',
|
|
109
|
+
'Marketing'
|
|
110
|
+
);
|
|
111
|
+
// @ts-expect-error
|
|
112
|
+
const [results, fields] = await runMysqlAsync(client, query);
|
|
113
|
+
expect(client.query).toHaveBeenCalledWith(
|
|
114
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?',
|
|
115
|
+
['Marketing']
|
|
116
|
+
);
|
|
117
|
+
expect(results).toBe(mockResults);
|
|
118
|
+
expect(fields).toBe(mockFields);
|
|
119
|
+
});
|
|
120
|
+
it('should also run with count (async)', async () => {
|
|
121
|
+
const mockResults1 = [
|
|
122
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
123
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
124
|
+
];
|
|
125
|
+
const mockFields1 = [{ name: 'id' }, { name: 'name' }, { name: 'dept' }];
|
|
126
|
+
const mockResults2 = [{ found_rows: 9 }];
|
|
127
|
+
const mockFields2 = [{ name: 'found_rows' }];
|
|
128
|
+
let callCount = 0;
|
|
129
|
+
const client = {
|
|
130
|
+
query: vi.fn((sql: string, bindings: any[]) => {
|
|
131
|
+
if (++callCount === 1) {
|
|
132
|
+
return Promise.resolve([mockResults1, mockFields1]);
|
|
133
|
+
}
|
|
134
|
+
return Promise.resolve([mockResults2, mockFields2]);
|
|
135
|
+
}),
|
|
136
|
+
};
|
|
137
|
+
const query = new SelectBuilder('SELECT * FROM users')
|
|
138
|
+
.where('dept', 'Marketing')
|
|
139
|
+
.limit(2)
|
|
140
|
+
.page(5);
|
|
141
|
+
const { records, total, pagination, fields } = await runMysqlAsyncWithCount(
|
|
142
|
+
// @ts-expect-error
|
|
143
|
+
client,
|
|
144
|
+
query
|
|
145
|
+
);
|
|
146
|
+
expect(client.query).toHaveBeenCalledWith(
|
|
147
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?\nOFFSET 8\nLIMIT 2',
|
|
148
|
+
['Marketing']
|
|
149
|
+
);
|
|
150
|
+
expect(records).toBe(mockResults1);
|
|
151
|
+
expect(total).toBe(9);
|
|
152
|
+
expect(pagination).toEqual({
|
|
153
|
+
page: 5,
|
|
154
|
+
prevPage: 4,
|
|
155
|
+
nextPage: null,
|
|
156
|
+
perPage: 2,
|
|
157
|
+
numPages: 5,
|
|
158
|
+
total: 9,
|
|
159
|
+
isFirst: false,
|
|
160
|
+
isLast: true,
|
|
161
|
+
});
|
|
162
|
+
expect(fields).toEqual(mockFields1);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
import type { EngineStyle } from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
import getPagination from '../getPagination/getPagination';
|
|
4
|
+
|
|
5
|
+
interface MysqlClient {
|
|
6
|
+
query: (
|
|
7
|
+
sql: string,
|
|
8
|
+
bindings: any[],
|
|
9
|
+
callback: (
|
|
10
|
+
err: Error | null,
|
|
11
|
+
results: Array<Record<string, any>>,
|
|
12
|
+
fields: any[]
|
|
13
|
+
) => void
|
|
14
|
+
) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface MysqlAsyncClient {
|
|
18
|
+
query: (
|
|
19
|
+
sql: string,
|
|
20
|
+
bindings: any[]
|
|
21
|
+
) => Promise<[Array<Record<string, any>>, any[]]>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _queryToPromise(
|
|
25
|
+
mysqlClient: MysqlClient,
|
|
26
|
+
sql: string,
|
|
27
|
+
bindings: any[]
|
|
28
|
+
): Promise<[Array<Record<string, any>>, any[]]> {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
mysqlClient.query(sql, bindings, (err, rows, fields) => {
|
|
31
|
+
if (err) {
|
|
32
|
+
reject(err);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
resolve([rows, fields]);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function runMysql(
|
|
41
|
+
mysqlClient: MysqlClient,
|
|
42
|
+
query: SelectBuilder
|
|
43
|
+
): Promise<[Array<Record<string, any>>, any[]]> {
|
|
44
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
45
|
+
return _queryToPromise(mysqlClient, sql, bindings);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function runMysqlWithCount(
|
|
49
|
+
mysqlClient: MysqlClient,
|
|
50
|
+
query: SelectBuilder,
|
|
51
|
+
{ countExpr = '*' }: { countExpr?: string } = {}
|
|
52
|
+
) {
|
|
53
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
54
|
+
const [records, fields] = await _queryToPromise(mysqlClient, sql, bindings);
|
|
55
|
+
if (records.length > 0) {
|
|
56
|
+
const { sql, bindings } = query.compileCount({
|
|
57
|
+
engine: 'mysql',
|
|
58
|
+
countExpr,
|
|
59
|
+
});
|
|
60
|
+
const [rows] = await _queryToPromise(mysqlClient, sql, bindings);
|
|
61
|
+
const total = rows[0].found_rows;
|
|
62
|
+
return { records, total, pagination: getPagination(query, total), fields };
|
|
63
|
+
} else {
|
|
64
|
+
return { records, total: 0, pagination: getPagination(query, 0), fields };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function runMysqlAsync(
|
|
69
|
+
mysqlClient: MysqlAsyncClient,
|
|
70
|
+
query: SelectBuilder
|
|
71
|
+
): Promise<[Array<Record<string, any>>, any[]]> {
|
|
72
|
+
const { sql, bindings } = query.compile({ engine: 'mysql' });
|
|
73
|
+
return mysqlClient.query(sql, bindings);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function runMysqlAsyncWithCount(
|
|
77
|
+
mysqlClient: MysqlAsyncClient,
|
|
78
|
+
query: SelectBuilder,
|
|
79
|
+
{
|
|
80
|
+
engine = 'mysql',
|
|
81
|
+
countExpr = '*',
|
|
82
|
+
}: { engine?: EngineStyle; countExpr?: string } = {}
|
|
83
|
+
) {
|
|
84
|
+
const { sql, bindings } = query.compile({ engine });
|
|
85
|
+
const [records, fields] = await mysqlClient.query(sql, bindings);
|
|
86
|
+
if (records.length > 0) {
|
|
87
|
+
const { sql, bindings } = query.compileCount({ engine, countExpr });
|
|
88
|
+
const [rows] = await mysqlClient.query(sql, bindings);
|
|
89
|
+
const total = rows[0].found_rows;
|
|
90
|
+
return { records, total, pagination: getPagination(query, total), fields };
|
|
91
|
+
} else {
|
|
92
|
+
return { records, total: 0, pagination: getPagination(query, 0), fields };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
import SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
import { runPrisma, runPrismaWithCount } from './runPrisma';
|
|
4
|
+
|
|
5
|
+
describe('runPrisma', () => {
|
|
6
|
+
it('should invoke prisma.$queryRawUnsafe with correct args', async () => {
|
|
7
|
+
const mockResults = [
|
|
8
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
9
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
10
|
+
];
|
|
11
|
+
const prisma = {
|
|
12
|
+
$queryRawUnsafe: vi.fn(() => mockResults),
|
|
13
|
+
};
|
|
14
|
+
const query = new SelectBuilder('SELECT * FROM users');
|
|
15
|
+
query.where('dept', 'Marketing');
|
|
16
|
+
// @ts-expect-error
|
|
17
|
+
const results = await runPrisma(prisma, query);
|
|
18
|
+
expect(prisma.$queryRawUnsafe).toHaveBeenCalledWith(
|
|
19
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?',
|
|
20
|
+
'Marketing'
|
|
21
|
+
);
|
|
22
|
+
expect(results).toBe(mockResults);
|
|
23
|
+
});
|
|
24
|
+
it('should also run with count', async () => {
|
|
25
|
+
const mockResults1 = [
|
|
26
|
+
{ id: 1, name: 'John', dept: 'Marketing' },
|
|
27
|
+
{ id: 2, name: 'Jane', dept: 'Marketing' },
|
|
28
|
+
];
|
|
29
|
+
const mockResults2 = [{ found_rows: 9 }];
|
|
30
|
+
let callCount = 0;
|
|
31
|
+
const prisma = {
|
|
32
|
+
$queryRawUnsafe: vi.fn(() => {
|
|
33
|
+
if (++callCount === 1) {
|
|
34
|
+
return mockResults1;
|
|
35
|
+
}
|
|
36
|
+
return mockResults2;
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
const query = new SelectBuilder('SELECT * FROM users')
|
|
40
|
+
.where('dept', 'Marketing')
|
|
41
|
+
.limit(2)
|
|
42
|
+
.page(5);
|
|
43
|
+
const { records, total, pagination } = await runPrismaWithCount(
|
|
44
|
+
// @ts-expect-error
|
|
45
|
+
prisma,
|
|
46
|
+
query
|
|
47
|
+
);
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
expect(prisma.$queryRawUnsafe.mock.calls[0][0]).toBe(
|
|
50
|
+
'SELECT\n *\nFROM users\nWHERE dept = ?\nOFFSET 8\nLIMIT 2'
|
|
51
|
+
);
|
|
52
|
+
// @ts-expect-error
|
|
53
|
+
expect(prisma.$queryRawUnsafe.mock.calls[0][1]).toBe('Marketing');
|
|
54
|
+
expect(records).toBe(mockResults1);
|
|
55
|
+
expect(total).toBe(9);
|
|
56
|
+
expect(pagination).toEqual({
|
|
57
|
+
page: 5,
|
|
58
|
+
prevPage: 4,
|
|
59
|
+
nextPage: null,
|
|
60
|
+
perPage: 2,
|
|
61
|
+
numPages: 5,
|
|
62
|
+
total: 9,
|
|
63
|
+
isFirst: false,
|
|
64
|
+
isLast: true,
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type SelectBuilder from '../SelectBuilder/SelectBuilder';
|
|
2
|
+
import type { EngineStyle } from '../SelectBuilder/SelectBuilder';
|
|
3
|
+
import getPagination from '../getPagination/getPagination';
|
|
4
|
+
|
|
5
|
+
interface PrismaClient {
|
|
6
|
+
$queryRawUnsafe: (
|
|
7
|
+
sql: string,
|
|
8
|
+
...bindings: any[]
|
|
9
|
+
) => Promise<Array<Record<string, any>>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function runPrisma(
|
|
13
|
+
prisma: PrismaClient,
|
|
14
|
+
query: SelectBuilder,
|
|
15
|
+
{ engine = undefined }: { engine?: EngineStyle } = {}
|
|
16
|
+
): Promise<Array<Record<string, any>>> {
|
|
17
|
+
const { sql, bindings } = query.compile({ engine });
|
|
18
|
+
return prisma.$queryRawUnsafe(sql, ...bindings);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runPrismaWithCount(
|
|
22
|
+
prisma: PrismaClient,
|
|
23
|
+
query: SelectBuilder,
|
|
24
|
+
{
|
|
25
|
+
engine = undefined,
|
|
26
|
+
countExpr = '*',
|
|
27
|
+
}: { engine?: EngineStyle; countExpr?: string } = {}
|
|
28
|
+
) {
|
|
29
|
+
const { sql, bindings } = query.compile({ engine });
|
|
30
|
+
const records = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
31
|
+
if (records.length > 0) {
|
|
32
|
+
const { sql, bindings } = query.compileCount({ engine, countExpr });
|
|
33
|
+
const result = await prisma.$queryRawUnsafe(sql, ...bindings);
|
|
34
|
+
const total = result[0].found_rows;
|
|
35
|
+
return { records, total, pagination: getPagination(query, total) };
|
|
36
|
+
} else {
|
|
37
|
+
return { records, total: 0, pagination: getPagination(query, 0) };
|
|
38
|
+
}
|
|
39
|
+
}
|