@prairielearn/postgres 1.6.0 → 1.7.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/dist/default-pool.d.ts +12 -0
- package/dist/default-pool.js +5 -2
- package/dist/default-pool.js.map +1 -1
- package/dist/loader.js +1 -1
- package/dist/loader.js.map +1 -1
- package/dist/pool.d.ts +6 -0
- package/dist/pool.js +67 -12
- package/dist/pool.js.map +1 -1
- package/dist/pool.test.js +142 -57
- package/dist/pool.test.js.map +1 -1
- package/package.json +11 -6
- package/src/default-pool.ts +3 -0
- package/src/loader.ts +1 -1
- package/src/pool.test.ts +201 -75
- package/src/pool.ts +100 -12
- package/tsconfig.json +1 -1
package/src/pool.test.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import chai from 'chai';
|
|
2
2
|
import chaiAsPromised from 'chai-as-promised';
|
|
3
|
+
import { Writable } from 'node:stream';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
3
5
|
import { z, ZodError } from 'zod';
|
|
4
6
|
|
|
5
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
queryAsync,
|
|
9
|
+
queryRows,
|
|
10
|
+
queryRow,
|
|
11
|
+
queryOptionalRow,
|
|
12
|
+
queryCursor,
|
|
13
|
+
queryValidatedCursor,
|
|
14
|
+
} from './default-pool';
|
|
6
15
|
import { makePostgresTestUtils } from './test-utils';
|
|
7
16
|
|
|
8
17
|
chai.use(chaiAsPromised);
|
|
@@ -12,24 +21,118 @@ const postgresTestUtils = makePostgresTestUtils({
|
|
|
12
21
|
database: 'prairielearn_postgres',
|
|
13
22
|
});
|
|
14
23
|
|
|
24
|
+
const WorkspaceSchema = z.object({
|
|
25
|
+
id: z.string(),
|
|
26
|
+
created_at: z.date(),
|
|
27
|
+
});
|
|
28
|
+
|
|
15
29
|
describe('@prairielearn/postgres', function () {
|
|
16
30
|
before(async () => {
|
|
17
31
|
await postgresTestUtils.createDatabase();
|
|
18
|
-
await queryAsync(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
await queryAsync(
|
|
32
|
+
await queryAsync(
|
|
33
|
+
'CREATE TABLE workspaces (id BIGSERIAL PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP);',
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
36
|
+
await queryAsync('INSERT INTO workspaces (id) SELECT s FROM generate_series(1, 100) AS s', {});
|
|
23
37
|
});
|
|
24
38
|
|
|
25
39
|
after(async () => {
|
|
26
40
|
await postgresTestUtils.dropDatabase();
|
|
27
41
|
});
|
|
28
42
|
|
|
43
|
+
describe('queryRows', () => {
|
|
44
|
+
it('handles single column', async () => {
|
|
45
|
+
const rows = await queryRows('SELECT id FROM workspaces WHERE id <= 10;', z.string());
|
|
46
|
+
assert.lengthOf(rows, 10);
|
|
47
|
+
assert.equal(rows[0], '1');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles multiple columns', async () => {
|
|
51
|
+
const rows = await queryRows('SELECT * FROM workspaces WHERE id <= 10;', WorkspaceSchema);
|
|
52
|
+
assert.lengthOf(rows, 10);
|
|
53
|
+
assert.equal(rows[0].id, '1');
|
|
54
|
+
assert.isNotNull(rows[0].created_at);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('handles parameters', async () => {
|
|
58
|
+
const rows = await queryRows(
|
|
59
|
+
'SELECT * FROM workspaces WHERE id <= $1;',
|
|
60
|
+
[10],
|
|
61
|
+
WorkspaceSchema
|
|
62
|
+
);
|
|
63
|
+
assert.lengthOf(rows, 10);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('queryRow', () => {
|
|
68
|
+
it('handles single column', async () => {
|
|
69
|
+
const row = await queryRow('SELECT id FROM workspaces WHERE id = 1;', z.string());
|
|
70
|
+
assert.equal(row, '1');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('handles multiple columns', async () => {
|
|
74
|
+
const row = await queryRow('SELECT * FROM workspaces WHERE id = 1;', WorkspaceSchema);
|
|
75
|
+
assert.equal(row.id, '1');
|
|
76
|
+
assert.isNotNull(row.created_at);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('handles parameters', async () => {
|
|
80
|
+
const row = await queryRow('SELECT * FROM workspaces WHERE id = $1;', [1], WorkspaceSchema);
|
|
81
|
+
assert.equal(row.id, '1');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('rejects results with zero rows', async () => {
|
|
85
|
+
const rows = queryRow('SELECT * FROM workspaces WHERE id = -1;', WorkspaceSchema);
|
|
86
|
+
await assert.isRejected(rows, 'Incorrect rowCount: 0');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects results with multiple rows', async () => {
|
|
90
|
+
const rows = queryRow('SELECT * FROM workspaces', WorkspaceSchema);
|
|
91
|
+
await assert.isRejected(rows, 'Incorrect rowCount: 100');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('queryOptionalRow', () => {
|
|
96
|
+
it('handles single column', async () => {
|
|
97
|
+
const row = await queryRow('SELECT id FROM workspaces WHERE id = 1;', z.string());
|
|
98
|
+
assert.equal(row, '1');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('handles multiple columns', async () => {
|
|
102
|
+
const row = await queryOptionalRow('SELECT * FROM workspaces WHERE id = 1;', WorkspaceSchema);
|
|
103
|
+
assert.isNotNull(row);
|
|
104
|
+
assert.equal(row?.id, '1');
|
|
105
|
+
assert.isNotNull(row?.created_at);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles parameters', async () => {
|
|
109
|
+
const row = await queryOptionalRow(
|
|
110
|
+
'SELECT * FROM workspaces WHERE id = $1;',
|
|
111
|
+
[1],
|
|
112
|
+
WorkspaceSchema
|
|
113
|
+
);
|
|
114
|
+
assert.isNotNull(row);
|
|
115
|
+
assert.equal(row?.id, '1');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('handles missing result', async () => {
|
|
119
|
+
const row = await queryOptionalRow(
|
|
120
|
+
'SELECT * FROM workspaces WHERE id = -1;',
|
|
121
|
+
WorkspaceSchema
|
|
122
|
+
);
|
|
123
|
+
assert.isNull(row);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('rejects with multiple rows', async () => {
|
|
127
|
+
const rows = queryOptionalRow('SELECT * FROM workspaces', WorkspaceSchema);
|
|
128
|
+
await assert.isRejected(rows, 'Incorrect rowCount: 100');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
29
132
|
describe('queryCursor', () => {
|
|
30
133
|
it('returns zero rows', async () => {
|
|
31
|
-
const cursor = await queryCursor('SELECT * FROM workspaces WHERE id =
|
|
32
|
-
|
|
134
|
+
const cursor = await queryCursor('SELECT * FROM workspaces WHERE id = 10000;', {});
|
|
135
|
+
const rowBatches = [];
|
|
33
136
|
for await (const rows of cursor.iterate(10)) {
|
|
34
137
|
rowBatches.push(rows);
|
|
35
138
|
}
|
|
@@ -48,13 +151,13 @@ describe('@prairielearn/postgres', function () {
|
|
|
48
151
|
});
|
|
49
152
|
|
|
50
153
|
it('returns all rows at once', async () => {
|
|
51
|
-
const cursor = queryCursor('SELECT * FROM workspaces;', {});
|
|
154
|
+
const cursor = queryCursor('SELECT * FROM workspaces WHERE id <= 10;', {});
|
|
52
155
|
const rowBatches = [];
|
|
53
156
|
for await (const rows of (await cursor).iterate(10)) {
|
|
54
157
|
rowBatches.push(rows);
|
|
55
158
|
}
|
|
56
159
|
assert.lengthOf(rowBatches, 1);
|
|
57
|
-
assert.lengthOf(rowBatches[0],
|
|
160
|
+
assert.lengthOf(rowBatches[0], 10);
|
|
58
161
|
});
|
|
59
162
|
|
|
60
163
|
it('handles errors', async () => {
|
|
@@ -80,88 +183,111 @@ describe('@prairielearn/postgres', function () {
|
|
|
80
183
|
});
|
|
81
184
|
|
|
82
185
|
describe('queryValidatedCursor', () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
id: z.string(),
|
|
86
|
-
});
|
|
87
|
-
const cursor = await queryValidatedCursor(
|
|
88
|
-
'SELECT * FROM workspaces ORDER BY id ASC;',
|
|
89
|
-
{},
|
|
90
|
-
WorkspaceSchema
|
|
91
|
-
);
|
|
92
|
-
const allRows = [];
|
|
93
|
-
for await (const rows of cursor.iterate(10)) {
|
|
94
|
-
allRows.push(...rows);
|
|
95
|
-
}
|
|
96
|
-
assert.lengthOf(allRows, 4);
|
|
97
|
-
const workspace = allRows[0] as any;
|
|
98
|
-
assert.equal(workspace.id, '1');
|
|
99
|
-
assert.isUndefined(workspace.state);
|
|
186
|
+
const WorkspaceSchema = z.object({
|
|
187
|
+
id: z.string(),
|
|
100
188
|
});
|
|
101
189
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
const cursor = await queryValidatedCursor(
|
|
107
|
-
'SELECT * FROM workspaces ORDER BY id ASC;',
|
|
108
|
-
{},
|
|
109
|
-
BadWorkspaceSchema
|
|
110
|
-
);
|
|
190
|
+
const BadWorkspaceSchema = z.object({
|
|
191
|
+
badProperty: z.string(),
|
|
192
|
+
});
|
|
111
193
|
|
|
112
|
-
|
|
194
|
+
describe('iterator', () => {
|
|
195
|
+
it('validates with provided schema', async () => {
|
|
196
|
+
const cursor = await queryValidatedCursor(
|
|
197
|
+
'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',
|
|
198
|
+
{},
|
|
199
|
+
WorkspaceSchema
|
|
200
|
+
);
|
|
113
201
|
const allRows = [];
|
|
114
202
|
for await (const rows of cursor.iterate(10)) {
|
|
115
203
|
allRows.push(...rows);
|
|
116
204
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
assert.instanceOf(maybeError, ZodError);
|
|
122
|
-
assert.lengthOf(maybeError.errors, 4);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('returns a stream', async () => {
|
|
126
|
-
const WorkspaceSchema = z.object({
|
|
127
|
-
id: z.string(),
|
|
205
|
+
assert.lengthOf(allRows, 10);
|
|
206
|
+
const workspace = allRows[0] as any;
|
|
207
|
+
assert.equal(workspace.id, '1');
|
|
208
|
+
assert.isUndefined(workspace.state);
|
|
128
209
|
});
|
|
129
|
-
const cursor = await queryValidatedCursor(
|
|
130
|
-
'SELECT * FROM workspaces ORDER BY id ASC;',
|
|
131
|
-
{},
|
|
132
|
-
WorkspaceSchema
|
|
133
|
-
);
|
|
134
|
-
const stream = cursor.stream(1);
|
|
135
|
-
const allRows = [];
|
|
136
|
-
for await (const row of stream) {
|
|
137
|
-
allRows.push(row);
|
|
138
|
-
}
|
|
139
210
|
|
|
140
|
-
|
|
141
|
-
|
|
211
|
+
it('throws error when validation fails', async () => {
|
|
212
|
+
const cursor = await queryValidatedCursor(
|
|
213
|
+
'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',
|
|
214
|
+
{},
|
|
215
|
+
BadWorkspaceSchema
|
|
216
|
+
);
|
|
142
217
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
218
|
+
async function readAllRows() {
|
|
219
|
+
const allRows = [];
|
|
220
|
+
for await (const rows of cursor.iterate(10)) {
|
|
221
|
+
allRows.push(...rows);
|
|
222
|
+
}
|
|
223
|
+
return allRows;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const maybeError = await readAllRows().catch((err) => err);
|
|
227
|
+
assert.instanceOf(maybeError, ZodError);
|
|
228
|
+
assert.lengthOf(maybeError.errors, 10);
|
|
146
229
|
});
|
|
147
|
-
|
|
148
|
-
'SELECT * FROM workspaces ORDER BY id ASC;',
|
|
149
|
-
{},
|
|
150
|
-
BadWorkspaceSchema
|
|
151
|
-
);
|
|
152
|
-
const stream = cursor.stream(1);
|
|
230
|
+
});
|
|
153
231
|
|
|
154
|
-
|
|
232
|
+
describe('stream', () => {
|
|
233
|
+
it('validates with provided schema', async () => {
|
|
234
|
+
const cursor = await queryValidatedCursor(
|
|
235
|
+
'SELECT * FROM workspaces WHERE id <= 10 ORDER BY id ASC;',
|
|
236
|
+
{},
|
|
237
|
+
WorkspaceSchema
|
|
238
|
+
);
|
|
239
|
+
const stream = cursor.stream(1);
|
|
155
240
|
const allRows = [];
|
|
156
241
|
for await (const row of stream) {
|
|
157
242
|
allRows.push(row);
|
|
158
243
|
}
|
|
159
|
-
return allRows;
|
|
160
|
-
}
|
|
161
244
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
245
|
+
assert.lengthOf(allRows, 10);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('emits an error when validation fails', async () => {
|
|
249
|
+
const cursor = await queryValidatedCursor(
|
|
250
|
+
'SELECT * FROM workspaces ORDER BY id ASC;',
|
|
251
|
+
{},
|
|
252
|
+
BadWorkspaceSchema
|
|
253
|
+
);
|
|
254
|
+
const stream = cursor.stream(1);
|
|
255
|
+
|
|
256
|
+
async function readAllRows() {
|
|
257
|
+
const allRows = [];
|
|
258
|
+
for await (const row of stream) {
|
|
259
|
+
allRows.push(row);
|
|
260
|
+
}
|
|
261
|
+
return allRows;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const maybeError = await readAllRows().catch((err) => err);
|
|
265
|
+
assert.instanceOf(maybeError, ZodError);
|
|
266
|
+
assert.lengthOf(maybeError.errors, 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('closes the cursor when the stream is closed', async () => {
|
|
270
|
+
const cursor = await queryValidatedCursor('SELECT * FROM workspaces;', {}, WorkspaceSchema);
|
|
271
|
+
const stream = cursor.stream(1);
|
|
272
|
+
|
|
273
|
+
const rows: any[] = [];
|
|
274
|
+
const ac = new AbortController();
|
|
275
|
+
const writable = new Writable({
|
|
276
|
+
objectMode: true,
|
|
277
|
+
write: function (chunk, _encoding, callback) {
|
|
278
|
+
rows.push(chunk);
|
|
279
|
+
|
|
280
|
+
// After receiving the first row, abort the stream. This lets us test
|
|
281
|
+
// that the underlying cursor is closed. If it is *not* closed, this
|
|
282
|
+
// `after` hook will fail with a timeout.
|
|
283
|
+
ac.abort();
|
|
284
|
+
callback();
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await assert.isRejected(pipeline(stream, writable, { signal: ac.signal }));
|
|
289
|
+
assert.lengthOf(rows, 1);
|
|
290
|
+
});
|
|
165
291
|
});
|
|
166
292
|
});
|
|
167
293
|
});
|
package/src/pool.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import pg, { QueryResult } from 'pg';
|
|
3
3
|
import Cursor from 'pg-cursor';
|
|
4
|
-
import path from 'node:path';
|
|
5
4
|
import debugFactory from 'debug';
|
|
6
5
|
import { callbackify } from 'node:util';
|
|
7
6
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -16,7 +15,7 @@ export interface CursorIterator<T> {
|
|
|
16
15
|
stream: (batchSize: number) => NodeJS.ReadWriteStream;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
const debug = debugFactory('
|
|
18
|
+
const debug = debugFactory('@prairielearn/postgres');
|
|
20
19
|
const lastQueryMap: WeakMap<pg.PoolClient, string> = new WeakMap();
|
|
21
20
|
const searchSchemaMap: WeakMap<pg.PoolClient, string> = new WeakMap();
|
|
22
21
|
|
|
@@ -239,7 +238,7 @@ export class PostgresPool {
|
|
|
239
238
|
|
|
240
239
|
// If we're inside a transaction, we'll reuse the same client to avoid a
|
|
241
240
|
// potential deadlock.
|
|
242
|
-
|
|
241
|
+
const client = this.alsClient.getStore() ?? (await this.pool.connect());
|
|
243
242
|
|
|
244
243
|
// If we're configured to use a particular schema, we'll store whether or
|
|
245
244
|
// not the search path has already been configured for this particular
|
|
@@ -475,7 +474,7 @@ export class PostgresPool {
|
|
|
475
474
|
async runInTransactionAsync<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T> {
|
|
476
475
|
// Check if we're already inside a transaction. If so, we won't start another one,
|
|
477
476
|
// as Postgres doesn't support nested transactions.
|
|
478
|
-
|
|
477
|
+
const client = this.alsClient.getStore();
|
|
479
478
|
const isNestedTransaction = client !== undefined;
|
|
480
479
|
const transactionClient = client ?? (await this.beginTransactionAsync());
|
|
481
480
|
|
|
@@ -754,7 +753,7 @@ export class PostgresPool {
|
|
|
754
753
|
model: Model
|
|
755
754
|
): Promise<z.infer<Model> | null> {
|
|
756
755
|
const results = await this.queryZeroOrOneRowAsync(query, params);
|
|
757
|
-
if (results.rows.length
|
|
756
|
+
if (results.rows.length === 0) {
|
|
758
757
|
return null;
|
|
759
758
|
} else {
|
|
760
759
|
return model.parse(results.rows[0]);
|
|
@@ -772,7 +771,7 @@ export class PostgresPool {
|
|
|
772
771
|
model: Model
|
|
773
772
|
): Promise<z.infer<Model>[]> {
|
|
774
773
|
const results = await this.queryAsync(query, params);
|
|
775
|
-
if (results.fields.length
|
|
774
|
+
if (results.fields.length !== 1) {
|
|
776
775
|
throw new Error(`Expected one column, got ${results.fields.length}`);
|
|
777
776
|
}
|
|
778
777
|
const columnName = results.fields[0].name;
|
|
@@ -791,7 +790,7 @@ export class PostgresPool {
|
|
|
791
790
|
model: Model
|
|
792
791
|
): Promise<z.infer<Model>> {
|
|
793
792
|
const results = await this.queryOneRowAsync(query, params);
|
|
794
|
-
if (results.fields.length
|
|
793
|
+
if (results.fields.length !== 1) {
|
|
795
794
|
throw new Error(`Expected one column, got ${results.fields.length}`);
|
|
796
795
|
}
|
|
797
796
|
const columnName = results.fields[0].name;
|
|
@@ -809,10 +808,10 @@ export class PostgresPool {
|
|
|
809
808
|
model: Model
|
|
810
809
|
): Promise<z.infer<Model> | null> {
|
|
811
810
|
const results = await this.queryZeroOrOneRowAsync(query, params);
|
|
812
|
-
if (results.fields.length
|
|
811
|
+
if (results.fields.length !== 1) {
|
|
813
812
|
throw new Error(`Expected one column, got ${results.fields.length}`);
|
|
814
813
|
}
|
|
815
|
-
if (results.rows.length
|
|
814
|
+
if (results.rows.length === 0) {
|
|
816
815
|
return null;
|
|
817
816
|
} else {
|
|
818
817
|
const columnName = results.fields[0].name;
|
|
@@ -856,13 +855,85 @@ export class PostgresPool {
|
|
|
856
855
|
model: Model
|
|
857
856
|
): Promise<z.infer<Model> | null> {
|
|
858
857
|
const results = await this.callZeroOrOneRowAsync(sprocName, params);
|
|
859
|
-
if (results.rows.length
|
|
858
|
+
if (results.rows.length === 0) {
|
|
860
859
|
return null;
|
|
861
860
|
} else {
|
|
862
861
|
return model.parse(results.rows[0]);
|
|
863
862
|
}
|
|
864
863
|
}
|
|
865
864
|
|
|
865
|
+
async queryRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
|
|
866
|
+
async queryRows<Model extends z.ZodTypeAny>(
|
|
867
|
+
sql: string,
|
|
868
|
+
params: QueryParams,
|
|
869
|
+
model: Model
|
|
870
|
+
): Promise<z.infer<Model>[]>;
|
|
871
|
+
async queryRows<Model extends z.ZodTypeAny>(
|
|
872
|
+
sql: string,
|
|
873
|
+
paramsOrSchema: QueryParams | Model,
|
|
874
|
+
maybeModel?: Model
|
|
875
|
+
) {
|
|
876
|
+
const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
|
|
877
|
+
const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
|
|
878
|
+
const results = await this.queryAsync(sql, params);
|
|
879
|
+
if (results.fields.length === 1) {
|
|
880
|
+
const columnName = results.fields[0].name;
|
|
881
|
+
const rawData = results.rows.map((row) => row[columnName]);
|
|
882
|
+
return z.array(model).parse(rawData);
|
|
883
|
+
} else {
|
|
884
|
+
return z.array(model).parse(results.rows);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
async queryRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
|
|
889
|
+
async queryRow<Model extends z.ZodTypeAny>(
|
|
890
|
+
sql: string,
|
|
891
|
+
params: QueryParams,
|
|
892
|
+
model: Model
|
|
893
|
+
): Promise<z.infer<Model>>;
|
|
894
|
+
async queryRow<Model extends z.ZodTypeAny>(
|
|
895
|
+
sql: string,
|
|
896
|
+
paramsOrSchema: QueryParams | Model,
|
|
897
|
+
maybeModel?: Model
|
|
898
|
+
) {
|
|
899
|
+
const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
|
|
900
|
+
const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
|
|
901
|
+
const results = await this.queryOneRowAsync(sql, params);
|
|
902
|
+
if (results.fields.length === 1) {
|
|
903
|
+
const columnName = results.fields[0].name;
|
|
904
|
+
return model.parse(results.rows[0][columnName]);
|
|
905
|
+
} else {
|
|
906
|
+
return model.parse(results.rows[0]);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async queryOptionalRow<Model extends z.ZodTypeAny>(
|
|
911
|
+
sql: string,
|
|
912
|
+
model: Model
|
|
913
|
+
): Promise<z.infer<Model> | null>;
|
|
914
|
+
async queryOptionalRow<Model extends z.ZodTypeAny>(
|
|
915
|
+
sql: string,
|
|
916
|
+
params: QueryParams,
|
|
917
|
+
model: Model
|
|
918
|
+
): Promise<z.infer<Model> | null>;
|
|
919
|
+
async queryOptionalRow<Model extends z.ZodTypeAny>(
|
|
920
|
+
sql: string,
|
|
921
|
+
paramsOrSchema: QueryParams | Model,
|
|
922
|
+
maybeModel?: Model
|
|
923
|
+
) {
|
|
924
|
+
const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);
|
|
925
|
+
const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;
|
|
926
|
+
const results = await this.queryZeroOrOneRowAsync(sql, params);
|
|
927
|
+
if (results.rows.length === 0) {
|
|
928
|
+
return null;
|
|
929
|
+
} else if (results.fields.length === 1) {
|
|
930
|
+
const columnName = results.fields[0].name;
|
|
931
|
+
return model.parse(results.rows[0][columnName]);
|
|
932
|
+
} else {
|
|
933
|
+
return model.parse(results.rows[0]);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
866
937
|
/**
|
|
867
938
|
* Returns a {@link Cursor} for the given query. The cursor can be used to
|
|
868
939
|
* read results in batches, which is useful for large result sets.
|
|
@@ -938,7 +1009,11 @@ export class PostgresPool {
|
|
|
938
1009
|
} catch (err: any) {
|
|
939
1010
|
throw enhanceError(err, sql, params);
|
|
940
1011
|
} finally {
|
|
941
|
-
|
|
1012
|
+
try {
|
|
1013
|
+
await cursor.close();
|
|
1014
|
+
} finally {
|
|
1015
|
+
client.release();
|
|
1016
|
+
}
|
|
942
1017
|
}
|
|
943
1018
|
},
|
|
944
1019
|
stream: function (batchSize: number) {
|
|
@@ -954,7 +1029,20 @@ export class PostgresPool {
|
|
|
954
1029
|
});
|
|
955
1030
|
|
|
956
1031
|
// TODO: use native `node:stream#compose` once it's stable.
|
|
957
|
-
|
|
1032
|
+
const generator = iterator.iterate(batchSize);
|
|
1033
|
+
const pipe = multipipe(Readable.from(generator), transform);
|
|
1034
|
+
|
|
1035
|
+
// When the underlying stream is closed, we need to make sure that the
|
|
1036
|
+
// cursor is also closed. We do this by calling `return()` on the generator,
|
|
1037
|
+
// which will trigger its `finally` block, which will in turn release
|
|
1038
|
+
// the client and close the cursor. The fact that the stream is already
|
|
1039
|
+
// closed by this point means that someone reading from the stream will
|
|
1040
|
+
// never actually see the `null` value that's returned.
|
|
1041
|
+
pipe.once('close', () => {
|
|
1042
|
+
generator.return(null);
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
return pipe;
|
|
958
1046
|
},
|
|
959
1047
|
};
|
|
960
1048
|
return iterator;
|
package/tsconfig.json
CHANGED