@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.
Files changed (85) hide show
  1. package/.github/workflows/publish.yml +118 -0
  2. package/ARCHITECTURE.md +313 -0
  3. package/CHANGELOG.md +65 -0
  4. package/README.md +1309 -129
  5. package/coverage/base.css +224 -0
  6. package/coverage/block-navigation.js +87 -0
  7. package/coverage/favicon.png +0 -0
  8. package/coverage/index.html +131 -0
  9. package/coverage/lcov-report/block-navigation.js +1 -1
  10. package/coverage/lcov-report/index.html +41 -11
  11. package/coverage/lcov-report/mysqlClient.ts.html +685 -0
  12. package/coverage/lcov-report/pgClient.ts.html +823 -0
  13. package/coverage/lcov-report/sorter.js +21 -7
  14. package/coverage/lcov.info +533 -0
  15. package/coverage/mysqlClient.ts.html +685 -0
  16. package/coverage/pgClient.ts.html +823 -0
  17. package/coverage/prettify.css +1 -0
  18. package/coverage/prettify.js +2 -0
  19. package/coverage/sort-arrow-sprite.png +0 -0
  20. package/coverage/sorter.js +210 -0
  21. package/dist/index.d.ts +5 -4
  22. package/dist/index.js +1 -1
  23. package/dist/index.js.map +1 -1
  24. package/dist/src/core/repository.d.ts +374 -0
  25. package/dist/src/core/repository.js +677 -0
  26. package/dist/src/core/repository.js.map +1 -0
  27. package/dist/src/{default → core}/types.d.ts +9 -1
  28. package/dist/src/{default → core}/types.js.map +1 -1
  29. package/dist/src/core/utils.d.ts +133 -0
  30. package/dist/src/{default → core}/utils.js +167 -0
  31. package/dist/src/core/utils.js.map +1 -0
  32. package/dist/src/db/IDatabaseClient.d.ts +10 -1
  33. package/dist/src/db/initDb.d.ts +120 -1
  34. package/dist/src/db/initDb.js +119 -0
  35. package/dist/src/db/initDb.js.map +1 -1
  36. package/dist/src/db/mysqlClient.d.ts +23 -1
  37. package/dist/src/db/mysqlClient.js +115 -0
  38. package/dist/src/db/mysqlClient.js.map +1 -1
  39. package/dist/src/db/pgClient.d.ts +22 -1
  40. package/dist/src/db/pgClient.js +127 -0
  41. package/dist/src/db/pgClient.js.map +1 -1
  42. package/dist/src/monitor/monitor.d.ts +6 -1
  43. package/dist/src/monitor/monitor.js +5 -0
  44. package/dist/src/monitor/monitor.js.map +1 -1
  45. package/dist/src/setupTests.d.ts +26 -0
  46. package/dist/src/setupTests.js +43 -0
  47. package/dist/src/setupTests.js.map +1 -0
  48. package/docs/INDEX.md +145 -0
  49. package/docs/methods/findFirst.md +394 -0
  50. package/docs/methods/findMany.md +587 -0
  51. package/docs/methods/insert.md +536 -0
  52. package/docs/methods/insertMany.md +627 -0
  53. package/docs/methods/joins.md +781 -0
  54. package/docs/methods/rawQuery.md +284 -0
  55. package/docs/methods/transactions.md +737 -0
  56. package/eslint.config.mjs +77 -0
  57. package/index.ts +5 -4
  58. package/jest.config.ts +23 -28
  59. package/package.json +52 -29
  60. package/scripts/release.sh +123 -0
  61. package/src/core/repository.ts +865 -0
  62. package/src/{default → core}/types.ts +11 -2
  63. package/src/{default → core}/utils.ts +168 -0
  64. package/src/db/IDatabaseClient.ts +11 -1
  65. package/src/db/__tests__/mysqlClient.test.ts +262 -0
  66. package/src/db/__tests__/pgClient.test.ts +260 -0
  67. package/src/db/initDb.ts +120 -1
  68. package/src/db/mysqlClient.ts +119 -3
  69. package/src/db/pgClient.ts +131 -3
  70. package/src/monitor/monitor.ts +5 -0
  71. package/src/setupTests.ts +45 -0
  72. package/tsconfig.test.json +21 -0
  73. package/.eslintignore +0 -4
  74. package/.eslintrc.json +0 -32
  75. package/coverage/clover.xml +0 -6
  76. package/coverage/coverage-final.json +0 -1
  77. package/dist/.eslintrc.json +0 -32
  78. package/dist/src/default/genericRepository.d.ts +0 -20
  79. package/dist/src/default/genericRepository.js +0 -192
  80. package/dist/src/default/genericRepository.js.map +0 -1
  81. package/dist/src/default/utils.d.ts +0 -9
  82. package/dist/src/default/utils.js.map +0 -1
  83. package/index.d.ts +0 -60
  84. package/src/default/genericRepository.ts +0 -320
  85. /package/dist/src/{default → core}/types.js +0 -0
