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