@prairielearn/postgres 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/.turbo/turbo-build.log +0 -0
- package/README.md +126 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +4 -0
- package/dist/loader.js +44 -0
- package/dist/loader.js.map +1 -0
- package/dist/pool.d.ts +256 -0
- package/dist/pool.js +712 -0
- package/dist/pool.js.map +1 -0
- package/dist/pool.test.d.ts +1 -0
- package/dist/pool.test.js +68 -0
- package/dist/pool.test.js.map +1 -0
- package/package.json +22 -0
- package/src/index.ts +3 -0
- package/src/loader.ts +39 -0
- package/src/pool.test.ts +49 -0
- package/src/pool.ts +788 -0
- package/tsconfig.json +8 -0
package/src/pool.ts
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import _ from 'lodash';
|
|
2
|
+
import pg, { QueryResult } from 'pg';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import debugFactory from 'debug';
|
|
5
|
+
import { callbackify } from 'node:util';
|
|
6
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
|
+
|
|
8
|
+
type Params = Record<string, any> | any[];
|
|
9
|
+
|
|
10
|
+
const debug = debugFactory('prairielib:' + path.basename(__filename, '.js'));
|
|
11
|
+
const lastQueryMap: WeakMap<pg.PoolClient, string> = new WeakMap();
|
|
12
|
+
const searchSchemaMap: WeakMap<pg.PoolClient, string> = new WeakMap();
|
|
13
|
+
|
|
14
|
+
function addDataToError(err: Error, data: Record<string, any>): Error {
|
|
15
|
+
(err as any).data = {
|
|
16
|
+
...((err as any).data ?? {}),
|
|
17
|
+
...data,
|
|
18
|
+
};
|
|
19
|
+
return err;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PostgresError extends Error {
|
|
23
|
+
public data: Record<string, any>;
|
|
24
|
+
|
|
25
|
+
constructor(message: string, data: Record<string, any>) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.data = data;
|
|
28
|
+
this.name = 'PostgresError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Formats a string for debugging.
|
|
34
|
+
*/
|
|
35
|
+
function debugString(s: string): string {
|
|
36
|
+
if (!_.isString(s)) return 'NOT A STRING';
|
|
37
|
+
s = s.replace(/\n/g, '\\n');
|
|
38
|
+
if (s.length > 78) s = s.substring(0, 75) + '...';
|
|
39
|
+
s = '"' + s + '"';
|
|
40
|
+
return s;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Formats a set of params for debugging.
|
|
45
|
+
*/
|
|
46
|
+
function debugParams(params: Params): string {
|
|
47
|
+
let s;
|
|
48
|
+
try {
|
|
49
|
+
s = JSON.stringify(params);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
s = 'CANNOT JSON STRINGIFY';
|
|
52
|
+
}
|
|
53
|
+
return debugString(s);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Given an SQL string and params, creates an array of params and an SQL string
|
|
58
|
+
* with any named dollar-sign placeholders replaced with parameters.
|
|
59
|
+
*/
|
|
60
|
+
function paramsToArray(sql: string, params: Params): { processedSql: string; paramsArray: any } {
|
|
61
|
+
if (typeof sql !== 'string') throw new Error('SQL must be a string');
|
|
62
|
+
if (Array.isArray(params)) {
|
|
63
|
+
return {
|
|
64
|
+
processedSql: sql,
|
|
65
|
+
paramsArray: params,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!_.isObjectLike(params)) throw new Error('params must be array or object');
|
|
69
|
+
|
|
70
|
+
const re = /\$([-_a-zA-Z0-9]+)/;
|
|
71
|
+
let result;
|
|
72
|
+
let processedSql = '';
|
|
73
|
+
let remainingSql = sql;
|
|
74
|
+
let nParams = 0;
|
|
75
|
+
const map: Record<string, string> = {};
|
|
76
|
+
let paramsArray: any[] = [];
|
|
77
|
+
while ((result = re.exec(remainingSql)) !== null) {
|
|
78
|
+
const v = result[1];
|
|
79
|
+
if (!_(map).has(v)) {
|
|
80
|
+
if (!_(params).has(v)) throw new Error(`Missing parameter: ${v}`);
|
|
81
|
+
if (_.isArray(params[v])) {
|
|
82
|
+
map[v] =
|
|
83
|
+
'ARRAY[' +
|
|
84
|
+
_.map(_.range(nParams + 1, nParams + params[v].length + 1), function (n) {
|
|
85
|
+
return '$' + n;
|
|
86
|
+
}).join(',') +
|
|
87
|
+
']';
|
|
88
|
+
nParams += params[v].length;
|
|
89
|
+
paramsArray = paramsArray.concat(params[v]);
|
|
90
|
+
} else {
|
|
91
|
+
nParams++;
|
|
92
|
+
map[v] = '$' + nParams;
|
|
93
|
+
paramsArray.push(params[v]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
processedSql += remainingSql.substring(0, result.index) + map[v];
|
|
97
|
+
remainingSql = remainingSql.substring(result.index + result[0].length);
|
|
98
|
+
}
|
|
99
|
+
processedSql += remainingSql;
|
|
100
|
+
remainingSql = '';
|
|
101
|
+
return { processedSql, paramsArray };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Escapes the given identifier for use in an SQL query. Useful for preventing
|
|
106
|
+
* SQL injection.
|
|
107
|
+
*/
|
|
108
|
+
function escapeIdentifier(identifier: string): string {
|
|
109
|
+
// Note that as of 2021-06-29 escapeIdentifier() is undocumented. See:
|
|
110
|
+
// https://github.com/brianc/node-postgres/pull/396
|
|
111
|
+
// https://github.com/brianc/node-postgres/issues/1978
|
|
112
|
+
// https://www.postgresql.org/docs/12/sql-syntax-lexical.html
|
|
113
|
+
return pg.Client.prototype.escapeIdentifier(identifier);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class PostgresPool {
|
|
117
|
+
/** The pool from which clients will be acquired. */
|
|
118
|
+
private pool: pg.Pool | null = null;
|
|
119
|
+
/**
|
|
120
|
+
* We use this to propagate the client associated with the current transaction
|
|
121
|
+
* to any nested queries. In the past, we had some nasty bugs associated with
|
|
122
|
+
* the fact that we tried to acquire new clients inside of transactions, which
|
|
123
|
+
* ultimately lead to a deadlock.
|
|
124
|
+
*/
|
|
125
|
+
private alsClient: AsyncLocalStorage<pg.PoolClient> = new AsyncLocalStorage();
|
|
126
|
+
private searchSchema: string | null = null;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Creates a new connection pool and attempts to connect to the database.
|
|
130
|
+
*/
|
|
131
|
+
async initAsync(
|
|
132
|
+
pgConfig: pg.PoolConfig,
|
|
133
|
+
idleErrorHandler: (error: Error, client: pg.PoolClient) => void
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
this.pool = new pg.Pool(pgConfig);
|
|
136
|
+
this.pool.on('error', function (err, client) {
|
|
137
|
+
const lastQuery = lastQueryMap.get(client);
|
|
138
|
+
idleErrorHandler(addDataToError(err, { lastQuery }), client);
|
|
139
|
+
});
|
|
140
|
+
this.pool.on('connect', (client) => {
|
|
141
|
+
client.on('error', (err) => {
|
|
142
|
+
const lastQuery = lastQueryMap.get(client);
|
|
143
|
+
idleErrorHandler(addDataToError(err, { lastQuery }), client);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
this.pool.on('remove', (client) => {
|
|
147
|
+
// This shouldn't be necessary, as `pg` currently allows clients to be
|
|
148
|
+
// garbage collected after they're removed. However, if `pg` someday
|
|
149
|
+
// starts reusing client objects across difference connections, this
|
|
150
|
+
// will ensure that we re-set the search path when the client reconnects.
|
|
151
|
+
searchSchemaMap.delete(client);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Attempt to connect to the database so that we can fail quickly if
|
|
155
|
+
// something isn't configured correctly.
|
|
156
|
+
let retryCount = 0;
|
|
157
|
+
const retryTimeouts = [500, 1000, 2000, 5000, 10000];
|
|
158
|
+
while (retryCount <= retryTimeouts.length) {
|
|
159
|
+
try {
|
|
160
|
+
const client = await this.pool.connect();
|
|
161
|
+
client.release();
|
|
162
|
+
return;
|
|
163
|
+
} catch (err: any) {
|
|
164
|
+
if (retryCount === retryTimeouts.length) {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`Could not connect to Postgres after ${retryTimeouts.length} attempts: ${err.message}`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const timeout = retryTimeouts[retryCount];
|
|
171
|
+
retryCount++;
|
|
172
|
+
await new Promise((resolve) => setTimeout(resolve, timeout));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Creates a new connection pool and attempts to connect to the database.
|
|
179
|
+
*/
|
|
180
|
+
init = callbackify(this.initAsync);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Closes the connection pool.
|
|
184
|
+
*/
|
|
185
|
+
async closeAsync(): Promise<void> {
|
|
186
|
+
if (!this.pool) return;
|
|
187
|
+
await this.pool.end();
|
|
188
|
+
this.pool = null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Closes the connection pool.
|
|
193
|
+
*/
|
|
194
|
+
close = callbackify(this.closeAsync);
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Gets a new client from the connection pool. If `err` is not null
|
|
198
|
+
* then `client` and `done` are undefined. If `err` is null then
|
|
199
|
+
* `client` is valid and can be used. The caller MUST call `done()` to
|
|
200
|
+
* release the client, whether or not errors occurred while using
|
|
201
|
+
* `client`. The client can call `done(truthy_value)` to force
|
|
202
|
+
* destruction of the client, but this should not be used except in
|
|
203
|
+
* unusual circumstances.
|
|
204
|
+
*/
|
|
205
|
+
async getClientAsync(): Promise<pg.PoolClient> {
|
|
206
|
+
if (!this.pool) {
|
|
207
|
+
throw new Error('Connection pool is not open');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// If we're inside a transaction, we'll reuse the same client to avoid a
|
|
211
|
+
// potential deadlock.
|
|
212
|
+
let client = this.alsClient.getStore() ?? (await this.pool.connect());
|
|
213
|
+
|
|
214
|
+
// If we're configured to use a particular schema, we'll store whether or
|
|
215
|
+
// not the search path has already been configured for this particular
|
|
216
|
+
// client. If we acquire a client and it's already had its search path
|
|
217
|
+
// set, we can avoid setting it again since the search path will persist
|
|
218
|
+
// for the life of the client.
|
|
219
|
+
//
|
|
220
|
+
// We do this check for each call to `getClient` instead of on
|
|
221
|
+
// `pool.connect` so that we don't have to be really careful about
|
|
222
|
+
// destroying old clients that were created before `setSearchSchema` was
|
|
223
|
+
// called. Instead, we'll just check if the search path matches the
|
|
224
|
+
// currently-desired schema, and if it's a mismatch (or doesn't exist
|
|
225
|
+
// at all), we re-set it for the current client.
|
|
226
|
+
//
|
|
227
|
+
// Note that this accidentally supports changing the search_path on the fly,
|
|
228
|
+
// although that's not something we currently do (or would be likely to do).
|
|
229
|
+
// It does NOT support clearing the existing search schema - e.g.,
|
|
230
|
+
// `setSearchSchema(null)` would not work as you expect. This is fine, as
|
|
231
|
+
// that's not something we ever do in practice.
|
|
232
|
+
const clientSearchSchema = searchSchemaMap.get(client);
|
|
233
|
+
if (this.searchSchema != null && clientSearchSchema !== this.searchSchema) {
|
|
234
|
+
const setSearchPathSql = `SET search_path TO ${escapeIdentifier(this.searchSchema)},public`;
|
|
235
|
+
try {
|
|
236
|
+
await this.queryWithClientAsync(client, setSearchPathSql, {});
|
|
237
|
+
} catch (err) {
|
|
238
|
+
client.release();
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
searchSchemaMap.set(client, this.searchSchema);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return client;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Gets a new client from the connection pool.
|
|
249
|
+
*/
|
|
250
|
+
getClient(callback: (error: Error | null, client?: pg.PoolClient, done?: () => void) => void) {
|
|
251
|
+
this.getClientAsync()
|
|
252
|
+
.then((client) => callback(null, client, client.release))
|
|
253
|
+
.catch((err) => callback(err));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Performs a query with the given client.
|
|
258
|
+
*/
|
|
259
|
+
async queryWithClientAsync(
|
|
260
|
+
client: pg.PoolClient,
|
|
261
|
+
sql: string,
|
|
262
|
+
params: Params
|
|
263
|
+
): Promise<pg.QueryResult> {
|
|
264
|
+
debug('queryWithClient()', 'sql:', debugString(sql));
|
|
265
|
+
debug('queryWithClient()', 'params:', debugParams(params));
|
|
266
|
+
const { processedSql, paramsArray } = paramsToArray(sql, params);
|
|
267
|
+
try {
|
|
268
|
+
lastQueryMap.set(client, processedSql);
|
|
269
|
+
const result = await client.query(processedSql, paramsArray);
|
|
270
|
+
debug('queryWithClient() success', 'rowCount:', result.rowCount);
|
|
271
|
+
return result;
|
|
272
|
+
} catch (err: any) {
|
|
273
|
+
// TODO: why do we do this?
|
|
274
|
+
const sqlError = JSON.parse(JSON.stringify(err));
|
|
275
|
+
sqlError.message = err.message;
|
|
276
|
+
throw addDataToError(err, {
|
|
277
|
+
sqlError: sqlError,
|
|
278
|
+
sql: sql,
|
|
279
|
+
sqlParams: params,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Performs a query with the given client.
|
|
286
|
+
*/
|
|
287
|
+
queryWithClient = callbackify(this.queryWithClientAsync);
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Performs a query with the given client. Errors if the query returns more
|
|
291
|
+
* than one row.
|
|
292
|
+
*/
|
|
293
|
+
async queryWithClientOneRowAsync(
|
|
294
|
+
client: pg.PoolClient,
|
|
295
|
+
sql: string,
|
|
296
|
+
params: Params
|
|
297
|
+
): Promise<pg.QueryResult> {
|
|
298
|
+
debug('queryWithClientOneRow()', 'sql:', debugString(sql));
|
|
299
|
+
debug('queryWithClientOneRow()', 'params:', debugParams(params));
|
|
300
|
+
const result = await this.queryWithClientAsync(client, sql, params);
|
|
301
|
+
if (result.rowCount !== 1) {
|
|
302
|
+
throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {
|
|
303
|
+
sql,
|
|
304
|
+
sqlParams: params,
|
|
305
|
+
result,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
debug('queryWithClientOneRow() success', 'rowCount:', result.rowCount);
|
|
309
|
+
return result;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Performs a query with the given client. Errors if the query returns more
|
|
314
|
+
* than one row.
|
|
315
|
+
*/
|
|
316
|
+
queryWithClientOneRow = callbackify(this.queryWithClientOneRowAsync);
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Performs a query with the given client. Errors if the query returns more
|
|
320
|
+
* than one row.
|
|
321
|
+
*/
|
|
322
|
+
async queryWithClientZeroOrOneRowAsync(
|
|
323
|
+
client: pg.PoolClient,
|
|
324
|
+
sql: string,
|
|
325
|
+
params: Params
|
|
326
|
+
): Promise<QueryResult> {
|
|
327
|
+
debug('queryWithClientZeroOrOneRow()', 'sql:', debugString(sql));
|
|
328
|
+
debug('queryWithClientZeroOrOneRow()', 'params:', debugParams(params));
|
|
329
|
+
const result = await this.queryWithClientAsync(client, sql, params);
|
|
330
|
+
if (result.rowCount > 1) {
|
|
331
|
+
throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {
|
|
332
|
+
sql,
|
|
333
|
+
sqlParams: params,
|
|
334
|
+
result,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
debug('queryWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Performs a query with the given client. Errors if the query returns more
|
|
343
|
+
* than one row.
|
|
344
|
+
*/
|
|
345
|
+
queryWithClientZeroOrOneRow = callbackify(this.queryWithClientZeroOrOneRowAsync);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Rolls back the current transaction for the given client.
|
|
349
|
+
*/
|
|
350
|
+
async rollbackWithClientAsync(client: pg.PoolClient) {
|
|
351
|
+
debug('rollbackWithClient()');
|
|
352
|
+
// From https://node-postgres.com/features/transactions
|
|
353
|
+
try {
|
|
354
|
+
await client.query('ROLLBACK');
|
|
355
|
+
// Only release the client if we weren't already inside a transaction.
|
|
356
|
+
if (this.alsClient.getStore() === undefined) {
|
|
357
|
+
client.release();
|
|
358
|
+
}
|
|
359
|
+
} catch (err: any) {
|
|
360
|
+
// If there was a problem rolling back the query, something is
|
|
361
|
+
// seriously messed up. Return the error to the release() function to
|
|
362
|
+
// close & remove this client from the pool. If you leave a client in
|
|
363
|
+
// the pool with an unaborted transaction, weird and hard to diagnose
|
|
364
|
+
// problems might happen.
|
|
365
|
+
client.release(err);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Rolls back the current transaction for the given client.
|
|
371
|
+
*/
|
|
372
|
+
rollbackWithClient(
|
|
373
|
+
client: pg.PoolClient,
|
|
374
|
+
_done: (release?: any) => void,
|
|
375
|
+
callback: (err: Error | null) => void
|
|
376
|
+
) {
|
|
377
|
+
// Note that we can't use `util.callbackify` here because this function
|
|
378
|
+
// has an additional unused `done` parameter for backwards compatibility.
|
|
379
|
+
this.rollbackWithClientAsync(client)
|
|
380
|
+
.then(() => callback(null))
|
|
381
|
+
.catch((err) => callback(err));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Begins a new transaction.
|
|
386
|
+
*/
|
|
387
|
+
async beginTransactionAsync(): Promise<pg.PoolClient> {
|
|
388
|
+
debug('beginTransaction()');
|
|
389
|
+
const client = await this.getClientAsync();
|
|
390
|
+
try {
|
|
391
|
+
await this.queryWithClientAsync(client, 'START TRANSACTION;', {});
|
|
392
|
+
return client;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
await this.rollbackWithClientAsync(client);
|
|
395
|
+
throw err;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Commits the transaction if err is null, otherwise rollbacks the transaction.
|
|
401
|
+
* Also releases the client.
|
|
402
|
+
*/
|
|
403
|
+
async endTransactionAsync(client: pg.PoolClient, err: Error | null | undefined) {
|
|
404
|
+
debug('endTransaction()');
|
|
405
|
+
if (err) {
|
|
406
|
+
try {
|
|
407
|
+
await this.rollbackWithClientAsync(client);
|
|
408
|
+
} catch (rollbackErr: any) {
|
|
409
|
+
throw addDataToError(rollbackErr, { prevErr: err, rollback: 'fail' });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Even though we successfully rolled back the transaction, there was
|
|
413
|
+
// still an error in the first place that necessitated a rollback. Re-throw
|
|
414
|
+
// that error here so that everything downstream of here will know about it.
|
|
415
|
+
throw addDataToError(err, { rollback: 'success' });
|
|
416
|
+
} else {
|
|
417
|
+
try {
|
|
418
|
+
await this.queryWithClientAsync(client, 'COMMIT', {});
|
|
419
|
+
} finally {
|
|
420
|
+
// Only release the client if we aren't nested inside another transaction.
|
|
421
|
+
if (this.alsClient.getStore() === undefined) {
|
|
422
|
+
client.release();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Commits the transaction if err is null, otherwise rollbacks the transaction.
|
|
430
|
+
* Also releases the client.
|
|
431
|
+
*/
|
|
432
|
+
endTransaction(
|
|
433
|
+
client: pg.PoolClient,
|
|
434
|
+
_done: (rollback?: any) => void,
|
|
435
|
+
err: Error | null | undefined,
|
|
436
|
+
callback: (error: Error | null) => void
|
|
437
|
+
): void {
|
|
438
|
+
this.endTransactionAsync(client, err)
|
|
439
|
+
.then(() => callback(null))
|
|
440
|
+
.catch((error) => callback(error));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Runs the specified function inside of a transaction. The function will
|
|
445
|
+
* receive a database client as an argument, but it can also make queries
|
|
446
|
+
* as usual, and the correct client will be used automatically.
|
|
447
|
+
*
|
|
448
|
+
* The transaction will be rolled back if the function throws an error, and
|
|
449
|
+
* will be committed otherwise.
|
|
450
|
+
*/
|
|
451
|
+
async runInTransactionAsync(fn: (client: pg.PoolClient) => Promise<void>): Promise<void> {
|
|
452
|
+
const client = await this.beginTransactionAsync();
|
|
453
|
+
try {
|
|
454
|
+
await this.alsClient.run(client, () => fn(client));
|
|
455
|
+
} catch (err: any) {
|
|
456
|
+
await this.endTransactionAsync(client, err);
|
|
457
|
+
throw err;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Note that we don't invoke `endTransactionAsync` inside the `try` block
|
|
461
|
+
// because we don't want an error thrown by it to trigger *another* call
|
|
462
|
+
// to `endTransactionAsync` in the `catch` block.
|
|
463
|
+
await this.endTransactionAsync(client, null);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Executes a query with the specified parameters.
|
|
468
|
+
*/
|
|
469
|
+
async queryAsync(sql: string, params: Params): Promise<QueryResult> {
|
|
470
|
+
debug('query()', 'sql:', debugString(sql));
|
|
471
|
+
debug('query()', 'params:', debugParams(params));
|
|
472
|
+
const client = await this.getClientAsync();
|
|
473
|
+
try {
|
|
474
|
+
return await this.queryWithClientAsync(client, sql, params);
|
|
475
|
+
} finally {
|
|
476
|
+
// Only release if we aren't nested in a transaction.
|
|
477
|
+
if (this.alsClient.getStore() === undefined) {
|
|
478
|
+
client.release();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Executes a query with the specified parameters.
|
|
485
|
+
*/
|
|
486
|
+
query = callbackify(this.queryAsync);
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Executes a query with the specified parameters. Errors if the query does
|
|
490
|
+
* not return exactly one row.
|
|
491
|
+
*/
|
|
492
|
+
async queryOneRowAsync(sql: string, params: Params): Promise<pg.QueryResult> {
|
|
493
|
+
debug('queryOneRow()', 'sql:', debugString(sql));
|
|
494
|
+
debug('queryOneRow()', 'params:', debugParams(params));
|
|
495
|
+
const result = await this.queryAsync(sql, params);
|
|
496
|
+
if (result.rowCount !== 1) {
|
|
497
|
+
throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {
|
|
498
|
+
sql,
|
|
499
|
+
sqlParams: params,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
debug('queryOneRow() success', 'rowCount:', result.rowCount);
|
|
503
|
+
return result;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Executes a query with the specified parameters. Errors if the query does
|
|
508
|
+
* not return exactly one row.
|
|
509
|
+
*/
|
|
510
|
+
queryOneRow = callbackify(this.queryOneRowAsync);
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Executes a query with the specified parameters. Errors if the query
|
|
514
|
+
* returns more than one row.
|
|
515
|
+
*/
|
|
516
|
+
async queryZeroOrOneRowAsync(sql: string, params: Params): Promise<pg.QueryResult> {
|
|
517
|
+
debug('queryZeroOrOneRow()', 'sql:', debugString(sql));
|
|
518
|
+
debug('queryZeroOrOneRow()', 'params:', debugParams(params));
|
|
519
|
+
const result = await this.queryAsync(sql, params);
|
|
520
|
+
if (result.rowCount > 1) {
|
|
521
|
+
throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {
|
|
522
|
+
sql,
|
|
523
|
+
sqlParams: params,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
debug('queryZeroOrOneRow() success', 'rowCount:', result.rowCount);
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Executes a query with the specified parameters. Errors if the query
|
|
532
|
+
* returns more than one row.
|
|
533
|
+
*/
|
|
534
|
+
queryZeroOrOneRow = callbackify(this.queryZeroOrOneRowAsync);
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Calls the given function with the specified parameters.
|
|
538
|
+
*/
|
|
539
|
+
async callAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {
|
|
540
|
+
debug('call()', 'function:', functionName);
|
|
541
|
+
debug('call()', 'params:', debugParams(params));
|
|
542
|
+
const placeholders = _.map(_.range(1, params.length + 1), (v) => '$' + v).join();
|
|
543
|
+
const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders});`;
|
|
544
|
+
const result = await this.queryAsync(sql, params);
|
|
545
|
+
debug('call() success', 'rowCount:', result.rowCount);
|
|
546
|
+
return result;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Calls the given function with the specified parameters.
|
|
551
|
+
*/
|
|
552
|
+
call = callbackify(this.callAsync);
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Calls the given function with the specified parameters. Errors if the
|
|
556
|
+
* function does not return exactly one row.
|
|
557
|
+
*/
|
|
558
|
+
async callOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {
|
|
559
|
+
debug('callOneRow()', 'function:', functionName);
|
|
560
|
+
debug('callOneRow()', 'params:', debugParams(params));
|
|
561
|
+
const result = await this.callAsync(functionName, params);
|
|
562
|
+
if (result.rowCount !== 1) {
|
|
563
|
+
throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {
|
|
564
|
+
functionName,
|
|
565
|
+
sqlParams: params,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
debug('callOneRow() success', 'rowCount:', result.rowCount);
|
|
569
|
+
return result;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Calls the given function with the specified parameters. Errors if the
|
|
574
|
+
* function does not return exactly one row.
|
|
575
|
+
*/
|
|
576
|
+
callOneRow = callbackify(this.callOneRowAsync);
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Calls the given function with the specified parameters. Errors if the
|
|
580
|
+
* function returns more than one row.
|
|
581
|
+
*/
|
|
582
|
+
async callZeroOrOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {
|
|
583
|
+
debug('callZeroOrOneRow()', 'function:', functionName);
|
|
584
|
+
debug('callZeroOrOneRow()', 'params:', debugParams(params));
|
|
585
|
+
const result = await this.callAsync(functionName, params);
|
|
586
|
+
if (result.rowCount > 1) {
|
|
587
|
+
throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {
|
|
588
|
+
functionName,
|
|
589
|
+
sqlParams: params,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
debug('callZeroOrOneRow() success', 'rowCount:', result.rowCount);
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Calls the given function with the specified parameters. Errors if the
|
|
598
|
+
* function returns more than one row.
|
|
599
|
+
*/
|
|
600
|
+
callZeroOrOneRow = callbackify(this.callZeroOrOneRowAsync);
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Calls a function with the specified parameters using a specific client.
|
|
604
|
+
*/
|
|
605
|
+
async callWithClientAsync(
|
|
606
|
+
client: pg.PoolClient,
|
|
607
|
+
functionName: string,
|
|
608
|
+
params: any[]
|
|
609
|
+
): Promise<pg.QueryResult> {
|
|
610
|
+
debug('callWithClient()', 'function:', functionName);
|
|
611
|
+
debug('callWithClient()', 'params:', debugParams(params));
|
|
612
|
+
const placeholders = _.map(_.range(1, params.length + 1), (v) => '$' + v).join();
|
|
613
|
+
const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders})`;
|
|
614
|
+
const result = await this.queryWithClientAsync(client, sql, params);
|
|
615
|
+
debug('callWithClient() success', 'rowCount:', result.rowCount);
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Calls a function with the specified parameters using a specific client.
|
|
621
|
+
*/
|
|
622
|
+
callWithClient = callbackify(this.callWithClientAsync);
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Calls a function with the specified parameters using a specific client.
|
|
626
|
+
* Errors if the function does not return exactly one row.
|
|
627
|
+
*/
|
|
628
|
+
async callWithClientOneRowAsync(
|
|
629
|
+
client: pg.PoolClient,
|
|
630
|
+
functionName: string,
|
|
631
|
+
params: any[]
|
|
632
|
+
): Promise<pg.QueryResult> {
|
|
633
|
+
debug('callWithClientOneRow()', 'function:', functionName);
|
|
634
|
+
debug('callWithClientOneRow()', 'params:', debugParams(params));
|
|
635
|
+
const result = await this.callWithClientAsync(client, functionName, params);
|
|
636
|
+
if (result.rowCount !== 1) {
|
|
637
|
+
throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {
|
|
638
|
+
functionName,
|
|
639
|
+
sqlParams: params,
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
debug('callWithClientOneRow() success', 'rowCount:', result.rowCount);
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Calls a function with the specified parameters using a specific client.
|
|
648
|
+
* Errors if the function does not return exactly one row.
|
|
649
|
+
*/
|
|
650
|
+
callWithClientOneRow = callbackify(this.callWithClientOneRowAsync);
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Calls a function with the specified parameters using a specific client.
|
|
654
|
+
* Errors if the function returns more than one row.
|
|
655
|
+
*/
|
|
656
|
+
async callWithClientZeroOrOneRowAsync(
|
|
657
|
+
client: pg.PoolClient,
|
|
658
|
+
functionName: string,
|
|
659
|
+
params: any[]
|
|
660
|
+
): Promise<pg.QueryResult> {
|
|
661
|
+
debug('callWithClientZeroOrOneRow()', 'function:', functionName);
|
|
662
|
+
debug('callWithClientZeroOrOneRow()', 'params:', debugParams(params));
|
|
663
|
+
const result = await this.callWithClientAsync(client, functionName, params);
|
|
664
|
+
if (result.rowCount > 1) {
|
|
665
|
+
throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {
|
|
666
|
+
functionName,
|
|
667
|
+
sqlParams: params,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
debug('callWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Calls a function with the specified parameters using a specific client.
|
|
676
|
+
* Errors if the function returns more than one row.
|
|
677
|
+
*/
|
|
678
|
+
callWithClientZeroOrOneRow = callbackify(this.callWithClientZeroOrOneRowAsync);
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Set the schema to use for the search path.
|
|
682
|
+
*
|
|
683
|
+
* @param schema The schema name to use (can be "null" to unset the search path)
|
|
684
|
+
*/
|
|
685
|
+
async setSearchSchema(schema: string) {
|
|
686
|
+
if (schema == null) {
|
|
687
|
+
this.searchSchema = schema;
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
await this.queryAsync(`CREATE SCHEMA IF NOT EXISTS ${escapeIdentifier(schema)}`, {});
|
|
692
|
+
// We only set searchSchema after CREATE to avoid the above query() call using searchSchema.
|
|
693
|
+
this.searchSchema = schema;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get the schema that is currently used for the search path.
|
|
698
|
+
*
|
|
699
|
+
* @return schema in use (may be `null` to indicate no schema)
|
|
700
|
+
*/
|
|
701
|
+
getSearchSchema(): string | null {
|
|
702
|
+
return this.searchSchema;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Generate, set, and return a random schema name.
|
|
707
|
+
*
|
|
708
|
+
* @param prefix The prefix of the new schema, only the first 28 characters will be used (after lowercasing).
|
|
709
|
+
* @returns The randomly-generated search schema.
|
|
710
|
+
*/
|
|
711
|
+
async setRandomSearchSchemaAsync(prefix: string): Promise<string> {
|
|
712
|
+
// truncated prefix (max 28 characters)
|
|
713
|
+
const truncPrefix = prefix.substring(0, 28);
|
|
714
|
+
// timestamp in format YYYY-MM-DDTHH:MM:SS.SSSZ (guaranteed to not exceed 27 characters in the spec)
|
|
715
|
+
const timestamp = new Date().toISOString();
|
|
716
|
+
// random 6-character suffix to avoid clashes (approx 2 billion possible values)
|
|
717
|
+
const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
|
718
|
+
const suffix = _.times(6, function () {
|
|
719
|
+
return _.sample(chars);
|
|
720
|
+
}).join('');
|
|
721
|
+
|
|
722
|
+
// Schema is guaranteed to have length at most 63 (= 28 + 1 + 27 + 1 + 6),
|
|
723
|
+
// which is the default PostgreSQL identifier limit.
|
|
724
|
+
// Note that this schema name will need quoting because of characters like ':', '-', etc
|
|
725
|
+
const schema = `${truncPrefix}_${timestamp}_${suffix}`;
|
|
726
|
+
await this.setSearchSchema(schema);
|
|
727
|
+
return schema;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Generate, set, and return a random schema name.
|
|
732
|
+
*/
|
|
733
|
+
setRandomSearchSchema = callbackify(this.setRandomSearchSchemaAsync);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const defaultPool = new PostgresPool();
|
|
737
|
+
|
|
738
|
+
// We re-expose all functions from the default pool here to account for the
|
|
739
|
+
// default case of a shared global pool of clients. If someone want to create
|
|
740
|
+
// their own pool, we expose the `PostgresPool` class.
|
|
741
|
+
//
|
|
742
|
+
// Note that we explicitly bind all functions to `defaultPool`. This ensures
|
|
743
|
+
// that they'll be invoked with the correct `this` context, specifically when
|
|
744
|
+
// this module is imported as `import * as db from '...'` and that import is
|
|
745
|
+
// subsequently transformed by Babel to `interopRequireWildcard(...)`.
|
|
746
|
+
export const init = defaultPool.init.bind(defaultPool);
|
|
747
|
+
export const initAsync = defaultPool.initAsync.bind(defaultPool);
|
|
748
|
+
export const close = defaultPool.close.bind(defaultPool);
|
|
749
|
+
export const closeAsync = defaultPool.closeAsync.bind(defaultPool);
|
|
750
|
+
export const getClientAsync = defaultPool.getClientAsync.bind(defaultPool);
|
|
751
|
+
export const getClient = defaultPool.getClient.bind(defaultPool);
|
|
752
|
+
export const queryWithClient = defaultPool.queryWithClient.bind(defaultPool);
|
|
753
|
+
export const queryWithClientAsync = defaultPool.queryWithClientAsync.bind(defaultPool);
|
|
754
|
+
export const queryWithClientOneRow = defaultPool.queryWithClientOneRow.bind(defaultPool);
|
|
755
|
+
export const queryWithClientOneRowAsync = defaultPool.queryWithClientOneRowAsync.bind(defaultPool);
|
|
756
|
+
export const queryWithClientZeroOrOneRow =
|
|
757
|
+
defaultPool.queryWithClientZeroOrOneRow.bind(defaultPool);
|
|
758
|
+
export const queryWithClientZeroOrOneRowAsync =
|
|
759
|
+
defaultPool.queryWithClientZeroOrOneRowAsync.bind(defaultPool);
|
|
760
|
+
export const rollbackWithClientAsync = defaultPool.rollbackWithClientAsync.bind(defaultPool);
|
|
761
|
+
export const rollbackWithClient = defaultPool.rollbackWithClient.bind(defaultPool);
|
|
762
|
+
export const beginTransactionAsync = defaultPool.beginTransactionAsync.bind(defaultPool);
|
|
763
|
+
export const endTransactionAsync = defaultPool.endTransactionAsync.bind(defaultPool);
|
|
764
|
+
export const endTransaction = defaultPool.endTransaction.bind(defaultPool);
|
|
765
|
+
export const runInTransactionAsync = defaultPool.runInTransactionAsync.bind(defaultPool);
|
|
766
|
+
export const query = defaultPool.query.bind(defaultPool);
|
|
767
|
+
export const queryAsync = defaultPool.queryAsync.bind(defaultPool);
|
|
768
|
+
export const queryOneRow = defaultPool.queryOneRow.bind(defaultPool);
|
|
769
|
+
export const queryOneRowAsync = defaultPool.queryOneRowAsync.bind(defaultPool);
|
|
770
|
+
export const queryZeroOrOneRow = defaultPool.queryZeroOrOneRow.bind(defaultPool);
|
|
771
|
+
export const queryZeroOrOneRowAsync = defaultPool.queryZeroOrOneRowAsync.bind(defaultPool);
|
|
772
|
+
export const call = defaultPool.call.bind(defaultPool);
|
|
773
|
+
export const callAsync = defaultPool.callAsync.bind(defaultPool);
|
|
774
|
+
export const callOneRow = defaultPool.callOneRow.bind(defaultPool);
|
|
775
|
+
export const callOneRowAsync = defaultPool.callOneRowAsync.bind(defaultPool);
|
|
776
|
+
export const callZeroOrOneRow = defaultPool.callZeroOrOneRow.bind(defaultPool);
|
|
777
|
+
export const callZeroOrOneRowAsync = defaultPool.callZeroOrOneRowAsync.bind(defaultPool);
|
|
778
|
+
export const callWithClient = defaultPool.callWithClient.bind(defaultPool);
|
|
779
|
+
export const callWithClientAsync = defaultPool.callWithClientAsync.bind(defaultPool);
|
|
780
|
+
export const callWithClientOneRow = defaultPool.callWithClientOneRow.bind(defaultPool);
|
|
781
|
+
export const callWithClientOneRowAsync = defaultPool.callWithClientOneRowAsync.bind(defaultPool);
|
|
782
|
+
export const callWithClientZeroOrOneRow = defaultPool.callWithClientZeroOrOneRow.bind(defaultPool);
|
|
783
|
+
export const callWithClientZeroOrOneRowAsync =
|
|
784
|
+
defaultPool.callWithClientZeroOrOneRowAsync.bind(defaultPool);
|
|
785
|
+
export const setSearchSchema = defaultPool.setSearchSchema.bind(defaultPool);
|
|
786
|
+
export const getSearchSchema = defaultPool.getSearchSchema.bind(defaultPool);
|
|
787
|
+
export const setRandomSearchSchema = defaultPool.setRandomSearchSchema.bind(defaultPool);
|
|
788
|
+
export const setRandomSearchSchemaAsync = defaultPool.setRandomSearchSchemaAsync.bind(defaultPool);
|