/* vibecoded :sob: */ import { Database } from "bun:sqlite"; export class KVDB { /** * @param {string} [filename="db.sqlite"] - The SQLite database file (e.g., "db.sqlite" or ":memory:") * @param {string} [table="main_kv"] - The table/context name (defaults to "main_kv") */ constructor(filename = "db.sqlite", table = "main_kv") { this.db = new Database(filename); this.db.run("PRAGMA journal_mode = WAL;"); // Sanitize table name to prevent SQL injection in DDL this.tableName = table.replace(/[^a-zA-Z0-9_]/g, ""); this.db.run(` CREATE TABLE IF NOT EXISTS "${this.tableName}" ( key TEXT PRIMARY KEY, value TEXT ) `); } /** * Switch database table/context without recreating database connections * @param {string} newTableName * @returns {KVDB} */ table(newTableName) { return new KVDB(this.db.filename, newTableName); } /** * Set a key or nested dot-notation property. * Examples: * await db.set("user", { name: "Alex" }) * await db.set("user.age", 30) * @param {string} keyPath * @param {any} value * @returns {Promise} */ async set(keyPath, value) { const parts = keyPath.split("."); const rootKey = parts[0]; if (parts.length === 1) { this.db.run( `INSERT INTO "${this.tableName}" (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [rootKey, JSON.stringify(value)] ); return; } const jsonPath = `$.${parts.slice(1).join(".")}`; this.db.run( `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}') ON CONFLICT(key) DO NOTHING`, [rootKey] ); this.db.run( `UPDATE "${this.tableName}" SET value = json_set(value, ?, json(?)) WHERE key = ?`, [jsonPath, JSON.stringify(value), rootKey] ); } /** * Get a key/nested property, OR find an element inside an array using a predicate function. * Examples: * await db.get("user.roles") -> returns whole array * await db.get("user.roles", (r) => r.id === "admin") -> returns matching role object * @param {string} keyPath * @param {Function} [predicate] - Optional matching function for array items * @returns {Promise} */ async get(keyPath, predicate) { const parts = keyPath.split("."); const rootKey = parts[0]; let data = null; if (parts.length === 1) { const row = this.db .query(`SELECT value FROM "${this.tableName}" WHERE key = ?`) .get(rootKey); data = row ? JSON.parse(row.value) : null; } else { const jsonPath = `$.${parts.slice(1).join(".")}`; const row = this.db .query( `SELECT json_extract(value, ?) AS result FROM "${this.tableName}" WHERE key = ?` ) .get(jsonPath, rootKey); if (row && row.result !== null) { if (typeof row.result === "string") { try { data = JSON.parse(row.result); } catch { data = row.result; } } else { data = row.result; } } } if (typeof predicate === "function") { if (!Array.isArray(data)) return null; return data.find(predicate) ?? null; } return data; } /** * Check if a key/property exists, OR check if an array contains a specific item / predicate match. * Examples: * await db.has("user.roles") -> true/false * await db.has("user.roles", (r) => r.id === "admin") -> true/false * await db.has("user.roles", "admin_role_id") -> true/false * @param {string} keyPath * @param {any | Function} [itemOrPredicate] * @returns {Promise} */ async has(keyPath, itemOrPredicate) { if (itemOrPredicate !== undefined) { const arr = await this.get(keyPath); if (!Array.isArray(arr)) return false; if (typeof itemOrPredicate === "function") { return arr.some(itemOrPredicate); } if (typeof itemOrPredicate === "object" && itemOrPredicate !== null) { const targetString = JSON.stringify(itemOrPredicate); return arr.some((el) => JSON.stringify(el) === targetString); } return arr.includes(itemOrPredicate); } const parts = keyPath.split("."); const rootKey = parts[0]; if (parts.length === 1) { const row = this.db .query(`SELECT 1 FROM "${this.tableName}" WHERE key = ?`) .get(rootKey); return Boolean(row); } const jsonPath = `$.${parts.slice(1).join(".")}`; const row = this.db .query( `SELECT json_type(value, ?) AS type FROM "${this.tableName}" WHERE key = ?` ) .get(jsonPath, rootKey); return Boolean(row && row.type !== null); } /** * Delete a key, property, OR remove item(s) from an array using a predicate or value. * Examples: * await db.delete("user.roles") -> deletes the roles key/property entirely * await db.delete("user.roles", (r) => r.id === "admin") -> pulls matching item from array * @param {string} keyPath * @param {any | Function} [itemOrPredicate] * @returns {Promise} */ async delete(keyPath, itemOrPredicate) { if (itemOrPredicate !== undefined) { const arr = await this.get(keyPath); if (!Array.isArray(arr)) return; let filtered; if (typeof itemOrPredicate === "function") { filtered = arr.filter((item) => !itemOrPredicate(item)); } else if (typeof itemOrPredicate === "object" && itemOrPredicate !== null) { const targetString = JSON.stringify(itemOrPredicate); filtered = arr.filter((item) => JSON.stringify(item) !== targetString); } else { filtered = arr.filter((item) => item !== itemOrPredicate); } await this.set(keyPath, filtered); return; } const parts = keyPath.split("."); const rootKey = parts[0]; if (parts.length === 1) { this.db.run(`DELETE FROM "${this.tableName}" WHERE key = ?`, [rootKey]); return; } const jsonPath = `$.${parts.slice(1).join(".")}`; this.db.run( `UPDATE "${this.tableName}" SET value = json_remove(value, ?) WHERE key = ?`, [jsonPath, rootKey] ); } /** * Appends an element to an array at the target key or nested path. * @param {string} keyPath * @param {any} value * @returns {Promise} */ async push(keyPath, value) { const parts = keyPath.split("."); const rootKey = parts[0]; const jsonPath = parts.length === 1 ? "$" : `$.${parts.slice(1).join(".")}`; if (parts.length === 1) { this.db.run( `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '[]') ON CONFLICT(key) DO NOTHING`, [rootKey] ); } else { this.db.run( `INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}') ON CONFLICT(key) DO NOTHING`, [rootKey] ); this.db.run( `UPDATE "${this.tableName}" SET value = json_set(value, ?, json('[]')) WHERE key = ? AND json_type(value, ?) IS NULL`, [jsonPath, rootKey, jsonPath] ); } const appendPath = `${jsonPath}[#]`; this.db.run( `UPDATE "${this.tableName}" SET value = json_insert(value, ?, json(?)) WHERE key = ?`, [appendPath, JSON.stringify(value), rootKey] ); } /** * Updates matching item(s) in an array using a predicate and an update callback/object. * @param {string} keyPath * @param {Function} predicate * @param {Object | Function} updateFnOrObject * @returns {Promise} */ async update(keyPath, predicate, updateFnOrObject) { const arr = await this.get(keyPath); if (!Array.isArray(arr)) return false; let updatedAny = false; const newArr = arr.map((item) => { if (!predicate(item)) return item; updatedAny = true; if (typeof updateFnOrObject === "function") { return updateFnOrObject(item); } if (typeof item === "object" && item !== null && typeof updateFnOrObject === "object") { return { ...item, ...updateFnOrObject }; } return updateFnOrObject; }); if (updatedAny) { await this.set(keyPath, newArr); } return updatedAny; } /** * Renames a key or nested property path without changing its value. * Examples: * await db.rename("user.roles", "user.rolesOld") * await db.rename("user.roles", "rolesOld") * await db.rename("user", "userOld") * @param {string} oldKeyPath * @param {string} newKeyPath * @returns {Promise} True if target key existed and was renamed */ async rename(oldKeyPath, newKeyPath) { if (!(await this.has(oldKeyPath))) return false; const oldParts = oldKeyPath.split("."); let targetNewPath = newKeyPath; if (oldParts.length > 1 && !newKeyPath.startsWith(oldParts.slice(0, -1).join(".") + ".")) { const parentPath = oldParts.slice(0, -1).join("."); targetNewPath = `${parentPath}.${newKeyPath}`; } const value = await this.get(oldKeyPath); const oldRoot = oldParts[0]; const newParts = targetNewPath.split("."); const newRoot = newParts[0]; if (oldParts.length === 1 && newParts.length === 1) { this.db.run( `UPDATE "${this.tableName}" SET key = ? WHERE key = ?`, [newRoot, oldRoot] ); } else { await this.set(targetNewPath, value); await this.delete(oldKeyPath); } return true; } }