release: WRNexusJS 0.5.10
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
|
||||
@@ -23,14 +23,21 @@ interface ExecMeta {
|
||||
insertId?: number | bigint;
|
||||
}
|
||||
|
||||
function runnerFor(client: BunSqlClient): TxHandle {
|
||||
/** Convert the framework's portable positional placeholders for PostgreSQL. */
|
||||
export function sqlForDialect(sql: string, dialect: Dialect): string {
|
||||
if (dialect !== "postgres" || !sql.includes("?")) return sql;
|
||||
let index = 0;
|
||||
return sql.replace(/\?/g, () => `$${++index}`);
|
||||
}
|
||||
|
||||
function runnerFor(client: BunSqlClient, dialect: Dialect): TxHandle {
|
||||
return {
|
||||
async query(sql, params = []): Promise<Row[]> {
|
||||
const rows = (await client.unsafe(sql, params)) as Iterable<Row>;
|
||||
const rows = (await client.unsafe(sqlForDialect(sql, dialect), params)) as Iterable<Row>;
|
||||
return Array.from(rows);
|
||||
},
|
||||
async exec(sql, params = []) {
|
||||
const meta = (await client.unsafe(sql, params)) as ExecMeta;
|
||||
const meta = (await client.unsafe(sqlForDialect(sql, dialect), params)) as ExecMeta;
|
||||
const id = meta.lastInsertRowid ?? meta.insertId;
|
||||
return {
|
||||
changes: Number(meta.affectedRows ?? meta.count ?? 0),
|
||||
@@ -44,13 +51,13 @@ function runnerFor(client: BunSqlClient): TxHandle {
|
||||
export function bunSql(url: string, dialect: Dialect): Driver {
|
||||
const Ctor = (Bun as unknown as { SQL: new (u: string) => BunSqlClient }).SQL;
|
||||
const client = new Ctor(url);
|
||||
const runner = runnerFor(client);
|
||||
const runner = runnerFor(client, dialect);
|
||||
return {
|
||||
dialect,
|
||||
query: runner.query,
|
||||
exec: runner.exec,
|
||||
transaction(fn) {
|
||||
return client.begin((tx) => fn(runnerFor(tx)));
|
||||
return client.begin((tx) => fn(runnerFor(tx, dialect)));
|
||||
},
|
||||
close() {
|
||||
return client.close();
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
import type { Db } from "./driver.ts";
|
||||
|
||||
const DEFAULT = "default";
|
||||
const registry = new Map<string, Db>();
|
||||
type DbFactory = () => Db;
|
||||
type RegistryEntry = { db?: Db; factory?: DbFactory };
|
||||
const registry = new Map<string, RegistryEntry>();
|
||||
|
||||
/** Set the default database (called by the runtime at startup). */
|
||||
export function setDb(db: Db): Db;
|
||||
@@ -21,7 +23,7 @@ export function setDb(name: string, db: Db): Db;
|
||||
export function setDb(a: string | Db, b?: Db): Db {
|
||||
const name = typeof a === "string" ? a : DEFAULT;
|
||||
const db = typeof a === "string" ? b! : a;
|
||||
registry.set(name, db);
|
||||
registry.set(name, { db });
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -30,10 +32,18 @@ export function registerDb(name: string, db: Db): Db {
|
||||
return setDb(name, db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a named database without opening its connection pool. The first
|
||||
* `getDb(name)` call creates and caches the connection.
|
||||
*/
|
||||
export function registerLazyDb(name: string, factory: DbFactory): void {
|
||||
registry.set(name, { factory });
|
||||
}
|
||||
|
||||
/** The default database, or a named one. Throws if it isn't configured. */
|
||||
export function getDb(name = DEFAULT): Db {
|
||||
const db = registry.get(name);
|
||||
if (!db) {
|
||||
const entry = registry.get(name);
|
||||
if (!entry) {
|
||||
throw new Error(
|
||||
name === DEFAULT
|
||||
? "No database configured. Add `db: { driver, url }` to wrnexus.config.ts."
|
||||
@@ -41,7 +51,11 @@ export function getDb(name = DEFAULT): Db {
|
||||
`(e.g. databases: { ${name}: { driver, url } }).`,
|
||||
);
|
||||
}
|
||||
return db;
|
||||
if (!entry.db) {
|
||||
if (!entry.factory) throw new Error(`Database '${name}' has no connection factory.`);
|
||||
entry.db = entry.factory();
|
||||
}
|
||||
return entry.db;
|
||||
}
|
||||
|
||||
/** Whether the default (or a named) database has been configured. */
|
||||
@@ -56,6 +70,8 @@ export function databaseNames(): string[] {
|
||||
|
||||
/** Close every configured database and clear the registry. */
|
||||
export async function closeDatabases(): Promise<void> {
|
||||
for (const db of registry.values()) await db.close();
|
||||
for (const entry of registry.values()) {
|
||||
if (entry.db) await entry.db.close();
|
||||
}
|
||||
registry.clear();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,15 @@ export { v, table, Column } from "./schema.ts";
|
||||
export type { Model, Columns, ColumnDef, BaseType } from "./schema.ts";
|
||||
export { createDb } from "./driver.ts";
|
||||
export type { Db, Driver, Row, ExecResult, TxHandle } from "./driver.ts";
|
||||
export { setDb, getDb, hasDb, registerDb, databaseNames, closeDatabases } from "./client.ts";
|
||||
export {
|
||||
setDb,
|
||||
getDb,
|
||||
hasDb,
|
||||
registerDb,
|
||||
registerLazyDb,
|
||||
databaseNames,
|
||||
closeDatabases,
|
||||
} from "./client.ts";
|
||||
export { createTableSql } from "./sql.ts";
|
||||
export type { Dialect } from "./sql.ts";
|
||||
export {
|
||||
|
||||
@@ -65,6 +65,7 @@ export async function appliedMigrations(db: Db): Promise<string[]> {
|
||||
|
||||
/** Apply an ordered migration list (each in a transaction). Returns applied names. */
|
||||
export async function applyMigrations(db: Db, migrations: readonly Migration[]): Promise<string[]> {
|
||||
if (migrations.length === 0) return [];
|
||||
const applied = new Set(await appliedMigrations(db));
|
||||
const pending = migrations.filter((migration) => !applied.has(migration.name));
|
||||
const done: string[] = [];
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { sqlForDialect } from "../src/adapters/bunsql.ts";
|
||||
|
||||
test("PostgreSQL converts portable placeholders to numbered parameters", () => {
|
||||
expect(sqlForDialect("SELECT * FROM users WHERE id = ? AND status = ?", "postgres")).toBe(
|
||||
"SELECT * FROM users WHERE id = $1 AND status = $2",
|
||||
);
|
||||
});
|
||||
|
||||
test("MySQL and parameter-free PostgreSQL statements remain unchanged", () => {
|
||||
expect(sqlForDialect("SELECT * FROM users WHERE id = ?", "mysql")).toBe(
|
||||
"SELECT * FROM users WHERE id = ?",
|
||||
);
|
||||
expect(sqlForDialect("CREATE TABLE users (id TEXT)", "postgres")).toBe(
|
||||
"CREATE TABLE users (id TEXT)",
|
||||
);
|
||||
});
|
||||
@@ -8,12 +8,14 @@ import {
|
||||
createDb,
|
||||
createTableSql,
|
||||
parseMigration,
|
||||
applyMigrations,
|
||||
migrate,
|
||||
status,
|
||||
rollback,
|
||||
parseQueries,
|
||||
generateQueriesFile,
|
||||
} from "../src/index.ts";
|
||||
import type { Db } from "../src/index.ts";
|
||||
import { sqlite } from "../src/adapters/sqlite.ts";
|
||||
import { bunSql } from "../src/adapters/bunsql.ts";
|
||||
|
||||
@@ -95,6 +97,20 @@ test("migration runner: parse, migrate, status, rollback", async () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
test("an empty migration set does not touch the database", async () => {
|
||||
const unreachable = async () => {
|
||||
throw new Error("database should remain lazy");
|
||||
};
|
||||
const db = {
|
||||
all: unreachable,
|
||||
one: unreachable,
|
||||
exec: unreachable,
|
||||
tx: unreachable,
|
||||
} as unknown as Db;
|
||||
|
||||
expect(await applyMigrations(db, [])).toEqual([]);
|
||||
});
|
||||
|
||||
test("query generator infers params and result types", () => {
|
||||
const q = parseQueries(
|
||||
"-- name: GetByEmail :one\nSELECT * FROM users WHERE email = :email;\n" +
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
getDb,
|
||||
hasDb,
|
||||
registerDb,
|
||||
registerLazyDb,
|
||||
databaseNames,
|
||||
closeDatabases,
|
||||
} from "../src/index.ts";
|
||||
@@ -45,3 +46,35 @@ test("getDb throws a helpful error for an unknown named database", async () => {
|
||||
expect(() => getDb("nope")).toThrow(/No database named 'nope'/);
|
||||
await closeDatabases();
|
||||
});
|
||||
|
||||
test("named databases can be registered without opening a connection", async () => {
|
||||
await closeDatabases();
|
||||
let calls = 0;
|
||||
const analytics = createDb(sqlite(":memory:"));
|
||||
|
||||
registerLazyDb("analytics", () => {
|
||||
calls++;
|
||||
return analytics;
|
||||
});
|
||||
|
||||
expect(hasDb("analytics")).toBe(true);
|
||||
expect(databaseNames()).toEqual(["analytics"]);
|
||||
expect(calls).toBe(0);
|
||||
expect(getDb("analytics")).toBe(analytics);
|
||||
expect(getDb("analytics")).toBe(analytics);
|
||||
expect(calls).toBe(1);
|
||||
|
||||
await closeDatabases();
|
||||
});
|
||||
|
||||
test("closing the registry does not instantiate unused lazy databases", async () => {
|
||||
await closeDatabases();
|
||||
let calls = 0;
|
||||
registerLazyDb("unused", () => {
|
||||
calls++;
|
||||
return createDb(sqlite(":memory:"));
|
||||
});
|
||||
|
||||
await closeDatabases();
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user