@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/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);