sql-love 0.9.4 → 0.9.5

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.
@@ -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
+ }