104 lines
2.8 KiB
TypeScript
104 lines
2.8 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> {
|
|
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;
|
|
constructor(node: StatementSync) {
|
|
this.#node = node;
|
|
}
|
|
|
|
/** Get one row */
|
|
get(...args: Args): Row | null {
|
|
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 new Error("Query returned no result");
|
|
return item;
|
|
}
|
|
iter(...args: Args): Iterator<Row> {
|
|
return this.array(...args)[Symbol.iterator]();
|
|
}
|
|
/** Get all rows */
|
|
array(...args: Args): Row[] {
|
|
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.#node.run(...args as any);
|
|
}
|
|
|
|
as<R>(Class: { new (): R }): Stmt<Args, R> {
|
|
this.#class = Class;
|
|
return this as any;
|
|
}
|
|
}
|
|
|
|
import { DatabaseSync, StatementSync } from "node:sqlite";
|
|
import * as path from "node:path";
|