Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/nperez0111/bookhive. Track your books, share your shelves, see what others are reading bookhive.buzz
atproto bluesky books bookshelf goodreads management-system
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190import { wrapBunSqliteForKysely } from "./bun-sqlite-kysely.js";import { defineDriver } from "unstorage";import { Kysely, SqliteDialect } from "kysely";import { Database as DatabaseSync } from "bun:sqlite";
interface TableSchema { [k: string]: { id: string; value: string; created_at: string; updated_at: string; };}
export type KvDb = Kysely<TableSchema>;
/** * Create a single shared KvDb connection for a SQLite file. * Pass the result to multiple sqliteKv drivers via the `db` option to share * a single connection to the same file. */export function createSharedKvDb(location: string): KvDb { const sqlite = new DatabaseSync(location); sqlite.exec("PRAGMA busy_timeout = 5000"); sqlite.exec("PRAGMA journal_mode = WAL"); return new Kysely<TableSchema>({ dialect: new SqliteDialect({ database: wrapBunSqliteForKysely(sqlite), }), });}
const DRIVER_NAME = "sqlite";
export default defineDriver< { location?: string; table: string; /** Pre-created shared KvDb connection. Use this when multiple drivers share the same SQLite file. */ db?: KvDb; }, KvDb>(({ location, table = "kv", db: sharedDb }) => { // _db is a per-driver-instance singleton — must live outside any function body // so it is not re-initialized on every getDb() call. let _db: KvDb | null = sharedDb ?? null; let _tableCreated = false;
function getDb(): KvDb { if (!_db) { if (!location) { throw new Error("SQLite location is required"); } const sqlite = new DatabaseSync(location); sqlite.exec("PRAGMA busy_timeout = 5000"); sqlite.exec("PRAGMA journal_mode = WAL"); _db = new Kysely<TableSchema>({ dialect: new SqliteDialect({ database: wrapBunSqliteForKysely(sqlite), }), }); }
if (!_tableCreated) { _tableCreated = true; // Create table if not exists (idempotent — safe to run on every cold start) void _db.schema .createTable(table) .ifNotExists() .addColumn("id", "text", (col) => col.primaryKey()) .addColumn("value", "text", (col) => col.notNull()) .addColumn("created_at", "text", (col) => col.notNull()) .addColumn("updated_at", "text", (col) => col.notNull()) .execute(); }
return _db; }
return { name: DRIVER_NAME, options: { ...(location !== undefined ? { location } : {}), table }, getInstance: getDb,
async hasItem(key) { const result = await getDb() .selectFrom(table) .select(["id"]) .where("id", "=", key) .executeTakeFirst(); return !!result; },
async getItem(key) { const result = await getDb() .selectFrom(table) .select(["value"]) .where("id", "=", key) .executeTakeFirst(); return result?.value ?? null; },
async setItem(key: string, value: string) { const now = new Date().toISOString(); await getDb() .insertInto(table) .values({ id: key, value, created_at: now, updated_at: now, }) .onConflict((oc) => oc.column("id").doUpdateSet((c) => ({ value: c.ref("excluded.value"), updated_at: c.ref("excluded.updated_at"), })), ) .execute(); },
async setItems(items) { const now = new Date().toISOString();
await getDb() .transaction() .execute(async (trx) => { await Promise.all( items.map(({ key, value }) => { return trx .insertInto(table) .values({ id: key, value, created_at: now, updated_at: now, }) .onConflict((oc) => oc.column("id").doUpdateSet({ value, updated_at: now, }), ) .execute(); }), ); }); },
async removeItem(key: string) { await getDb().deleteFrom(table).where("id", "=", key).execute(); },
async getMeta(key: string) { const result = await getDb() .selectFrom(table) .select(["created_at", "updated_at"]) .where("id", "=", key) .executeTakeFirst(); if (!result) { return null; } const mtime = new Date(result.updated_at); return { birthtime: new Date(result.created_at), mtime, /** Used by readThroughCache for TTL; setItem updates updated_at. */ timestamp: mtime.getTime(), }; },
async getKeys(base = "") { const results = await getDb() .selectFrom(table) .select(["id"]) .where("id", "like", `${base}%`) .execute(); return results.map((r) => r.id); },
async clear() { await getDb().deleteFrom(table).execute(); },
async dispose() { await getDb().destroy(); }, };});