first commit
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Postgres + MySQL adapters built on Bun's native SQL client (`Bun.SQL`) — no
|
||||
* external driver dependency. Both speak the same `Driver` interface; only the
|
||||
* dialect (and thus the DDL types + placeholder style) differ.
|
||||
*
|
||||
* `Bun.SQL` pools connections, so transactions use its managed `begin(fn)` to
|
||||
* keep BEGIN/…/COMMIT on one reserved connection.
|
||||
*/
|
||||
|
||||
import type { Dialect } from "../sql.ts";
|
||||
import type { Driver, Row, TxHandle } from "../driver.ts";
|
||||
|
||||
interface BunSqlClient {
|
||||
unsafe(query: string, params?: unknown[]): Promise<unknown>;
|
||||
begin<T>(fn: (tx: BunSqlClient) => Promise<T>): Promise<T>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
interface ExecMeta {
|
||||
count?: number;
|
||||
affectedRows?: number;
|
||||
lastInsertRowid?: number | bigint;
|
||||
insertId?: number | bigint;
|
||||
}
|
||||
|
||||
function runnerFor(client: BunSqlClient): TxHandle {
|
||||
return {
|
||||
async query(sql, params = []): Promise<Row[]> {
|
||||
const rows = (await client.unsafe(sql, params)) as Iterable<Row>;
|
||||
return Array.from(rows);
|
||||
},
|
||||
async exec(sql, params = []) {
|
||||
const meta = (await client.unsafe(sql, params)) as ExecMeta;
|
||||
const id = meta.lastInsertRowid ?? meta.insertId;
|
||||
return {
|
||||
changes: Number(meta.affectedRows ?? meta.count ?? 0),
|
||||
lastInsertId: id != null ? Number(id) : undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a Bun.sql-backed driver for the given connection URL + dialect. */
|
||||
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);
|
||||
return {
|
||||
dialect,
|
||||
query: runner.query,
|
||||
exec: runner.exec,
|
||||
transaction(fn) {
|
||||
return client.begin((tx) => fn(runnerFor(tx)));
|
||||
},
|
||||
close() {
|
||||
return client.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** PostgreSQL adapter (`postgres://user:pass@host:5432/db`). Placeholders: `$N`. */
|
||||
export function postgres(url: string): Driver {
|
||||
return bunSql(url, "postgres");
|
||||
}
|
||||
|
||||
/** MySQL adapter (`mysql://user:pass@host:3306/db`). Placeholders: `?`. */
|
||||
export function mysql(url: string): Driver {
|
||||
return bunSql(url, "mysql");
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* MongoDB adapter. Mongo is a document store, not SQL, so it does NOT use the
|
||||
* SQL `Driver`/migrations/query-generator. Instead it exposes a small, typed
|
||||
* collection API keyed by your models — reads are still coerced through
|
||||
* `model.parse`, so results match your schema.
|
||||
*
|
||||
* The `mongodb` driver is imported lazily (install it to use Mongo); the core
|
||||
* `@wrnexus/db` stays dependency-free.
|
||||
*
|
||||
* const db = await mongo(process.env.MONGO_URL!, "app");
|
||||
* const repo = db.collection(users);
|
||||
* await repo.insert({ email, name });
|
||||
* const active = await repo.find({ active: true });
|
||||
*/
|
||||
|
||||
import type { Model } from "../schema.ts";
|
||||
|
||||
// Minimal shape of the `mongodb` driver we rely on (typed locally so this file
|
||||
// compiles without `mongodb` installed).
|
||||
interface MongoCollection {
|
||||
find(filter: object, options?: object): { toArray(): Promise<Record<string, unknown>[]> };
|
||||
findOne(filter: object): Promise<Record<string, unknown> | null>;
|
||||
insertOne(doc: object): Promise<{ insertedId: unknown }>;
|
||||
insertMany(docs: object[]): Promise<{ insertedCount: number }>;
|
||||
updateMany(
|
||||
filter: object,
|
||||
update: object,
|
||||
): Promise<{ matchedCount: number; modifiedCount: number }>;
|
||||
deleteMany(filter: object): Promise<{ deletedCount: number }>;
|
||||
countDocuments(filter: object): Promise<number>;
|
||||
}
|
||||
interface MongoDatabase {
|
||||
collection(name: string): MongoCollection;
|
||||
}
|
||||
interface MongoClientLike {
|
||||
connect(): Promise<unknown>;
|
||||
db(name?: string): MongoDatabase;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface FindOptions {
|
||||
sort?: Record<string, 1 | -1>;
|
||||
limit?: number;
|
||||
skip?: number;
|
||||
}
|
||||
|
||||
export interface MongoRepo<T> {
|
||||
find(filter?: Record<string, unknown>, options?: FindOptions): Promise<T[]>;
|
||||
findOne(filter: Record<string, unknown>): Promise<T | null>;
|
||||
insert(doc: Partial<T>): Promise<{ id: unknown }>;
|
||||
insertMany(docs: Partial<T>[]): Promise<{ count: number }>;
|
||||
update(
|
||||
filter: Record<string, unknown>,
|
||||
patch: Partial<T>,
|
||||
): Promise<{ matched: number; modified: number }>;
|
||||
delete(filter: Record<string, unknown>): Promise<{ deleted: number }>;
|
||||
count(filter?: Record<string, unknown>): Promise<number>;
|
||||
}
|
||||
|
||||
export interface MongoDb {
|
||||
collection<T>(model: Model<T>): MongoRepo<T>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Map Mongo's `_id` to `id` so documents line up with model columns. */
|
||||
function normalize(doc: Record<string, unknown>): Record<string, unknown> {
|
||||
if (doc && doc._id != null && doc.id == null) {
|
||||
const { _id, ...rest } = doc;
|
||||
return { id: String(_id), ...rest };
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
/** Connect to MongoDB and return a model-aware collection API. */
|
||||
export async function mongo(url: string, dbName?: string): Promise<MongoDb> {
|
||||
// Non-literal specifier so this compiles without `mongodb` installed.
|
||||
const specifier: string = "mongodb";
|
||||
const mod = (await import(specifier)) as { MongoClient: new (u: string) => MongoClientLike };
|
||||
const client = new mod.MongoClient(url);
|
||||
await client.connect();
|
||||
const database = client.db(dbName);
|
||||
|
||||
return {
|
||||
collection<T>(model: Model<T>): MongoRepo<T> {
|
||||
const col = database.collection(model.name);
|
||||
return {
|
||||
async find(filter = {}, options = {}) {
|
||||
const docs = await col.find(filter, options).toArray();
|
||||
return docs.map((d) => model.parse(normalize(d)));
|
||||
},
|
||||
async findOne(filter) {
|
||||
const doc = await col.findOne(filter);
|
||||
return doc ? model.parse(normalize(doc)) : null;
|
||||
},
|
||||
async insert(doc) {
|
||||
const r = await col.insertOne(doc as object);
|
||||
return { id: r.insertedId };
|
||||
},
|
||||
async insertMany(docs) {
|
||||
const r = await col.insertMany(docs as object[]);
|
||||
return { count: r.insertedCount };
|
||||
},
|
||||
async update(filter, patch) {
|
||||
const r = await col.updateMany(filter, { $set: patch });
|
||||
return { matched: r.matchedCount, modified: r.modifiedCount };
|
||||
},
|
||||
async delete(filter) {
|
||||
const r = await col.deleteMany(filter);
|
||||
return { deleted: r.deletedCount };
|
||||
},
|
||||
count(filter = {}) {
|
||||
return col.countDocuments(filter);
|
||||
},
|
||||
};
|
||||
},
|
||||
close() {
|
||||
return client.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { mysql } from "./bunsql.ts";
|
||||
@@ -0,0 +1 @@
|
||||
export { postgres } from "./bunsql.ts";
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* SQLite adapter, built on Bun's zero-dependency `bun:sqlite`. Use a file URL
|
||||
* (`file:./dev.db`) or the default in-memory database (great for tests). This is
|
||||
* the reference adapter — it needs no external service to run.
|
||||
*/
|
||||
|
||||
import { Database } from "bun:sqlite";
|
||||
import type { Driver, ExecResult, Row, TxHandle } from "../driver.ts";
|
||||
|
||||
/** SQLite can only bind numbers/strings/bigints/null/blobs — coerce JS values. */
|
||||
function bind(params: unknown[]): unknown[] {
|
||||
return params.map((p) => {
|
||||
if (p === true) return 1;
|
||||
if (p === false) return 0;
|
||||
if (p === undefined) return null;
|
||||
if (p instanceof Date) return p.toISOString();
|
||||
return p;
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a SQLite driver. `url` may be `file:./x.db`, a path, or `:memory:`. */
|
||||
export function sqlite(url = ":memory:"): Driver {
|
||||
const path = url.replace(/^(file:|sqlite:)/, "") || ":memory:";
|
||||
const database = new Database(path);
|
||||
database.exec("PRAGMA foreign_keys = ON;");
|
||||
|
||||
const runner: TxHandle = {
|
||||
async query(sql, params = []): Promise<Row[]> {
|
||||
return database.query(sql).all(...(bind(params) as never[])) as Row[];
|
||||
},
|
||||
async exec(sql, params = []): Promise<ExecResult> {
|
||||
if (params.length === 0) {
|
||||
database.exec(sql); // DDL / multi-statement
|
||||
return { changes: 0 };
|
||||
}
|
||||
const result = database.query(sql).run(...(bind(params) as never[]));
|
||||
return { changes: result.changes, lastInsertId: Number(result.lastInsertRowid) };
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
dialect: "sqlite",
|
||||
query: runner.query,
|
||||
exec: runner.exec,
|
||||
async transaction(fn) {
|
||||
database.exec("BEGIN");
|
||||
try {
|
||||
const result = await fn(runner);
|
||||
database.exec("COMMIT");
|
||||
return result;
|
||||
} catch (err) {
|
||||
database.exec("ROLLBACK");
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
close() {
|
||||
database.close();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user