Bumps all 47 packages, the root manifest and the VS Code extension to 0.8.6, and rebuilds the editor compiler, language server and extension bundles that embed the version. The release carries the output delivery fix: camelCase outputs now reach parent bindings, and 18 components emit through output.* instead of hand-built CustomEvents. See the 0.8.6 migration entry for what changes for consumers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wrnexus/db
Rollout-safe migrations
Run wrnexus db check in CI before deployment. The analyzer reports stable
diagnostics for drops, renames, type changes, new/enforced required columns,
and potentially blocking index creation, with an expand/backfill/switch/contract
recommendation. wrnexus db migrate blocks critical issues in pending
migrations. --allow-breaking is an explicit operator override; already-applied
migrations do not block later releases.
The database layer for WrNexus: TS models as the single source of truth for DDL, validation, and result typing, plus a driver-based
Dbclient, migrations, and a sqlc-style query generator.
Part of the WrNexus 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 WrNexus app needs
persistence.
Installation
bun add @wrnexus/db
Private package — the machine must be authenticated to the
wrnexusnpm 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()— idempotently rejects new top-level work, drains active queries and transactions, then closes the underlying pool.
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(). Registry shutdown clears registrations first, attempts every open database, and reports close failures together withAggregateErrorinstead of leaking later pools.
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, options?)— apply all pending (each in a transaction); returns applied names.rollback(db, dir, options?)— 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 },
);
For deployments, { dryRun: true } reports pending names without applying
their SQL, signal cancels safely between migrations, and the default
database-backed lock prevents concurrent deploy runners. A live lock produces
WRN-DB-MIGRATION-LOCKED; crash-stale locks expire after lockTimeoutMs (five
minutes by default). Disable it with lock: false only when an external deploy
coordinator already guarantees exclusivity.
const pending = await migrate(db, "app/db/migrations", { dryRun: true });
await migrate(db, "app/db/migrations", {
signal: shutdownController.signal,
lockTimeoutMs: 10 * 60_000,
});
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(Postgres/MySQL). Migrations/scaffolding usenode:fs/node:path. - Works with
@wrnexus/core—sqliteSessionStoreimplements itsSessionBackend;getDb/setDbare wired by the WrNexus runtime fromwrnexus.config.ts. - The
mongodbnpm package is an optional, lazily-imported peer — install it only if you use@wrnexus/db/mongo. The core package stays dependency-free.
Repository and transaction helpers
Repositories accept an immutable equality scope, normally { column: "tenant_id", value: ctx.tenant.id }. The scope is injected into every read, count, update and delete, while create
overwrites any caller-supplied tenant value. This makes accidental cross-tenant CRUD through the
repository API fail closed.
import { createRepository, retryTransaction, databaseHealth, batch } from "@wrnexus/db";
const users = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
});
const user = await users.require(42);
await users.update(42, { active: true });
Repository identifiers are validated, writes may be restricted to an allowlist, and values always use query parameters. Infrastructure packages remain helper-only and do not add UI dependencies to server code.
0.8 repository and transaction helpers
import { createRepository, databaseHealth, firstOrThrow, retryTransaction } from "@wrnexus/db";
const usersRepo = createRepository<User>(db, {
table: "users",
allowedColumns: ["email", "name", "active"],
maxListLimit: 250,
});
const users = await usersRepo.all({
orderBy: "name",
direction: "asc",
limit: 50,
offset: 0,
});
Repository SQL identifiers are validated and values remain parameterized. Placeholder generation is dialect-aware: PostgreSQL uses $1, $2, and SQLite/MySQL use ?. List limits are bounded.
retryTransaction() retries recognized serialization, deadlock, and database-lock errors by default. Supply shouldRetry for application-specific retryable errors; ordinary validation or business errors are not retried automatically.