@@ -0,0 +1,260 @@
1
+ import { createPgClient } from '../pgClient'
2
+ import { Pool } from 'pg'
3
+
4
+ // Mock pg module
5
+ jest.mock('pg', () => ({
6
+ Pool: 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('PgClient', () => {
26
+ let mockPool: jest.Mocked<Pool>
27
+
28
+ beforeEach(() => {
29
+ mockPool = {
30
+ query: jest.fn(),
31
+ connect: jest.fn(),
32
+ end: jest.fn(),
33
+ } as any
34
+ ;(Pool as jest.MockedClass<typeof Pool>).mockImplementation(() => mockPool)
35
+ })
36
+
37
+ afterEach(() => {
38
+ jest.clearAllMocks()
39
+ })
40
+
41
+ describe('createPgClient', () => {
42
+ it('should return client with correct clientType', async () => {
43
+ const client = await createPgClient(mockPool)
44
+ expect(client.clientType).toBe('pg')
45
+ })
46
+
47
+ it('should execute query and return rows', async () => {
48
+ const mockRows = [{ id: 1, name: 'John Doe' }]
49
+ mockPool.query.mockResolvedValue({ rows: mockRows, rowCount: 1 } as any)
50
+ const client = await createPgClient(mockPool)
51
+ const result = await client.query('SELECT * FROM users WHERE id = $1', [
52
+ 1,
53
+ ])
54
+ expect(mockPool.query).toHaveBeenCalledWith(
55
+ 'SELECT * FROM users WHERE id = $1',
56
+ [1]
57
+ )
58
+ expect(result).toEqual(mockRows)
59
+ })
60
+
61
+ it('should handle query without parameters', async () => {
62
+ const mockRows = [{ id: 1, name: 'John Doe' }]
63
+ mockPool.query.mockResolvedValue({ rows: mockRows, rowCount: 1 } as any)
64
+ const client = await createPgClient(mockPool)
65
+ const result = await client.query('SELECT * FROM users')
66
+ expect(mockPool.query).toHaveBeenCalledWith(
67
+ 'SELECT * FROM users',
68
+ undefined
69
+ )
70
+ expect(result).toEqual(mockRows)
71
+ })
72
+
73
+ it('should handle empty result set', async () => {
74
+ mockPool.query.mockResolvedValue({ rows: [], rowCount: 0 } as any)
75
+ const client = await createPgClient(mockPool)
76
+ const result = await client.query('SELECT * FROM users WHERE id = $1', [
77
+ 999,
78
+ ])
79
+ expect(result).toEqual([])
80
+ })
81
+
82
+ it('should handle query errors', async () => {
83
+ const error = new Error('Database connection failed')
84
+ mockPool.query.mockRejectedValue(error)
85
+ const client = await createPgClient(mockPool)
86
+ await expect(client.query('SELECT * FROM users')).rejects.toThrow(
87
+ 'Database connection failed'
88
+ )
89
+ })
90
+
91
+ it('should handle retry options', async () => {
92
+ const mockRows = [{ id: 1, name: 'John Doe' }]
93
+ mockPool.query.mockResolvedValue({ rows: mockRows, rowCount: 1 } as any)
94
+ const retryOptions = {
95
+ retries: 3,
96
+ factor: 2,
97
+ minTimeout: 1000,
98
+ maxTimeout: 5000,
99
+ }
100
+ const client = await createPgClient(mockPool, retryOptions)
101
+ const result = await client.query('SELECT * FROM users')
102
+ expect(result).toEqual(mockRows)
103
+ })
104
+
105
+ it('should handle transient errors with retry', async () => {
106
+ const transientError = new Error('Connection lost')
107
+ ;(transientError as any).code = 'ECONNRESET'
108
+ const mockRows = [{ id: 1, name: 'John Doe' }]
109
+ // First call fails with transient error, second succeeds
110
+ mockPool.query
111
+ .mockRejectedValueOnce(transientError)
112
+ .mockResolvedValueOnce({ rows: mockRows, rowCount: 1 } as any)
113
+ const retryOptions = {
114
+ retries: 3,
115
+ factor: 2,
116
+ minTimeout: 100,
117
+ maxTimeout: 500,
118
+ }
119
+ const client = await createPgClient(mockPool, retryOptions)
120
+ const result = await client.query('SELECT * FROM users')
121
+ expect(mockPool.query).toHaveBeenCalledTimes(2)
122
+ expect(result).toEqual(mockRows)
123
+ })
124
+
125
+ it('should throw non-transient errors immediately', async () => {
126
+ const permanentError = new Error('Table does not exist')
127
+ ;(permanentError as any).code = 'ER_NO_SUCH_TABLE'
128
+ mockPool.query.mockRejectedValue(permanentError)
129
+ const retryOptions = {
130
+ retries: 3,
131
+ factor: 2,
132
+ minTimeout: 100,
133
+ maxTimeout: 500,
134
+ }
135
+ const client = await createPgClient(mockPool, retryOptions)
136
+ await expect(
137
+ client.query('SELECT * FROM nonexistent_table')
138
+ ).rejects.toThrow('Table does not exist')
139
+ expect(mockPool.query).toHaveBeenCalledTimes(1)
140
+ })
141
+
142
+ it('should install unaccent extension when requested', async () => {
143
+ mockPool.query
144
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 } as any) // Extension not found
145
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 } as any) // Extension created
146
+ .mockResolvedValueOnce({ rows: [{ id: 1 }], rowCount: 1 } as any) // Query result
147
+ const client = await createPgClient(mockPool, undefined, undefined, true)
148
+ await client.query('SELECT * FROM users')
149
+ expect(mockPool.query).toHaveBeenCalledWith(
150
+ expect.stringContaining(
151
+ "SELECT 1 FROM pg_extension WHERE extname = 'unaccent'"
152
+ )
153
+ )
154
+ expect(mockPool.query).toHaveBeenCalledWith(
155
+ expect.stringContaining('CREATE EXTENSION IF NOT EXISTS unaccent')
156
+ )
157
+ })
158
+
159
+ describe('beginTransaction', () => {
160
+ let mockClient: any
161
+
162
+ beforeEach(() => {
163
+ mockClient = {
164
+ query: jest.fn(),
165
+ release: jest.fn(),
166
+ }
167
+ mockPool.connect.mockResolvedValue(mockClient)
168
+ })
169
+
170
+ it('should create a transaction client', async () => {
171
+ const client = await createPgClient(mockPool)
172
+ const transaction = await client.beginTransaction()
173
+
174
+ expect(mockPool.connect).toHaveBeenCalled()
175
+ expect(mockClient.query).toHaveBeenCalledWith('BEGIN')
176
+ expect(transaction).toHaveProperty('query')
177
+ expect(transaction).toHaveProperty('commit')
178
+ expect(transaction).toHaveProperty('rollback')
179
+ })
180
+
181
+ it('should execute queries within transaction', async () => {
182
+ const mockRows = [{ id: 1, name: 'John Doe' }]
183
+ mockClient.query.mockResolvedValue({ rows: mockRows, rowCount: 1 })
184
+
185
+ const client = await createPgClient(mockPool)
186
+ const transaction = await client.beginTransaction()
187
+ const result = await transaction.query(
188
+ 'SELECT * FROM users WHERE id = $1',
189
+ [1]
190
+ )
191
+
192
+ expect(mockClient.query).toHaveBeenCalledWith(
193
+ 'SELECT * FROM users WHERE id = $1',
194
+ [1]
195
+ )
196
+ expect(result).toEqual(mockRows)
197
+ })
198
+
199
+ it('should commit transaction successfully', async () => {
200
+ const client = await createPgClient(mockPool)
201
+ const transaction = await client.beginTransaction()
202
+ await transaction.commit()
203
+
204
+ expect(mockClient.query).toHaveBeenCalledWith('COMMIT')
205
+ expect(mockClient.release).toHaveBeenCalled()
206
+ })
207
+
208
+ it('should rollback transaction on error', async () => {
209
+ const client = await createPgClient(mockPool)
210
+ const transaction = await client.beginTransaction()
211
+ await transaction.rollback()
212
+
213
+ expect(mockClient.query).toHaveBeenCalledWith('ROLLBACK')
214
+ expect(mockClient.release).toHaveBeenCalled()
215
+ })
216
+
217
+ it('should release connection even if commit fails', async () => {
218
+ const commitError = new Error('Commit failed')
219
+
220
+ // Mock the sequence: BEGIN succeeds, COMMIT fails
221
+ mockClient.query
222
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
223
+ .mockRejectedValueOnce(commitError) // COMMIT
224
+
225
+ const client = await createPgClient(mockPool)
226
+ const transaction = await client.beginTransaction()
227
+
228
+ await expect(transaction.commit()).rejects.toThrow('Commit failed')
229
+ expect(mockClient.release).toHaveBeenCalled()
230
+ })
231
+
232
+ it('should release connection even if rollback fails', async () => {
233
+ const rollbackError = new Error('Rollback failed')
234
+
235
+ // Mock the sequence: BEGIN succeeds, ROLLBACK fails
236
+ mockClient.query
237
+ .mockResolvedValueOnce({ rows: [], rowCount: 0 }) // BEGIN
238
+ .mockRejectedValueOnce(rollbackError) // ROLLBACK
239
+
240
+ const client = await createPgClient(mockPool)
241
+ const transaction = await client.beginTransaction()
242
+
243
+ await expect(transaction.rollback()).rejects.toThrow('Rollback failed')
244
+ expect(mockClient.release).toHaveBeenCalled()
245
+ })
246
+
247
+ it('should release connection if BEGIN fails', async () => {
248
+ const beginError = new Error('Begin transaction failed')
249
+ mockClient.query.mockRejectedValue(beginError)
250
+
251
+ const client = await createPgClient(mockPool)
252
+
253
+ await expect(client.beginTransaction()).rejects.toThrow(
254
+ 'Begin transaction failed'
255
+ )
256
+ expect(mockClient.release).toHaveBeenCalled()
257
+ })
258
+ })
259
+ })
260
+ })
package/src/db/initDb.ts CHANGED
@@ -3,11 +3,58 @@ import { createPool as createMySqlPool } from 'mysql2/promise'
3
3
  import { createPgClient } from './pgClient'
