Files
WRNexusJS/packages/db/src/driver.ts
T
Clintchiz 586a6db8ff
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s
release: WRNexusJS 0.8.0
2026-08-02 23:18:51 +05:30

114 lines
4.0 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>;
}
interface DbLifecycle {
closing: boolean;
active: Set<Promise<unknown>>;
closePromise?: Promise<void>;
}
/** Build a `Db` over a query runner (the driver at top level, or a transaction). */
function dbOver(
runner: TxHandle,
driver: Driver,
lifecycle: DbLifecycle,
transactionScope = false,
): Db {
function run<T>(operation: () => Promise<T>): Promise<T> {
if (lifecycle.closing && !transactionScope) {
return Promise.reject(new Error("WRN-DB-CLOSED: database is closing or closed"));
}
const promise = Promise.resolve().then(operation);
lifecycle.active.add(promise);
void promise.finally(() => lifecycle.active.delete(promise)).catch(() => {});
return promise;
}
const db: Db = {
driver,
async all(sql, params = [], model) {
const rows = await run(() => 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 run(() => runner.exec(sql, params));
},
async tx(fn) {
// Top level opens a real transaction; inside one, reuse the current tx.
if (runner === driver)
return run(() => driver.transaction((tx) => fn(dbOver(tx, driver, lifecycle, true))));
return fn(db);
},
async createTable(model) {
await run(() => runner.exec(createTableSql(model, driver.dialect)));
},
close() {
if (lifecycle.closePromise) return lifecycle.closePromise;
lifecycle.closing = true;
lifecycle.closePromise = (async () => {
await Promise.allSettled([...lifecycle.active]);
await driver.close();
})();
return lifecycle.closePromise;
},
};
return db;
}
/** Build a `Db` client from a driver. */
export function createDb(driver: Driver): Db {
return dbOver(driver, driver, { closing: false, active: new Set() });
}