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

@truxie/drizzle

v0.1.0

Published

Drizzle ORM for truxie — a lazily-connected database, an ambient TransactionHost with savepoints and after-commit hooks, a transaction interceptor and a health indicator

Readme

@truxie/drizzle

Drizzle ORM for truxie: a Postgres database, an ambient TransactionHost with savepoints and after-commit callbacks, an opt-in TransactionInterceptor, and a health indicator.

npm install @truxie/drizzle drizzle-orm postgres
@Module({
  imports: [DrizzleModule.forRoot({url: process.env.DATABASE_URL, schema, max: 20, applicationName: 'api'})],
})
export class AppModule {}

The client connects on its first query, so the application boots without a reachable database. On shutdown the pool closes last, after everything that injected it has stopped (truxie ≥ 0.2 tears down dependents first).

Configuring

// Options known up front
DrizzleModule.forRoot({url, schema})

// Options from configuration
DrizzleModule.forRootAsync({
  imports: [ConfigModule],
  inject: [DATABASE_CONFIG],
  useFactory: (config: DatabaseConfig) => ({url: config.url, max: config.poolSize}),
})

// A database you built yourself: PGlite in tests, node-postgres, a shared client
DrizzleModule.forClient(drizzle(pglite), {close: () => pglite.close()})

| Option | | |---|---| | url | Postgres connection string (forRoot / forRootAsync). | | max | Pool size. It must cover concurrent transactions plus workers. | | applicationName | Reported to Postgres as application_name. | | schema | Drizzle schema, for db.query.*. | | closeTimeoutSec | How long client.end() waits for running queries on shutdown. Default 5. | | onTransactionStart(tx) | Runs right after every outermost BEGIN, never for savepoints. Use it for set_config(...) and row-level security. | | onError(err) | Receives errors thrown by afterCommit callbacks. Default console.error. |

postgres is only loaded by forRoot / forRootAsync, so a process using forClient doesn't need it installed.

Repositories read tx.db on every call

TransactionHost.db is the open transaction of the current async context, or the root database outside one. Read it on every call and never store it in a field, or a repository keeps using whichever handle it saw first.

@Injectable()
@Inject(TransactionHost)
export class OrderRepository {
  constructor(private readonly tx: TransactionHost) {}

  async create(input: NewOrder): Promise<Order> {
    const [row] = await this.tx.db.insert(orders).values(input).returning()
    return toDomain(row)
  }
}

Use cases own the transaction

Wrap multi-write work in run. Everything the repositories do inside it joins one transaction without a handle being passed down.

await this.tx.run(async () => {
  const order = await this.orders.create(input)
  await this.stock.reserve(order.items)
  // Enqueued only once the transaction commits, so the job never points at rows that were rolled back
  this.tx.afterCommit(() => this.queue.add('ship', {orderId: order.id}))
})
  • Commit and rollback. run(fn, cfg?) commits when fn resolves and rolls back when it throws. cfg is drizzle's PgTransactionConfig (isolationLevel, accessMode, deferrable).
  • Nesting. A nested run opens a savepoint. If it fails, only that level rolls back, and the outer fn can catch the error and carry on. A nested run can't ask for a different isolation level; it throws instead of silently ignoring the request.
  • afterCommit(fn). Callbacks run after the outermost commit, outside the transaction, in the order they were registered. A savepoint's callbacks are dropped if it rolls back. Outside a transaction, fn runs on the next microtask. Errors go to onError and are never rethrown, because the work they follow has already committed.
  • inTransaction tells you whether the current context is inside run.

The context is kept in an AsyncLocalStorage on globalThis, so two copies of this package in one bundle still share it.

Pitfalls

  • Never call a slow external service inside run (an LLM, a payment provider, another HTTP API). The transaction holds a pool connection for the whole call. Do the call first, or after the commit with afterCommit.
  • Await everything started inside run. Un-awaited work keeps using a transaction that has already ended.
  • Size the pool (max) for concurrent transactions plus workers.

Per-route transactions

@Post('/orders')
@UseInterceptors(TransactionInterceptor)
create(@Body() body: CreateOrder) { … }

The handler and the interceptors after this one run in one transaction. Prefer run in the use case whenever the route also calls something slow.

Health

DrizzleHealthIndicator runs select 1 and has the shape of @truxie/health's HealthIndicator. Pass the instance, because the health module constructs classes without injecting anything:

HealthModule.forRootAsync({
  inject: [DrizzleHealthIndicator],
  useFactory: (db: DrizzleHealthIndicator) => ({checks: [{name: 'database', check: db}]}),
})

Errors

try {
  await this.users.create(input)
} catch (err) {
  if (isUniqueViolation(err)) throw new ConflictError('Email already registered')
  throw err
}

isUniqueViolation checks for SQLSTATE 23505 through the whole cause chain, so drizzle's DrizzleQueryError wrapper is seen through.

Testing

import {withRollback} from '@truxie/drizzle/testing'

it('creates the order', () => withRollback(host, async () => {
  await createOrder.execute(input)
  expect(await orders.count()).toBe(1)
}))

withRollback(host, fn) runs fn in a transaction that is always rolled back and returns fn's result. afterCommit callbacks registered inside it never run. With PGlite, wire the database through DrizzleModule.forClient(drizzle(pglite)).

Exports

DrizzleModule, TransactionHost, TransactionInterceptor, DrizzleHealthIndicator, isUniqueViolation, DRIZZLE_DB (the root Database), DRIZZLE_OPTIONS, and the types Database, DrizzleModuleOptions, DrizzleClientOptions and DrizzleTransactionOptions. @truxie/drizzle/testing exports withRollback.