@prairielearn/postgres 1.2.0 → 1.4.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/CHANGELOG.md +12 -0
- package/README.md +35 -0
- package/dist/default-pool.d.ts +24 -18
- package/dist/default-pool.js +6 -1
- package/dist/default-pool.js.map +1 -1
- package/dist/default-pool.test.js +19 -14
- package/dist/default-pool.test.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/pool.d.ts +66 -38
- package/dist/pool.js +112 -26
- package/dist/pool.js.map +1 -1
- package/package.json +7 -3
- package/src/default-pool.test.ts +22 -17
- package/src/default-pool.ts +4 -0
- package/src/index.ts +1 -0
- package/src/pool.ts +160 -41
package/src/pool.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import pg, { QueryResult } from 'pg';
|
|
3
|
+
import Cursor from 'pg-cursor';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import debugFactory from 'debug';
|
|
5
6
|
import { callbackify } from 'node:util';
|
|
6
7
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
8
|
import { z } from 'zod';
|
|
8
9
|
|
|
9
|
-
type
|
|
10
|
+
export type QueryParams = Record<string, any> | any[];
|
|
11
|
+
|
|
12
|
+
export interface CursorIterator<T> {
|
|
13
|
+
iterate: (batchSize: number) => AsyncGenerator<T[]>;
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
const debug = debugFactory('prairielib:' + path.basename(__filename, '.js'));
|
|
12
17
|
const lastQueryMap: WeakMap<pg.PoolClient, string> = new WeakMap();
|
|
@@ -44,7 +49,7 @@ function debugString(s: string): string {
|
|
|
44
49
|
/**
|
|
45
50
|
* Formats a set of params for debugging.
|
|
46
51
|
*/
|
|
47
|
-
function debugParams(params:
|
|
52
|
+
function debugParams(params: QueryParams): string {
|
|
48
53
|
let s;
|
|
49
54
|
try {
|
|
50
55
|
s = JSON.stringify(params);
|
|
@@ -58,7 +63,10 @@ function debugParams(params: Params): string {
|
|
|
58
63
|
* Given an SQL string and params, creates an array of params and an SQL string
|
|
59
64
|
* with any named dollar-sign placeholders replaced with parameters.
|
|
60
65
|
*/
|
|
61
|
-
function paramsToArray(
|
|
66
|
+
function paramsToArray(
|
|
67
|
+
sql: string,
|
|
68
|
+
params: QueryParams
|
|
69
|
+
): { processedSql: string; paramsArray: any } {
|
|
62
70
|
if (typeof sql !== 'string') throw new Error('SQL must be a string');
|
|
63
71
|
if (Array.isArray(params)) {
|
|
64
72
|
return {
|
|
@@ -114,6 +122,22 @@ function escapeIdentifier(identifier: string): string {
|
|
|
114
122
|
return pg.Client.prototype.escapeIdentifier(identifier);
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
function enhanceError(err: Error, sql: string, params: QueryParams): Error {
|
|
126
|
+
// Copy the error so we don't end up with a circular reference in the
|
|
127
|
+
// final error.
|
|
128
|
+
const sqlError = { ...err };
|
|
129
|
+
|
|
130
|
+
// `message` is a non-enumerable property, so we need to copy it manually to
|
|
131
|
+
// the error object.
|
|
132
|
+
sqlError.message = err.message;
|
|
133
|
+
|
|
134
|
+
return addDataToError(err, {
|
|
135
|
+
sqlError: sqlError,
|
|
136
|
+
sql: sql,
|
|
137
|
+
sqlParams: params,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
117
141
|
export class PostgresPool {
|
|
118
142
|
/** The pool from which clients will be acquired. */
|
|
119
143
|
private pool: pg.Pool | null = null;
|
|
@@ -125,6 +149,8 @@ export class PostgresPool {
|
|
|
125
149
|
*/
|
|
126
150
|
private alsClient: AsyncLocalStorage<pg.PoolClient> = new AsyncLocalStorage();
|
|
127
151
|
private searchSchema: string | null = null;
|
|
152
|
+
/** Tracks the total number of queries executed by this pool. */
|
|
153
|
+
private _queryCount = 0;
|
|
128
154
|
|
|
129
155
|
/**
|
|
130
156
|
* Creates a new connection pool and attempts to connect to the database.
|
|
@@ -260,8 +286,9 @@ export class PostgresPool {
|
|
|
260
286
|
async queryWithClientAsync(
|
|
261
287
|
client: pg.PoolClient,
|
|
262
288
|
sql: string,
|
|
263
|
-
params:
|
|
289
|
+
params: QueryParams
|
|
264
290
|
): Promise<pg.QueryResult> {
|
|
291
|
+
this._queryCount += 1;
|
|
265
292
|
debug('queryWithClient()', 'sql:', debugString(sql));
|
|
266
293
|
debug('queryWithClient()', 'params:', debugParams(params));
|
|
267
294
|
const { processedSql, paramsArray } = paramsToArray(sql, params);
|
|
@@ -271,14 +298,7 @@ export class PostgresPool {
|
|
|
271
298
|
debug('queryWithClient() success', 'rowCount:', result.rowCount);
|
|
272
299
|
return result;
|
|
273
300
|
} catch (err: any) {
|
|
274
|
-
|
|
275
|
-
const sqlError = JSON.parse(JSON.stringify(err));
|
|
276
|
-
sqlError.message = err.message;
|
|
277
|
-
throw addDataToError(err, {
|
|
278
|
-
sqlError: sqlError,
|
|
279
|
-
sql: sql,
|
|
280
|
-
sqlParams: params,
|
|
281
|
-
});
|
|
301
|
+
throw enhanceError(err, sql, params);
|
|
282
302
|
}
|
|
283
303
|
}
|
|
284
304
|
|
|
@@ -294,7 +314,7 @@ export class PostgresPool {
|
|
|
294
314
|
async queryWithClientOneRowAsync(
|
|
295
315
|
client: pg.PoolClient,
|
|
296
316
|
sql: string,
|
|
297
|
-
params:
|
|
317
|
+
params: QueryParams
|
|
298
318
|
): Promise<pg.QueryResult> {
|
|
299
319
|
debug('queryWithClientOneRow()', 'sql:', debugString(sql));
|
|
300
320
|
debug('queryWithClientOneRow()', 'params:', debugParams(params));
|
|
@@ -323,7 +343,7 @@ export class PostgresPool {
|
|
|
323
343
|
async queryWithClientZeroOrOneRowAsync(
|
|
324
344
|
client: pg.PoolClient,
|
|
325
345
|
sql: string,
|
|
326
|
-
params:
|
|
346
|
+
params: QueryParams
|
|
327
347
|
): Promise<QueryResult> {
|
|
328
348
|
debug('queryWithClientZeroOrOneRow()', 'sql:', debugString(sql));
|
|
329
349
|
debug('queryWithClientZeroOrOneRow()', 'params:', debugParams(params));
|
|
@@ -470,7 +490,7 @@ export class PostgresPool {
|
|
|
470
490
|
/**
|
|
471
491
|
* Executes a query with the specified parameters.
|
|
472
492
|
*/
|
|
473
|
-
async queryAsync(sql: string, params:
|
|
493
|
+
async queryAsync(sql: string, params: QueryParams): Promise<QueryResult> {
|
|
474
494
|
debug('query()', 'sql:', debugString(sql));
|
|
475
495
|
debug('query()', 'params:', debugParams(params));
|
|
476
496
|
const client = await this.getClientAsync();
|
|
@@ -493,7 +513,7 @@ export class PostgresPool {
|
|
|
493
513
|
* Executes a query with the specified parameters. Errors if the query does
|
|
494
514
|
* not return exactly one row.
|
|
495
515
|
*/
|
|
496
|
-
async queryOneRowAsync(sql: string, params:
|
|
516
|
+
async queryOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {
|
|
497
517
|
debug('queryOneRow()', 'sql:', debugString(sql));
|
|
498
518
|
debug('queryOneRow()', 'params:', debugParams(params));
|
|
499
519
|
const result = await this.queryAsync(sql, params);
|
|
@@ -517,7 +537,7 @@ export class PostgresPool {
|
|
|
517
537
|
* Executes a query with the specified parameters. Errors if the query
|
|
518
538
|
* returns more than one row.
|
|
519
539
|
*/
|
|
520
|
-
async queryZeroOrOneRowAsync(sql: string, params:
|
|
540
|
+
async queryZeroOrOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {
|
|
521
541
|
debug('queryZeroOrOneRow()', 'sql:', debugString(sql));
|
|
522
542
|
debug('queryZeroOrOneRow()', 'params:', debugParams(params));
|
|
523
543
|
const result = await this.queryAsync(sql, params);
|
|
@@ -682,12 +702,12 @@ export class PostgresPool {
|
|
|
682
702
|
callWithClientZeroOrOneRow = callbackify(this.callWithClientZeroOrOneRowAsync);
|
|
683
703
|
|
|
684
704
|
/**
|
|
685
|
-
* Wrapper around {@link queryAsync} that
|
|
686
|
-
*
|
|
705
|
+
* Wrapper around {@link queryAsync} that parses the resulting rows with the
|
|
706
|
+
* given Zod schema. Returns only the rows of the query.
|
|
687
707
|
*/
|
|
688
708
|
async queryValidatedRows<Model extends z.ZodTypeAny>(
|
|
689
709
|
query: string,
|
|
690
|
-
params:
|
|
710
|
+
params: QueryParams,
|
|
691
711
|
model: Model
|
|
692
712
|
): Promise<z.infer<Model>[]> {
|
|
693
713
|
const results = await this.queryAsync(query, params);
|
|
@@ -695,12 +715,12 @@ export class PostgresPool {
|
|
|
695
715
|
}
|
|
696
716
|
|
|
697
717
|
/**
|
|
698
|
-
* Wrapper around {@link queryOneRowAsync} that
|
|
699
|
-
*
|
|
718
|
+
* Wrapper around {@link queryOneRowAsync} that parses the resulting row with
|
|
719
|
+
* the given Zod schema. Returns only a single row of the query.
|
|
700
720
|
*/
|
|
701
721
|
async queryValidatedOneRow<Model extends z.ZodTypeAny>(
|
|
702
722
|
query: string,
|
|
703
|
-
params:
|
|
723
|
+
params: QueryParams,
|
|
704
724
|
model: Model
|
|
705
725
|
): Promise<z.infer<Model>> {
|
|
706
726
|
const results = await this.queryOneRowAsync(query, params);
|
|
@@ -708,13 +728,12 @@ export class PostgresPool {
|
|
|
708
728
|
}
|
|
709
729
|
|
|
710
730
|
/**
|
|
711
|
-
* Wrapper around {@link queryZeroOrOneRowAsync} that
|
|
712
|
-
*
|
|
713
|
-
* Returns either the single row of the query or `null`.
|
|
731
|
+
* Wrapper around {@link queryZeroOrOneRowAsync} that parses the resulting row
|
|
732
|
+
* (if any) with the given Zod schema. Returns either a single row or `null`.
|
|
714
733
|
*/
|
|
715
734
|
async queryValidatedZeroOrOneRow<Model extends z.ZodTypeAny>(
|
|
716
735
|
query: string,
|
|
717
|
-
params:
|
|
736
|
+
params: QueryParams,
|
|
718
737
|
model: Model
|
|
719
738
|
): Promise<z.infer<Model> | null> {
|
|
720
739
|
const results = await this.queryZeroOrOneRowAsync(query, params);
|
|
@@ -727,12 +746,12 @@ export class PostgresPool {
|
|
|
727
746
|
|
|
728
747
|
/**
|
|
729
748
|
* Wrapper around {@link queryAsync} that validates that only one column is
|
|
730
|
-
* returned and the data in it
|
|
749
|
+
* returned and parses the data in it with the given Zod schema. Returns only
|
|
731
750
|
* the single column of the query as an array.
|
|
732
751
|
*/
|
|
733
752
|
async queryValidatedSingleColumnRows<Model extends z.ZodTypeAny>(
|
|
734
753
|
query: string,
|
|
735
|
-
params:
|
|
754
|
+
params: QueryParams,
|
|
736
755
|
model: Model
|
|
737
756
|
): Promise<z.infer<Model>[]> {
|
|
738
757
|
const results = await this.queryAsync(query, params);
|
|
@@ -746,12 +765,12 @@ export class PostgresPool {
|
|
|
746
765
|
|
|
747
766
|
/**
|
|
748
767
|
* Wrapper around {@link queryOneRowAsync} that validates that only one column
|
|
749
|
-
* is returned and the data in it
|
|
768
|
+
* is returned and parses the data in it with the given Zod schema. Returns
|
|
750
769
|
* only the single entry.
|
|
751
770
|
*/
|
|
752
771
|
async queryValidatedSingleColumnOneRow<Model extends z.ZodTypeAny>(
|
|
753
772
|
query: string,
|
|
754
|
-
params:
|
|
773
|
+
params: QueryParams,
|
|
755
774
|
model: Model
|
|
756
775
|
): Promise<z.infer<Model>> {
|
|
757
776
|
const results = await this.queryOneRowAsync(query, params);
|
|
@@ -764,12 +783,12 @@ export class PostgresPool {
|
|
|
764
783
|
|
|
765
784
|
/**
|
|
766
785
|
* Wrapper around {@link queryZeroOrOneRowAsync} that validates that only one
|
|
767
|
-
* column is returned and the data in it
|
|
768
|
-
*
|
|
786
|
+
* column is returned and parses the data in it (if any) with the given Zod
|
|
787
|
+
* schema. Returns either the single row of the query or `null`.
|
|
769
788
|
*/
|
|
770
789
|
async queryValidatedSingleColumnZeroOrOneRow<Model extends z.ZodTypeAny>(
|
|
771
790
|
query: string,
|
|
772
|
-
params:
|
|
791
|
+
params: QueryParams,
|
|
773
792
|
model: Model
|
|
774
793
|
): Promise<z.infer<Model> | null> {
|
|
775
794
|
const results = await this.queryZeroOrOneRowAsync(query, params);
|
|
@@ -785,8 +804,8 @@ export class PostgresPool {
|
|
|
785
804
|
}
|
|
786
805
|
|
|
787
806
|
/**
|
|
788
|
-
* Wrapper around {@link callAsync} that
|
|
789
|
-
*
|
|
807
|
+
* Wrapper around {@link callAsync} that parses the resulting rows with the
|
|
808
|
+
* given Zod schema. Returns only the rows.
|
|
790
809
|
*/
|
|
791
810
|
async callValidatedRows<Model extends z.ZodTypeAny>(
|
|
792
811
|
sprocName: string,
|
|
@@ -798,8 +817,8 @@ export class PostgresPool {
|
|
|
798
817
|
}
|
|
799
818
|
|
|
800
819
|
/**
|
|
801
|
-
* Wrapper around {@link callOneRowAsync} that
|
|
802
|
-
*
|
|
820
|
+
* Wrapper around {@link callOneRowAsync} that parses the resulting rows with
|
|
821
|
+
* the given Zod schema. Returns only a single row.
|
|
803
822
|
*/
|
|
804
823
|
async callValidatedOneRow<Model extends z.ZodTypeAny>(
|
|
805
824
|
sprocName: string,
|
|
@@ -811,9 +830,8 @@ export class PostgresPool {
|
|
|
811
830
|
}
|
|
812
831
|
|
|
813
832
|
/**
|
|
814
|
-
* Wrapper around {@link callZeroOrOneRowAsync} that
|
|
815
|
-
*
|
|
816
|
-
* Returns at most a single row.
|
|
833
|
+
* Wrapper around {@link callZeroOrOneRowAsync} that parses the resulting row
|
|
834
|
+
* (if any) with the given Zod schema. Returns at most a single row.
|
|
817
835
|
*/
|
|
818
836
|
async callValidatedZeroOrOneRow<Model extends z.ZodTypeAny>(
|
|
819
837
|
sprocName: string,
|
|
@@ -828,6 +846,87 @@ export class PostgresPool {
|
|
|
828
846
|
}
|
|
829
847
|
}
|
|
830
848
|
|
|
849
|
+
/**
|
|
850
|
+
* Returns a {@link Cursor} for the given query. The cursor can be used to
|
|
851
|
+
* read results in batches, which is useful for large result sets.
|
|
852
|
+
*/
|
|
853
|
+
async queryCursorWithClient(
|
|
854
|
+
client: pg.PoolClient,
|
|
855
|
+
sql: string,
|
|
856
|
+
params: QueryParams
|
|
857
|
+
): Promise<Cursor> {
|
|
858
|
+
this._queryCount += 1;
|
|
859
|
+
debug('queryCursorWithClient()', 'sql:', debugString(sql));
|
|
860
|
+
debug('queryCursorWithClient()', 'params:', debugParams(params));
|
|
861
|
+
const { processedSql, paramsArray } = paramsToArray(sql, params);
|
|
862
|
+
lastQueryMap.set(client, processedSql);
|
|
863
|
+
return client.query(new Cursor(processedSql, paramsArray));
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Returns an {@link CursorIterator} that can be used to iterate over the
|
|
868
|
+
* results of the query in batches, which is useful for large result sets.
|
|
869
|
+
*/
|
|
870
|
+
async queryCursor<Model extends z.ZodTypeAny>(
|
|
871
|
+
sql: string,
|
|
872
|
+
params: QueryParams
|
|
873
|
+
): Promise<CursorIterator<z.infer<Model>>> {
|
|
874
|
+
return this.queryValidatedCursorInternal(sql, params);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Returns an {@link CursorIterator} that can be used to iterate over the
|
|
879
|
+
* results of the query in batches, which is useful for large result sets.
|
|
880
|
+
* Each row will be parsed by the given Zod schema.
|
|
881
|
+
*/
|
|
882
|
+
async queryValidatedCursor<Model extends z.ZodTypeAny>(
|
|
883
|
+
sql: string,
|
|
884
|
+
params: QueryParams,
|
|
885
|
+
model: Model
|
|
886
|
+
): Promise<CursorIterator<z.infer<Model>>> {
|
|
887
|
+
return this.queryValidatedCursorInternal(sql, params, model);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private async queryValidatedCursorInternal<Model extends z.ZodTypeAny>(
|
|
891
|
+
sql: string,
|
|
892
|
+
params: QueryParams,
|
|
893
|
+
model?: Model
|
|
894
|
+
): Promise<CursorIterator<z.infer<Model>>> {
|
|
895
|
+
const client = await this.getClientAsync();
|
|
896
|
+
const cursor = await this.queryCursorWithClient(client, sql, params);
|
|
897
|
+
|
|
898
|
+
let iterateCalled = false;
|
|
899
|
+
return {
|
|
900
|
+
iterate: async function* (batchSize: number) {
|
|
901
|
+
// Safety check: if someone calls iterate multiple times, they're
|
|
902
|
+
// definitely doing something wrong.
|
|
903
|
+
if (iterateCalled) {
|
|
904
|
+
throw new Error('iterate() called multiple times');
|
|
905
|
+
}
|
|
906
|
+
iterateCalled = true;
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
while (true) {
|
|
910
|
+
const rows = await cursor.read(batchSize);
|
|
911
|
+
if (rows.length === 0) {
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
if (model) {
|
|
916
|
+
yield z.array(model).parse(rows);
|
|
917
|
+
} else {
|
|
918
|
+
yield rows;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
} catch (err: any) {
|
|
922
|
+
throw enhanceError(err, sql, params);
|
|
923
|
+
} finally {
|
|
924
|
+
client.release();
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
831
930
|
/**
|
|
832
931
|
* Set the schema to use for the search path.
|
|
833
932
|
*
|
|
@@ -882,4 +981,24 @@ export class PostgresPool {
|
|
|
882
981
|
* Generate, set, and return a random schema name.
|
|
883
982
|
*/
|
|
884
983
|
setRandomSearchSchema = callbackify(this.setRandomSearchSchemaAsync);
|
|
984
|
+
|
|
985
|
+
/** The number of established connections. */
|
|
986
|
+
get totalCount() {
|
|
987
|
+
return this.pool?.totalCount ?? 0;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** The number of idle connections. */
|
|
991
|
+
get idleCount() {
|
|
992
|
+
return this.pool?.idleCount ?? 0;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** The number of queries waiting for a connection to become available. */
|
|
996
|
+
get waitingCount() {
|
|
997
|
+
return this.pool?.waitingCount ?? 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** The total number of queries that have been executed by this pool. */
|
|
1001
|
+
get queryCount() {
|
|
1002
|
+
return this._queryCount;
|
|
1003
|
+
}
|
|
885
1004
|
}
|