Files
restloom/packages/postgres/src/index.ts
T

51 lines
2.1 KiB
TypeScript

import { SqlRenderer, type Dialect, type Tx } from '@crudy/restloom';
import type { Pool } from 'pg';
import { mapError } from './errors.js';
import { introspect } from './introspect.js';
export { mapError } from './errors.js';
/** Postgres writes every primitive the core's defaults assume; subclass it to change one. */
export class PostgresRenderer extends SqlRenderer {}
export interface PostgresOptions {
renderer?: SqlRenderer;
}
/**
* The Postgres dialect over a `pg` Pool. Every request runs in one transaction
* with a local statement timeout, read-only for reads, so a `transaction` hook
* can set the role and session settings that row security reads.
*/
export function postgres(pool: Pool, options: PostgresOptions = {}): Dialect {
return {
name: 'postgres',
capabilities: { returning: true, estimatedCount: true, rowSecurity: true, procedures: true },
decode: (value) => value,
renderer: options.renderer ?? new PostgresRenderer(),
introspect: (scope) => introspect(pool, scope),
async transaction(fn, { timeoutMs, readOnly }) {
const client = await pool.connect();
try {
await client.query(readOnly ? 'BEGIN READ ONLY' : 'BEGIN');
await client.query(`SET LOCAL statement_timeout = ${Math.max(1, Math.floor(timeoutMs))}`);
const tx: Tx = { query: async (sql, params) => (await client.query(sql, params)).rows };
const out = await fn(tx);
await client.query('COMMIT');
return out;
} catch (err) {
await client.query('ROLLBACK').catch(() => undefined);
throw err;
} finally {
client.release();
}
},
mapError,
async estimate(tx, sql, params) {
const [row] = await tx.query(`EXPLAIN (FORMAT JSON) ${sql}`, params);
const plan = (row?.['QUERY PLAN'] as { Plan?: { 'Plan Rows'?: number } }[] | undefined)?.[0]?.Plan;
return typeof plan?.['Plan Rows'] === 'number' ? plan['Plan Rows'] : null;
},
};
}