135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
// Guard against reloads and bundler duplication.
|
|
// @ts-ignore
|
|
const map = globalThis[Symbol.for("clover.db")] ??= new Map<
|
|
string,
|
|
WrappedDatabase
|
|
>();
|
|
|
|
export function getDb(file: string) {
|
|
let db: WrappedDatabase | null = map.get(file);
|
|
if (db) return db;
|
|
const fileWithExt = file.includes(".") ? file : file + ".sqlite";
|
|
db = new WrappedDatabase(
|
|
new DatabaseSync(path.join(".clover/", fileWithExt)),
|
|
);
|
|
map.set(file, db);
|
|
return db;
|
|
}
|
|
|
|
export class WrappedDatabase {
|
|
node: DatabaseSync;
|
|
stmtTableMigrate: WeakRef<StatementSync> | null = null;
|
|
|
|
constructor(node: DatabaseSync) {
|
|
this.node = node;
|
|
this.node.exec(`
|
|
create table if not exists clover_migrations (
|
|
key text not null primary key,
|
|
version integer not null
|
|
);
|
|
`);
|
|
}
|
|
|
|
// TODO: add migration support
|
|
// the idea is you keep `schema` as the new schema but can add
|
|
// migrations to the mix really easily.
|
|
table(name: string, schema: string) {
|
|
let s = this.stmtTableMigrate?.deref();
|
|
s ?? (this.stmtTableMigrate = new WeakRef(
|
|
s = this.node.prepare(`
|
|
insert or ignore into clover_migrations
|
|
(key, version) values (?, ?);
|
|
`),
|
|
));
|
|
const { changes } = s.run(name, 1);
|
|
if (changes === 1) this.node.exec(schema);
|
|
}
|
|
|
|
prepare<Args extends unknown[] = [], Result = unknown>(
|
|
query: string,
|
|
): Stmt<Args, Result> {
|
|
query = query.trim();
|
|
const lines = query.split("\n");
|
|
const trim = Math.min(
|
|
...lines.map((line) =>
|
|
line.trim().length === 0 ? Infinity : line.match(/^\s*/)![0].length
|
|
),
|
|
);
|
|
query = lines.map((x) => x.slice(trim)).join("\n");
|
|
|
|
let prepared;
|
|
try {
|
|
prepared = this.node.prepare(query);
|
|
} catch (err) {
|
|
if (err) (err as { query: string }).query = query;
|
|
throw err;
|
|
}
|
|
return new Stmt(prepared);
|
|
}
|
|
}
|
|
|
|
export class Stmt<Args extends unknown[] = unknown[], Row = unknown> {
|
|
#node: StatementSync;
|
|
#class: any | null = null;
|
|
query: string;
|
|
|
|
constructor(node: StatementSync) {
|
|
this.#node = node;
|
|
this.query = node.sourceSQL;
|
|
}
|
|
|
|
/** Get one row */
|
|
get(...args: Args): Row | null {
|
|
return this.#wrap(args, () => {
|
|
const item = this.#node.get(...args as any) as Row;
|
|
if (!item) return null;
|
|
const C = this.#class;
|
|
if (C) Object.setPrototypeOf(item, C.prototype);
|
|
return item;
|
|
});
|
|
}
|
|
getNonNull(...args: Args) {
|
|
const item = this.get(...args);
|
|
if (!item) {
|
|
throw this.#wrap(args, () => new Error("Query returned no result"));
|
|
}
|
|
return item;
|
|
}
|
|
iter(...args: Args): IterableIterator<Row> {
|
|
return this.#wrap(args, () => this.array(...args)[Symbol.iterator]());
|
|
}
|
|
/** Get all rows */
|
|
array(...args: Args): Row[] {
|
|
return this.#wrap(args, () => {
|
|
const array = this.#node.all(...args as any) as Row[];
|
|
const C = this.#class;
|
|
if (C) array.forEach((item) => Object.setPrototypeOf(item, C.prototype));
|
|
return array;
|
|
});
|
|
}
|
|
/** Return the number of changes / row ID */
|
|
run(...args: Args) {
|
|
return this.#wrap(args, () => this.#node.run(...args as any));
|
|
}
|
|
|
|
as<R>(Class: { new (): R }): Stmt<Args, R> {
|
|
this.#class = Class;
|
|
return this as any;
|
|
}
|
|
|
|
#wrap<T>(args: unknown[], fn: () => T) {
|
|
try {
|
|
return fn();
|
|
} catch (err: any) {
|
|
if (err && typeof err === "object") {
|
|
err.query = this.query;
|
|
args = args.flat(Infinity);
|
|
err.queryArgs = args.length === 1 ? args[0] : args;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
import { DatabaseSync, StatementSync } from "node:sqlite";
|
|
import * as path from "node:path";
|