npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@liam-public/node-postgres

v0.4.0

Published

Node.js Postgres helpers for pooling, transactions, and migrations.

Readme

@liam-public/node-postgres

Node.js Postgres helpers for pooling, transactions, and migrations. A thin, schema-neutral layer over pg and node-pg-migrate whose main job is to make connection policy explicit instead of inherited.

Exports

  • createPool(databaseUrl, options?)pg.Pool with bounded waits by default.
  • withTransaction(pool, fn)BEGIN / COMMIT / ROLLBACK around fn, with the client destroyed rather than pooled if it cannot be unwound.
  • resolveCommitFailurePolicy() — reads and validates POSTGRES_COMMIT_FAILURE_POLICY.
  • waitForDatabase(databaseUrl, { retries, delayMs, logger }) — startup readiness poll.
  • runMigrations(databaseUrl, migrationsDir, { migrationsTable })node-pg-migrate runner with stale-lock recovery. Services sharing a database MUST pass a scoped migrationsTable.

Timeouts are bounded by default

pg's own defaults are "wait forever" for connection acquisition, statements, and queries. A Postgres that errors surfaces promptly; one that hangs — a failover mid-transaction, a network blackhole, a saturated instance — makes every caller wait indefinitely and exhausts the pool. createPool therefore applies bounded defaults, all overridable:

| Option | Default | Where it is enforced | | --- | --- | --- | | connectionTimeoutMillis | 5_000 | Client-side, on pool acquisition | | statementTimeoutMillis | 30_000 | Server-side (statement_timeout) | | queryTimeoutMillis | 30_000 | Client-side, per query | | idleTimeoutMillis, max | pg's defaults | Pool | | idleInTransactionSessionTimeoutMillis, applicationName, keepAlive, keepAliveInitialDelayMillis | unset | See below |

Keep both statement and query timeouts. They fail differently and neither subsumes the other: statement_timeout is enforced by the server and cancels the work, but only helps while the server is alive and executing. queryTimeoutMillis is enforced client-side and is the only one that fires when the connection is a black hole. For that last case, keepAlive: true is worth adding — TCP keepalive detects a blackholed socket that no query-level deadline can observe.

Set applicationName in every service. Without it pg_stat_activity.application_name reads node for every consumer of this package, and "which service is holding these connections" is the first question anyone asks during an incident.

The defaults are exported as POOL_DEFAULTS, so a consumer can read or widen one without restating the number.

What 0 means

0 disables a timeout, with one caveat worth stating precisely. connectionTimeoutMillis: 0 and queryTimeoutMillis: 0 mean "no timeout". statementTimeoutMillis: 0 sends no statement_timeout in the startup packet, so the server's own configured statement_timeout applies — usually unlimited, but not guaranteed to be. If you need certainty, set the value explicitly.

Timeouts are per pool, so use more than one pool

A single blanket statement_timeout cannot serve both a request path that must fail in milliseconds and a nightly rollup that legitimately runs for minutes. It does not have to: statement_timeout, idle_in_transaction_session_timeout, and application_name are negotiated in each connection's startup packet, so two pools against the same database with different timeouts is a supported pattern and the two do not interfere.

// Request path — fail fast, so a hung database rejects rather than hangs.
const pool = createPool(config.databaseUrl, {
  connectionTimeoutMillis: 2_000,
  statementTimeoutMillis: 5_000,
  queryTimeoutMillis: 5_000,
  keepAlive: true,
  applicationName: 'checkout',
})

// Background jobs — long statements are expected and correct.
const jobPool = createPool(config.databaseUrl, {
  connectionTimeoutMillis: 5_000,
  statementTimeoutMillis: 300_000,
  applicationName: 'checkout:jobs',
})

Do not run migrations through a request-path pool. DDL such as creating an index on a large table will blow past a 30-second statement_timeout and be cancelled part-way. runMigrations opens its own untimed connections and is unaffected.

