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

@geekmidas/db

v1.0.0

Published

Database utilities for Kysely with flexible transaction management. Provides helpers for working with database connections and transactions in a type-safe way.

Downloads

376

Readme

@geekmidas/db

Database utilities for Kysely with flexible transaction management. Provides helpers for working with database connections and transactions in a type-safe way.

Features

  • Flexible Transaction Handling: Works with Kysely, Transaction, and ControlledTransaction
  • Automatic Transaction Detection: Reuses existing transactions when nested
  • Type-Safe: Full TypeScript support with generic database schemas
  • Connection Abstraction: Single helper for all database connection types
  • Zero Dependencies: Only peer dependency on Kysely

Installation

pnpm add @geekmidas/db

Peer Dependencies

pnpm add kysely pg

Quick Start

import { withTransaction } from '@geekmidas/db/kysely';
import type { DatabaseConnection } from '@geekmidas/db/kysely';
import { Kysely } from 'kysely';

interface Database {
  users: {
    id: string;
    email: string;
    name: string;
  };
  posts: {
    id: string;
    userId: string;
    title: string;
  };
}

async function createUserWithPost(
  db: DatabaseConnection<Database>,
  userData: { email: string; name: string },
  postData: { title: string }
) {
  return withTransaction(db, async (trx) => {
    // Create user
    const user = await trx
      .insertInto('users')
      .values(userData)
      .returningAll()
      .executeTakeFirstOrThrow();

    // Create post for user
    const post = await trx
      .insertInto('posts')
      .values({
        userId: user.id,
        title: postData.title
      })
      .returningAll()
      .executeTakeFirstOrThrow();

    return { user, post };
  });
}

API Reference

withTransaction

Execute a callback within a transaction. If the connection is already a transaction, it reuses it. Otherwise, it creates a new transaction.

function withTransaction<DB, T>(
  db: DatabaseConnection<DB>,
  cb: (trx: Transaction<DB>) => Promise<T>
): Promise<T>

Parameters:

  • db - A database connection (Kysely, Transaction, or ControlledTransaction)
  • cb - Callback function that receives the transaction

Returns:

  • Promise resolving to the callback's return value

DatabaseConnection<T>

Type union for all supported database connection types:

type DatabaseConnection<T> =
  | Kysely<T>
  | Transaction<T>
  | ControlledTransaction<T>;

Usage Examples

Basic Transaction

import { withTransaction } from '@geekmidas/db/kysely';
import { Kysely } from 'kysely';

const db = new Kysely<Database>({ /* config */ });

async function transferFunds(fromId: string, toId: string, amount: number) {
  return withTransaction(db, async (trx) => {
    // Deduct from sender
    await trx
      .updateTable('accounts')
      .set({ balance: sql`balance - ${amount}` })
      .where('id', '=', fromId)
      .execute();

    // Add to receiver
    await trx
      .updateTable('accounts')
      .set({ balance: sql`balance + ${amount}` })
      .where('id', '=', toId)
      .execute();

    return { success: true };
  });
}

Nested Transactions

The helper automatically detects existing transactions and reuses them:

async function createUser(
  db: DatabaseConnection<Database>,
  email: string
) {
  return withTransaction(db, async (trx) => {
    const user = await trx
      .insertInto('users')
      .values({ email })
      .returningAll()
      .executeTakeFirstOrThrow();

    // This will reuse the same transaction
    await createAuditLog(trx, 'user_created', user.id);

    return user;
  });
}

async function createAuditLog(
  db: DatabaseConnection<Database>,
  action: string,
  userId: string
) {
  // If db is already a transaction, it's reused
  return withTransaction(db, async (trx) => {
    await trx
      .insertInto('audit_logs')
      .values({ action, userId, timestamp: new Date() })
      .execute();
  });
}

Repository Pattern

Use DatabaseConnection type in repositories for flexibility:

import type { DatabaseConnection } from '@geekmidas/db/kysely';
import { withTransaction } from '@geekmidas/db/kysely';

class UserRepository {
  constructor(private db: DatabaseConnection<Database>) {}

  async create(data: NewUser): Promise<User> {
    return withTransaction(this.db, async (trx) => {
      return trx
        .insertInto('users')
        .values(data)
        .returningAll()
        .executeTakeFirstOrThrow();
    });
  }

  async update(id: string, data: UserUpdate): Promise<User> {
    return withTransaction(this.db, async (trx) => {
      return trx
        .updateTable('users')
        .set(data)
        .where('id', '=', id)
        .returningAll()
        .executeTakeFirstOrThrow();
    });
  }
}

// Can be used with any connection type
const db = new Kysely<Database>({ /* config */ });
const repo = new UserRepository(db);

// Or within a transaction
await withTransaction(db, async (trx) => {
  const repo = new UserRepository(trx);
  await repo.create({ email: '[email protected]' });
});

Service Pattern with Transactions

import type { DatabaseConnection } from '@geekmidas/db/kysely';
import { withTransaction } from '@geekmidas/db/kysely';

class OrderService {
  constructor(
    private db: DatabaseConnection<Database>,
    private inventoryService: InventoryService,
    private paymentService: PaymentService
  ) {}

