This commit is contained in:
2026-09-23 18:46:15 +02:00
parent e010811d3a
commit aa1d60b19e
8 changed files with 95 additions and 166 deletions

View File

@@ -2,35 +2,40 @@
vibecoded :sob:
*/
import { Database } from "bun:sqlite";
import { Pool } from "pg";
export class KVDB {
/**
* @param {string} [filename="db.sqlite"] - The SQLite database file (e.g., "db.sqlite" or ":memory:")
* @param {Pool | object} poolOrConfig - A pg.Pool instance or pg connection config object
* @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;");
constructor(poolOrConfig, table = "main_kv") {
this.pool = poolOrConfig instanceof Pool ? poolOrConfig : new Pool(poolOrConfig);
// Sanitize table name to prevent SQL injection in DDL
this.tableName = table.replace(/[^a-zA-Z0-9_]/g, "");
}
this.db.run(`
/**
* Initialize the database table. Call this once after instantiating the class.
* @returns {Promise<void>}
*/
async init() {
await this.pool.query(`
CREATE TABLE IF NOT EXISTS "${this.tableName}" (
key TEXT PRIMARY KEY,
value TEXT
value JSONB
)
`);
}
/**
* Switch database table/context without recreating database connections
* Switch database table/context sharing the same connection pool
* @param {string} newTableName
* @returns {KVDB}
*/
table(newTableName) {
return new KVDB(this.db.filename, newTableName);
return new KVDB(this.pool, newTableName);
}
/**
@@ -45,37 +50,35 @@ export class KVDB {
async set(keyPath, value) {
const parts = keyPath.split(".");
const rootKey = parts[0];
const jsonValue = JSON.stringify(value);
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)]
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb)
ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
[rootKey, jsonValue]
);
return;
}
const jsonPath = `$.${parts.slice(1).join(".")}`;
const pathArr = parts.slice(1);
this.db.run(
`INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}')
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb)
ON CONFLICT(key) DO NOTHING`,
[rootKey]
);
this.db.run(
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = json_set(value, ?, json(?))
WHERE key = ?`,
[jsonPath, JSON.stringify(value), rootKey]
SET value = jsonb_set(value, $1::text[], $2::jsonb, true)
WHERE key = $3`,
[pathArr, jsonValue, 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<any>}
@@ -86,29 +89,19 @@ export class KVDB {
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;
const { rows } = await this.pool.query(
`SELECT value FROM "${this.tableName}" WHERE key = $1`,
[rootKey]
);
// pg parses JSONB automatically
data = rows.length > 0 ? rows[0].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;
}
}
const pathArr = parts.slice(1);
const { rows } = await this.pool.query(
`SELECT value #> $1::text[] AS result FROM "${this.tableName}" WHERE key = $2`,
[pathArr, rootKey]
);
data = (rows.length > 0 && rows[0].result !== undefined) ? rows[0].result : null;
}
if (typeof predicate === "function") {
@@ -121,10 +114,6 @@ export class KVDB {
/**
* 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<boolean>}
@@ -150,27 +139,25 @@ export class KVDB {
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 { rows } = await this.pool.query(
`SELECT 1 FROM "${this.tableName}" WHERE key = $1`,
[rootKey]
);
return rows.length > 0;
}
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);
const pathArr = parts.slice(1);
const { rows } = await this.pool.query(
`SELECT 1 FROM "${this.tableName}"
WHERE key = $1 AND value #> $2::text[] IS NOT NULL`,
[rootKey, pathArr]
);
return Boolean(row && row.type !== null);
return rows.length > 0;
}
/**
* 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<void>}
@@ -198,16 +185,16 @@ export class KVDB {
const rootKey = parts[0];
if (parts.length === 1) {
this.db.run(`DELETE FROM "${this.tableName}" WHERE key = ?`, [rootKey]);
await this.pool.query(`DELETE FROM "${this.tableName}" WHERE key = $1`, [rootKey]);
return;
}
const jsonPath = `$.${parts.slice(1).join(".")}`;
this.db.run(
const pathArr = parts.slice(1);
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = json_remove(value, ?)
WHERE key = ?`,
[jsonPath, rootKey]
SET value = value #- $1::text[]
WHERE key = $2`,
[pathArr, rootKey]
);
}
@@ -220,35 +207,40 @@ export class KVDB {
async push(keyPath, value) {
const parts = keyPath.split(".");
const rootKey = parts[0];
const jsonPath = parts.length === 1 ? "$" : `$.${parts.slice(1).join(".")}`;
const wrappedValue = JSON.stringify([value]);
if (parts.length === 1) {
this.db.run(
`INSERT INTO "${this.tableName}" (key, value) VALUES (?, '[]')
ON CONFLICT(key) DO NOTHING`,
[rootKey]
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, $2::jsonb)
ON CONFLICT(key) DO UPDATE
SET value = CASE
WHEN jsonb_typeof("${this.tableName}".value) = 'array'
THEN "${this.tableName}".value || $2::jsonb
ELSE $2::jsonb
END`,
[rootKey, wrappedValue]
);
} else {
this.db.run(
`INSERT INTO "${this.tableName}" (key, value) VALUES (?, '{}')
const pathArr = parts.slice(1);
await this.pool.query(
`INSERT INTO "${this.tableName}" (key, value) VALUES ($1, '{}'::jsonb)
ON CONFLICT(key) DO NOTHING`,
[rootKey]
);
this.db.run(
await this.pool.query(
`UPDATE "${this.tableName}"
SET value = json_set(value, ?, json('[]'))
WHERE key = ? AND json_type(value, ?) IS NULL`,
[jsonPath, rootKey, jsonPath]
SET value = jsonb_set(
value,
$1::text[],
COALESCE(value #> $1::text[], '[]'::jsonb) || $2::jsonb,
true
)
WHERE key = $3`,
[pathArr, wrappedValue, rootKey]
);
}
const appendPath = `${jsonPath}[#]`;
this.db.run(
`UPDATE "${this.tableName}"
SET value = json_insert(value, ?, json(?))
WHERE key = ?`,
[appendPath, JSON.stringify(value), rootKey]
);
}
/**
@@ -290,10 +282,6 @@ export class KVDB {
/**
* 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<boolean>} True if target key existed and was renamed
@@ -316,8 +304,8 @@ export class KVDB {
const newRoot = newParts[0];
if (oldParts.length === 1 && newParts.length === 1) {
this.db.run(
`UPDATE "${this.tableName}" SET key = ? WHERE key = ?`,
await this.pool.query(
`UPDATE "${this.tableName}" SET key = $1 WHERE key = $2`,
[newRoot, oldRoot]
);
} else {