Skip to content

DB Kit

Database abstraction using Drizzle ORM. It supports PostgreSQL, MySQL and SQLite.

import { App } from '@iskra-bun/core';
import { DbDriver } from '@iskra-bun/db-kit';
const app = new App({
name: 'MiApp',
db: {
driver: 'sqlite',
url: 'app.db',
},
});
const db = new DbDriver();
app.register(db);
await app.start();
// Usar Drizzle ORM — db.db is undefined until the driver starts, so guard it
const result = db.db!.select().from(users).all();

db.db is typed IskraDrizzleDb<TSchema> | undefined: it is undefined before app.start() and again after stop(). Guard it (if (!db.db) ...), use the non-null assertion when you know the app has started, or check liveness with db.ping().

Driver Package Example URL
postgres postgres.js postgres://user:pass@localhost:5432/db
mysql mysql2 (connection pool) mysql://user:pass@localhost:3306/db
sqlite better-sqlite3 / bun:sqlite app.db or :memory:

The DbDriver automatically detects whether it is running on Bun and uses bun:sqlite instead of better-sqlite3. The mysql driver opens a connection pool (mysql2.createPool) rather than a single connection.

DbDriver accepts an optional Drizzle schema type parameter so db.query.* is fully typed. Without the type argument the driver behaves exactly as before (untyped, Record<string, never>).

import * as schema from './src/db/schema';
// Pass your schema as the type argument
const db = new DbDriver<typeof schema>();
app.register(db);
await app.start();
// db.db is now typed (and still `| undefined` until started — guard it)
const users = await db.db!.query.users.findMany({ where: eq(schema.users.active, true) });

app.context.get('db') returns the same driver, but typed with the default schema (DbDriver | undefined): the context registry cannot know the schema you passed, so db.query.* is untyped through it. Keep a reference to the typed driver instance and use it, or cast what the context returns:

// Recommended: export the typed instance and import it where you need it
export const db = new DbDriver<typeof schema>();
// Where only the app is at hand
const db = app.context.get('db') as DbDriver<typeof schema> | undefined;

The exported helper types IskraDrizzleDb<TSchema> and IskraDrizzleTx<TSchema> are available if you need to annotate function parameters:

import type { IskraDrizzleDb, IskraDrizzleTx } from '@iskra-bun/db-kit';
src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: text('created_at').default('CURRENT_TIMESTAMP'),
});

Use DbDriver.transaction(fn) instead of reaching into the raw db handle. Drizzle rolls back automatically on throw; failures are wrapped in QueryError.

const result = await db.transaction(async (tx) => {
await tx.insert(schema.orders).values({ userId: 1, total: 99 });
await tx.update(schema.inventory).set({ stock: sql`stock - 1` }).where(eq(schema.inventory.id, 42));
return { ok: true };
});

Signature:

transaction<R>(fn: (tx: IskraDrizzleTx<TSchema>) => Promise<R>): Promise<R>

Register a callback that receives every SQL statement and its bound parameters. Must be called before app.start() — the logger is wired at connection time.

db.setOnQuery((query, params) => {
console.log('[sql]', query, params);
// forward to OpenTelemetry, Datadog, etc.
});
app.register(db);
await app.start();

Signature:

setOnQuery(onQuery: (query: string, params: unknown[]) => void): void

A throwing callback is swallowed so observability never breaks a real query.

DbDriver.ping() runs a trivial SELECT 1 and resolves true on success or false on any failure — it never rejects. Use it for readiness probes or health endpoints (pairs well with web-kit’s addReadinessCheck).

const alive = await db.ping();
// true → database is reachable
// false → not started, or connection dropped

Signature:

ping(): Promise<boolean>

See Migrations for the full guide.

// Run migrations programmatically
await db.runMigrations(undefined, './drizzle');

runMigrations applies the migrations generated in migrationsDir with Drizzle’s migrator, over the connection opened by start() (it used to run drizzle-kit migrate, which ignored both arguments and failed without a drizzle.config.ts). MigrationHelper still passes --schema and --out to drizzle-kit generate and --config to every command.

MigrationHelper (and the db-kit CLI) only run the drizzle-kit installed in the project, found in node_modules/.bin of the working directory or a parent, through bunx --no-install drizzle-kit. drizzle-kit is a devDependency: where it is not installed (e.g. after bun install --production) they fail with a MigrationError asking to bun add -d drizzle-kit. They used to run bunx drizzle-kit, which then downloaded the latest release from npm and ran it with DATABASE_URL in its environment.

When a migration command exits non-zero, the captured drizzle-kit stderr is scrubbed of credentials before it is attached to MigrationError.context.stderr. drizzle-kit echoes the full connection string on failure, so any embedded //user:password@host is rewritten to //***:***@host — the rest of the diagnostic survives intact, so connection strings never leak into logs.

You can also use MigrationHelper directly if you need finer control:

import { MigrationHelper } from '@iskra-bun/db-kit';
const helper = new MigrationHelper({
dialect: 'sqlite', // 'postgresql' | 'mysql' | 'sqlite'
dbUrl: 'app.db',
schemaPath: './src/db/schema.ts',
migrationsDir: './drizzle',
configPath: './drizzle.config.ts', // optional — passed as --config
});
await helper.generate('add_users'); // drizzle-kit generate --name add_users
await helper.migrate(); // drizzle-kit migrate
await helper.push(); // drizzle-kit push (dev shortcut)

Flag mapping per command:

Method --schema --out --config
generate yes yes if set
migrate — — if set
push yes — if set
drop — yes if set
import { ConnectionError, QueryError, MigrationError } from '@iskra-bun/db-kit';
// Thrown automatically on failure:
// ConnectionError: code='CONNECTION_ERROR'
// QueryError: code='QUERY_ERROR' (also thrown by transaction())
// MigrationError: code='MIGRATION_ERROR'