All files / src/drivers pg.ts

100% Statements 22/22
100% Branches 10/10
100% Functions 4/4
100% Lines 22/22

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92                                                                                                    1x 14x 14x 117x 116x 116x             117x       117x 117x 117x   14x         12x 12x   14x       11x 11x 9x 8x 7x   11x 14x 14x  
/**
 * `pg` (node-postgres) adapter for `pgrls-test`.
 *
 * Usage:
 *
 * ```ts
 * import { Client } from 'pg';
 * import { PgrlsTestClient, pgDriver } from 'pgrls-test';
 *
 * const pg = new Client({ connectionString: process.env.DATABASE_URL });
 * await pg.connect();
 * const client = new PgrlsTestClient(pgDriver(pg));
 * ```
 *
 * The adapter is a thin wrapper that:
 * 1. Maps `query(sql, params)` → `client.query(sql, values)` and
 *    normalizes the result shape to `QueryResult`.
 * 2. Maps `rollback()` → `client.query('ROLLBACK')` (works even
 *    in aborted-transaction state — Postgres always accepts
 *    `ROLLBACK`).
 * 3. Maps `isInsufficientPrivilege(err)` → `err.code === '42501'`.
 *
 * `pg` is a peer dependency. Users who don't use it never pull
 * it in; users who do use it get type-checked construction.
 */
import type { Driver, QueryResult } from './types.js';
 
/**
 * Subset of `pg.Client` (or a `pg.Pool` wrapper) that the
 * adapter needs. We deliberately don't `import { Client } from
 * 'pg'` here — that would force `pg` to be a runtime dep. The
 * structural interface is enough for the adapter and lets users
 * pass a Pool client, a connection-checked-out client, or any
 * compatible shape.
 */
export interface PgQueryable {
  query(
    text: string,
    values?: readonly unknown[],
  ): Promise<{
    rows: Record<string, unknown>[];
    command: string;
    rowCount: number | null;
  }>;
}
 
/**
 * Wrap a `pg.Client` (or any object satisfying `PgQueryable`)
 * into the `Driver` shape the rest of `pgrls-test` consumes.
 */
export function pgDriver(client: PgQueryable): Driver {
  return {
    async query(sql: string, params?: readonly unknown[]): Promise<QueryResult> {
      const result = await client.query(sql, params);
      return {
        rows: result.rows,
        // pg returns the command verb as the first whitespace-
        // delimited token in the command tag (e.g. "UPDATE 5").
        // It already strips the count, so result.command is
        // exactly the verb like "UPDATE", "SELECT", "INSERT".
        // Upper-case it defensively in case some pg fork or
        // future version changes the casing.
        command: (result.command || '').toUpperCase(),
        // pg returns `rowCount: null` for commands that don't
        // produce a count (rare — utility statements). Map to
        // 0 so callers don't have to null-check.
        rowCount: result.rowCount ?? 0,
      };
    },
 
    async rollback(): Promise<void> {
      // ROLLBACK is accepted in any transaction state — including
      // the "current transaction is aborted" state that follows
      // a SQL error. Issuing it via the same query path keeps the
      // adapter to one entry point.
      await client.query('ROLLBACK');
    },
 
    isInsufficientPrivilege(error: unknown): boolean {
      // pg attaches the SQLSTATE as a string `code` property on
      // the thrown error. 42501 is "insufficient_privilege" —
      // the class RLS policy rejections surface as.
      return (
        typeof error === 'object' &&
        error !== null &&
        'code' in error &&
        (error as { code?: unknown }).code === '42501'
      );
    },
  };
}