Transactions

const order = await withTransaction(pool, async (client) => {
  const { rows } = await client.query('INSERT INTO orders (total) VALUES ($1) RETURNING id', [total])
  await client.query('INSERT INTO order_lines (order_id) VALUES ($1)', [rows[0].id])
  return rows[0]
})

If fn throws, the transaction is rolled back and the original error is rethrown — not the rollback error, which is a symptom rather than the cause. If the rollback also fails, the client is destroyed instead of returned to the pool: pg rejects a query that hits query_timeout without closing the socket, so the server may still be executing on that connection and reusing it risks picking up the previous query's results.

Note that fn receives a single checked-out client. Do not use pool inside it — those queries run on a different connection and outside the transaction.

POSTGRES_COMMIT_FAILURE_POLICY

A failed COMMIT is ambiguous: the connection may be dead, or it may be perfectly healthy and the server may simply have refused the transaction. The two readings call for opposite handling, so the choice is configurable.

| Value | Behaviour on a failed COMMIT | | --- | --- | | rollback (default) | Issue a ROLLBACK and keep the client if it succeeds; destroy it if the rollback fails too. | | destroy | Discard the client immediately, without attempting a rollback. |

Default to rollback. A commit rejected for a server-side reason — a serialization failure under REPEATABLE READ, a deferred constraint firing at commit time — leaves a healthy connection, and discarding it makes retry-heavy workloads pay for a reconnect every time. Choose destroy when you would rather not reason about the state of a connection whose commit failed, and can afford the reconnect.

This governs the commit path only. A throwing callback is always rolled back, whatever the policy — an open transaction has to be unwound.

An unrecognised value throws rather than falling back to the default, so a typo cannot silently leave the wrong policy in force. Call resolveCommitFailurePolicy() during startup to surface a bad value at boot rather than at the first failed commit.

waitForDatabase

await waitForDatabase(process.env.DATABASE_URL!, {
  retries: 10,      // default: 10, so up to 11 attempts
  delayMs: 2_000,   // default: 2_000
  logger: console,  // optional { warn(msg, err) }
})

Polls SELECT 1 on a fresh single-connection pool per attempt (5s connect timeout) and rethrows the last error once retries are exhausted. For start-up ordering when the database may not be accepting connections yet.

runMigrations

await runMigrations(process.env.DATABASE_URL!, './migrations', {
  migrationsTable: 'pgmigrations_orders',
})

Runs node-pg-migrate up to the latest migration.

Services sharing a database must pass a scoped migrationsTable. node-pg-migrate's checkOrder compares tracked migrations positionally against the service's own migration files, so interleaved entries from a sibling service break it. The default stays pgmigrations for backward compatibility. The name must be a plain SQL identifier ([A-Za-z_][A-Za-z0-9_]*) — it is validated, because it is interpolated into the lock-clearing statement below.

Stale-lock recovery. If node-pg-migrate reports Another migration is already running, this waits 90 seconds, clears <migrationsTable>_lock, and retries once. The wait is what makes it safe: a migration genuinely in flight elsewhere has time to finish and drop the lock itself, so what gets cleared is a lock left behind by a pod that died mid-migration — the case that otherwise wedges a deploy until someone clears it by hand. A migration that legitimately runs longer than 90 seconds should not use this path.

Tests

pnpm test runs the unit suite: fast, no Docker, and it covers the wiring — option translation, defaulting, and which release path a transaction takes.

pnpm test:integration runs the suite that needs a real server, via testcontainers. These cover the properties the unit tests cannot observe: that statement_timeout is genuinely negotiated on the connection, that an overrunning statement comes back as 57014 query_canceled from the server, that a saturated pool rejects instead of queueing, that 0 disables rather than fires immediately, and that two pools on one database keep their own timeouts. The unresponsive-connection case is exercised through a TCP relay that stops forwarding mid-session, which is the one scenario a server-side timeout can never catch.