4
4
  import { createMysqlClient } from './mysqlClient'
5
5
  import { IDatabaseClient } from './IDatabaseClient'
6
- import { DBClients, RetryOptions } from '../default/types'
6
+ import { DBClients, RetryOptions } from '../core/types'
7
7
 
8
8
  const dbClients: Record<string, IDatabaseClient> = {}
9
9
  const defaultName = 'default'
10
10
 
11
+ /**
12
+ * Initialize a database client with the specified configuration
13
+ *
14
+ * @template T - The type of connection options (PoolConfig for PostgreSQL or PoolOptions for MySQL)
15
+ * @param config - Configuration object for database initialization
16
+ * @param config.name - Optional name for the database client (defaults to 'default')
17
+ * @param config.type - Database type ('pg' for PostgreSQL or 'mysql' for MySQL)
18
+ * @param config.options - Connection options specific to the database type
19
+ * @param config.retryOptions - Optional retry configuration for failed queries
20
+ * @param config.installUnaccentExtension - Optional flag to install unaccent extension (PostgreSQL only)
21
+ * @returns Promise<void> - Resolves when the database client is successfully initialized
22
+ *
23
+ * @throws {Error} When type is not provided or is invalid
24
+ * @throws {Error} When connection options are not provided
25
+ * @throws {Error} When an unsupported database type is specified
26
+ *
27
+ * @example
28
+ * // Initialize PostgreSQL client
29
+ * await initDb({
30
+ * name: 'myPostgresDb',
31
+ * type: 'pg',
32
+ * options: {
33
+ * host: 'localhost',
34
+ * port: 5432,
35
+ * database: 'mydb',
36
+ * user: 'user',
37
+ * password: 'password'
38
+ * },
39
+ * retryOptions: { maxRetries: 3, delay: 1000 },
40
+ * installUnaccentExtension: true
41
+ * });
42
+ *
43
+ * @example
44
+ * // Initialize MySQL client
45
+ * await initDb({
46
+ * name: 'myMysqlDb',
47
+ * type: 'mysql',
48
+ * options: {
49
+ * host: 'localhost',
50
+ * port: 3306,
51
+ * database: 'mydb',
52
+ * user: 'user',
53
+ * password: 'password'
54
+ * },
55
+ * retryOptions: { maxRetries: 3, delay: 1000 }
56
+ * });
57
+ */
11
58
  export const initDb = async <T>(config: {
12
59
  name?: string
13
60
  type: DBClients
@@ -48,6 +95,53 @@ export const initDb = async <T>(config: {
48
95
  }
49
96
  }
50
97
 
98
+ /**
99
+ * Initialize a database client with the specified configuration
100
+ *
101
+ * @template T - The type of connection options (PoolConfig for PostgreSQL or PoolOptions for MySQL)
102
+ * @param config - Configuration object for database initialization
103
+ * @param config.name - Optional name for the database client (defaults to 'default')
104
+ * @param config.type - Database type ('pg' for PostgreSQL or 'mysql' for MySQL)
105
+ * @param config.options - Connection options specific to the database type
106
+ * @param config.retryOptions - Optional retry configuration for failed queries
107
+ * @param config.installUnaccentExtension - Optional flag to install unaccent extension (PostgreSQL only)
108
+ * @returns Promise<void> - Resolves when the database client is successfully initialized
109
+ *
110
+ * @throws {Error} When database type is not provided or is invalid
111
+ * @throws {Error} When connection options are not provided
112
+ * @throws {Error} When an unsupported database type is specified
113
+ *
114
+ * @example
115
+ * // Initialize PostgreSQL client
116
+ * await initDb({
117
+ * name: 'myPostgresDb',
118
+ * type: 'pg',
119
+ * options: {
120
+ * host: 'localhost',
121
+ * port: 5432,
122
+ * database: 'mydb',
123
+ * user: 'user',
124
+ * password: 'password'
125
+ * },
126
+ * retryOptions: { maxRetries: 3, delay: 1000 },
127
+ * installUnaccentExtension: true
128
+ * });
129
+ *
130
+ * @example
131
+ * // Initialize MySQL client
132
+ * await initDb({
133
+ * name: 'myMysqlDb',
134
+ * type: 'mysql',
135
+ * options: {
136
+ * host: 'localhost',
137
+ * port: 3306,
138
+ * database: 'mydb',
139
+ * user: 'user',
140
+ * password: 'password'
141
+ * },
142
+ * retryOptions: { maxRetries: 3, delay: 1000 }
143
+ * });
144
+ */
51
145
  export const getDbClient = (name?: string): IDatabaseClient => {
52
146
  const client = dbClients[name || defaultName]
53
147
  if (!client) {
@@ -57,6 +151,31 @@ export const getDbClient = (name?: string): IDatabaseClient => {
57
151
  return client
58
152
  }
59
153
 
154
+ /**
155
+ * Retrieves a specific database client by name
156
+ *
157
+ * @param name - Optional name of the database client to retrieve. If not provided, returns the default client
158
+ * @returns IDatabaseClient - The requested database client instance
159
+ *
160
+ * @throws {Error} When the specified database client name is not found or not initialized
161
+ *
162
+ * @example
163
+ * // Get default database client
164
+ * const defaultClient = getDbClient();
165
+ *
166
+ * @example
167
+ * // Get specific database client by name
168
+ * const postgresClient = getDbClient('myPostgresDb');
169
+ * const mysqlClient = getDbClient('myMysqlDb');
170
+ *
171
+ * @example
172
+ * // Handle error when client is not found
173
+ * try {
174
+ * const client = getDbClient('nonExistentDb');
175
+ * } catch (error) {
176
+ * console.error('Database client not found:', error.message);
177
+ * }
178
+ */
60
179
  export const getAllDbClients = (): Record<string, IDatabaseClient> => {
61
180
  return dbClients
62
181
  }
@@ -1,7 +1,7 @@
1
- import { Pool } from 'mysql2/promise'
1
+ import { Pool, PoolConnection } from 'mysql2/promise'
2
2
  import promiseRetry from 'promise-retry'
3
- import { IDatabaseClient } from './IDatabaseClient'
4
- import { RetryOptions } from '../default/types'
3
+ import { IDatabaseClient, ITransactionClient } from './IDatabaseClient'
4
+ import { RetryOptions } from '../core/types'
5
5
  import { monitor, MonitorEvents } from '../monitor/monitor'
6
6
 
7
7
  const transientErrorCodes = new Set([
@@ -11,10 +11,55 @@ const transientErrorCodes = new Set([
11
11
  'ECONNREFUSED',
12
12
  ])
13
13
 
14
+ /**
15
+ * Checks if the given error is a transient error that can be retried
16
+ *
17
+ * Transient errors are temporary network or connection issues that typically
18
+ * resolve themselves and can be safely retried. This function checks if the
19
+ * error code matches known transient error patterns for MySQL connections.
20
+ *
21
+ * @param error - The error object to check
22
+ * @returns boolean - True if the error is transient and can be retried, false otherwise
23
+ *
24
+ * @example
25
+ * try {
26
+ * const result = await pool.execute(sql, params);
27
+ * } catch (error) {
28
+ * if (isTransientError(error)) {
29
+ * // Retry the operation
30
+ * console.log('Transient error detected, retrying...');
31
+ * } else {
32
+ * // Handle permanent error
33
+ * throw error;
34
+ * }
35
+ * }
36
+ */
14
37
  function isTransientError(error: any): boolean {
15
38
  return error && error.code && transientErrorCodes.has(error.code)
16
39
  }
17
40
 
41
+ /**
42
+ * Creates a MySQL database client
43
+ *
44
+ * This function initializes a MySQL database client with the provided pool configuration
45
+ * and optional retry options. It sets up monitoring for connection events and query operations.
46
+ *
47
+ * @param pool - The MySQL pool instance
48
+ * @param retryOptions - Optional retry options for failed queries
49
+ * @returns IDatabaseClient - The MySQL database client instance
50
+ *
51
+ * @example
52
+ * const mysqlClient = createMysqlClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
53
+ *
54
+ * @example
55
+ * const mysqlClient = createMysqlClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
56
+ *
57
+ * @example
58
+ * const mysqlClient = createMysqlClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
59
+ *
60
+ * @example
61
+ * const mysqlClient = createMysqlClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
62
+ */
18
63
  export const createMysqlClient = (
19
64
  pool: Pool,
20
65
  retryOptions?: RetryOptions
@@ -80,5 +125,76 @@ export const createMysqlClient = (
80
125
  }
81
126
  }, retryOptions)
82
127
  },
128
+ beginTransaction: async (): Promise<ITransactionClient> => {
129
+ const connection: PoolConnection = await pool.getConnection()
130
+
131
+ try {
132
+ await connection.beginTransaction()
133
+
134
+ return {
135
+ query: async <T>(sql: string, params?: any[]): Promise<T> => {
136
+ const startTime = Date.now()
137
+ try {
138
+ monitor.emit(MonitorEvents.QUERY_START, {
139
+ clientType: 'mysql',
140
+ sql,
141
+ params,
142
+ attempt: 1,
143
+ inTransaction: true,
144
+ })
145
+
146
+ const [rows] = await connection.execute(sql, params)
147
+
148
+ const elapsedTime = Date.now() - startTime
149
+ monitor.emit(MonitorEvents.QUERY_END, {
150
+ clientType: 'mysql',
151
+ sql,
152
+ params,
153
+ attempt: 1,
154
+ elapsedTime,
155
+ inTransaction: true,
156
+ })
157
+
158
+ return rows as unknown as T
159
+ } catch (error) {
160
+ const elapsedTime = Date.now() - startTime
161
+ monitor.emit(MonitorEvents.QUERY_ERROR, {
162
+ clientType: 'mysql',
163
+ sql,
164
+ params,
165
+ attempt: 1,
166
+ elapsedTime,
167
+ error,
168
+ inTransaction: true,
169
+ })
170
+ throw error
171
+ }
172
+ },
173
+ commit: async (): Promise<void> => {
174
+ try {
175
+ await connection.commit()
176
+ monitor.emit(MonitorEvents.TRANSACTION_COMMIT, {
177
+ clientType: 'mysql',
178
+ })
179
+ } finally {
180
+ connection.release()
181
+ }
182
+ },
183
+ rollback: async (): Promise<void> => {
184
+ try {
185
+ await connection.rollback()
186
+ monitor.emit(MonitorEvents.TRANSACTION_ROLLBACK, {
187
+ clientType: 'mysql',
188
+ })
189
+ } finally {
190
+ connection.release()
191
+ }
192
+ },
193
+ }
194
+ } catch (error) {
195
+ connection.release()
196
+ throw error
197
+ }
198
+ },
83
199
  }
84
200
  }
@@ -1,15 +1,51 @@
1
- import { Pool, PoolConfig } from 'pg'
1
+ import { Pool, PoolConfig, PoolClient } from 'pg'
2
2
  import promiseRetry from 'promise-retry'
3
- import { IDatabaseClient } from './IDatabaseClient'
4
- import { RetryOptions } from '../default/types'
3
+ import { IDatabaseClient, ITransactionClient } from './IDatabaseClient'
4
+ import { RetryOptions } from '../core/types'
5
5
  import { monitor, MonitorEvents } from '../monitor/monitor'
6
6
 
7
7
  const transientErrorCodes = new Set(['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED'])
8
8
 
9
+ /**
10
+ * Checks if the given error is a transient error that can be retried
11
+ *
12
+ * Transient errors are temporary network or connection issues that typically
13
+ * resolve themselves and can be safely retried. This function checks if the
14
+ * error code matches known transient error patterns for PostgreSQL connections.
15
+ *
16
+ * @param error - The error object to check
17
+ * @returns boolean - True if the error is transient and can be retried, false otherwise
18
+ *
19
+ * @example
20
+ * try {
21
+ * const result = await pool.query(sql, params);
22
+ * } catch (error) {
23
+ * if (isTransientError(error)) {
24
+ * // Retry the operation
25
+ * console.log('Transient error detected, retrying...');
26
+ * } else {
27
+ * // Handle permanent error
28
+ * throw error;
29
+ * }
30
+ * }
31
+ */
9
32
  function isTransientError(error: any): boolean {
10
33
  return error && error.code && transientErrorCodes.has(error.code)
11
34
  }
12
35
 
36
+ /**
37
+ * Ensures that the unaccent extension is installed in the PostgreSQL database
38
+ *
39
+ * This function checks if the unaccent extension is installed in the database
40
+ * and installs it if it is not. It also emits a connection created event to
41
+ * the monitor.
42
+ *
43
+ * @param pool - The PostgreSQL pool instance
44
+ * @returns Promise<void> - Resolves when the unaccent extension is installed or already exists
45
+ *
46
+ * @example
47
+ * await ensureUnaccentExtension(pool);
48
+ */
13
49
  async function ensureUnaccentExtension(pool: Pool): Promise<void> {
14
50
  try {
15
51
  const checkResult = await pool.query(`
@@ -42,6 +78,27 @@ async function ensureUnaccentExtension(pool: Pool): Promise<void> {
42
78
  }
43
79
  }
44
80
 
81
+ /**
82
+ * Creates a PostgreSQL database client
83
+ *
84
+ * This function initializes a PostgreSQL database client with the provided pool configuration
85
+ * and optional retry options. It sets up monitoring for connection events and query operations.
86
+ *
87
+ * @param pool - The PostgreSQL pool instance
88
+ * @param retryOptions - Optional retry options for failed queries
89
+ * @param poolConfig - Optional pool configuration
90
+ * @param installUnaccentExtension - Optional flag to install unaccent extension
91
+ * @returns IDatabaseClient - The PostgreSQL database client instance
92
+ *
93
+ * @example
94
+ * const pgClient = await createPgClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
95
+ *
96
+ * @example
97
+ * const pgClient = await createPgClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
98
+ *
99
+ * @example
100
+ * const pgClient = await createPgClient(pool, { retries: 3, factor: 2, minTimeout: 1000 });
101
+ */
45
102
  export const createPgClient = async (
46
103
  pool: Pool,
47
104
  retryOptions?: RetryOptions,
@@ -114,5 +171,76 @@ export const createPgClient = async (
114
171
  }
115
172
  }, retryOptions)
116
173
  },
174
+ beginTransaction: async (): Promise<ITransactionClient> => {
175
+ const client: PoolClient = await pool.connect()
176
+
177
+ try {
178
+ await client.query('BEGIN')
179
+
180
+ return {
181
+ query: async <T>(sql: string, params?: any[]): Promise<T> => {
182
+ const startTime = Date.now()
183
+ try {
184
+ monitor.emit(MonitorEvents.QUERY_START, {
185
+ clientType: 'pg',
186
+ sql,
187
+ params,
188
+ attempt: 1,
189
+ inTransaction: true,
190
+ })
191
+
192
+ const { rows } = await client.query(sql, params)
193
+
194
+ const elapsedTime = Date.now() - startTime
195
+ monitor.emit(MonitorEvents.QUERY_END, {
196
+ clientType: 'pg',
197
+ sql,
198
+ params,
199
+ attempt: 1,
200
+ elapsedTime,
201
+ inTransaction: true,
202
+ })
203
+
204
+ return rows as unknown as T
205
+ } catch (error) {
206
+ const elapsedTime = Date.now() - startTime
207
+ monitor.emit(MonitorEvents.QUERY_ERROR, {
208
+ clientType: 'pg',
209
+ sql,
210
+ params,
211
+ attempt: 1,
212
+ elapsedTime,
213
+ error,
214
+ inTransaction: true,
215
+ })
216
+ throw error
217
+ }
218
+ },
219
+ commit: async (): Promise<void> => {
220
+ try {
221
+ await client.query('COMMIT')
222
+ monitor.emit(MonitorEvents.TRANSACTION_COMMIT, {
223
+ clientType: 'pg',
224
+ })
225
+ } finally {
226
+ client.release()
227
+ }
228
+ },
229
+ rollback: async (): Promise<void> => {
230
+ try {
231
+ await client.query('ROLLBACK')
232
+ monitor.emit(MonitorEvents.TRANSACTION_ROLLBACK, {
233
+ clientType: 'pg',
234
+ })
235
+ } finally {
236
+ client.release()
237
+ }
238
+ },
239
+ }
240
+ } catch (error) {
241
+ client.release()
242
+ throw error
243
+ }
244
+ },
117
245
  }
118
246
  }