114 lines
4.0 KiB
TypeScript
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() });
|
|
}
|