  async createOrder(
    userId: string,
    items: OrderItem[]
  ): Promise<Order> {
    return withTransaction(this.db, async (trx) => {
      // All operations share the same transaction
      const order = await trx
        .insertInto('orders')
        .values({ userId, status: 'pending' })
        .returningAll()
        .executeTakeFirstOrThrow();

      // These services can accept the transaction
      await this.inventoryService.reserveItems(trx, items);
      await this.paymentService.processPayment(trx, order.id);

      // Update order status
      return trx
        .updateTable('orders')
        .set({ status: 'completed' })
        .where('id', '=', order.id)
        .returningAll()
        .executeTakeFirstOrThrow();
    });
  }
}

class InventoryService {
  async reserveItems(
    db: DatabaseConnection<Database>,
    items: OrderItem[]
  ) {
    return withTransaction(db, async (trx) => {
      for (const item of items) {
        await trx
          .updateTable('inventory')
          .set({ reserved: sql`reserved + ${item.quantity}` })
          .where('productId', '=', item.productId)
          .execute();
      }
    });
  }
}

Error Handling

Transactions automatically roll back on errors:

import { withTransaction } from '@geekmidas/db/kysely';

async function processOrder(db: DatabaseConnection<Database>, orderId: string) {
  try {
    return await withTransaction(db, async (trx) => {
      const order = await trx
        .selectFrom('orders')
        .where('id', '=', orderId)
        .selectAll()
        .executeTakeFirstOrThrow();

      if (order.status !== 'pending') {
        throw new Error('Order already processed');
      }

      // Update order
      await trx
        .updateTable('orders')
        .set({ status: 'processing' })
        .where('id', '=', orderId)
        .execute();

      // If this throws, the entire transaction rolls back
      await processPayment(trx, orderId);

      return order;
    });
  } catch (error) {
    console.error('Transaction failed:', error);
    // Transaction has been rolled back
    throw error;
  }
}

Testing with Transactions

Use transactions for test isolation:

import { describe, it, beforeEach, afterEach } from 'vitest';
import type { Transaction } from 'kysely';

describe('UserRepository', () => {
  let trx: Transaction<Database>;

  beforeEach(async () => {
    trx = await db.transaction().execute(async (t) => t);
  });

  afterEach(async () => {
    await trx.rollback();
  });

  it('should create user', async () => {
    const repo = new UserRepository(trx);
    const user = await repo.create({ email: '[email protected]' });

    expect(user.email).toBe('[email protected]');
    // Transaction will be rolled back after test
  });
});

Type Safety

The package provides full type safety for database operations:

import type { DatabaseConnection } from '@geekmidas/db/kysely';
import { withTransaction } from '@geekmidas/db/kysely';

interface Database {
  users: {
    id: Generated<string>;
    email: string;
    name: string | null;
  };
}

function updateUser(
  db: DatabaseConnection<Database>,
  id: string,
  data: { name: string }
) {
  return withTransaction(db, async (trx) => {
    // Full autocomplete and type checking
    return trx
      .updateTable('users')
      .set(data) // Type-checked against users table
      .where('id', '=', id)
      .returningAll()
      .executeTakeFirstOrThrow();
  });
}

Advanced Patterns

Unit of Work Pattern

class UnitOfWork {
  private transaction: Transaction<Database> | null = null;

  constructor(private db: Kysely<Database>) {}

  async begin() {
    this.transaction = await this.db.transaction().execute(async (t) => t);
  }

  async commit() {
    if (this.transaction) {
      await this.transaction.commit();
      this.transaction = null;
    }
  }

  async rollback() {
    if (this.transaction) {
      await this.transaction.rollback();
      this.transaction = null;
    }
  }

  getConnection(): DatabaseConnection<Database> {
    return this.transaction || this.db;
  }
}

// Usage
const uow = new UnitOfWork(db);
await uow.begin();

try {
  const userRepo = new UserRepository(uow.getConnection());
  const orderRepo = new OrderRepository(uow.getConnection());

  await userRepo.create({ email: '[email protected]' });
  await orderRepo.create({ userId: '123' });

  await uow.commit();
} catch (error) {
  await uow.rollback();
  throw error;
}

Connection Pooling

import { Pool } from 'pg';
import { Kysely, PostgresDialect } from 'kysely';

const pool = new Pool({
  host: 'localhost',
  database: 'mydb',
  max: 10
});

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool })
});

// Use with withTransaction
async function processData(data: unknown[]) {
  return withTransaction(db, async (trx) => {
    // Each transaction gets a connection from the pool
    for (const item of data) {
      await trx.insertInto('items').values(item).execute();
    }
  });
}

Best Practices

  1. Use DatabaseConnection Type: Accept DatabaseConnection in functions that work with transactions

    async function myFunction(db: DatabaseConnection<Database>) { }
  2. Let withTransaction Handle Reuse: Don't manually check for transaction type

    // Good
    await withTransaction(db, async (trx) => { });
    
    // Avoid
    if (db.isTransaction) { /* ... */ } else { /* ... */ }
  3. Keep Transactions Short: Execute quickly to avoid blocking

    // Good
    await withTransaction(db, async (trx) => {
      await trx.insertInto('users').values(data).execute();
    });
    
    // Avoid long-running operations
    await withTransaction(db, async (trx) => {
      await fetch('https://api.example.com'); // Bad!
    });
  4. Error Handling: Let transactions roll back automatically on errors

  5. Testing: Use transactions for test isolation with automatic rollback

Related Packages

License

MIT