@starbemtech/star-db-query-builder 1.0.38 → 1.2.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/.github/workflows/publish.yml +118 -0
- package/ARCHITECTURE.md +313 -0
- package/CHANGELOG.md +65 -0
- package/README.md +1309 -129
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/lcov-report/block-navigation.js +1 -1
- package/coverage/lcov-report/index.html +41 -11
- package/coverage/lcov-report/mysqlClient.ts.html +685 -0
- package/coverage/lcov-report/pgClient.ts.html +823 -0
- package/coverage/lcov-report/sorter.js +21 -7
- package/coverage/lcov.info +533 -0
- package/coverage/mysqlClient.ts.html +685 -0
- package/coverage/pgClient.ts.html +823 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/src/core/repository.d.ts +374 -0
- package/dist/src/core/repository.js +677 -0
- package/dist/src/core/repository.js.map +1 -0
- package/dist/src/{default → core}/types.d.ts +9 -1
- package/dist/src/{default → core}/types.js.map +1 -1
- package/dist/src/core/utils.d.ts +133 -0
- package/dist/src/{default → core}/utils.js +167 -0
- package/dist/src/core/utils.js.map +1 -0
- package/dist/src/db/IDatabaseClient.d.ts +10 -1
- package/dist/src/db/initDb.d.ts +120 -1
- package/dist/src/db/initDb.js +119 -0
- package/dist/src/db/initDb.js.map +1 -1
- package/dist/src/db/mysqlClient.d.ts +23 -1
- package/dist/src/db/mysqlClient.js +115 -0
- package/dist/src/db/mysqlClient.js.map +1 -1
- package/dist/src/db/pgClient.d.ts +22 -1
- package/dist/src/db/pgClient.js +127 -0
- package/dist/src/db/pgClient.js.map +1 -1
- package/dist/src/monitor/monitor.d.ts +6 -1
- package/dist/src/monitor/monitor.js +5 -0
- package/dist/src/monitor/monitor.js.map +1 -1
- package/dist/src/setupTests.d.ts +26 -0
- package/dist/src/setupTests.js +43 -0
- package/dist/src/setupTests.js.map +1 -0
- package/docs/INDEX.md +145 -0
- package/docs/methods/findFirst.md +394 -0
- package/docs/methods/findMany.md +587 -0
- package/docs/methods/insert.md +536 -0
- package/docs/methods/insertMany.md +627 -0
- package/docs/methods/joins.md +781 -0
- package/docs/methods/rawQuery.md +284 -0
- package/docs/methods/transactions.md +737 -0
- package/eslint.config.mjs +77 -0
- package/index.ts +5 -4
- package/jest.config.ts +23 -28
- package/package.json +52 -29
- package/scripts/release.sh +123 -0
- package/src/core/repository.ts +865 -0
- package/src/{default → core}/types.ts +11 -2
- package/src/{default → core}/utils.ts +168 -0
- package/src/db/IDatabaseClient.ts +11 -1
- package/src/db/__tests__/mysqlClient.test.ts +262 -0
- package/src/db/__tests__/pgClient.test.ts +260 -0
- package/src/db/initDb.ts +120 -1
- package/src/db/mysqlClient.ts +119 -3
- package/src/db/pgClient.ts +131 -3
- package/src/monitor/monitor.ts +5 -0
- package/src/setupTests.ts +45 -0
- package/tsconfig.test.json +21 -0
- package/.eslintignore +0 -4
- package/.eslintrc.json +0 -32
- package/coverage/clover.xml +0 -6
- package/coverage/coverage-final.json +0 -1
- package/dist/.eslintrc.json +0 -32
- package/dist/src/default/genericRepository.d.ts +0 -20
- package/dist/src/default/genericRepository.js +0 -192
- package/dist/src/default/genericRepository.js.map +0 -1
- package/dist/src/default/utils.d.ts +0 -9
- package/dist/src/default/utils.js.map +0 -1
- package/index.d.ts +0 -60
- package/src/default/genericRepository.ts +0 -320
- /package/dist/src/{default → core}/types.js +0 -0
|
@@ -13,7 +13,7 @@ export interface QueryExec {
|
|
|
13
13
|
values?: any[]
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
type SimpleValue = string | number | boolean | Date
|
|
16
|
+
type SimpleValue = string | number | boolean | Date | null
|
|
17
17
|
|
|
18
18
|
export interface OperatorCondition {
|
|
19
19
|
operator:
|
|
@@ -55,7 +55,6 @@ export type DBClients = 'pg' | 'mysql'
|
|
|
55
55
|
|
|
56
56
|
export type OrderBy = { field: string; direction: 'ASC' | 'DESC' }[]
|
|
57
57
|
|
|
58
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
59
58
|
export interface QueryParams<T> {
|
|
60
59
|
tableName: string
|
|
61
60
|
dbClient: IDatabaseClient
|
|
@@ -86,3 +85,13 @@ export interface QueryBuilder {
|
|
|
86
85
|
limit?: string
|
|
87
86
|
offset?: string
|
|
88
87
|
}
|
|
88
|
+
|
|
89
|
+
export interface RawQueryParams {
|
|
90
|
+
dbClient: IDatabaseClient
|
|
91
|
+
sql: string
|
|
92
|
+
params?: any[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface TransactionParams {
|
|
96
|
+
dbClient: IDatabaseClient
|
|
97
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { Conditions, Condition, OrderBy, DBClients } from './types'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Converts an array of strings to a comma-separated string with quotes
|
|
5
|
+
*
|
|
6
|
+
* This function takes an array of strings and converts it to a comma-separated string
|
|
7
|
+
* with quotes. It handles the differences between PostgreSQL and MySQL syntax for
|
|
8
|
+
* string arrays.
|
|
9
|
+
*
|
|
10
|
+
* @param items - The array of strings to convert
|
|
11
|
+
* @param clientType - The type of database client
|
|
12
|
+
* @returns A comma-separated string with quotes
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const items = ['item1', 'item2', 'item3']
|
|
16
|
+
* const clientType = 'pg'
|
|
17
|
+
* const result = arrayToStringWithQuotes(items, clientType)
|
|
18
|
+
* // result will be: 'item1', 'item2', 'item3'
|
|
19
|
+
*/
|
|
3
20
|
const arrayToStringWithQuotes = (
|
|
4
21
|
items: string[],
|
|
5
22
|
clientType: DBClients
|
|
@@ -10,9 +27,52 @@ const arrayToStringWithQuotes = (
|
|
|
10
27
|
return itemsWithQuotes.join(', ')
|
|
11
28
|
}
|
|
12
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Generates a PostgreSQL placeholder for a parameter
|
|
32
|
+
*
|
|
33
|
+
* This function generates a PostgreSQL placeholder for a parameter. It returns
|
|
34
|
+
* a string with a dollar sign and the index of the parameter.
|
|
35
|
+
*
|
|
36
|
+
* @param index - The index of the parameter
|
|
37
|
+
* @returns A PostgreSQL placeholder
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* const index = 1
|
|
41
|
+
* const placeholder = pgPlaceholderGenerator(index)
|
|
42
|
+
* // placeholder will be: $1
|
|
43
|
+
*/
|
|
13
44
|
const pgPlaceholderGenerator = (index: number) => `$${index}`
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Generates a MySQL placeholder for a parameter
|
|
48
|
+
*
|
|
49
|
+
* This function generates a MySQL placeholder for a parameter. It returns
|
|
50
|
+
* a string with a question mark.
|
|
51
|
+
*
|
|
52
|
+
* @returns A MySQL placeholder
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const placeholder = mysqlPlaceholderGenerator()
|
|
56
|
+
* // placeholder will be: ?
|
|
57
|
+
*/
|
|
14
58
|
const mysqlPlaceholderGenerator = () => `?`
|
|
15
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Creates a SELECT clause for a query
|
|
62
|
+
*
|
|
63
|
+
* This function creates a SELECT clause for a query. It takes an array of fields
|
|
64
|
+
* and a database client type and returns a string with the fields separated by commas.
|
|
65
|
+
*
|
|
66
|
+
* @param fields - The array of fields to select
|
|
67
|
+
* @param clientType - The type of database client
|
|
68
|
+
* @returns A string with the fields separated by commas
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const fields = ['id', 'name', 'email']
|
|
72
|
+
* const clientType = 'pg'
|
|
73
|
+
* const result = createSelectFields(fields, clientType)
|
|
74
|
+
* // result will be: "id, name, email"
|
|
75
|
+
*/
|
|
16
76
|
export const createSelectFields = (
|
|
17
77
|
fields: string[] = [],
|
|
18
78
|
clientType: DBClients
|
|
@@ -22,6 +82,22 @@ export const createSelectFields = (
|
|
|
22
82
|
: '*'
|
|
23
83
|
}
|
|
24
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Generates placeholders for a query
|
|
87
|
+
*
|
|
88
|
+
* This function generates placeholders for a query. It takes an array of keys
|
|
89
|
+
* and a database client type and returns a string with the placeholders separated by commas.
|
|
90
|
+
*
|
|
91
|
+
* @param keys - The array of keys to generate placeholders for
|
|
92
|
+
* @param clientType - The type of database client
|
|
93
|
+
* @returns A string with the placeholders separated by commas
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const keys = ['id', 'name', 'email']
|
|
97
|
+
* const clientType = 'pg'
|
|
98
|
+
* const result = generatePlaceholders(keys, clientType)
|
|
99
|
+
* // result will be: $1, $2, $3
|
|
100
|
+
*/
|
|
25
101
|
export const generatePlaceholders = (
|
|
26
102
|
keys: any[],
|
|
27
103
|
clientType: DBClients
|
|
@@ -31,6 +107,22 @@ export const generatePlaceholders = (
|
|
|
31
107
|
.join(', ')
|
|
32
108
|
}
|
|
33
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Generates a SET clause for a query
|
|
112
|
+
*
|
|
113
|
+
* This function generates a SET clause for a query. It takes an array of keys
|
|
114
|
+
* and a database client type and returns a string with the keys and placeholders separated by commas.
|
|
115
|
+
*
|
|
116
|
+
* @param keys - The array of keys to generate SET clause for
|
|
117
|
+
* @param clientType - The type of database client
|
|
118
|
+
* @returns A string with the keys and placeholders separated by commas
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const keys = ['id', 'name', 'email']
|
|
122
|
+
* const clientType = 'pg'
|
|
123
|
+
* const result = generateSetClause(keys, clientType)
|
|
124
|
+
* // result will be: "id = $1, name = $2, email = $3"
|
|
125
|
+
*/
|
|
34
126
|
export const generateSetClause = (
|
|
35
127
|
keys: any[],
|
|
36
128
|
clientType: DBClients
|
|
@@ -42,6 +134,26 @@ export const generateSetClause = (
|
|
|
42
134
|
.join(', ')
|
|
43
135
|
}
|
|
44
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Creates a WHERE clause for a query
|
|
139
|
+
*
|
|
140
|
+
* This function creates a WHERE clause for a query. It takes an array of conditions
|
|
141
|
+
* and a database client type and returns a string with the conditions separated by AND.
|
|
142
|
+
*
|
|
143
|
+
* @param conditions - The array of conditions to create WHERE clause for
|
|
144
|
+
* @param startIndex - The index of the first parameter
|
|
145
|
+
* @param clientType - The type of database client
|
|
146
|
+
* @param unaccent - Whether to use unaccent function
|
|
147
|
+
* @returns A string with the conditions separated by AND
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const conditions = [{ field: 'name', operator: '=', value: 'John Doe' }]
|
|
151
|
+
* const startIndex = 1
|
|
152
|
+
* const clientType = 'pg'
|
|
153
|
+
* const unaccent = true
|
|
154
|
+
* const result = createWhereClause(conditions, startIndex, clientType, unaccent)
|
|
155
|
+
* // result will be: "name = $1"
|
|
156
|
+
*/
|
|
45
157
|
export const createWhereClause = <T>(
|
|
46
158
|
conditions: Conditions<T> = {},
|
|
47
159
|
startIndex = 1,
|
|
@@ -167,22 +279,78 @@ export const createWhereClause = <T>(
|
|
|
167
279
|
return [whereClause, values, index]
|
|
168
280
|
}
|
|
169
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Creates an ORDER BY clause for a query
|
|
284
|
+
*
|
|
285
|
+
* This function creates an ORDER BY clause for a query. It takes an array of
|
|
286
|
+
* order by fields and returns a string with the fields separated by commas.
|
|
287
|
+
*
|
|
288
|
+
* @param orderBy - The array of order by fields
|
|
289
|
+
* @returns A string with the fields separated by commas
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* const orderBy = [{ field: 'created_at', direction: 'DESC' }]
|
|
293
|
+
* const result = createOrderByClause(orderBy)
|
|
294
|
+
* // result will be: "ORDER BY created_at DESC"
|
|
295
|
+
*/
|
|
170
296
|
export const createOrderByClause = (orderBy?: OrderBy) => {
|
|
171
297
|
if (!orderBy || orderBy.length === 0) return ''
|
|
172
298
|
const clause = orderBy.map((o) => `${o.field} ${o.direction}`).join(', ')
|
|
173
299
|
return ` ORDER BY ${clause}`
|
|
174
300
|
}
|
|
175
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Creates a GROUP BY clause for a query
|
|
304
|
+
*
|
|
305
|
+
* This function creates a GROUP BY clause for a query. It takes an array of
|
|
306
|
+
* group by fields and returns a string with the fields separated by commas.
|
|
307
|
+
*
|
|
308
|
+
* @param groupBy - The array of group by fields
|
|
309
|
+
* @returns A string with the fields separated by commas
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* const groupBy = ['status']
|
|
313
|
+
* const result = createGroupByClause(groupBy)
|
|
314
|
+
* // result will be: "GROUP BY status"
|
|
315
|
+
*/
|
|
176
316
|
export const createGroupByClause = (groupBy?: string[]) => {
|
|
177
317
|
if (!groupBy || groupBy.length === 0) return ''
|
|
178
318
|
return ` GROUP BY ${groupBy.join(', ')}`
|
|
179
319
|
}
|
|
180
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Creates a LIMIT clause for a query
|
|
323
|
+
*
|
|
324
|
+
* This function creates a LIMIT clause for a query. It takes a limit number
|
|
325
|
+
* and returns a string with the limit.
|
|
326
|
+
*
|
|
327
|
+
* @param limit - The limit number
|
|
328
|
+
* @returns A string with the limit
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* const limit = 10
|
|
332
|
+
* const result = createLimitClause(limit)
|
|
333
|
+
* // result will be: "LIMIT 10"
|
|
334
|
+
*/
|
|
181
335
|
export const createLimitClause = (limit?: number) => {
|
|
182
336
|
if (!limit) return ''
|
|
183
337
|
return ` LIMIT ${limit}`
|
|
184
338
|
}
|
|
185
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Creates an OFFSET clause for a query
|
|
342
|
+
*
|
|
343
|
+
* This function creates an OFFSET clause for a query. It takes an offset number
|
|
344
|
+
* and returns a string with the offset.
|
|
345
|
+
*
|
|
346
|
+
* @param offset - The offset number
|
|
347
|
+
* @returns A string with the offset
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* const offset = 10
|
|
351
|
+
* const result = createOffsetClause(offset)
|
|
352
|
+
* // result will be: "OFFSET 10"
|
|
353
|
+
*/
|
|
186
354
|
export const createOffsetClause = (offset?: number) => {
|
|
187
355
|
if (!offset) return ''
|
|
188
356
|
return ` OFFSET ${offset}`
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { DBClients } from '../
|
|
1
|
+
import { DBClients } from '../core/types'
|
|
2
2
|
|
|
3
|
+
export interface ITransactionClient {
|
|
4
|
+
query: <T>(sql: string, params?: any[]) => Promise<T>
|
|
5
|
+
commit: () => Promise<void>
|
|
6
|
+
rollback: () => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Database client interface
|
|
11
|
+
*/
|
|
3
12
|
export type IDatabaseClient = {
|
|
4
13
|
clientType: DBClients
|
|
5
14
|
query: <T>(sql: string, params?: any[]) => Promise<T>
|
|
15
|
+
beginTransaction: () => Promise<ITransactionClient>
|
|
6
16
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { createMysqlClient } from '../mysqlClient'
|
|
2
|
+
import { createPool, Pool } from 'mysql2/promise'
|
|
3
|
+
|
|
4
|
+
// Mock mysql2 module
|
|
5
|
+
jest.mock('mysql2/promise', () => ({
|
|
6
|
+
createPool: jest.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
// Mock monitor
|
|
10
|
+
jest.mock('../../monitor/monitor', () => ({
|
|
11
|
+
monitor: {
|
|
12
|
+
emit: jest.fn(),
|
|
13
|
+
},
|
|
14
|
+
MonitorEvents: {
|
|
15
|
+
CONNECTION_CREATED: 'CONNECTION_CREATED',
|
|
16
|
+
QUERY_START: 'QUERY_START',
|
|
17
|
+
QUERY_END: 'QUERY_END',
|
|
18
|
+
QUERY_ERROR: 'QUERY_ERROR',
|
|
19
|
+
RETRY_ATTEMPT: 'RETRY_ATTEMPT',
|
|
20
|
+
TRANSACTION_COMMIT: 'TRANSACTION_COMMIT',
|
|
21
|
+
TRANSACTION_ROLLBACK: 'TRANSACTION_ROLLBACK',
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
describe('MySqlClient', () => {
|
|
26
|
+
let mockPool: jest.Mocked<Pool>
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
mockPool = {
|
|
30
|
+
query: jest.fn(),
|
|
31
|
+
connect: jest.fn(),
|
|
32
|
+
end: jest.fn(),
|
|
33
|
+
execute: jest.fn(),
|
|
34
|
+
getConnection: jest.fn(),
|
|
35
|
+
config: {},
|
|
36
|
+
} as any
|
|
37
|
+
;(createPool as jest.MockedFunction<typeof createPool>).mockReturnValue(
|
|
38
|
+
mockPool
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
jest.clearAllMocks()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('createMysqlClient', () => {
|
|
47
|
+
it('should return client with correct clientType', () => {
|
|
48
|
+
const client = createMysqlClient(mockPool)
|
|
49
|
+
|
|
50
|
+
expect(client.clientType).toBe('mysql')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should execute query and return rows', async () => {
|
|
54
|
+
const mockRows = [{ id: 1, name: 'John Doe' }]
|
|
55
|
+
mockPool.execute.mockResolvedValue([mockRows, []] as any)
|
|
56
|
+
|
|
57
|
+
const client = createMysqlClient(mockPool)
|
|
58
|
+
const result = await client.query('SELECT * FROM users WHERE id = ?', [1])
|
|
59
|
+
|
|
60
|
+
expect(mockPool.execute).toHaveBeenCalledWith(
|
|
61
|
+
'SELECT * FROM users WHERE id = ?',
|
|
62
|
+
[1]
|
|
63
|
+
)
|
|
64
|
+
expect(result).toEqual(mockRows)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should handle query without parameters', async () => {
|
|
68
|
+
const mockRows = [{ id: 1, name: 'John Doe' }]
|
|
69
|
+
mockPool.execute.mockResolvedValue([mockRows, []] as any)
|
|
70
|
+
|
|
71
|
+
const client = createMysqlClient(mockPool)
|
|
72
|
+
const result = await client.query('SELECT * FROM users')
|
|
73
|
+
|
|
74
|
+
expect(mockPool.execute).toHaveBeenCalledWith(
|
|
75
|
+
'SELECT * FROM users',
|
|
76
|
+
undefined
|
|
77
|
+
)
|
|
78
|
+
expect(result).toEqual(mockRows)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should handle empty result set', async () => {
|
|
82
|
+
mockPool.execute.mockResolvedValue([[], []] as any)
|
|
83
|
+
|
|
84
|
+
const client = createMysqlClient(mockPool)
|
|
85
|
+
const result = await client.query('SELECT * FROM users WHERE id = ?', [
|
|
86
|
+
999,
|
|
87
|
+
])
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual([])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should handle query errors', async () => {
|
|
93
|
+
const error = new Error('Database connection failed')
|
|
94
|
+
mockPool.execute.mockRejectedValue(error)
|
|
95
|
+
|
|
96
|
+
const client = createMysqlClient(mockPool)
|
|
97
|
+
|
|
98
|
+
await expect(client.query('SELECT * FROM users')).rejects.toThrow(
|
|
99
|
+
'Database connection failed'
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should handle retry options', async () => {
|
|
104
|
+
const mockRows = [{ id: 1, name: 'John Doe' }]
|
|
105
|
+
mockPool.execute.mockResolvedValue([mockRows, []] as any)
|
|
106
|
+
|
|
107
|
+
const retryOptions = {
|
|
108
|
+
retries: 3,
|
|
109
|
+
factor: 2,
|
|
110
|
+
minTimeout: 1000,
|
|
111
|
+
maxTimeout: 5000,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const client = createMysqlClient(mockPool, retryOptions)
|
|
115
|
+
const result = await client.query('SELECT * FROM users')
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual(mockRows)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should handle transient errors with retry', async () => {
|
|
121
|
+
const transientError = new Error('Connection lost')
|
|
122
|
+
;(transientError as any).code = 'ECONNRESET'
|
|
123
|
+
|
|
124
|
+
const mockRows = [{ id: 1, name: 'John Doe' }]
|
|
125
|
+
|
|
126
|
+
// First call fails with transient error, second succeeds
|
|
127
|
+
mockPool.execute
|
|
128
|
+
.mockRejectedValueOnce(transientError)
|
|
129
|
+
.mockResolvedValueOnce([mockRows, []] as any)
|
|
130
|
+
|
|
131
|
+
const retryOptions = {
|
|
132
|
+
retries: 3,
|
|
133
|
+
factor: 2,
|
|
134
|
+
minTimeout: 100,
|
|
135
|
+
maxTimeout: 500,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const client = createMysqlClient(mockPool, retryOptions)
|
|
139
|
+
const result = await client.query('SELECT * FROM users')
|
|
140
|
+
|
|
141
|
+
expect(mockPool.execute).toHaveBeenCalledTimes(2)
|
|
142
|
+
expect(result).toEqual(mockRows)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should throw non-transient errors immediately', async () => {
|
|
146
|
+
const permanentError = new Error('Table does not exist')
|
|
147
|
+
;(permanentError as any).code = 'ER_NO_SUCH_TABLE'
|
|
148
|
+
|
|
149
|
+
mockPool.execute.mockRejectedValue(permanentError)
|
|
150
|
+
|
|
151
|
+
const retryOptions = {
|
|
152
|
+
retries: 3,
|
|
153
|
+
factor: 2,
|
|
154
|
+
minTimeout: 100,
|
|
155
|
+
maxTimeout: 500,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const client = createMysqlClient(mockPool, retryOptions)
|
|
159
|
+
|
|
160
|
+
await expect(
|
|
161
|
+
client.query('SELECT * FROM nonexistent_table')
|
|
162
|
+
).rejects.toThrow('Table does not exist')
|
|
163
|
+
expect(mockPool.execute).toHaveBeenCalledTimes(1)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
describe('beginTransaction', () => {
|
|
167
|
+
let mockConnection: any
|
|
168
|
+
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
mockConnection = {
|
|
171
|
+
execute: jest.fn(),
|
|
172
|
+
beginTransaction: jest.fn(),
|
|
173
|
+
commit: jest.fn(),
|
|
174
|
+
rollback: jest.fn(),
|
|
175
|
+
release: jest.fn(),
|
|
176
|
+
}
|
|
177
|
+
mockPool.getConnection.mockResolvedValue(mockConnection)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should create a transaction client', async () => {
|
|
181
|
+
const client = createMysqlClient(mockPool)
|
|
182
|
+
const transaction = await client.beginTransaction()
|
|
183
|
+
|
|
184
|
+
expect(mockPool.getConnection).toHaveBeenCalled()
|
|
185
|
+
expect(mockConnection.beginTransaction).toHaveBeenCalled()
|
|
186
|
+
expect(transaction).toHaveProperty('query')
|
|
187
|
+
expect(transaction).toHaveProperty('commit')
|
|
188
|
+
expect(transaction).toHaveProperty('rollback')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should execute queries within transaction', async () => {
|
|
192
|
+
const mockRows = [{ id: 1, name: 'John Doe' }]
|
|
193
|
+
mockConnection.execute.mockResolvedValue([mockRows, []])
|
|
194
|
+
|
|
195
|
+
const client = createMysqlClient(mockPool)
|
|
196
|
+
const transaction = await client.beginTransaction()
|
|
197
|
+
const result = await transaction.query(
|
|
198
|
+
'SELECT * FROM users WHERE id = ?',
|
|
199
|
+
[1]
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
expect(mockConnection.execute).toHaveBeenCalledWith(
|
|
203
|
+
'SELECT * FROM users WHERE id = ?',
|
|
204
|
+
[1]
|
|
205
|
+
)
|
|
206
|
+
expect(result).toEqual(mockRows)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should commit transaction successfully', async () => {
|
|
210
|
+
const client = createMysqlClient(mockPool)
|
|
211
|
+
const transaction = await client.beginTransaction()
|
|
212
|
+
await transaction.commit()
|
|
213
|
+
|
|
214
|
+
expect(mockConnection.commit).toHaveBeenCalled()
|
|
215
|
+
expect(mockConnection.release).toHaveBeenCalled()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
it('should rollback transaction on error', async () => {
|
|
219
|
+
const client = createMysqlClient(mockPool)
|
|
220
|
+
const transaction = await client.beginTransaction()
|
|
221
|
+
await transaction.rollback()
|
|
222
|
+
|
|
223
|
+
expect(mockConnection.rollback).toHaveBeenCalled()
|
|
224
|
+
expect(mockConnection.release).toHaveBeenCalled()
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('should release connection even if commit fails', async () => {
|
|
228
|
+
const commitError = new Error('Commit failed')
|
|
229
|
+
mockConnection.commit.mockRejectedValue(commitError)
|
|
230
|
+
|
|
231
|
+
const client = createMysqlClient(mockPool)
|
|
232
|
+
const transaction = await client.beginTransaction()
|
|
233
|
+
|
|
234
|
+
await expect(transaction.commit()).rejects.toThrow('Commit failed')
|
|
235
|
+
expect(mockConnection.release).toHaveBeenCalled()
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should release connection even if rollback fails', async () => {
|
|
239
|
+
const rollbackError = new Error('Rollback failed')
|
|
240
|
+
mockConnection.rollback.mockRejectedValue(rollbackError)
|
|
241
|
+
|
|
242
|
+
const client = createMysqlClient(mockPool)
|
|
243
|
+
const transaction = await client.beginTransaction()
|
|
244
|
+
|
|
245
|
+
await expect(transaction.rollback()).rejects.toThrow('Rollback failed')
|
|
246
|
+
expect(mockConnection.release).toHaveBeenCalled()
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should release connection if beginTransaction fails', async () => {
|
|
250
|
+
const beginError = new Error('Begin transaction failed')
|
|
251
|
+
mockConnection.beginTransaction.mockRejectedValue(beginError)
|
|
252
|
+
|
|
253
|
+
const client = createMysqlClient(mockPool)
|
|
254
|
+
|
|
255
|
+
await expect(client.beginTransaction()).rejects.toThrow(
|
|
256
|
+
'Begin transaction failed'
|
|
257
|
+
)
|
|
258
|
+
expect(mockConnection.release).toHaveBeenCalled()
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
|
+
})
|