// 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 | 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( query: string, ): Stmt { return new Stmt(this.node.prepare(query)); } } export class Stmt { #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 { 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(Class: { new (): R }): Stmt { 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";