@wrnexus/db
Database adapters, typed queries, models, migrations, and sessions.
bun add @wrnexus/db@0.2.15The database layer for WRNexusJS: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based Db client, migrations, and a sqlc-style query generator.
Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.
Overview
@wrnexus/db is the server-side data layer. You describe tables as TypeScript models (the v column builder + table()); those models drive migrations, coerce raw DB rows into typed objects, and feed the query generator. A thin Driver interface is implemented by adapters for SQLite (bun:sqlite), Postgres/MySQL (Bun.SQL), and MongoDB. The Db client adds ergonomics — model-mapped all/one, transactions, createTable, pagination, and batched relation loading. A process-wide registry (getDb/setDb) exposes configured connections to pages and API routes. Reach for it whenever a WRNexusJS app needs persistence.
Installation
bun add @wrnexus/db
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).
API
The core entry (@wrnexus/db) is dependency-free; adapters and connectors live in subpaths so importing the core doesn't pull in every driver.
| Subpath | Exports |
|---|---|
@wrnexus/db | v, table, Column, createDb, createTableSql, the client registry (setDb/getDb/…), migrations, the query generator, and query helpers |
@wrnexus/db/connect | connectFromConfig, resolveDbUrl, DbConfig — resolve a config to a live SQL Db |
@wrnexus/db/session | sqliteSessionStore — a bun:sqlite session backend for @wrnexus/core |
@wrnexus/db/sqlite | sqlite(url?) driver |
@wrnexus/db/postgres | postgres(url) driver |
@wrnexus/db/mysql | mysql(url) driver |
@wrnexus/db/mongo | mongo(url, dbName?) document API |
Schema — v, table, Column
table(name, columns) returns a Model<T>. Columns are built with v:
import { v, table } from "@wrnexus/db";
const users = table("users", {
id: v.id(), // auto-increment primary key
email: v.text().unique(),
name: v.text().optional(), // NULLable
age: v.int().default(0),
active: v.bool().default(true),
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
});
Column builders: v.id, v.text (alias v.string), v.int, v.real (alias v.number), v.bool (alias v.boolean), v.timestamp, v.json. BaseType values are "id" | "text" | "int" | "real" | "bool" | "timestamp" | "json".
Column modifiers (chainable): .optional(), .unique(), .default(value) (use the sentinel "now" for a current-timestamp default), .primaryKey(), .references(table, column = "id"). .coerce(raw) converts a raw DB value to its JS type.
A Model<T> exposes: name, columns, parse(row) (coerces a raw row into a typed T; unknown columns pass through), and describe() (returns each column's ColumnDef, for migrations and the generator).
Driver & client — createDb, Db, Driver
createDb(driver: Driver): Db
A Driver (implemented by adapters) exposes dialect, query(sql, params?), exec(sql, params?), transaction(fn), and close(). createDb wraps it in a Db:
all<T>(sql, params?, model?)— all rows, mapped throughmodel.parsewhen a model is given.one<T>(sql, params?, model?)— first row ornull.exec(sql, params?)—Promise<ExecResult>({ changes, lastInsertId? }).tx(fn)— runfn(db)in a transaction; rolls back on throw. Nestedtxreuses the current transaction.createTable(model)— runs the model'sCREATE TABLE IF NOT EXISTSDDL.close().
Every query is parameterized (positional params). createTableSql(model, dialect, ifNotExists?) renders CREATE TABLE directly; Dialect is "sqlite" | "postgres" | "mysql".
Client registry — getDb / setDb
A process-wide registry the runtime configures at startup from wrnexus.config.ts (the db setting is the default; databases.<name> entries are named).
setDb(db)/setDb(name, db)— set the default or a named connection.registerDb(name, db)— alias ofsetDb(name, db).getDb(name = "default")— the default or a namedDb(throws if unconfigured).hasDb(name?),databaseNames(),closeDatabases().
const users = await getDb().all("SELECT * FROM users");
const events = await getDb("analytics").all("SELECT * FROM hits");
Adapters
@wrnexus/db/sqlite—sqlite(url = ":memory:").urlmay befile:./dev.db, a raw path, or:memory:. Built onbun:sqlite; no external service.@wrnexus/db/postgres—postgres(url)(e.g.postgres://user:pass@host:5432/db, placeholders$N).@wrnexus/db/mysql—mysql(url)(e.g.mysql://user:pass@host:3306/db, placeholders?). Postgres/MySQL both use Bun's nativeBun.SQLclient and its pooledbegin()for transactions.@wrnexus/db/mongo—mongo(url, dbName?). A document API, not SQL:db.collection(model)returns aMongoRepo<T>withfind,findOne,insert,insertMany,update,delete,count. Reads are coerced throughmodel.parse(_idis mapped toid). Themongodbdriver is imported lazily — install it to use Mongo.
Migrations
Migrations are .sql files (in e.g. app/db/migrations), each split into -- +up and -- +down sections. A file with no markers is treated entirely as up. Applied names are recorded in a _wire_migrations table so each runs once.
parseMigration(name, content)→Migration({ name, up, down }).loadMigrations(dir)— parse all.sqlfiles, sorted by filename.appliedMigrations(db)— applied names, oldest first.migrate(db, dir)— apply all pending (each in a transaction); returns applied names.rollback(db, dir)— roll back the most recent; returns its name ornull.status(db, dir)—{ name, applied }[]for every migration file.scaffoldMigration(dir, name, dialect, models?)— write a new numbered migration; withmodelsit generatesCREATE/DROPfor every table (referenced tables first via topological sort). Returns the file path.
Query generator (sqlc-style)
Turns annotated SQL into typed TS functions; params and result types are inferred from the models, and rows map back through model.parse when the selected columns are model columns.
parseQueries(content)→QueryDef[]from-- name: X :one|:many|:execblocks.generateQueriesFile(queries, models, dialect)→ thequeries.gen.tssource.modelsis aModelRef[]({ varName, model }). Rewrites:nameplaceholders to positional ($N/?) form.
QueryKind is "one" | "many" | "exec".
Query helpers
paginate(db, { sql, params?, countSql?, model? }, opts?)— offset pagination. Pass the base SELECT without a LIMIT; it appends the page window and derivestotalvia a COUNT subquery.PageOptions:{ page?, perPage?, maxPerPage? }(defaults page 1, perPage 20, maxPerPage 100). ReturnsPaginated<T>(items, page, perPage, total, totalPages, hasNext, hasPrev).loadRelated(db, parents, opts)— load a relation for many parents in ONE query and attach it (no N+1).RelationOptions:{ table, foreignKey, as, localKey?, single?, model? }—single: trueattaches one child (belongsTo), otherwise an array (hasMany). Table/foreign-key names are validated as identifiers.
Session store
@wrnexus/db/session exports sqliteSessionStore(path = "sessions.db"), a persistent, process-shared SessionBackend (from @wrnexus/core) backed by bun:sqlite (WAL mode). Sessions survive restarts and are shared by every worker on the same file.
Usage
Define models, connect, create tables, and query with typed results:
import { v, table, createDb } from "@wrnexus/db";
import { sqlite } from "@wrnexus/db/sqlite";
const users = table<{ id: number; email: string; name: string | null }>("users", {
id: v.id(),
email: v.text().unique(),
name: v.text().optional(),
createdAt: v.timestamp().default("now"),
});
const db = createDb(sqlite("file:./dev.db"));
await db.createTable(users);
await db.exec("INSERT INTO users (email) VALUES (?)", ["a@b.com"]);
const list = await db.all("SELECT * FROM users", [], users); // rows typed + coerced
const one = await db.one("SELECT * FROM users WHERE id = ?", [1], users);
await db.tx(async (tx) => {
await tx.exec("UPDATE users SET name = ? WHERE id = ?", ["Ada", 1]);
});
Resolve a config to a live SQL Db, and register it:
import { connectFromConfig } from "@wrnexus/db/connect";
import { setDb, getDb } from "@wrnexus/db";
setDb(connectFromConfig({ driver: "sqlite", url: "file:./dev.db" }, process.cwd()));
const rows = await getDb().all("SELECT * FROM users");
Run migrations and paginate:
import { migrate, paginate } from "@wrnexus/db";
await migrate(db, "app/db/migrations");
const pageTwo = await paginate(
db,
{ sql: "SELECT * FROM users ORDER BY id", model: users },
{ page: 2 },
);
MongoDB (document API):
import { mongo } from "@wrnexus/db/mongo";
const mdb = await mongo(process.env.MONGO_URL!, "app");
const repo = mdb.collection(users);
await repo.insert({ email: "a@b.com" });
const active = await repo.find({ active: true });
Configuration
connectFromConfig (and the runtime) read a DbConfig ({ driver, url }) where driver is sqlite | postgres | mysql. resolveDbUrl(url, appRoot?) resolves a relative file:/sqlite: URL against the app root. MongoDB is not a SQL driver — use @wrnexus/db/mongo directly.
Requirements / Notes
- Bun-only. Uses
bun:sqlite(SQLite adapter + session store) andBun.SQL - Works with
@wrnexus/core—sqliteSessionStoreimplements its - The
mongodbnpm package is an optional, lazily-imported peer — install it
(Postgres/MySQL). Migrations/scaffolding use node:fs/node:path.
SessionBackend; getDb/setDb are wired by the WRNexusJS runtime from wrnexus.config.ts.
only if you use @wrnexus/db/mongo. The core package stays dependency-free.
Complete TypeScript API
This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.
import { M as Model } from './schema-tVurYsbL.js';
export { B as BaseType, C as Column, a as ColumnDef, b as Columns, t as table, v } from './schema-tVurYsbL.js';
import { a as Db, b as Dialect, R as Row } from './driver-DA53QHkO.js';
export { D as Driver, E as ExecResult, T as TxHandle, c as createDb, d as createTableSql } from './driver-DA53QHkO.js';
/**
* A process-wide database registry. The framework configures it at server
* startup from `wrnexus.config.ts`: the `db` setting becomes the **default**
* connection, and each entry under `databases` becomes a **named** connection.
* Pages and API routes then call `getDb()` for the default, or `getDb("<name>")`
* for a named one, to run queries (including the generated typed functions).
*
* const users = await getDb().all("SELECT * FROM users"); // default db
* const events = await getDb("analytics").all("SELECT * FROM hits"); // named db
*/
/** Set the default database (called by the runtime at startup). */
declare function setDb(db: Db): Db;
/** Set a named database (from `databases.<name>` in config). */
declare function setDb(name: string, db: Db): Db;
/** Register a named database. Alias of `setDb(name, db)` for readability. */
declare function registerDb(name: string, db: Db): Db;
/** The default database, or a named one. Throws if it isn't configured. */
declare function getDb(name?: string): Db;
/** Whether the default (or a named) database has been configured. */
declare function hasDb(name?: string): boolean;
/** Names of all configured databases (the default appears as "default"). */
declare function databaseNames(): string[];
/** Close every configured database and clear the registry. */
declare function closeDatabases(): Promise<void>;
/**
* Migration runner. Migrations are `.sql` files in `app/db/migrations`, each
* split into `-- +up` and `-- +down` sections. Applied migrations are recorded
* in a `_wire_migrations` table so they run exactly once, newest-last.
*
* `scaffoldMigration(..., models)` writes an initial migration straight from the
* TS models — the source of truth — so you don't hand-write the first schema.
*/
interface Migration {
name: string;
up: string;
down: string;
}
/** Split a migration file into its `up` and `down` SQL sections. */
declare function parseMigration(name: string, content: string): Migration;
/** Load and parse all migration files in a directory, sorted by filename. */
declare function loadMigrations(dir: string): Migration[];
/** Names of already-applied migrations, oldest first. */
declare function appliedMigrations(db: Db): Promise<string[]>;
/** Apply all pending migrations (each in a transaction). Returns applied names. */
declare function migrate(db: Db, dir: string): Promise<string[]>;
/** Roll back the most recently applied migration. Returns its name, or null. */
declare function rollback(db: Db, dir: string): Promise<string | null>;
/** Full status: every migration file with whether it has been applied. */
declare function status(db: Db, dir: string): Promise<{
name: string;
applied: boolean;
}[]>;
/**
* Write a new migration file. With `models`, the `up`/`down` are generated from
* the TS models (create/drop every table); otherwise empty stubs are written.
* Returns the created file path.
*/
declare function scaffoldMigration(dir: string, name: string, dialect: Dialect, models?: Model[]): string;
/**
* sqlc-style query generator. Annotated SQL in `app/db/queries/*.sql` becomes
* typed TS functions whose params + results are inferred from the TS models and
* whose rows are mapped back through `model.parse`.
*
* -- name: GetUserByEmail :one
* SELECT * FROM users WHERE email = :email;
*
* → GetUserByEmail(db, { email: string }): Promise<{…} | null>
*
* Type inference is best-effort (comparisons + INSERT column lists + SELECT list
* vs the model); anything it can't resolve becomes `unknown`.
*/
type QueryKind = "one" | "many" | "exec";
interface QueryDef {
name: string;
kind: QueryKind;
sql: string;
}
/** A model plus the variable name it is exported under (for imports). */
interface ModelRef {
varName: string;
model: Model;
}
/** Parse annotated queries from one `.sql` file's contents. */
declare function parseQueries(content: string): QueryDef[];
/** Generate the full `queries.gen.ts` source. */
declare function generateQueriesFile(queries: QueryDef[], models: ModelRef[], dialect: Dialect): string;
/**
* Query ergonomics built on the `Db` client: offset pagination and a batched
* relation loader (avoids N+1). Both are dialect-aware — placeholders follow the
* driver's style (`$N` for Postgres, `?` for SQLite/MySQL).
*/
interface PageOptions {
page?: number;
perPage?: number;
/** Upper bound on perPage. Default 100. */
maxPerPage?: number;
}
interface Paginated<T> {
items: T[];
page: number;
perPage: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
/**
* Paginate a SELECT. Pass the base query WITHOUT a LIMIT; the helper appends the
* page window and derives the total with a COUNT over the same query.
*
* await paginate(db, { sql: "SELECT * FROM users ORDER BY name", model: users }, { page: 2 })
*/
declare function paginate<T = Row>(db: Db, query: {
sql: string;
params?: unknown[];
countSql?: string;
model?: Model<T>;
}, opts?: PageOptions): Promise<Paginated<T>>;
interface RelationOptions<C> {
/** Parent field whose value matches the child's foreign key. Default "id". */
localKey?: string;
/** Child table to load from. */
table: string;
/** Child column that references the parent. */
foreignKey: string;
/** Property name to attach on each parent. */
as: string;
/** true → attach a single child (belongsTo); false → an array (hasMany). */
single?: boolean;
/** Map child rows through a model. */
model?: Model<C>;
}
/**
* Load a relation for a set of parent rows in ONE query and attach it to each
* parent (no N+1). Returns the same parents, each with `opts.as` populated.
*
* await loadRelated(db, users, { table: "posts", foreignKey: "userId", as: "posts" })
*/
declare function loadRelated<P extends Row, C extends Row = Row>(db: Db, parents: P[], opts: RelationOptions<C>): Promise<(P & Record<string, C | C[] | null>)[]>;
export { Db, Dialect, type Migration, Model, type ModelRef, type PageOptions, type Paginated, type QueryDef, type QueryKind, type RelationOptions, Row, appliedMigrations, closeDatabases, databaseNames, generateQueriesFile, getDb, hasDb, loadMigrations, loadRelated, migrate, paginate, parseMigration, parseQueries, registerDb, rollback, scaffoldMigration, setDb, status };
Examples
Copy-ready examples taken from this package's published documentation.
Example 1
bun add @wrnexus/dbExample 2
import { v, table } from "@wrnexus/db";
const users = table("users", {
id: v.id(), // auto-increment primary key
email: v.text().unique(),
name: v.text().optional(), // NULLable
age: v.int().default(0),
active: v.bool().default(true),
createdAt: v.timestamp().default("now"), // CURRENT_TIMESTAMP
});Example 3
createDb(driver: Driver): DbExample 4
const users = await getDb().all("SELECT * FROM users");
const events = await getDb("analytics").all("SELECT * FROM hits");