@juit/pgproxy-pool 1.0.0
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 +8 -0
- package/dist/connection.cjs +309 -0
- package/dist/connection.cjs.map +6 -0
- package/dist/connection.d.ts +112 -0
- package/dist/connection.mjs +273 -0
- package/dist/connection.mjs.map +6 -0
- package/dist/events.cjs +56 -0
- package/dist/events.cjs.map +6 -0
- package/dist/events.d.ts +16 -0
- package/dist/events.mjs +31 -0
- package/dist/events.mjs.map +6 -0
- package/dist/index.cjs +34 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.mjs +8 -0
- package/dist/index.mjs.map +6 -0
- package/dist/libpq.cjs +32 -0
- package/dist/libpq.cjs.map +6 -0
- package/dist/libpq.d.ts +6 -0
- package/dist/libpq.mjs +7 -0
- package/dist/libpq.mjs.map +6 -0
- package/dist/pool.cjs +486 -0
- package/dist/pool.cjs.map +6 -0
- package/dist/pool.d.ts +159 -0
- package/dist/pool.mjs +451 -0
- package/dist/pool.mjs.map +6 -0
- package/dist/queue.cjs +70 -0
- package/dist/queue.cjs.map +6 -0
- package/dist/queue.d.ts +7 -0
- package/dist/queue.mjs +45 -0
- package/dist/queue.mjs.map +6 -0
- package/package.json +47 -0
- package/src/connection.ts +494 -0
- package/src/events.ts +42 -0
- package/src/index.ts +17 -0
- package/src/libpq.ts +10 -0
- package/src/pool.ts +683 -0
- package/src/queue.ts +47 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
import { Emitter } from './events'
|
|
5
|
+
import { LibPQ } from './libpq'
|
|
6
|
+
import { Queue } from './queue'
|
|
7
|
+
|
|
8
|
+
import type { Logger } from './index'
|
|
9
|
+
|
|
10
|
+
/* ========================================================================== *
|
|
11
|
+
* INTERNALS *
|
|
12
|
+
* ========================================================================== */
|
|
13
|
+
|
|
14
|
+
/** Mappings to convert our options into LibPQ's own keys */
|
|
15
|
+
const optionKeys = {
|
|
16
|
+
address: 'hostaddr',
|
|
17
|
+
applicationName: 'application_name',
|
|
18
|
+
connectTimeout: 'connect_timeout',
|
|
19
|
+
database: 'dbname',
|
|
20
|
+
gssLibrary: 'gsslib',
|
|
21
|
+
host: 'host',
|
|
22
|
+
keepalives: 'keepalives',
|
|
23
|
+
keepalivesCount: 'keepalives_count',
|
|
24
|
+
keepalivesIdle: 'keepalives_idle',
|
|
25
|
+
keepalivesInterval: 'keepalives_interval',
|
|
26
|
+
kerberosServiceName: 'krbsrvname',
|
|
27
|
+
password: 'password',
|
|
28
|
+
port: 'port',
|
|
29
|
+
sslCertFile: 'sslcert',
|
|
30
|
+
sslCompression: 'sslcompression',
|
|
31
|
+
sslCrlFile: 'sslcrl',
|
|
32
|
+
sslKeyFile: 'sslkey',
|
|
33
|
+
sslMode: 'sslmode',
|
|
34
|
+
sslRootCertFile: 'sslrootcert',
|
|
35
|
+
user: 'user',
|
|
36
|
+
} as const satisfies Record<keyof ConnectionOptions, string>
|
|
37
|
+
|
|
38
|
+
/** Quote a parameter value for options */
|
|
39
|
+
function quoteParamValue(value: string): string {
|
|
40
|
+
value = value.replace(/\\/g, '\\\\').replace(/'/g, '\\\'')
|
|
41
|
+
return `'${value}'`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The {@link FinalizationRegistry} ensuring LibPQ gets finalized */
|
|
45
|
+
const finalizer = new FinalizationRegistry<LibPQ>( /* coverage ignore next */ (pq) => {
|
|
46
|
+
pq.finish()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
/* ========================================================================== *
|
|
50
|
+
* TYPES *
|
|
51
|
+
* ========================================================================== */
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Connection options
|
|
55
|
+
*
|
|
56
|
+
* See https://www.postgresql.org/docs/9.3/libpq-connect.html#LIBPQ-PARAMKEYWORDS
|
|
57
|
+
*/
|
|
58
|
+
export interface ConnectionOptions {
|
|
59
|
+
/** The database name. */
|
|
60
|
+
database?: string
|
|
61
|
+
|
|
62
|
+
/** Name of host to connect to. */
|
|
63
|
+
host?: string,
|
|
64
|
+
|
|
65
|
+
/** IPv4 or IPv6 numeric IP address of host to connect to. */
|
|
66
|
+
address?: string,
|
|
67
|
+
|
|
68
|
+
/** Port number to connect to at the server host. */
|
|
69
|
+
port?: number
|
|
70
|
+
|
|
71
|
+
/** PostgreSQL user name to connect as. */
|
|
72
|
+
user?: string
|
|
73
|
+
|
|
74
|
+
/** Password to be used if the server demands password authentication. */
|
|
75
|
+
password?: string
|
|
76
|
+
|
|
77
|
+
/** Maximum wait for connection, in seconds. */
|
|
78
|
+
connectTimeout?: number
|
|
79
|
+
|
|
80
|
+
/** The `application_name` as it will appear in `pg_stat_activity`. */
|
|
81
|
+
applicationName?: string
|
|
82
|
+
|
|
83
|
+
/** Controls whether client-side TCP keepalives are used. */
|
|
84
|
+
keepalives?: boolean
|
|
85
|
+
|
|
86
|
+
/** The number of seconds of inactivity after which TCP should send a keepalive message to the server. */
|
|
87
|
+
keepalivesIdle?: number
|
|
88
|
+
|
|
89
|
+
/** The number of seconds after which a TCP keepalive message that is not acknowledged by the server should be retransmitted. */
|
|
90
|
+
keepalivesInterval?: number
|
|
91
|
+
|
|
92
|
+
/** The number of TCP keepalives that can be lost before the client's connection to the server is considered dead. */
|
|
93
|
+
keepalivesCount?: number
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* This option determines whether or with what priority a secure SSL TCP/IP
|
|
97
|
+
* connection will be negotiated with the server. There are six modes:
|
|
98
|
+
* * `disable`: only try a non-SSL connection
|
|
99
|
+
* * `allow`: first try a non-SSL connection; if that fails, try an SSL connection
|
|
100
|
+
* * `prefer` _(default)_: first try an SSL connection; if that fails, try a non-SSL connection
|
|
101
|
+
* * `require`: only try an SSL connection. If a root CA file is present, verify the certificate in the same way as if verify-ca was specified
|
|
102
|
+
* * `verify-ca`: only try an SSL connection, and verify that the server certificate is issued by a trusted certificate authority (CA)
|
|
103
|
+
* * `verify-full`: only try an SSL connection, verify that the server certificate is issued by a trusted CA and that the server host name matches that in the certificate
|
|
104
|
+
*/
|
|
105
|
+
sslMode?:
|
|
106
|
+
| 'disable'
|
|
107
|
+
| 'allow'
|
|
108
|
+
| 'prefer'
|
|
109
|
+
| 'require'
|
|
110
|
+
| 'verify-ca'
|
|
111
|
+
| 'verify-full'
|
|
112
|
+
|
|
113
|
+
/** If set to `true` (default), data sent over SSL connections will be compressed */
|
|
114
|
+
sslCompression?: boolean
|
|
115
|
+
|
|
116
|
+
/** The file name of the client SSL certificate */
|
|
117
|
+
sslCertFile?: string
|
|
118
|
+
|
|
119
|
+
/** The location for the secret key used for the client certificate. */
|
|
120
|
+
sslKeyFile?: string
|
|
121
|
+
|
|
122
|
+
/** The name of a file containing SSL certificate authority (CA) certificate(s). */
|
|
123
|
+
sslRootCertFile?: string
|
|
124
|
+
|
|
125
|
+
/** The file name of the SSL certificate revocation list (CRL). */
|
|
126
|
+
sslCrlFile?: string
|
|
127
|
+
|
|
128
|
+
/** Kerberos service name to use when authenticating with Kerberos 5 or GSSAPI. */
|
|
129
|
+
kerberosServiceName?: string
|
|
130
|
+
|
|
131
|
+
/** GSS library to use for GSSAPI authentication. Only used on Windows. */
|
|
132
|
+
gssLibrary?: 'gssapi'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Describes the result of a PostgreSQL query */
|
|
136
|
+
export interface ConnectionQueryResult {
|
|
137
|
+
/** Command executed (normally `SELECT`, or `INSERT`, ...) */
|
|
138
|
+
command: string
|
|
139
|
+
/** Number of rows affected by this query (e.g. added rows in `INSERT`) */
|
|
140
|
+
rowCount: number
|
|
141
|
+
/** Fields description with `name` (column name) and `oid` (type) */
|
|
142
|
+
fields: [ name: string, oid: number ][]
|
|
143
|
+
/** Result rows, as an array of unparsed `string` results from `libpq` */
|
|
144
|
+
rows: (string | null)[][]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The type identifying */
|
|
148
|
+
export type ConnectionQueryParams = (string | number | bigint | null)[]
|
|
149
|
+
|
|
150
|
+
/** Events generated by our {@link Connection} */
|
|
151
|
+
interface ConnectionEvents {
|
|
152
|
+
error: (error: Error) => unknown
|
|
153
|
+
connected: () => unknown
|
|
154
|
+
destroyed: () => unknown
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ========================================================================== *
|
|
158
|
+
* CONNECTION OPTIONS *
|
|
159
|
+
* ========================================================================== */
|
|
160
|
+
|
|
161
|
+
/** Convert our options into a string suitable for LibPQ */
|
|
162
|
+
export function convertOptions(options: ConnectionOptions): string {
|
|
163
|
+
const params: string[] = []
|
|
164
|
+
for (const [ option, value ] of Object.entries(options)) {
|
|
165
|
+
if (value == null) continue
|
|
166
|
+
|
|
167
|
+
const key = optionKeys[option as keyof ConnectionOptions]
|
|
168
|
+
if (! key) continue
|
|
169
|
+
|
|
170
|
+
const string =
|
|
171
|
+
typeof value === 'boolean' ? value ? '1' : '0' :
|
|
172
|
+
typeof value === 'number' ? value.toString() :
|
|
173
|
+
typeof value === 'string' ? value :
|
|
174
|
+
/* coverage ignore next */
|
|
175
|
+
assert.fail(`Invalid type for option ${option}`)
|
|
176
|
+
|
|
177
|
+
if (string.length === 0) continue
|
|
178
|
+
|
|
179
|
+
params.push(`${key}=${quoteParamValue(string)}`)
|
|
180
|
+
}
|
|
181
|
+
return params.join(' ')
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ========================================================================== *
|
|
185
|
+
* CONNECTION *
|
|
186
|
+
* ========================================================================== */
|
|
187
|
+
|
|
188
|
+
/** Our *minimalistic* PostgreSQL connection wrapping `libpq`. */
|
|
189
|
+
export class Connection extends Emitter<ConnectionEvents> {
|
|
190
|
+
/** The unique ID of this connection */
|
|
191
|
+
public id: string
|
|
192
|
+
|
|
193
|
+
/** Queue for serializing queries to the database */
|
|
194
|
+
private readonly _queue: Queue = new Queue()
|
|
195
|
+
/** Option string to use when calling `connect` */
|
|
196
|
+
private readonly _options: string
|
|
197
|
+
|
|
198
|
+
/** Current instance of `libpq` */
|
|
199
|
+
private _pq: LibPQ
|
|
200
|
+
/** A flag indicating that `destroy()` has been invoked... */
|
|
201
|
+
private _destroyed: boolean = false
|
|
202
|
+
|
|
203
|
+
/** Create a connection with the specified `LibPQ` parameters string */
|
|
204
|
+
constructor(logger: Logger, params?: string)
|
|
205
|
+
/** Create a connection with the specified configuration options */
|
|
206
|
+
constructor(logger: Logger, options?: ConnectionOptions)
|
|
207
|
+
/* Overloaded constructor */
|
|
208
|
+
constructor(logger: Logger, options: string | ConnectionOptions = {}) {
|
|
209
|
+
super(logger)
|
|
210
|
+
|
|
211
|
+
this.id = randomUUID()
|
|
212
|
+
this._logger = logger
|
|
213
|
+
const params = typeof options === 'string' ? options : convertOptions(options)
|
|
214
|
+
this._options = `fallback_application_name='pool:${this.id}' ${params}`
|
|
215
|
+
|
|
216
|
+
this._pq = new LibPQ()
|
|
217
|
+
finalizer.register(this, this._pq, this._pq)
|
|
218
|
+
|
|
219
|
+
this.on('error', () => {
|
|
220
|
+
finalizer.unregister(this._pq)
|
|
221
|
+
this._pq.finish()
|
|
222
|
+
this._destroyed = true
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
logger.debug(`Connection "${this.id}" created`)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ===== GETTERS ========================================================== */
|
|
229
|
+
|
|
230
|
+
/** Check whether this {@link Connection} is connected or not */
|
|
231
|
+
get connected(): boolean {
|
|
232
|
+
return !! this._pq.connected
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Check whether this {@link Connection} is destroyed or not */
|
|
236
|
+
get destroyed(): boolean {
|
|
237
|
+
return this._destroyed
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Return the version of the server we're connected to */
|
|
241
|
+
get serverVersion(): string {
|
|
242
|
+
assert(this._pq.connected, 'Not connected')
|
|
243
|
+
const version = this._pq.serverVersion()
|
|
244
|
+
return `${Math.floor(version / 10000)}.${version % 10000}`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ===== PUBLIC =========================================================== */
|
|
248
|
+
|
|
249
|
+
/** Connect this {@link Connection} (fails if connected already) */
|
|
250
|
+
async connect(): Promise<Connection> {
|
|
251
|
+
assert(! this._pq.connected, `Connection "${this.id}" already connected`)
|
|
252
|
+
assert(! this._destroyed, `Connection "${this.id}" already destroyed`)
|
|
253
|
+
|
|
254
|
+
this._logger.debug(`Connection "${this.id}" connecting`)
|
|
255
|
+
|
|
256
|
+
/* Turn LibPQ's own `connect` function into a promise */
|
|
257
|
+
const promise = new Promise<boolean>((resolve, reject) => {
|
|
258
|
+
this._pq.connect(this._options, (error) => {
|
|
259
|
+
/* On error, simply finish (regardless) and fail cleaning up the error */
|
|
260
|
+
if (error) {
|
|
261
|
+
return reject(new Error(error.message.trim() || 'Unknown connection error'))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Ensure that our connection is setup as non-blocking, fail otherwise */
|
|
265
|
+
if (! this._pq.setNonBlocking(true)) {
|
|
266
|
+
return reject(new Error(`Unable to set connection "${this.id}" as non-blocking`))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* Done, return LibPQ's connected status */
|
|
270
|
+
return resolve(this._pq.connected)
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
/* Rewrap the promise into an async/await to fix error stack traces */
|
|
275
|
+
try {
|
|
276
|
+
const connected = await promise
|
|
277
|
+
|
|
278
|
+
if (this._destroyed) throw new Error(`Connection "${this.id}" aborted`)
|
|
279
|
+
if (! connected) throw new Error(`Connection "${this.id}" not connected`)
|
|
280
|
+
|
|
281
|
+
this._logger.info(`Connection "${this.id}" connected (server version ${this.serverVersion})`)
|
|
282
|
+
this._emit('connected')
|
|
283
|
+
return this
|
|
284
|
+
} catch (error: any) {
|
|
285
|
+
if (error instanceof Error) Error.captureStackTrace(error)
|
|
286
|
+
|
|
287
|
+
finalizer.unregister(this._pq)
|
|
288
|
+
this._pq.finish()
|
|
289
|
+
this._destroyed = true
|
|
290
|
+
|
|
291
|
+
this._emit('error', error)
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Destroy this {@link Connection} releasing all related resources */
|
|
297
|
+
destroy(): void {
|
|
298
|
+
if (this._destroyed) return
|
|
299
|
+
|
|
300
|
+
finalizer.unregister(this._pq)
|
|
301
|
+
this._pq.finish()
|
|
302
|
+
this._destroyed = true
|
|
303
|
+
|
|
304
|
+
this._emit('destroyed')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** ===== QUERY INTERFACE ================================================= */
|
|
308
|
+
|
|
309
|
+
/** Execute a (possibly parameterised) query with this {@link Connection} */
|
|
310
|
+
async query(text: string, params?: ConnectionQueryParams): Promise<ConnectionQueryResult> {
|
|
311
|
+
/* Enqueue a new query, and return a Promise to its result */
|
|
312
|
+
const promise = this._queue.enqueue(() => {
|
|
313
|
+
assert(this._pq.connected, `Connection "${this.id}" not connected`)
|
|
314
|
+
|
|
315
|
+
/* Create a new "Query" handling all I/O and run it, wrapping the call
|
|
316
|
+
* in an async/await to properly contextualize error stack traces */
|
|
317
|
+
return new Query(this._pq, this._logger)
|
|
318
|
+
.on('error', (error) => this._emit('error', error))
|
|
319
|
+
.run(text, params)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
return await promise
|
|
324
|
+
} catch (error: any) {
|
|
325
|
+
if (error instanceof Error) Error.captureStackTrace(error)
|
|
326
|
+
throw error
|
|
327
|
+
} finally {
|
|
328
|
+
/* Forget the connection if the query terminated it */
|
|
329
|
+
if (! this._pq.connected) this.destroy()
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Cancel (if possible) the currently running query */
|
|
334
|
+
cancel(): void {
|
|
335
|
+
assert(this._pq.connected, `Connection "${this.id}" not connected`)
|
|
336
|
+
|
|
337
|
+
/* Remember, PQcancel creates a temporary connection to issue the cancel
|
|
338
|
+
* so it doesn't affect the current query (must still be read in full!) */
|
|
339
|
+
const cancel = this._pq.cancel()
|
|
340
|
+
if (cancel === true) return
|
|
341
|
+
|
|
342
|
+
/* coverage ignore next */
|
|
343
|
+
throw new Error(cancel || 'Unknown error canceling')
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* ========================================================================== *
|
|
348
|
+
* QUERY INTERFACE *
|
|
349
|
+
* ========================================================================== */
|
|
350
|
+
|
|
351
|
+
/** Internal implementation of a query, sending and awaiting a result */
|
|
352
|
+
class Query extends Emitter {
|
|
353
|
+
constructor(private _pq: LibPQ, logger: Logger) {
|
|
354
|
+
super(logger)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** Run a query, sending it and flushing it, then reading results */
|
|
358
|
+
run(text: string, params: ConnectionQueryParams = []): Promise<ConnectionQueryResult> {
|
|
359
|
+
return new Promise<ConnectionQueryResult>((resolve, reject) => {
|
|
360
|
+
/* Send the query to the server and check it was sent */
|
|
361
|
+
const sent = params.length > 0 ?
|
|
362
|
+
this._pq.sendQueryParams(text, params as any[]) :
|
|
363
|
+
this._pq.sendQuery(text)
|
|
364
|
+
|
|
365
|
+
if (! sent) throw this._fail('sendQuery', 'Unable to send query')
|
|
366
|
+
|
|
367
|
+
/* Make sure the query is flushed all the way without errors */
|
|
368
|
+
this._flushQuery((error) => {
|
|
369
|
+
if (error) return reject(error)
|
|
370
|
+
|
|
371
|
+
/* Prepare the callback to be executed when the connection is readable */
|
|
372
|
+
const readableCallback = (): void => this._read(onResult)
|
|
373
|
+
|
|
374
|
+
/* Prepare the callback to be executed when results are fully read */
|
|
375
|
+
const onResult = (error?: Error): void => {
|
|
376
|
+
/* Regardless on whether there was an error, stop reading... */
|
|
377
|
+
this._pq.stopReader()
|
|
378
|
+
this._pq.off('readable', readableCallback)
|
|
379
|
+
|
|
380
|
+
/* If there was an error, simply reject our result */
|
|
381
|
+
if (error) {
|
|
382
|
+
this._pq.clear()
|
|
383
|
+
return reject(error)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* If successful, prepare the result and resolve */
|
|
387
|
+
const result = this._createResult()
|
|
388
|
+
this._pq.clear()
|
|
389
|
+
resolve(result)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* Start the reading loop, */
|
|
393
|
+
this._pq.on('readable', readableCallback)
|
|
394
|
+
this._pq.startReader()
|
|
395
|
+
})
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* === ERRORS ============================================================= */
|
|
400
|
+
|
|
401
|
+
private _fail(syscall: string, message: string): Error {
|
|
402
|
+
const text = (this._pq.errorMessage() || '').trim() || message
|
|
403
|
+
const error = Object.assign(new Error(`${text} (${syscall})`), { syscall })
|
|
404
|
+
this._pq.finish()
|
|
405
|
+
|
|
406
|
+
this._emit('error', error)
|
|
407
|
+
return error
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* === INTERNALS ========================================================== */
|
|
411
|
+
|
|
412
|
+
private _error?: Error
|
|
413
|
+
|
|
414
|
+
private _flushQuery(cb: (error?: Error) => void): void {
|
|
415
|
+
const result = this._pq.flush()
|
|
416
|
+
|
|
417
|
+
/* No errors, continue */
|
|
418
|
+
if (result === 0) return cb() // 0 is "success"
|
|
419
|
+
|
|
420
|
+
/* Error flushing the query */
|
|
421
|
+
if (result === -1) cb(this._fail('flush', 'Unable to flush query'))
|
|
422
|
+
|
|
423
|
+
/* Not flushed yet, wait and retry */
|
|
424
|
+
this._pq.writable(/* coverage ignore next */ () => this._flushQuery(cb))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
private _read(onResult: (error?: Error) => void): void {
|
|
428
|
+
/* Read waiting data from the socket */
|
|
429
|
+
if (! this._pq.consumeInput()) {
|
|
430
|
+
return onResult(this._fail('consumeInput', 'Unable to consume input'))
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Check if there is still outstanding data, if so, wait for it all to come in */
|
|
434
|
+
if (this._pq.isBusy()) /* coverage ignore next */ return
|
|
435
|
+
|
|
436
|
+
/* Load our result object */
|
|
437
|
+
while (this._pq.getResult()) {
|
|
438
|
+
/* Check the status of the result */
|
|
439
|
+
const status = this._pq.resultStatus()
|
|
440
|
+
switch (status) {
|
|
441
|
+
case 'PGRES_FATAL_ERROR':
|
|
442
|
+
this._error = new Error(`SQL Fatal Error: ${this._pq.resultErrorMessage().trim()}`)
|
|
443
|
+
break
|
|
444
|
+
|
|
445
|
+
case 'PGRES_TUPLES_OK':
|
|
446
|
+
case 'PGRES_COMMAND_OK':
|
|
447
|
+
case 'PGRES_EMPTY_QUERY':
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
default:
|
|
451
|
+
return onResult(this._fail('resultStatus', `Unrecognized status ${status}`))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* If reading multiple results, sometimes the following results might
|
|
455
|
+
* cause a blocking read. in this scenario yield back off the reader
|
|
456
|
+
* until libpq is readable */
|
|
457
|
+
if (this._pq.isBusy()) /* coverage ignore next */ return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* All done, invoke our callback for completion! */
|
|
461
|
+
onResult(this._error)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* === RESULT ============================================================= */
|
|
465
|
+
|
|
466
|
+
/** Create a {@link ConnectionQueryResult} from the data currently held by `libpq` */
|
|
467
|
+
private _createResult(): ConnectionQueryResult {
|
|
468
|
+
const command = this._pq.cmdStatus().split(' ')[0]!
|
|
469
|
+
const rowCount = parseInt(this._pq.cmdTuples() || '0')
|
|
470
|
+
|
|
471
|
+
const nfields = this._pq.nfields()
|
|
472
|
+
const ntuples = this._pq.ntuples()
|
|
473
|
+
|
|
474
|
+
const fields: ConnectionQueryResult['fields'] = new Array(nfields)
|
|
475
|
+
const rows: ConnectionQueryResult['rows'] = new Array(ntuples)
|
|
476
|
+
|
|
477
|
+
/* Looad up all the fields (name & type) from the query results */
|
|
478
|
+
for (let i = 0; i < nfields; i++) {
|
|
479
|
+
fields[i] = [ this._pq.fname(i), this._pq.ftype(i) ]
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* Load up all the results, row-by-row, column-by-column */
|
|
483
|
+
for (let i = 0; i < ntuples; i++) {
|
|
484
|
+
const row: (string | null)[] = rows[i] = new Array(nfields)
|
|
485
|
+
for (let j = 0; j < nfields; j++) {
|
|
486
|
+
const value = this._pq.getvalue(i, j)
|
|
487
|
+
row[j] = (value === '') && (this._pq.getisnull(i, j)) ? null : value
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* All done */
|
|
492
|
+
return { command, rowCount, fields, rows }
|
|
493
|
+
}
|
|
494
|
+
}
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events'
|
|
2
|
+
|
|
3
|
+
import type { Logger } from './index'
|
|
4
|
+
|
|
5
|
+
interface Events {
|
|
6
|
+
error: (error: Error) => unknown
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type EventParams<E, K extends keyof E> =
|
|
10
|
+
E[K] extends ((...args: any[]) => unknown) ? Parameters<E[K]> : never
|
|
11
|
+
|
|
12
|
+
type EventCallback<E, K extends keyof E> =
|
|
13
|
+
E[K] extends ((...args: any[]) => unknown) ? E[K] : never
|
|
14
|
+
|
|
15
|
+
export class Emitter<E = Events> {
|
|
16
|
+
private _emitter = new EventEmitter()
|
|
17
|
+
|
|
18
|
+
constructor(protected _logger: Logger) {}
|
|
19
|
+
|
|
20
|
+
protected _emit<K extends keyof E>(event: K & string, ...args: EventParams<E, K>): void {
|
|
21
|
+
try {
|
|
22
|
+
this._emitter.emit(event, ...args)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
this._logger.error(`Error in "${event}" handler`, error)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
on<K extends keyof E>(event: K & string, callback: EventCallback<E, K>): this {
|
|
29
|
+
this._emitter.on(event, callback)
|
|
30
|
+
return this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
once<K extends keyof E>(event: K & string, callback: EventCallback<E, K>): this {
|
|
34
|
+
this._emitter.once(event, callback)
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
off<K extends keyof E>(event: K & string, callback: EventCallback<E, K>): this {
|
|
39
|
+
this._emitter.off(event, callback)
|
|
40
|
+
return this
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { Connection } from './connection'
|
|
2
|
+
export { ConnectionPool } from './pool'
|
|
3
|
+
|
|
4
|
+
export type { ConnectionOptions, ConnectionQueryResult as ConnectionQueryResult } from './connection'
|
|
5
|
+
export type { ConnectionPoolOptions, ConnectionPoolStats } from './pool'
|
|
6
|
+
|
|
7
|
+
/** A base Logger class that can be used to inject */
|
|
8
|
+
export interface Logger {
|
|
9
|
+
/** Log a message at `DEBUG` level */
|
|
10
|
+
readonly debug: (...args: any[]) => void
|
|
11
|
+
/** Log a message at `INFO` level */
|
|
12
|
+
readonly info: (...args: any[]) => void
|
|
13
|
+
/** Log a message at `WARN` level */
|
|
14
|
+
readonly warn: (...args: any[]) => void
|
|
15
|
+
/** Log a message at `ERROR` level */
|
|
16
|
+
readonly error: (...args: any[]) => void
|
|
17
|
+
}
|
package/src/libpq.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
|
|
3
|
+
import type libpq from 'libpq'
|
|
4
|
+
|
|
5
|
+
// LibPQ has a nasty tendency to emit the path of its source directory when
|
|
6
|
+
// the parent module is not specified, and this happens *always* in ESM mode.
|
|
7
|
+
// By manually creating the require function, we can avoid this (aesthetics)
|
|
8
|
+
export type LibPQ = libpq
|
|
9
|
+
export type LibPQConstructor = { new(): LibPQ }
|
|
10
|
+
export const LibPQ: LibPQConstructor = createRequire(__fileurl)('libpq')
|