@prairielearn/postgres 5.0.3 → 6.0.1

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.d.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import pg, { type QueryResult } from 'pg';
2
2
  import { z } from 'zod';
3
3
  export type QueryParams = Record<string, any> | any[];
4
+ /**
5
+ * Type constraint for row schemas accepted by query functions.
6
+ * Accepts `z.object(...)`, unions/intersections/discriminated unions of objects,
7
+ * transforms/refinements of any of those, branded variants, and `z.unknown()`
8
+ * as an escape hatch.
9
+ */
10
+ type AnyObjectLikeSchema = z.AnyZodObject | z.ZodEffects<AnyObjectLikeSchema, any, any> | z.ZodIntersection<AnyObjectLikeSchema, AnyObjectLikeSchema> | z.ZodUnion<Readonly<[AnyObjectLikeSchema, ...AnyObjectLikeSchema[]]>> | z.ZodDiscriminatedUnion<string, z.AnyZodObject[]>;
11
+ export type AnyRowSchema = AnyObjectLikeSchema | z.ZodBranded<AnyObjectLikeSchema, any> | z.ZodUnknown;
4
12
  export interface CursorIterator<T> {
5
13
  iterate: (batchSize: number) => AsyncGenerator<T[]>;
6
14
  stream: (batchSize: number) => NodeJS.ReadWriteStream;
@@ -128,18 +136,30 @@ export declare class PostgresPool {
128
136
  * Errors if the sproc returns more than one row.
129
137
  */
130
138
  callWithClientZeroOrOneRowAsync(client: pg.PoolClient, functionName: string, params: any[]): Promise<pg.QueryResult>;
131
- queryRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
132
- queryRows<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>[]>;
133
- queryRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
134
- queryRow<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>>;
135
- queryOptionalRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model> | null>;
136
- queryOptionalRow<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model> | null>;
137
- callRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
138
- callRows<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model>[]>;
139
- callRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
140
- callRow<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model>>;
141
- callOptionalRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model> | null>;
142
- callOptionalRow<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model> | null>;
139
+ queryRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;
140
+ queryRows<Model extends AnyRowSchema>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>[]>;
141
+ queryRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;
142
+ queryRow<Model extends AnyRowSchema>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>>;
143
+ queryOptionalRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model> | null>;
144
+ queryOptionalRow<Model extends AnyRowSchema>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model> | null>;
145
+ callRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;
146
+ callRows<Model extends AnyRowSchema>(sql: string, params: any[], model: Model): Promise<z.infer<Model>[]>;
147
+ callRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;
148
+ callRow<Model extends AnyRowSchema>(sql: string, params: any[], model: Model): Promise<z.infer<Model>>;
149
+ callOptionalRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model> | null>;
150
+ callOptionalRow<Model extends AnyRowSchema>(sql: string, params: any[], model: Model): Promise<z.infer<Model> | null>;
151
+ queryScalars<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
152
+ queryScalars<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>[]>;
153
+ queryScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
154
+ queryScalar<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model>>;
155
+ queryOptionalScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model> | null>;
156
+ queryOptionalScalar<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<z.infer<Model> | null>;
157
+ callScalars<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;
158
+ callScalars<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model>[]>;
159
+ callScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;
160
+ callScalar<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model>>;
161
+ callOptionalScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model> | null>;
162
+ callOptionalScalar<Model extends z.ZodTypeAny>(sql: string, params: any[], model: Model): Promise<z.infer<Model> | null>;
143
163
  /**
144
164
  * Executes a query with the specified parameters. Returns the number of rows affected.
145
165
  */
@@ -149,8 +169,8 @@ export declare class PostgresPool {
149
169
  */
150
170
  executeRow(sql: string, params?: QueryParams): Promise<void>;
151
171
  private queryCursorWithClient;
152
- queryCursor<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<CursorIterator<z.infer<Model>>>;
153
- queryCursor<Model extends z.ZodTypeAny>(sql: string, params: QueryParams, model: Model): Promise<CursorIterator<z.infer<Model>>>;
172
+ queryCursor<Model extends AnyRowSchema>(sql: string, model: Model): Promise<CursorIterator<z.infer<Model>>>;
173
+ queryCursor<Model extends AnyRowSchema>(sql: string, params: QueryParams, model: Model): Promise<CursorIterator<z.infer<Model>>>;
154
174
  private queryCursorInternal;
155
175
  /**
156
176
  * Set the schema to use for the search path.
@@ -186,4 +206,5 @@ export declare class PostgresPool {
186
206
  /** The total number of queries that have been executed by this pool. */
187
207
  get queryCount(): number;
188
208
  }
209
+ export {};
189
210
  //# sourceMappingURL=pool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport { Readable, Transform } from 'node:stream';\n\nimport debugfn from 'debug';\nimport { difference, sampleSize } from 'es-toolkit';\nimport multipipe from 'multipipe';\nimport pg, { DatabaseError, type QueryResult, escapeLiteral } from 'pg';\nimport Cursor from 'pg-cursor';\nimport { z } from 'zod';\n\nexport type QueryParams = Record<string, any> | any[];\n\nexport interface CursorIterator<T> {\n iterate: (batchSize: number) => AsyncGenerator<T[]>;\n stream: (batchSize: number) => NodeJS.ReadWriteStream;\n}\n\nexport interface PostgresPoolConfig extends pg.PoolConfig {\n errorOnUnusedParameters?: boolean;\n}\n\nconst debug = debugfn('@prairielearn/postgres');\nconst lastQueryMap = new WeakMap<pg.PoolClient, string>();\nconst searchSchemaMap = new WeakMap<pg.PoolClient, string>();\n\nfunction addDataToError(err: Error, data: Record<string, any>): Error {\n (err as any).data = {\n ...(err as any).data,\n ...data,\n };\n return err;\n}\n\n/** @knipignore */\nexport class PostgresError extends Error {\n public data: Record<string, any>;\n\n constructor(message: string, data: Record<string, any>) {\n super(message);\n this.data = data;\n this.name = 'PostgresError';\n }\n}\n\n/**\n * Formats a string for debugging.\n */\nfunction debugString(s: string): string {\n if (typeof s !== 'string') return 'NOT A STRING';\n s = s.replaceAll('\\n', '\\\\n');\n if (s.length > 78) s = s.slice(0, 75) + '...';\n s = '\"' + s + '\"';\n return s;\n}\n\n/**\n * Formats a set of params for debugging.\n */\nfunction debugParams(params: QueryParams): string {\n let s;\n try {\n s = JSON.stringify(params);\n } catch {\n s = 'CANNOT JSON STRINGIFY';\n }\n return debugString(s);\n}\n\n/**\n * Given an SQL string and params, creates an array of params and an SQL string\n * with any named dollar-sign placeholders replaced with parameters.\n */\nfunction paramsToArray(\n sql: string,\n params: QueryParams,\n errorOnUnusedParameters: boolean,\n): { processedSql: string; paramsArray: any } {\n if (typeof sql !== 'string') throw new Error('SQL must be a string');\n if (Array.isArray(params)) {\n return {\n processedSql: sql,\n paramsArray: params,\n };\n }\n if (params == null || typeof params !== 'object') {\n throw new Error('params must be array or object');\n }\n\n const re = /\\$([-_a-zA-Z0-9]+)/;\n let result;\n let processedSql = '';\n let remainingSql = sql;\n let nParams = 0;\n const map: Record<string, string> = {};\n let paramsArray: any[] = [];\n while ((result = re.exec(remainingSql)) !== null) {\n const v = result[1];\n if (!(v in map)) {\n if (!(v in params)) throw new Error(`Missing parameter: ${v}`);\n if (Array.isArray(params[v])) {\n map[v] = 'ARRAY[' + params[v].map((_, n) => '$' + (n + nParams + 1)).join(',') + ']';\n nParams += params[v].length;\n paramsArray = paramsArray.concat(params[v]);\n } else {\n nParams++;\n map[v] = '$' + nParams;\n paramsArray.push(params[v]);\n }\n }\n processedSql += remainingSql.slice(0, result.index) + map[v];\n remainingSql = remainingSql.slice(result.index + result[0].length);\n }\n processedSql += remainingSql;\n remainingSql = '';\n if (errorOnUnusedParameters) {\n const unusedKeys = difference(Object.keys(params), Object.keys(map));\n if (unusedKeys.length > 0) {\n throw new Error(`Unused parameters in SQL query: ${JSON.stringify(unusedKeys)}`);\n }\n }\n return { processedSql, paramsArray };\n}\n\n/**\n * Escapes the given identifier for use in an SQL query. Useful for preventing\n * SQL injection.\n */\nfunction escapeIdentifier(identifier: string): string {\n // Note that as of 2021-06-29 escapeIdentifier() is undocumented. See:\n // https://github.com/brianc/node-postgres/pull/396\n // https://github.com/brianc/node-postgres/issues/1978\n // https://www.postgresql.org/docs/current/sql-syntax-lexical.html\n return pg.Client.prototype.escapeIdentifier(identifier);\n}\n\nfunction enhanceError(err: Error, sql: string, params: QueryParams): Error {\n // Copy the error so we don't end up with a circular reference in the\n // final error.\n const sqlError = {\n ...err,\n // `message` is a non-enumerable property, so we need to copy it manually to\n // the error object.\n message: err.message,\n };\n\n const errorHasPosition = err instanceof DatabaseError && err.position != null;\n\n return addDataToError(err, {\n sqlError,\n // If the error has a `position` field, we need to use the processed source\n // (where e.g. `$foobar` has been replaced with `$1`) so that the position\n // is accurate.\n sql: errorHasPosition ? paramsToArray(sql, params, false).processedSql : sql,\n sqlParams: params,\n });\n}\n\nexport class PostgresPool {\n /** The pool from which clients will be acquired. */\n private pool: pg.Pool | null = null;\n /**\n * We use this to propagate the client associated with the current transaction\n * to any nested queries. In the past, we had some nasty bugs associated with\n * the fact that we tried to acquire new clients inside of transactions, which\n * ultimately lead to a deadlock.\n */\n private alsClient = new AsyncLocalStorage<pg.PoolClient>();\n private searchSchema: string | null = null;\n /** Tracks the total number of queries executed by this pool. */\n private _queryCount = 0;\n private errorOnUnusedParameters = false;\n\n /**\n * Creates a new connection pool and attempts to connect to the database.\n */\n async initAsync(\n pgConfig: PostgresPoolConfig,\n idleErrorHandler: (error: Error, client: pg.PoolClient) => void,\n ): Promise<void> {\n if (this.pool != null) {\n throw new Error('Postgres pool already initialized');\n }\n this.errorOnUnusedParameters = pgConfig.errorOnUnusedParameters ?? false;\n this.pool = new pg.Pool(pgConfig);\n this.pool.on('error', function (err, client) {\n const lastQuery = lastQueryMap.get(client);\n idleErrorHandler(addDataToError(err, { lastQuery }), client);\n });\n this.pool.on('connect', (client) => {\n client.on('error', (err) => {\n const lastQuery = lastQueryMap.get(client);\n idleErrorHandler(addDataToError(err, { lastQuery }), client);\n });\n });\n this.pool.on('remove', (client) => {\n // This shouldn't be necessary, as `pg` currently allows clients to be\n // garbage collected after they're removed. However, if `pg` someday\n // starts reusing client objects across difference connections, this\n // will ensure that we re-set the search path when the client reconnects.\n searchSchemaMap.delete(client);\n });\n\n // Attempt to connect to the database so that we can fail quickly if\n // something isn't configured correctly.\n let retryCount = 0;\n const retryTimeouts = [500, 1000, 2000, 5000, 10000];\n while (retryCount <= retryTimeouts.length) {\n try {\n const client = await this.pool.connect();\n client.release();\n return;\n } catch (err: any) {\n if (retryCount === retryTimeouts.length) {\n throw new Error(`Could not connect to Postgres after ${retryTimeouts.length} attempts`, {\n cause: err,\n });\n }\n\n const timeout = retryTimeouts[retryCount];\n retryCount++;\n await new Promise((resolve) => setTimeout(resolve, timeout));\n }\n }\n }\n\n /**\n * Closes the connection pool.\n */\n async closeAsync(): Promise<void> {\n if (!this.pool) return;\n await this.pool.end();\n this.pool = null;\n }\n\n /**\n * Gets a new client from the connection pool. The caller MUST call `release()` to\n * release the client, whether or not errors occurred while using\n * `client`. The client can call `done(truthy_value)` to force\n * destruction of the client, but this should not be used except in\n * unusual circumstances.\n */\n async getClientAsync(): Promise<pg.PoolClient> {\n if (!this.pool) {\n throw new Error('Connection pool is not open');\n }\n\n // If we're inside a transaction, we'll reuse the same client to avoid a\n // potential deadlock.\n const client = this.alsClient.getStore() ?? (await this.pool.connect());\n\n // If we're configured to use a particular schema, we'll store whether or\n // not the search path has already been configured for this particular\n // client. If we acquire a client and it's already had its search path\n // set, we can avoid setting it again since the search path will persist\n // for the life of the client.\n //\n // We do this check for each call to `getClient` instead of on\n // `pool.connect` so that we don't have to be really careful about\n // destroying old clients that were created before `setSearchSchema` was\n // called. Instead, we'll just check if the search path matches the\n // currently-desired schema, and if it's a mismatch (or doesn't exist\n // at all), we re-set it for the current client.\n //\n // Note that this accidentally supports changing the search_path on the fly,\n // although that's not something we currently do (or would be likely to do).\n // It does NOT support clearing the existing search schema - e.g.,\n // `setSearchSchema(null)` would not work as you expect. This is fine, as\n // that's not something we ever do in practice.\n const clientSearchSchema = searchSchemaMap.get(client);\n if (this.searchSchema != null && clientSearchSchema !== this.searchSchema) {\n const setSearchPathSql = `SET search_path TO ${escapeIdentifier(this.searchSchema)},public`;\n try {\n await this.queryWithClientAsync(client, setSearchPathSql, {});\n } catch (err) {\n client.release();\n throw err;\n }\n searchSchemaMap.set(client, this.searchSchema);\n }\n\n return client;\n }\n\n /**\n * Performs a query with the given client.\n */\n async queryWithClientAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<pg.QueryResult> {\n this._queryCount += 1;\n debug('queryWithClient()', 'sql:', debugString(sql));\n debug('queryWithClient()', 'params:', debugParams(params));\n const { processedSql, paramsArray } = paramsToArray(sql, params, this.errorOnUnusedParameters);\n try {\n lastQueryMap.set(client, processedSql);\n const result = await client.query(processedSql, paramsArray);\n debug('queryWithClient() success', 'rowCount:', result.rowCount);\n return result;\n } catch (err: any) {\n throw enhanceError(err, sql, params);\n }\n }\n\n /**\n * Performs a query with the given client. Errors if the query returns more\n * than one row.\n */\n async queryWithClientOneRowAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<pg.QueryResult> {\n debug('queryWithClientOneRow()', 'sql:', debugString(sql));\n debug('queryWithClientOneRow()', 'params:', debugParams(params));\n const result = await this.queryWithClientAsync(client, sql, params);\n if (result.rowCount !== 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n result,\n });\n }\n debug('queryWithClientOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Performs a query with the given client. Errors if the query returns more\n * than one row.\n */\n async queryWithClientZeroOrOneRowAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<QueryResult> {\n debug('queryWithClientZeroOrOneRow()', 'sql:', debugString(sql));\n debug('queryWithClientZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.queryWithClientAsync(client, sql, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n result,\n });\n }\n debug('queryWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Rolls back the current transaction for the given client.\n */\n async rollbackWithClientAsync(client: pg.PoolClient) {\n debug('rollbackWithClient()');\n // From https://node-postgres.com/features/transactions\n try {\n await client.query('ROLLBACK');\n // Only release the client if we weren't already inside a transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n } catch (err: any) {\n // If there was a problem rolling back the query, something is\n // seriously messed up. Return the error to the release() function to\n // close & remove this client from the pool. If you leave a client in\n // the pool with an unaborted transaction, weird and hard to diagnose\n // problems might happen.\n client.release(err);\n }\n }\n\n /**\n * Begins a new transaction.\n */\n async beginTransactionAsync(): Promise<pg.PoolClient> {\n debug('beginTransaction()');\n const client = await this.getClientAsync();\n try {\n await this.queryWithClientAsync(client, 'START TRANSACTION;', {});\n return client;\n } catch (err) {\n await this.rollbackWithClientAsync(client);\n throw err;\n }\n }\n\n /**\n * Commits the transaction if err is null, otherwise rollbacks the transaction.\n * Also releases the client.\n */\n async endTransactionAsync(client: pg.PoolClient, err: Error | null | undefined) {\n debug('endTransaction()');\n if (err) {\n try {\n await this.rollbackWithClientAsync(client);\n } catch (rollbackErr: any) {\n throw addDataToError(rollbackErr, { prevErr: err, rollback: 'fail' });\n }\n\n // Even though we successfully rolled back the transaction, there was\n // still an error in the first place that necessitated a rollback. Re-throw\n // that error here so that everything downstream of here will know about it.\n throw addDataToError(err, { rollback: 'success' });\n } else {\n try {\n await this.queryWithClientAsync(client, 'COMMIT', {});\n } finally {\n // Only release the client if we aren't nested inside another transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n }\n }\n }\n\n /**\n * Runs the specified function inside of a transaction. The function will\n * receive a database client as an argument, but it can also make queries\n * as usual, and the correct client will be used automatically.\n *\n * The transaction will be rolled back if the function throws an error, and\n * will be committed otherwise.\n */\n async runInTransactionAsync<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T> {\n // Check if we're already inside a transaction. If so, we won't start another one,\n // as Postgres doesn't support nested transactions.\n const client = this.alsClient.getStore();\n const isNestedTransaction = client !== undefined;\n const transactionClient = client ?? (await this.beginTransactionAsync());\n\n let result: T;\n try {\n result = await this.alsClient.run(transactionClient, () => fn(transactionClient));\n } catch (err: any) {\n if (!isNestedTransaction) {\n // If we're inside another transaction, we assume that the root transaction\n // will catch this error and roll back the transaction.\n await this.endTransactionAsync(transactionClient, err);\n }\n throw err;\n }\n\n if (!isNestedTransaction) {\n // If we're inside another transaction; don't commit it prematurely. Allow\n // the root transaction to commit it instead.\n //\n // Note that we don't invoke `endTransactionAsync` inside the `try` block\n // because we don't want an error thrown by it to trigger *another* call\n // to `endTransactionAsync` in the `catch` block.\n await this.endTransactionAsync(transactionClient, null);\n }\n\n return result;\n }\n\n /**\n * Executes a query with the specified parameters.\n *\n * @deprecated Use {@link execute} instead.\n *\n * Using the return value of this function directly is not recommended. Instead, use\n * {@link queryRows}, {@link queryRow}, or {@link queryOptionalRow}.\n */\n async queryAsync(sql: string, params: QueryParams): Promise<QueryResult> {\n debug('query()', 'sql:', debugString(sql));\n debug('query()', 'params:', debugParams(params));\n const client = await this.getClientAsync();\n try {\n return await this.queryWithClientAsync(client, sql, params);\n } finally {\n // Only release if we aren't nested in a transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n }\n }\n\n /**\n * Executes a query with the specified parameters. Errors if the query does\n * not return exactly one row.\n *\n * @deprecated Use {@link executeRow} or {@link queryRow} instead.\n */\n async queryOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {\n debug('queryOneRow()', 'sql:', debugString(sql));\n debug('queryOneRow()', 'params:', debugParams(params));\n const result = await this.queryAsync(sql, params);\n if (result.rowCount !== 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n });\n }\n debug('queryOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Executes a query with the specified parameters. Errors if the query\n * returns more than one row.\n */\n async queryZeroOrOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {\n debug('queryZeroOrOneRow()', 'sql:', debugString(sql));\n debug('queryZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.queryAsync(sql, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n });\n }\n debug('queryZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters.\n */\n async callAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('call()', 'function:', functionName);\n debug('call()', 'params:', debugParams(params));\n const placeholders = params.map((_, v) => '$' + (v + 1)).join(',');\n const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders});`;\n const result = await this.queryAsync(sql, params);\n debug('call() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters. Errors if the\n * sproc does not return exactly one row.\n */\n async callOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('callOneRow()', 'function:', functionName);\n debug('callOneRow()', 'params:', debugParams(params));\n const result = await this.callAsync(functionName, params);\n if (result.rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters. Errors if the\n * sproc returns more than one row.\n */\n async callZeroOrOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('callZeroOrOneRow()', 'function:', functionName);\n debug('callZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.callAsync(functionName, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a sproc with the specified parameters using a specific client.\n */\n async callWithClientAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClient()', 'function:', functionName);\n debug('callWithClient()', 'params:', debugParams(params));\n const placeholders = params.map((_, v) => '$' + (v + 1)).join(',');\n const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders})`;\n const result = await this.queryWithClientAsync(client, sql, params);\n debug('callWithClient() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a sproc with the specified parameters using a specific client.\n * Errors if the sproc does not return exactly one row.\n */\n async callWithClientOneRowAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClientOneRow()', 'function:', functionName);\n debug('callWithClientOneRow()', 'params:', debugParams(params));\n const result = await this.callWithClientAsync(client, functionName, params);\n if (result.rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callWithClientOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a function with the specified parameters using a specific client.\n * Errors if the sproc returns more than one row.\n */\n async callWithClientZeroOrOneRowAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClientZeroOrOneRow()', 'function:', functionName);\n debug('callWithClientZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.callWithClientAsync(client, functionName, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n async queryRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;\n async queryRows<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Executes a query with the specified parameters. Returns an array of rows\n * that conform to the given Zod schema.\n *\n * If the query returns a single column, the return value will be a list of column values.\n */\n async queryRows<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryAsync(sql, params);\n if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n const rawData = results.rows.map((row) => row[columnName]);\n return z.array(model).parse(rawData);\n } else {\n return z.array(model).parse(results.rows);\n }\n }\n\n async queryRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;\n async queryRow<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Executes a query with the specified parameters. Returns exactly one row that conforms to the given Zod schema.\n *\n * If the query returns a single column, the return value will be the column value itself.\n */\n async queryRow<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryOneRowAsync(sql, params);\n if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n return model.parse(results.rows[0][columnName]);\n } else {\n return model.parse(results.rows[0]);\n }\n }\n\n async queryOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async queryOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Executes a query with the specified parameters. Returns either null or a\n * single row that conforms to the given Zod schema, and errors otherwise.\n *\n * If the query returns a single column, the return value will be the column value itself.\n */\n async queryOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryZeroOrOneRowAsync(sql, params);\n if (results.rows.length === 0) {\n return null;\n } else if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n return model.parse(results.rows[0][columnName]);\n } else {\n return model.parse(results.rows[0]);\n }\n }\n\n async callRows<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>[]>;\n async callRows<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Calls the given sproc with the specified parameters.\n * Errors if the sproc does not return anything.\n */\n async callRows<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callAsync(sql, params);\n if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n const rawData = results.rows.map((row) => row[columnName]);\n return z.array(model).parse(rawData);\n } else {\n return z.array(model).parse(results.rows);\n }\n }\n\n async callRow<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;\n async callRow<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Calls the given sproc with the specified parameters.\n * Returns exactly one row from the sproc that conforms to the given Zod schema.\n */\n async callRow<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callOneRowAsync(sql, params);\n if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n return model.parse(results.rows[0][columnName]);\n } else {\n return model.parse(results.rows[0]);\n }\n }\n\n async callOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async callOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Calls the given sproc with the specified parameters. Returns either null\n * or a single row that conforms to the given Zod schema.\n */\n async callOptionalRow<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callZeroOrOneRowAsync(sql, params);\n if (results.rows.length === 0) {\n return null;\n } else if (results.fields.length === 1) {\n const columnName = results.fields[0].name;\n return model.parse(results.rows[0][columnName]);\n } else {\n return model.parse(results.rows[0]);\n }\n }\n\n /**\n * Executes a query with the specified parameters. Returns the number of rows affected.\n */\n async execute(sql: string, params: QueryParams = {}): Promise<number> {\n const result = await this.queryAsync(sql, params);\n return result.rowCount ?? 0;\n }\n\n /**\n * Executes a query with the specified parameter, and errors if the query doesn't return exactly one row.\n */\n async executeRow(sql: string, params: QueryParams = {}) {\n const rowCount = await this.execute(sql, params);\n if (rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + rowCount, {\n sql,\n sqlParams: params,\n });\n }\n }\n\n /**\n * Returns a {@link Cursor} for the given query. The cursor can be used to\n * read results in batches, which is useful for large result sets.\n */\n private async queryCursorWithClient(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<Cursor> {\n this._queryCount += 1;\n debug('queryCursorWithClient()', 'sql:', debugString(sql));\n debug('queryCursorWithClient()', 'params:', debugParams(params));\n const { processedSql, paramsArray } = paramsToArray(sql, params, this.errorOnUnusedParameters);\n lastQueryMap.set(client, processedSql);\n return client.query(new Cursor(processedSql, paramsArray));\n }\n\n async queryCursor<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<CursorIterator<z.infer<Model>>>;\n\n async queryCursor<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<CursorIterator<z.infer<Model>>>;\n\n /**\n * Returns an {@link CursorIterator} that can be used to iterate over the\n * results of the query in batches, which is useful for large result sets.\n * Each row will be parsed by the given Zod schema.\n */\n async queryCursor<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: Model | QueryParams,\n maybeModel?: Model,\n ): Promise<CursorIterator<z.infer<Model>>> {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n return this.queryCursorInternal(sql, params, model);\n }\n\n private async queryCursorInternal<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model?: Model,\n ): Promise<CursorIterator<z.infer<Model>>> {\n const client = await this.getClientAsync();\n const cursor = await this.queryCursorWithClient(client, sql, params);\n\n let iterateCalled = false;\n let rowKeys: string[] | null = null;\n const iterator: CursorIterator<z.infer<Model>> = {\n async *iterate(batchSize: number) {\n // Safety check: if someone calls iterate multiple times, they're\n // definitely doing something wrong.\n if (iterateCalled) {\n throw new Error('iterate() called multiple times');\n }\n iterateCalled = true;\n\n try {\n while (true) {\n const rows = await cursor.read(batchSize);\n if (rows.length === 0) {\n break;\n }\n\n if (rowKeys === null) {\n rowKeys = Object.keys(rows[0] ?? {});\n }\n const flattened =\n rowKeys.length === 1 ? rows.map((row) => row[(rowKeys as string[])[0]]) : rows;\n if (model) {\n yield z.array(model).parse(flattened);\n } else {\n yield flattened;\n }\n }\n } catch (err: any) {\n throw enhanceError(err, sql, params);\n } finally {\n try {\n await cursor.close();\n } finally {\n client.release();\n }\n }\n },\n stream(batchSize: number) {\n const transform = new Transform({\n readableObjectMode: true,\n writableObjectMode: true,\n transform(chunk, _encoding, callback) {\n for (const row of chunk) {\n this.push(row);\n }\n callback();\n },\n });\n\n // TODO: use native `node:stream#compose` once it's stable.\n const generator = iterator.iterate(batchSize);\n const pipe = multipipe(Readable.from(generator), transform);\n\n // When the underlying stream is closed, we need to make sure that the\n // cursor is also closed. We do this by calling `return()` on the generator,\n // which will trigger its `finally` block, which will in turn release\n // the client and close the cursor. The fact that the stream is already\n // closed by this point means that someone reading from the stream will\n // never actually see the `null` value that's returned.\n pipe.once('close', () => {\n generator.return(null);\n });\n\n return pipe;\n },\n };\n return iterator;\n }\n\n /**\n * Set the schema to use for the search path.\n *\n * @param schema The schema name to use (can be \"null\" to unset the search path)\n */\n async setSearchSchema(schema: string | null) {\n if (schema == null) {\n this.searchSchema = schema;\n return;\n }\n\n await this.queryAsync(`CREATE SCHEMA IF NOT EXISTS ${escapeIdentifier(schema)}`, {});\n // We only set searchSchema after CREATE to avoid the above query() call using searchSchema.\n this.searchSchema = schema;\n }\n\n /**\n * Get the schema that is currently used for the search path.\n *\n * @returns schema in use (may be `null` to indicate no schema)\n */\n getSearchSchema(): string | null {\n return this.searchSchema;\n }\n\n /**\n * Generate, set, and return a random schema name.\n *\n * @param prefix The prefix of the new schema, only the first 28 characters will be used (after lowercasing).\n * @returns The randomly-generated search schema.\n */\n async setRandomSearchSchemaAsync(prefix: string): Promise<string> {\n // truncated prefix (max 28 characters)\n const truncPrefix = prefix.slice(0, 28);\n // timestamp in format YYYY-MM-DDTHH:MM:SS.SSSZ (guaranteed to not exceed 27 characters in the spec)\n const timestamp = new Date().toISOString();\n // random 6-character suffix to avoid clashes (approx 1.4 billion possible values)\n const suffix = sampleSize([...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'], 6).join('');\n\n // Schema is guaranteed to have length at most 63 (= 28 + 1 + 27 + 1 + 6),\n // which is the default PostgreSQL identifier limit.\n // Note that this schema name will need quoting because of characters like ':', '-', etc\n const schema = `${truncPrefix}_${timestamp}_${suffix}`;\n await this.setSearchSchema(schema);\n return schema;\n }\n\n /**\n * Deletes all schemas starting with the given prefix.\n *\n * @param prefix The prefix of the schemas to delete.\n */\n async clearSchemasStartingWith(prefix: string): Promise<void> {\n // Sanity check against deleting public, pg_, information_schema, etc.\n if (prefix === 'public' || prefix.startsWith('pg_') || prefix === 'information_schema') {\n throw new Error(`Cannot clear schema starting with ${prefix}`);\n }\n // Sanity check against a bad prefix.\n if (prefix.length < 4) {\n throw new Error(`Prefix is too short: ${prefix}`);\n }\n\n await this.queryAsync(\n `DO $$\n DECLARE\n r RECORD;\n BEGIN\n FOR r IN\n SELECT nspname\n FROM pg_namespace\n WHERE nspname LIKE ${escapeLiteral(prefix + '%')}\n AND nspname NOT LIKE 'pg_temp_%'\n LOOP\n EXECUTE format('DROP SCHEMA IF EXISTS %I CASCADE;', r.nspname);\n COMMIT; -- avoid shared memory exhaustion\n END LOOP;\n END $$;`,\n {},\n );\n }\n\n /** The number of established connections. */\n get totalCount() {\n return this.pool?.totalCount ?? 0;\n }\n\n /** The number of idle connections. */\n get idleCount() {\n return this.pool?.idleCount ?? 0;\n }\n\n /** The number of queries waiting for a connection to become available. */\n get waitingCount() {\n return this.pool?.waitingCount ?? 0;\n }\n\n /** The total number of queries that have been executed by this pool. */\n get queryCount() {\n return this._queryCount;\n }\n}\n"]}
1
+ {"version":3,"file":"pool.d.ts","sourceRoot":"","sources":["../src/pool.ts"],"names":[],"mappings":"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","sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport { Readable, Transform } from 'node:stream';\n\nimport debugfn from 'debug';\nimport { difference, sampleSize } from 'es-toolkit';\nimport multipipe from 'multipipe';\nimport pg, { DatabaseError, type QueryResult, escapeLiteral } from 'pg';\nimport Cursor from 'pg-cursor';\nimport { z } from 'zod';\n\nexport type QueryParams = Record<string, any> | any[];\n\n/**\n * Type constraint for row schemas accepted by query functions.\n * Accepts `z.object(...)`, unions/intersections/discriminated unions of objects,\n * transforms/refinements of any of those, branded variants, and `z.unknown()`\n * as an escape hatch.\n */\ntype AnyObjectLikeSchema =\n | z.AnyZodObject\n | z.ZodEffects<AnyObjectLikeSchema, any, any>\n | z.ZodIntersection<AnyObjectLikeSchema, AnyObjectLikeSchema>\n | z.ZodUnion<Readonly<[AnyObjectLikeSchema, ...AnyObjectLikeSchema[]]>>\n | z.ZodDiscriminatedUnion<string, z.AnyZodObject[]>;\nexport type AnyRowSchema =\n | AnyObjectLikeSchema\n | z.ZodBranded<AnyObjectLikeSchema, any>\n | z.ZodUnknown;\n\nexport interface CursorIterator<T> {\n iterate: (batchSize: number) => AsyncGenerator<T[]>;\n stream: (batchSize: number) => NodeJS.ReadWriteStream;\n}\n\nexport interface PostgresPoolConfig extends pg.PoolConfig {\n errorOnUnusedParameters?: boolean;\n}\n\nconst debug = debugfn('@prairielearn/postgres');\nconst lastQueryMap = new WeakMap<pg.PoolClient, string>();\nconst searchSchemaMap = new WeakMap<pg.PoolClient, string>();\n\nfunction addDataToError(err: Error, data: Record<string, any>): Error {\n (err as any).data = {\n ...(err as any).data,\n ...data,\n };\n return err;\n}\n\n/** @knipignore */\nexport class PostgresError extends Error {\n public data: Record<string, any>;\n\n constructor(message: string, data: Record<string, any>) {\n super(message);\n this.data = data;\n this.name = 'PostgresError';\n }\n}\n\n/**\n * Formats a string for debugging.\n */\nfunction debugString(s: string): string {\n if (typeof s !== 'string') return 'NOT A STRING';\n s = s.replaceAll('\\n', '\\\\n');\n if (s.length > 78) s = s.slice(0, 75) + '...';\n s = '\"' + s + '\"';\n return s;\n}\n\n/**\n * Formats a set of params for debugging.\n */\nfunction debugParams(params: QueryParams): string {\n let s;\n try {\n s = JSON.stringify(params);\n } catch {\n s = 'CANNOT JSON STRINGIFY';\n }\n return debugString(s);\n}\n\n/**\n * Given an SQL string and params, creates an array of params and an SQL string\n * with any named dollar-sign placeholders replaced with parameters.\n */\nfunction paramsToArray(\n sql: string,\n params: QueryParams,\n errorOnUnusedParameters: boolean,\n): { processedSql: string; paramsArray: any } {\n if (typeof sql !== 'string') throw new Error('SQL must be a string');\n if (Array.isArray(params)) {\n return {\n processedSql: sql,\n paramsArray: params,\n };\n }\n if (params == null || typeof params !== 'object') {\n throw new Error('params must be array or object');\n }\n\n const re = /\\$([-_a-zA-Z0-9]+)/;\n let result;\n let processedSql = '';\n let remainingSql = sql;\n let nParams = 0;\n const map: Record<string, string> = {};\n let paramsArray: any[] = [];\n while ((result = re.exec(remainingSql)) !== null) {\n const v = result[1];\n if (!(v in map)) {\n if (!(v in params)) throw new Error(`Missing parameter: ${v}`);\n if (Array.isArray(params[v])) {\n map[v] = 'ARRAY[' + params[v].map((_, n) => '$' + (n + nParams + 1)).join(',') + ']';\n nParams += params[v].length;\n paramsArray = paramsArray.concat(params[v]);\n } else {\n nParams++;\n map[v] = '$' + nParams;\n paramsArray.push(params[v]);\n }\n }\n processedSql += remainingSql.slice(0, result.index) + map[v];\n remainingSql = remainingSql.slice(result.index + result[0].length);\n }\n processedSql += remainingSql;\n remainingSql = '';\n if (errorOnUnusedParameters) {\n const unusedKeys = difference(Object.keys(params), Object.keys(map));\n if (unusedKeys.length > 0) {\n throw new Error(`Unused parameters in SQL query: ${JSON.stringify(unusedKeys)}`);\n }\n }\n return { processedSql, paramsArray };\n}\n\n/**\n * Escapes the given identifier for use in an SQL query. Useful for preventing\n * SQL injection.\n */\nfunction escapeIdentifier(identifier: string): string {\n // Note that as of 2021-06-29 escapeIdentifier() is undocumented. See:\n // https://github.com/brianc/node-postgres/pull/396\n // https://github.com/brianc/node-postgres/issues/1978\n // https://www.postgresql.org/docs/current/sql-syntax-lexical.html\n return pg.Client.prototype.escapeIdentifier(identifier);\n}\n\nfunction assertSingleColumn(result: pg.QueryResult, context: Record<string, any>): string {\n if (result.fields.length !== 1) {\n throw new PostgresError(\n `Expected exactly one column, but found ${result.fields.length}`,\n context,\n );\n }\n return result.fields[0].name;\n}\n\nfunction enhanceError(err: Error, sql: string, params: QueryParams): Error {\n // Copy the error so we don't end up with a circular reference in the\n // final error.\n const sqlError = {\n ...err,\n // `message` is a non-enumerable property, so we need to copy it manually to\n // the error object.\n message: err.message,\n };\n\n const errorHasPosition = err instanceof DatabaseError && err.position != null;\n\n return addDataToError(err, {\n sqlError,\n // If the error has a `position` field, we need to use the processed source\n // (where e.g. `$foobar` has been replaced with `$1`) so that the position\n // is accurate.\n sql: errorHasPosition ? paramsToArray(sql, params, false).processedSql : sql,\n sqlParams: params,\n });\n}\n\nexport class PostgresPool {\n /** The pool from which clients will be acquired. */\n private pool: pg.Pool | null = null;\n /**\n * We use this to propagate the client associated with the current transaction\n * to any nested queries. In the past, we had some nasty bugs associated with\n * the fact that we tried to acquire new clients inside of transactions, which\n * ultimately lead to a deadlock.\n */\n private alsClient = new AsyncLocalStorage<pg.PoolClient>();\n private searchSchema: string | null = null;\n /** Tracks the total number of queries executed by this pool. */\n private _queryCount = 0;\n private errorOnUnusedParameters = false;\n\n /**\n * Creates a new connection pool and attempts to connect to the database.\n */\n async initAsync(\n pgConfig: PostgresPoolConfig,\n idleErrorHandler: (error: Error, client: pg.PoolClient) => void,\n ): Promise<void> {\n if (this.pool != null) {\n throw new Error('Postgres pool already initialized');\n }\n this.errorOnUnusedParameters = pgConfig.errorOnUnusedParameters ?? false;\n this.pool = new pg.Pool(pgConfig);\n this.pool.on('error', function (err, client) {\n const lastQuery = lastQueryMap.get(client);\n idleErrorHandler(addDataToError(err, { lastQuery }), client);\n });\n this.pool.on('connect', (client) => {\n client.on('error', (err) => {\n const lastQuery = lastQueryMap.get(client);\n idleErrorHandler(addDataToError(err, { lastQuery }), client);\n });\n });\n this.pool.on('remove', (client) => {\n // This shouldn't be necessary, as `pg` currently allows clients to be\n // garbage collected after they're removed. However, if `pg` someday\n // starts reusing client objects across difference connections, this\n // will ensure that we re-set the search path when the client reconnects.\n searchSchemaMap.delete(client);\n });\n\n // Attempt to connect to the database so that we can fail quickly if\n // something isn't configured correctly.\n let retryCount = 0;\n const retryTimeouts = [500, 1000, 2000, 5000, 10000];\n while (retryCount <= retryTimeouts.length) {\n try {\n const client = await this.pool.connect();\n client.release();\n return;\n } catch (err: any) {\n if (retryCount === retryTimeouts.length) {\n throw new Error(`Could not connect to Postgres after ${retryTimeouts.length} attempts`, {\n cause: err,\n });\n }\n\n const timeout = retryTimeouts[retryCount];\n retryCount++;\n await new Promise((resolve) => setTimeout(resolve, timeout));\n }\n }\n }\n\n /**\n * Closes the connection pool.\n */\n async closeAsync(): Promise<void> {\n if (!this.pool) return;\n await this.pool.end();\n this.pool = null;\n }\n\n /**\n * Gets a new client from the connection pool. The caller MUST call `release()` to\n * release the client, whether or not errors occurred while using\n * `client`. The client can call `done(truthy_value)` to force\n * destruction of the client, but this should not be used except in\n * unusual circumstances.\n */\n async getClientAsync(): Promise<pg.PoolClient> {\n if (!this.pool) {\n throw new Error('Connection pool is not open');\n }\n\n // If we're inside a transaction, we'll reuse the same client to avoid a\n // potential deadlock.\n const client = this.alsClient.getStore() ?? (await this.pool.connect());\n\n // If we're configured to use a particular schema, we'll store whether or\n // not the search path has already been configured for this particular\n // client. If we acquire a client and it's already had its search path\n // set, we can avoid setting it again since the search path will persist\n // for the life of the client.\n //\n // We do this check for each call to `getClient` instead of on\n // `pool.connect` so that we don't have to be really careful about\n // destroying old clients that were created before `setSearchSchema` was\n // called. Instead, we'll just check if the search path matches the\n // currently-desired schema, and if it's a mismatch (or doesn't exist\n // at all), we re-set it for the current client.\n //\n // Note that this accidentally supports changing the search_path on the fly,\n // although that's not something we currently do (or would be likely to do).\n // It does NOT support clearing the existing search schema - e.g.,\n // `setSearchSchema(null)` would not work as you expect. This is fine, as\n // that's not something we ever do in practice.\n const clientSearchSchema = searchSchemaMap.get(client);\n if (this.searchSchema != null && clientSearchSchema !== this.searchSchema) {\n const setSearchPathSql = `SET search_path TO ${escapeIdentifier(this.searchSchema)},public`;\n try {\n await this.queryWithClientAsync(client, setSearchPathSql, {});\n } catch (err) {\n client.release();\n throw err;\n }\n searchSchemaMap.set(client, this.searchSchema);\n }\n\n return client;\n }\n\n /**\n * Performs a query with the given client.\n */\n async queryWithClientAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<pg.QueryResult> {\n this._queryCount += 1;\n debug('queryWithClient()', 'sql:', debugString(sql));\n debug('queryWithClient()', 'params:', debugParams(params));\n const { processedSql, paramsArray } = paramsToArray(sql, params, this.errorOnUnusedParameters);\n try {\n lastQueryMap.set(client, processedSql);\n const result = await client.query(processedSql, paramsArray);\n debug('queryWithClient() success', 'rowCount:', result.rowCount);\n return result;\n } catch (err: any) {\n throw enhanceError(err, sql, params);\n }\n }\n\n /**\n * Performs a query with the given client. Errors if the query returns more\n * than one row.\n */\n async queryWithClientOneRowAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<pg.QueryResult> {\n debug('queryWithClientOneRow()', 'sql:', debugString(sql));\n debug('queryWithClientOneRow()', 'params:', debugParams(params));\n const result = await this.queryWithClientAsync(client, sql, params);\n if (result.rowCount !== 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n result,\n });\n }\n debug('queryWithClientOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Performs a query with the given client. Errors if the query returns more\n * than one row.\n */\n async queryWithClientZeroOrOneRowAsync(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<QueryResult> {\n debug('queryWithClientZeroOrOneRow()', 'sql:', debugString(sql));\n debug('queryWithClientZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.queryWithClientAsync(client, sql, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n result,\n });\n }\n debug('queryWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Rolls back the current transaction for the given client.\n */\n async rollbackWithClientAsync(client: pg.PoolClient) {\n debug('rollbackWithClient()');\n // From https://node-postgres.com/features/transactions\n try {\n await client.query('ROLLBACK');\n // Only release the client if we weren't already inside a transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n } catch (err: any) {\n // If there was a problem rolling back the query, something is\n // seriously messed up. Return the error to the release() function to\n // close & remove this client from the pool. If you leave a client in\n // the pool with an unaborted transaction, weird and hard to diagnose\n // problems might happen.\n client.release(err);\n }\n }\n\n /**\n * Begins a new transaction.\n */\n async beginTransactionAsync(): Promise<pg.PoolClient> {\n debug('beginTransaction()');\n const client = await this.getClientAsync();\n try {\n await this.queryWithClientAsync(client, 'START TRANSACTION;', {});\n return client;\n } catch (err) {\n await this.rollbackWithClientAsync(client);\n throw err;\n }\n }\n\n /**\n * Commits the transaction if err is null, otherwise rollbacks the transaction.\n * Also releases the client.\n */\n async endTransactionAsync(client: pg.PoolClient, err: Error | null | undefined) {\n debug('endTransaction()');\n if (err) {\n try {\n await this.rollbackWithClientAsync(client);\n } catch (rollbackErr: any) {\n throw addDataToError(rollbackErr, { prevErr: err, rollback: 'fail' });\n }\n\n // Even though we successfully rolled back the transaction, there was\n // still an error in the first place that necessitated a rollback. Re-throw\n // that error here so that everything downstream of here will know about it.\n throw addDataToError(err, { rollback: 'success' });\n } else {\n try {\n await this.queryWithClientAsync(client, 'COMMIT', {});\n } finally {\n // Only release the client if we aren't nested inside another transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n }\n }\n }\n\n /**\n * Runs the specified function inside of a transaction. The function will\n * receive a database client as an argument, but it can also make queries\n * as usual, and the correct client will be used automatically.\n *\n * The transaction will be rolled back if the function throws an error, and\n * will be committed otherwise.\n */\n async runInTransactionAsync<T>(fn: (client: pg.PoolClient) => Promise<T>): Promise<T> {\n // Check if we're already inside a transaction. If so, we won't start another one,\n // as Postgres doesn't support nested transactions.\n const client = this.alsClient.getStore();\n const isNestedTransaction = client !== undefined;\n const transactionClient = client ?? (await this.beginTransactionAsync());\n\n let result: T;\n try {\n result = await this.alsClient.run(transactionClient, () => fn(transactionClient));\n } catch (err: any) {\n if (!isNestedTransaction) {\n // If we're inside another transaction, we assume that the root transaction\n // will catch this error and roll back the transaction.\n await this.endTransactionAsync(transactionClient, err);\n }\n throw err;\n }\n\n if (!isNestedTransaction) {\n // If we're inside another transaction; don't commit it prematurely. Allow\n // the root transaction to commit it instead.\n //\n // Note that we don't invoke `endTransactionAsync` inside the `try` block\n // because we don't want an error thrown by it to trigger *another* call\n // to `endTransactionAsync` in the `catch` block.\n await this.endTransactionAsync(transactionClient, null);\n }\n\n return result;\n }\n\n /**\n * Executes a query with the specified parameters.\n *\n * @deprecated Use {@link execute} instead.\n *\n * Using the return value of this function directly is not recommended. Instead, use\n * {@link queryRows}, {@link queryRow}, or {@link queryOptionalRow}.\n */\n async queryAsync(sql: string, params: QueryParams): Promise<QueryResult> {\n debug('query()', 'sql:', debugString(sql));\n debug('query()', 'params:', debugParams(params));\n const client = await this.getClientAsync();\n try {\n return await this.queryWithClientAsync(client, sql, params);\n } finally {\n // Only release if we aren't nested in a transaction.\n if (this.alsClient.getStore() === undefined) {\n client.release();\n }\n }\n }\n\n /**\n * Executes a query with the specified parameters. Errors if the query does\n * not return exactly one row.\n *\n * @deprecated Use {@link executeRow} or {@link queryRow} instead.\n */\n async queryOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {\n debug('queryOneRow()', 'sql:', debugString(sql));\n debug('queryOneRow()', 'params:', debugParams(params));\n const result = await this.queryAsync(sql, params);\n if (result.rowCount !== 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n });\n }\n debug('queryOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Executes a query with the specified parameters. Errors if the query\n * returns more than one row.\n */\n async queryZeroOrOneRowAsync(sql: string, params: QueryParams): Promise<pg.QueryResult> {\n debug('queryZeroOrOneRow()', 'sql:', debugString(sql));\n debug('queryZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.queryAsync(sql, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError(`Incorrect rowCount: ${result.rowCount}`, {\n sql,\n sqlParams: params,\n });\n }\n debug('queryZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters.\n */\n async callAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('call()', 'function:', functionName);\n debug('call()', 'params:', debugParams(params));\n const placeholders = params.map((_, v) => '$' + (v + 1)).join(',');\n const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders});`;\n const result = await this.queryAsync(sql, params);\n debug('call() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters. Errors if the\n * sproc does not return exactly one row.\n */\n async callOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('callOneRow()', 'function:', functionName);\n debug('callOneRow()', 'params:', debugParams(params));\n const result = await this.callAsync(functionName, params);\n if (result.rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls the given sproc with the specified parameters. Errors if the\n * sproc returns more than one row.\n */\n async callZeroOrOneRowAsync(functionName: string, params: any[]): Promise<pg.QueryResult> {\n debug('callZeroOrOneRow()', 'function:', functionName);\n debug('callZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.callAsync(functionName, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a sproc with the specified parameters using a specific client.\n */\n async callWithClientAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClient()', 'function:', functionName);\n debug('callWithClient()', 'params:', debugParams(params));\n const placeholders = params.map((_, v) => '$' + (v + 1)).join(',');\n const sql = `SELECT * FROM ${escapeIdentifier(functionName)}(${placeholders})`;\n const result = await this.queryWithClientAsync(client, sql, params);\n debug('callWithClient() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a sproc with the specified parameters using a specific client.\n * Errors if the sproc does not return exactly one row.\n */\n async callWithClientOneRowAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClientOneRow()', 'function:', functionName);\n debug('callWithClientOneRow()', 'params:', debugParams(params));\n const result = await this.callWithClientAsync(client, functionName, params);\n if (result.rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callWithClientOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n /**\n * Calls a function with the specified parameters using a specific client.\n * Errors if the sproc returns more than one row.\n */\n async callWithClientZeroOrOneRowAsync(\n client: pg.PoolClient,\n functionName: string,\n params: any[],\n ): Promise<pg.QueryResult> {\n debug('callWithClientZeroOrOneRow()', 'function:', functionName);\n debug('callWithClientZeroOrOneRow()', 'params:', debugParams(params));\n const result = await this.callWithClientAsync(client, functionName, params);\n if (result.rowCount == null || result.rowCount > 1) {\n throw new PostgresError('Incorrect rowCount: ' + result.rowCount, {\n functionName,\n sqlParams: params,\n });\n }\n debug('callWithClientZeroOrOneRow() success', 'rowCount:', result.rowCount);\n return result;\n }\n\n async queryRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;\n async queryRows<Model extends AnyRowSchema>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Executes a query with the specified parameters. Returns an array of rows\n * that conform to the given Zod schema.\n */\n async queryRows<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryAsync(sql, params);\n return z.array(model).parse(results.rows);\n }\n\n async queryRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;\n async queryRow<Model extends AnyRowSchema>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Executes a query with the specified parameters. Returns exactly one row that conforms to the given Zod schema.\n */\n async queryRow<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryOneRowAsync(sql, params);\n return model.parse(results.rows[0]);\n }\n\n async queryOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async queryOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Executes a query with the specified parameters. Returns either null or a\n * single row that conforms to the given Zod schema, and errors otherwise.\n */\n async queryOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryZeroOrOneRowAsync(sql, params);\n if (results.rows.length === 0) {\n return null;\n }\n return model.parse(results.rows[0]);\n }\n\n async callRows<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>[]>;\n async callRows<Model extends AnyRowSchema>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Calls the given sproc with the specified parameters.\n * Errors if the sproc does not return anything.\n */\n async callRows<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callAsync(sql, params);\n return z.array(model).parse(results.rows);\n }\n\n async callRow<Model extends AnyRowSchema>(sql: string, model: Model): Promise<z.infer<Model>>;\n async callRow<Model extends AnyRowSchema>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Calls the given sproc with the specified parameters.\n * Returns exactly one row from the sproc that conforms to the given Zod schema.\n */\n async callRow<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callOneRowAsync(sql, params);\n return model.parse(results.rows[0]);\n }\n\n async callOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async callOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Calls the given sproc with the specified parameters. Returns either null\n * or a single row that conforms to the given Zod schema.\n */\n async callOptionalRow<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callZeroOrOneRowAsync(sql, params);\n if (results.rows.length === 0) {\n return null;\n }\n return model.parse(results.rows[0]);\n }\n\n async queryScalars<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model>[]>;\n async queryScalars<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Executes a query and returns all values from a single column, validated\n * against the given Zod schema. Errors if the query returns more than one column.\n */\n async queryScalars<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryAsync(sql, params);\n const columnName = assertSingleColumn(results, { sql, sqlParams: params });\n return z.array(model).parse(results.rows.map((row) => row[columnName]));\n }\n\n async queryScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;\n async queryScalar<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Executes a query and returns a single value from a single column, validated\n * against the given Zod schema. Errors if the query does not return exactly\n * one row or returns more than one column.\n */\n async queryScalar<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryOneRowAsync(sql, params);\n const columnName = assertSingleColumn(results, { sql, sqlParams: params });\n return model.parse(results.rows[0][columnName]);\n }\n\n async queryOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async queryOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Executes a query and returns a single value from a single column, or null\n * if no rows are returned. Validated against the given Zod schema. Errors if\n * the query returns more than one row or more than one column.\n */\n async queryOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: QueryParams | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.queryZeroOrOneRowAsync(sql, params);\n const columnName = assertSingleColumn(results, { sql, sqlParams: params });\n if (results.rows.length === 0) {\n return null;\n }\n return model.parse(results.rows[0][columnName]);\n }\n\n async callScalars<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model>[]>;\n async callScalars<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>[]>;\n /**\n * Calls the given sproc and returns all values from a single column, validated\n * against the given Zod schema. Errors if the sproc returns more than one column.\n */\n async callScalars<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callAsync(sql, params);\n const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });\n return z.array(model).parse(results.rows.map((row) => row[columnName]));\n }\n\n async callScalar<Model extends z.ZodTypeAny>(sql: string, model: Model): Promise<z.infer<Model>>;\n async callScalar<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model>>;\n /**\n * Calls the given sproc and returns a single value from a single column, validated\n * against the given Zod schema. Errors if the sproc does not return exactly\n * one row or returns more than one column.\n */\n async callScalar<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callOneRowAsync(sql, params);\n const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });\n return model.parse(results.rows[0][columnName]);\n }\n\n async callOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n model: Model,\n ): Promise<z.infer<Model> | null>;\n async callOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n params: any[],\n model: Model,\n ): Promise<z.infer<Model> | null>;\n /**\n * Calls the given sproc and returns a single value from a single column, or\n * null if no rows are returned. Validated against the given Zod schema.\n * Errors if the sproc returns more than one row or more than one column.\n */\n async callOptionalScalar<Model extends z.ZodTypeAny>(\n sql: string,\n paramsOrSchema: any[] | Model,\n maybeModel?: Model,\n ) {\n const params = maybeModel === undefined ? [] : (paramsOrSchema as any[]);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n const results = await this.callZeroOrOneRowAsync(sql, params);\n const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });\n if (results.rows.length === 0) {\n return null;\n }\n return model.parse(results.rows[0][columnName]);\n }\n\n /**\n * Executes a query with the specified parameters. Returns the number of rows affected.\n */\n async execute(sql: string, params: QueryParams = {}): Promise<number> {\n const result = await this.queryAsync(sql, params);\n return result.rowCount ?? 0;\n }\n\n /**\n * Executes a query with the specified parameter, and errors if the query doesn't return exactly one row.\n */\n async executeRow(sql: string, params: QueryParams = {}) {\n const rowCount = await this.execute(sql, params);\n if (rowCount !== 1) {\n throw new PostgresError('Incorrect rowCount: ' + rowCount, {\n sql,\n sqlParams: params,\n });\n }\n }\n\n /**\n * Returns a {@link Cursor} for the given query. The cursor can be used to\n * read results in batches, which is useful for large result sets.\n */\n private async queryCursorWithClient(\n client: pg.PoolClient,\n sql: string,\n params: QueryParams,\n ): Promise<Cursor> {\n this._queryCount += 1;\n debug('queryCursorWithClient()', 'sql:', debugString(sql));\n debug('queryCursorWithClient()', 'params:', debugParams(params));\n const { processedSql, paramsArray } = paramsToArray(sql, params, this.errorOnUnusedParameters);\n lastQueryMap.set(client, processedSql);\n return client.query(new Cursor(processedSql, paramsArray));\n }\n\n async queryCursor<Model extends AnyRowSchema>(\n sql: string,\n model: Model,\n ): Promise<CursorIterator<z.infer<Model>>>;\n\n async queryCursor<Model extends AnyRowSchema>(\n sql: string,\n params: QueryParams,\n model: Model,\n ): Promise<CursorIterator<z.infer<Model>>>;\n\n /**\n * Returns an {@link CursorIterator} that can be used to iterate over the\n * results of the query in batches, which is useful for large result sets.\n * Each row will be parsed by the given Zod schema.\n */\n async queryCursor<Model extends AnyRowSchema>(\n sql: string,\n paramsOrSchema: Model | QueryParams,\n maybeModel?: Model,\n ): Promise<CursorIterator<z.infer<Model>>> {\n const params = maybeModel === undefined ? {} : (paramsOrSchema as QueryParams);\n const model = maybeModel === undefined ? (paramsOrSchema as Model) : maybeModel;\n return this.queryCursorInternal(sql, params, model);\n }\n\n private async queryCursorInternal<Model extends AnyRowSchema>(\n sql: string,\n params: QueryParams,\n model?: Model,\n ): Promise<CursorIterator<z.infer<Model>>> {\n const client = await this.getClientAsync();\n const cursor = await this.queryCursorWithClient(client, sql, params);\n\n let iterateCalled = false;\n const iterator: CursorIterator<z.infer<Model>> = {\n async *iterate(batchSize: number) {\n // Safety check: if someone calls iterate multiple times, they're\n // definitely doing something wrong.\n if (iterateCalled) {\n throw new Error('iterate() called multiple times');\n }\n iterateCalled = true;\n\n try {\n while (true) {\n const rows = await cursor.read(batchSize);\n if (rows.length === 0) {\n break;\n }\n\n if (model) {\n yield z.array(model).parse(rows);\n } else {\n yield rows;\n }\n }\n } catch (err: any) {\n throw enhanceError(err, sql, params);\n } finally {\n try {\n await cursor.close();\n } finally {\n client.release();\n }\n }\n },\n stream(batchSize: number) {\n const transform = new Transform({\n readableObjectMode: true,\n writableObjectMode: true,\n transform(chunk, _encoding, callback) {\n for (const row of chunk) {\n this.push(row);\n }\n callback();\n },\n });\n\n // TODO: use native `node:stream#compose` once it's stable.\n const generator = iterator.iterate(batchSize);\n const pipe = multipipe(Readable.from(generator), transform);\n\n // When the underlying stream is closed, we need to make sure that the\n // cursor is also closed. We do this by calling `return()` on the generator,\n // which will trigger its `finally` block, which will in turn release\n // the client and close the cursor. The fact that the stream is already\n // closed by this point means that someone reading from the stream will\n // never actually see the `null` value that's returned.\n pipe.once('close', () => {\n generator.return(null);\n });\n\n return pipe;\n },\n };\n return iterator;\n }\n\n /**\n * Set the schema to use for the search path.\n *\n * @param schema The schema name to use (can be \"null\" to unset the search path)\n */\n async setSearchSchema(schema: string | null) {\n if (schema == null) {\n this.searchSchema = schema;\n return;\n }\n\n await this.queryAsync(`CREATE SCHEMA IF NOT EXISTS ${escapeIdentifier(schema)}`, {});\n // We only set searchSchema after CREATE to avoid the above query() call using searchSchema.\n this.searchSchema = schema;\n }\n\n /**\n * Get the schema that is currently used for the search path.\n *\n * @returns schema in use (may be `null` to indicate no schema)\n */\n getSearchSchema(): string | null {\n return this.searchSchema;\n }\n\n /**\n * Generate, set, and return a random schema name.\n *\n * @param prefix The prefix of the new schema, only the first 28 characters will be used (after lowercasing).\n * @returns The randomly-generated search schema.\n */\n async setRandomSearchSchemaAsync(prefix: string): Promise<string> {\n // truncated prefix (max 28 characters)\n const truncPrefix = prefix.slice(0, 28);\n // timestamp in format YYYY-MM-DDTHH:MM:SS.SSSZ (guaranteed to not exceed 27 characters in the spec)\n const timestamp = new Date().toISOString();\n // random 6-character suffix to avoid clashes (approx 1.4 billion possible values)\n const suffix = sampleSize([...'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'], 6).join('');\n\n // Schema is guaranteed to have length at most 63 (= 28 + 1 + 27 + 1 + 6),\n // which is the default PostgreSQL identifier limit.\n // Note that this schema name will need quoting because of characters like ':', '-', etc\n const schema = `${truncPrefix}_${timestamp}_${suffix}`;\n await this.setSearchSchema(schema);\n return schema;\n }\n\n /**\n * Deletes all schemas starting with the given prefix.\n *\n * @param prefix The prefix of the schemas to delete.\n */\n async clearSchemasStartingWith(prefix: string): Promise<void> {\n // Sanity check against deleting public, pg_, information_schema, etc.\n if (prefix === 'public' || prefix.startsWith('pg_') || prefix === 'information_schema') {\n throw new Error(`Cannot clear schema starting with ${prefix}`);\n }\n // Sanity check against a bad prefix.\n if (prefix.length < 4) {\n throw new Error(`Prefix is too short: ${prefix}`);\n }\n\n await this.queryAsync(\n `DO $$\n DECLARE\n r RECORD;\n BEGIN\n FOR r IN\n SELECT nspname\n FROM pg_namespace\n WHERE nspname LIKE ${escapeLiteral(prefix + '%')}\n AND nspname NOT LIKE 'pg_temp_%'\n LOOP\n EXECUTE format('DROP SCHEMA IF EXISTS %I CASCADE;', r.nspname);\n COMMIT; -- avoid shared memory exhaustion\n END LOOP;\n END $$;`,\n {},\n );\n }\n\n /** The number of established connections. */\n get totalCount() {\n return this.pool?.totalCount ?? 0;\n }\n\n /** The number of idle connections. */\n get idleCount() {\n return this.pool?.idleCount ?? 0;\n }\n\n /** The number of queries waiting for a connection to become available. */\n get waitingCount() {\n return this.pool?.waitingCount ?? 0;\n }\n\n /** The total number of queries that have been executed by this pool. */\n get queryCount() {\n return this._queryCount;\n }\n}\n"]}
package/dist/pool.js CHANGED
@@ -113,6 +113,12 @@ function escapeIdentifier(identifier) {
113
113
  // https://www.postgresql.org/docs/current/sql-syntax-lexical.html
114
114
  return pg.Client.prototype.escapeIdentifier(identifier);
115
115
  }
116
+ function assertSingleColumn(result, context) {
117
+ if (result.fields.length !== 1) {
118
+ throw new PostgresError(`Expected exactly one column, but found ${result.fields.length}`, context);
119
+ }
120
+ return result.fields[0].name;
121
+ }
116
122
  function enhanceError(err, sql, params) {
117
123
  // Copy the error so we don't end up with a circular reference in the
118
124
  // final error.
@@ -560,44 +566,25 @@ export class PostgresPool {
560
566
  /**
561
567
  * Executes a query with the specified parameters. Returns an array of rows
562
568
  * that conform to the given Zod schema.
563
- *
564
- * If the query returns a single column, the return value will be a list of column values.
565
569
  */
566
570
  async queryRows(sql, paramsOrSchema, maybeModel) {
567
571
  const params = maybeModel === undefined ? {} : paramsOrSchema;
568
572
  const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
569
573
  const results = await this.queryAsync(sql, params);
570
- if (results.fields.length === 1) {
571
- const columnName = results.fields[0].name;
572
- const rawData = results.rows.map((row) => row[columnName]);
573
- return z.array(model).parse(rawData);
574
- }
575
- else {
576
- return z.array(model).parse(results.rows);
577
- }
574
+ return z.array(model).parse(results.rows);
578
575
  }
579
576
  /**
580
577
  * Executes a query with the specified parameters. Returns exactly one row that conforms to the given Zod schema.
581
- *
582
- * If the query returns a single column, the return value will be the column value itself.
583
578
  */
584
579
  async queryRow(sql, paramsOrSchema, maybeModel) {
585
580
  const params = maybeModel === undefined ? {} : paramsOrSchema;
586
581
  const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
587
582
  const results = await this.queryOneRowAsync(sql, params);
588
- if (results.fields.length === 1) {
589
- const columnName = results.fields[0].name;
590
- return model.parse(results.rows[0][columnName]);
591
- }
592
- else {
593
- return model.parse(results.rows[0]);
594
- }
583
+ return model.parse(results.rows[0]);
595
584
  }
596
585
  /**
597
586
  * Executes a query with the specified parameters. Returns either null or a
598
587
  * single row that conforms to the given Zod schema, and errors otherwise.
599
- *
600
- * If the query returns a single column, the return value will be the column value itself.
601
588
  */
602
589
  async queryOptionalRow(sql, paramsOrSchema, maybeModel) {
603
590
  const params = maybeModel === undefined ? {} : paramsOrSchema;
@@ -606,13 +593,7 @@ export class PostgresPool {
606
593
  if (results.rows.length === 0) {
607
594
  return null;
608
595
  }
609
- else if (results.fields.length === 1) {
610
- const columnName = results.fields[0].name;
611
- return model.parse(results.rows[0][columnName]);
612
- }
613
- else {
614
- return model.parse(results.rows[0]);
615
- }
596
+ return model.parse(results.rows[0]);
616
597
  }
617
598
  /**
618
599
  * Calls the given sproc with the specified parameters.
@@ -622,14 +603,7 @@ export class PostgresPool {
622
603
  const params = maybeModel === undefined ? [] : paramsOrSchema;
623
604
  const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
624
605
  const results = await this.callAsync(sql, params);
625
- if (results.fields.length === 1) {
626
- const columnName = results.fields[0].name;
627
- const rawData = results.rows.map((row) => row[columnName]);
628
- return z.array(model).parse(rawData);
629
- }
630
- else {
631
- return z.array(model).parse(results.rows);
632
- }
606
+ return z.array(model).parse(results.rows);
633
607
  }
634
608
  /**
635
609
  * Calls the given sproc with the specified parameters.
@@ -639,13 +613,7 @@ export class PostgresPool {
639
613
  const params = maybeModel === undefined ? [] : paramsOrSchema;
640
614
  const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
641
615
  const results = await this.callOneRowAsync(sql, params);
642
- if (results.fields.length === 1) {
643
- const columnName = results.fields[0].name;
644
- return model.parse(results.rows[0][columnName]);
645
- }
646
- else {
647
- return model.parse(results.rows[0]);
648
- }
616
+ return model.parse(results.rows[0]);
649
617
  }
650
618
  /**
651
619
  * Calls the given sproc with the specified parameters. Returns either null
@@ -658,13 +626,83 @@ export class PostgresPool {
658
626
  if (results.rows.length === 0) {
659
627
  return null;
660
628
  }
661
- else if (results.fields.length === 1) {
662
- const columnName = results.fields[0].name;
663
- return model.parse(results.rows[0][columnName]);
629
+ return model.parse(results.rows[0]);
630
+ }
631
+ /**
632
+ * Executes a query and returns all values from a single column, validated
633
+ * against the given Zod schema. Errors if the query returns more than one column.
634
+ */
635
+ async queryScalars(sql, paramsOrSchema, maybeModel) {
636
+ const params = maybeModel === undefined ? {} : paramsOrSchema;
637
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
638
+ const results = await this.queryAsync(sql, params);
639
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
640
+ return z.array(model).parse(results.rows.map((row) => row[columnName]));
641
+ }
642
+ /**
643
+ * Executes a query and returns a single value from a single column, validated
644
+ * against the given Zod schema. Errors if the query does not return exactly
645
+ * one row or returns more than one column.
646
+ */
647
+ async queryScalar(sql, paramsOrSchema, maybeModel) {
648
+ const params = maybeModel === undefined ? {} : paramsOrSchema;
649
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
650
+ const results = await this.queryOneRowAsync(sql, params);
651
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
652
+ return model.parse(results.rows[0][columnName]);
653
+ }
654
+ /**
655
+ * Executes a query and returns a single value from a single column, or null
656
+ * if no rows are returned. Validated against the given Zod schema. Errors if
657
+ * the query returns more than one row or more than one column.
658
+ */
659
+ async queryOptionalScalar(sql, paramsOrSchema, maybeModel) {
660
+ const params = maybeModel === undefined ? {} : paramsOrSchema;
661
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
662
+ const results = await this.queryZeroOrOneRowAsync(sql, params);
663
+ const columnName = assertSingleColumn(results, { sql, sqlParams: params });
664
+ if (results.rows.length === 0) {
665
+ return null;
664
666
  }
665
- else {
666
- return model.parse(results.rows[0]);
667
+ return model.parse(results.rows[0][columnName]);
668
+ }
669
+ /**
670
+ * Calls the given sproc and returns all values from a single column, validated
671
+ * against the given Zod schema. Errors if the sproc returns more than one column.
672
+ */
673
+ async callScalars(sql, paramsOrSchema, maybeModel) {
674
+ const params = maybeModel === undefined ? [] : paramsOrSchema;
675
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
676
+ const results = await this.callAsync(sql, params);
677
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
678
+ return z.array(model).parse(results.rows.map((row) => row[columnName]));
679
+ }
680
+ /**
681
+ * Calls the given sproc and returns a single value from a single column, validated
682
+ * against the given Zod schema. Errors if the sproc does not return exactly
683
+ * one row or returns more than one column.
684
+ */
685
+ async callScalar(sql, paramsOrSchema, maybeModel) {
686
+ const params = maybeModel === undefined ? [] : paramsOrSchema;
687
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
688
+ const results = await this.callOneRowAsync(sql, params);
689
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
690
+ return model.parse(results.rows[0][columnName]);
691
+ }
692
+ /**
693
+ * Calls the given sproc and returns a single value from a single column, or
694
+ * null if no rows are returned. Validated against the given Zod schema.
695
+ * Errors if the sproc returns more than one row or more than one column.
696
+ */
697
+ async callOptionalScalar(sql, paramsOrSchema, maybeModel) {
698
+ const params = maybeModel === undefined ? [] : paramsOrSchema;
699
+ const model = maybeModel === undefined ? paramsOrSchema : maybeModel;
700
+ const results = await this.callZeroOrOneRowAsync(sql, params);
701
+ const columnName = assertSingleColumn(results, { functionName: sql, sqlParams: params });
702
+ if (results.rows.length === 0) {
703
+ return null;
667
704
  }
705
+ return model.parse(results.rows[0][columnName]);
668
706
  }
669
707
  /**
670
708
  * Executes a query with the specified parameters. Returns the number of rows affected.
@@ -711,7 +749,6 @@ export class PostgresPool {
711
749
  const client = await this.getClientAsync();
712
750
  const cursor = await this.queryCursorWithClient(client, sql, params);
713
751
  let iterateCalled = false;
714
- let rowKeys = null;
715
752
  const iterator = {
716
753
  async *iterate(batchSize) {
717
754
  // Safety check: if someone calls iterate multiple times, they're
@@ -726,15 +763,11 @@ export class PostgresPool {
726
763
  if (rows.length === 0) {
727
764
  break;
728
765
  }
729
- if (rowKeys === null) {
730
- rowKeys = Object.keys(rows[0] ?? {});
731
- }
732
- const flattened = rowKeys.length === 1 ? rows.map((row) => row[rowKeys[0]]) : rows;
733
766
  if (model) {
734
- yield z.array(model).parse(flattened);
767
+ yield z.array(model).parse(rows);
735
768
  }
736
769
  else {
737
- yield flattened;
770
+ yield rows;
738
771
  }
739
772
  }
740
773
  }