UNPKG

neverchange

Version:

NeverChange is a database solution for web applications using SQLite WASM and OPFS.

313 lines (310 loc) 11.1 kB
var E = Object.defineProperty; var d = (l, t, e) => t in l ? E(l, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : l[t] = e; var u = (l, t, e) => d(l, typeof t != "symbol" ? t + "" : t, e); import { sqlite3Worker1Promiser as w } from "@sqlite.org/sqlite-wasm"; const g = { version: 0, up: async (l) => { await l.execute(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); } }, f = (l) => { const t = l.replace(/^\uFEFF/, ""); if (t.length === 0) return []; const e = []; let i = [], o = "", r = !1, n = !1; const s = () => { i.push(o), n && e.push(i), i = [], o = "", n = !1; }; for (let a = 0; a < t.length; a++) { const c = t[a], p = t[a + 1]; if (c === '"') { n = !0, r && p === '"' ? (o += '"', a++) : r ? r = !1 : o === "" ? r = !0 : o += c; continue; } if (!r && c === ",") { n = !0, i.push(o), o = ""; continue; } if (!r && (c === ` ` || c === "\r")) { c === "\r" && p === ` ` && a++, s(); continue; } n = !0, o += c; } if (r) throw new Error("CSV parsing error: unclosed quoted field"); return n && s(), e; }; let T = 0; class O { constructor(t, e = {}) { u(this, "dbPromise", null); u(this, "dbId", null); u(this, "migrations", []); u(this, "transactionDepth", 0); u(this, "savepointStack", []); this.dbName = t, this.options = e, this.options.debug = e.debug ?? !1, this.options.isMigrationActive = e.isMigrationActive ?? !0, this.options.isMigrationActive && this.addMigrations([g]); } log(...t) { this.options.debug && console.log(...t); } async init() { if (!this.dbPromise) try { this.dbPromise = this.initializeDatabase(), await this.dbPromise, this.options.isMigrationActive && (await this.createMigrationTable(), await this.runMigrations()); } catch (t) { throw console.error("Failed to initialize database:", t), t; } } async initializeDatabase() { this.log("Loading and initializing SQLite3 module..."); const t = await this.getPromiser(); this.log("Done initializing. Opening database..."); const e = await this.openDatabase(t); return this.dbId = e.result.dbId, this.log("Database initialized successfully"), t; } async getPromiser() { return new Promise( (t) => { w({ onready: (e) => t(e) }); } ); } async openDatabase(t) { try { const e = await t("open", { filename: `file:${this.dbName}.sqlite3?vfs=opfs` }); return this.log("OPFS database opened:", e.result.filename), e; } catch (e) { console.warn( "OPFS is not available, falling back to in-memory database:", e ); const i = await t("open", { filename: ":memory:" }); return this.log("In-memory database opened"), i; } } async execute(t, e = []) { try { return await (await this.getPromiserOrThrow())("exec", { sql: t, bind: e, dbId: this.dbId }); } catch (i) { throw console.error("Error executing SQL:", i), i; } } async query(t, e = []) { return (await (await this.getPromiserOrThrow())("exec", { sql: t, bind: e, rowMode: "object", dbId: this.dbId })).result.resultRows || []; } async close() { this.dbId && (await (await this.getPromiserOrThrow())("close", { dbId: this.dbId }), this.dbId = null, this.dbPromise = null); } addMigrations(t) { this.migrations.push(...t), this.migrations.sort((e, i) => e.version - i.version); } async createMigrationTable() { await this.execute(` CREATE TABLE IF NOT EXISTS migrations ( version INTEGER PRIMARY KEY, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); } async getCurrentVersion() { var i; return (await this.query( "SELECT name FROM sqlite_master WHERE type='table'" )).some((o) => o.name === "migrations") && ((i = (await this.query( "SELECT MAX(version) as version FROM migrations" ))[0]) == null ? void 0 : i.version) || 0; } async runMigrations() { const t = await this.getCurrentVersion(), e = this.migrations.filter( (i) => i.version > t ); for (const i of e) this.log(`Running migration to version ${i.version}`), await i.up(this), await this.execute("INSERT INTO migrations (version) VALUES (?)", [ i.version ]), this.log(`Migration to version ${i.version} completed`); } async getPromiserOrThrow() { if (!this.dbPromise) throw new Error("Database not initialized. Call init() first."); return this.dbPromise; } escapeBlob(t) { return `X'${Array.from(new Uint8Array(t), (e) => e.toString(16).padStart(2, "0")).join("")}'`; } async dumpDatabase(t = {}) { const { compatibilityMode: e = !1, table: i } = t; let o = ""; e && (o += `PRAGMA foreign_keys = OFF; BEGIN TRANSACTION; `); const r = i ? "SELECT type, name, sql FROM sqlite_master WHERE type='table' AND name = ?" : "SELECT type, name, sql FROM sqlite_master WHERE sql NOT NULL AND name != 'sqlite_sequence'", n = await this.query(r, i ? [i] : []); for (const s of n) if (s.type === "table") { o += `${s.sql}; `; const a = await this.query(`SELECT * FROM ${s.name}`); for (const c of a) { const p = Object.keys(c).join(", "), m = Object.values(c).map((h) => h instanceof Uint8Array ? this.escapeBlob(h) : h === null ? "NULL" : typeof h == "string" ? `'${h.replace(/'/g, "''")}'` : h).join(", "); o += `INSERT INTO ${s.name} (${p}) VALUES (${m}); `; } o += ` `; } if (!i) try { const s = await this.query( "SELECT * FROM sqlite_sequence" ); if (s.length > 0) { o += `DELETE FROM sqlite_sequence; `; for (const a of s) o += `INSERT INTO sqlite_sequence VALUES('${a.name}', ${a.seq}); `; o += ` `; } } catch { this.log("sqlite_sequence table does not exist, skipping..."); } if (!i) for (const s of n) s.type !== "table" && (o += `${s.sql}; `); return e && (o += `COMMIT; `), o; } async importDump(t, e = {}) { const { compatibilityMode: i = !1 } = e, o = t.split(";").map((r) => r.trim()).filter(Boolean); i || (await this.execute("PRAGMA foreign_keys=OFF"), await this.execute("BEGIN TRANSACTION")); try { const r = await this.query(` SELECT type, name FROM sqlite_master WHERE type IN ('table', 'view', 'index') AND name != 'sqlite_sequence' `); for (const { type: n, name: s } of r) await this.execute(`DROP ${n} IF EXISTS ${s}`); for (const n of o) n !== "COMMIT" && await this.execute(n); i || (await this.execute("COMMIT"), await this.execute("PRAGMA foreign_keys = ON")); } catch (r) { throw i || (await this.execute("ROLLBACK"), await this.execute("PRAGMA foreign_keys = ON")), r; } } async dumpTableToCSV(t, e = {}) { const i = await this.query(`SELECT * FROM ${t}`), o = Object.keys( i[0] || (await this.query(`PRAGMA table_info(${t})`)).reduce( (s, a) => ({ ...s, [a.name]: "" }), {} ) ).map((s) => e.quoteAllFields ? `"${s}"` : s).join(","); if (i.length === 0) return `${o}\r `; const r = (s) => { const a = (s == null ? void 0 : s.toString()) || ""; return e.quoteAllFields || a.includes(",") || a.includes(` `) || a.includes('"') ? `"${a.replace(/"/g, '""')}"` : a; }, n = i.map( (s) => Object.values(s).map(r).join(",") ); return `${o}\r ${n.join(`\r `)}\r `; } async importCSVToTable(t, e) { const i = f(e); if (i.length === 0) return; const [o, ...r] = i, n = o; for (let s = 0; s < r.length; s++) { const a = r[s]; if (a.length !== n.length) throw new Error( `CSV row ${s + 2} has ${a.length} fields, but header has ${n.length} columns` ); const c = n.map(() => "?").join(","); await this.execute( `INSERT INTO ${t} (${n.join(",")}) VALUES (${c})`, a ); } } /** * execute a transaction. * - if it's top-level, it will be a top-level transaction. * - if it's nested, it will be a nested transaction. */ async transaction(t) { if (this.transactionDepth === 0) { await this.execute("BEGIN TRANSACTION"), this.transactionDepth = 1, this.log("BEGIN TRANSACTION (top-level)"); try { const e = await t(this); return await this.execute("COMMIT"), this.log("COMMIT (top-level)"), this.transactionDepth = 0, e; } catch (e) { throw await this.execute("ROLLBACK"), this.log("ROLLBACK (top-level)"), this.transactionDepth = 0, e; } } else { this.transactionDepth++; const e = `sp_${T++}`; this.savepointStack.push(e), this.log(`BEGIN NESTED TRANSACTION: SAVEPOINT ${e}`), await this.execute(`SAVEPOINT ${e}`); try { const i = await t(this); return this.log(`RELEASE SAVEPOINT ${e}`), await this.execute(`RELEASE SAVEPOINT ${e}`), this.savepointStack.pop(), this.transactionDepth--, i; } catch (i) { throw this.log(`ROLLBACK TO ${e}`), await this.execute(`ROLLBACK TO ${e}`), this.savepointStack.pop(), this.transactionDepth--, i; } } } /** * explicitly rollback the current transaction. * - if it's top-level, it will rollback all changes. * - if it's nested, it will rollback to the last created savepoint. */ async rollback() { if (this.transactionDepth <= 0) throw new Error("rollback() called but no active transaction exists."); if (this.transactionDepth === 1) throw this.log("ROLLBACK (top-level)"), await this.execute("ROLLBACK"), this.transactionDepth = 0, new Error("Transaction rolled back (top-level)."); { const t = this.savepointStack.pop(); throw this.transactionDepth--, t ? (this.log(`ROLLBACK TO ${t} (nested)`), await this.execute(`ROLLBACK TO ${t}`), new Error(`Transaction rolled back to savepoint ${t}.`)) : new Error("rollback() called but no matching savepoint found."); } } /** * if you want to commit explicitly, you can call this method. * (but it will be committed automatically when the transaction(cb) ends.) */ async commit() { if (this.transactionDepth <= 0) throw new Error("commit() called but no active transaction exists."); if (this.transactionDepth === 1) await this.execute("COMMIT"), this.log("COMMIT (top-level)"), this.transactionDepth = 0; else { const t = this.savepointStack.pop(); if (this.transactionDepth--, !t) throw new Error("commit() called but no matching savepoint found."); this.log(`RELEASE SAVEPOINT ${t} (nested by user)`), await this.execute(`RELEASE SAVEPOINT ${t}`); } } } export { O as NeverChangeDB };