102 lines
2.7 KiB
TypeScript
102 lines
2.7 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, lastInsertRowid } = s.run(name, 1);
|
||
|
console.log(changes, lastInsertRowid);
|
||
|
if (changes === 1) {
|
||
|
this.node.exec(schema);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
prepare<Args extends unknown[] = [], Result = unknown>(
|
||
|
query: string,
|
||
|
): Stmt<Args, Result> {
|
||
|
return new Stmt(this.node.prepare(query));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 fs from "./fs.ts";
|
||
|
import * as path from "node:path";
|