Files
WRNexusJS/packages/db/src/driver.ts
T
2026-07-12 15:55:18 +05:30

87 lines
3.1 KiB
TypeScript

/**
* The database driver interface and the `Db` client built on top of it.
*
* Adapters (SQLite now; Postgres/MySQL/Mongo later) implement `Driver`. The
* client adds ergonomics: `all`/`one` optionally map rows through a model's
* `.parse` (so results match your schema), `tx` wraps work in a transaction, and
* `createTable` runs a model's DDL. Every query is parameterized.
*/
import { createTableSql, type Dialect } from "./sql.ts";
import type { Model } from "./schema.ts";
export type Row = Record<string, unknown>;
export interface ExecResult {
changes: number;
lastInsertId?: number;
}
/** The minimal query surface — the driver itself and each transaction expose it. */
export interface TxHandle {
/** Run a query returning rows (SELECT). Params are positional. */
query(sql: string, params?: unknown[]): Promise<Row[]>;
/** Run a statement (INSERT/UPDATE/DELETE/DDL). */
exec(sql: string, params?: unknown[]): Promise<ExecResult>;
}
export interface Driver extends TxHandle {
dialect: Dialect;
/**
* Run `fn` inside a transaction on a single reserved connection, committing
* on success and rolling back on throw. (Pooled drivers must reserve one
* connection so BEGIN/…/COMMIT don't span connections.)
*/
transaction<T>(fn: (tx: TxHandle) => Promise<T>): Promise<T>;
close(): void | Promise<void>;
}
export interface Db {
driver: Driver;
/** All matching rows, mapped through `model.parse` when a model is given. */
all<T = Row>(sql: string, params?: unknown[], model?: Model<T>): Promise<T[]>;
/** The first matching row (or null), mapped through `model.parse`. */
one<T = Row>(sql: string, params?: unknown[], model?: Model<T>): Promise<T | null>;
exec(sql: string, params?: unknown[]): Promise<ExecResult>;
/** Run `fn` inside a transaction; rolls back if it throws. */
tx<T>(fn: (db: Db) => Promise<T>): Promise<T>;
/** Create a table from its model (`CREATE TABLE IF NOT EXISTS`). */
createTable(model: Model): Promise<void>;
close(): void | Promise<void>;
}
/** Build a `Db` over a query runner (the driver at top level, or a transaction). */
function dbOver(runner: TxHandle, driver: Driver): Db {
const db: Db = {
driver,
async all(sql, params = [], model) {
const rows = await runner.query(sql, params);
return (model ? rows.map((r) => model.parse(r)) : rows) as never;
},
async one(sql, params = [], model) {
const rows = await db.all(sql, params, model as never);
return (rows[0] ?? null) as never;
},
exec(sql, params = []) {
return runner.exec(sql, params);
},
async tx(fn) {
// Top level opens a real transaction; inside one, reuse the current tx.
if (runner === driver) return driver.transaction((tx) => fn(dbOver(tx, driver)));
return fn(db);
},
async createTable(model) {
await runner.exec(createTableSql(model, driver.dialect));
},
close() {
return driver.close();
},
};
return db;
}
/** Build a `Db` client from a driver. */
export function createDb(driver: Driver): Db {
return dbOver(driver, driver);
}