UNPKG

sqlitekv

Version:

SQLiteKV is a key-value store built on top of SQLite3. This project allows you to perform basic key-value operations with the additional power of SQLite3, making it suitable for applications requiring persistent, lightweight, and reliable storage.

265 lines (264 loc) 8.57 kB
import sqlite3 from "sqlite3"; import { open } from "sqlite"; import fs from "fs/promises"; import path from "path"; export class SQLiteDatabase { dbFilename; db = null; tableName; autoCommit; inTransaction = false; dbPath; journalMode; sqliteMode; logQueries; constructor(dbFilename = "database.sqlite", tableName = "kv_store", autoCommit = true, journalMode = "WAL", sqliteMode = "disk", logQueries = false) { this.dbFilename = dbFilename; this.tableName = tableName; this.autoCommit = autoCommit; this.journalMode = journalMode; this.sqliteMode = sqliteMode; this.dbPath = this.getDbPathByMode(dbFilename, sqliteMode); this.logQueries = logQueries; } getDbPathByMode(dbFilename, mode) { switch (mode) { case "memory": return ":memory:"; case "temp": return path.join(process.cwd(), "temp.sqlite"); case "disk": default: return path.join(process.cwd(), dbFilename); } } logQuery(query, params) { if (this.logQueries) { const formattedParams = params.map((param) => JSON.stringify(param)); let formattedQuery = query; formattedParams.forEach((param) => { formattedQuery = formattedQuery.replace("?", param); }); console.log(`Executing Query: ${formattedQuery}`); } } async init() { if (this.db) { return; } if (this.sqliteMode === "disk") { await this.ensureDirectoryExists(path.dirname(this.dbPath)); } this.db = await open({ filename: this.dbPath, driver: sqlite3.Database }); await this.createTable(); await this.setJournalMode(this.journalMode); } async createTable() { if (!this.db) { throw new Error("Database not initialized"); } const query = `CREATE TABLE IF NOT EXISTS ${this.tableName} ( key TEXT PRIMARY KEY, value TEXT, expiry INTEGER, one_time INTEGER DEFAULT 0 )`; await this.db.run(query); } async setJournalMode(mode) { if (!this.db) { throw new Error("Database not initialized"); } const query = `PRAGMA journal_mode = ${mode.toUpperCase()};`; await this.db.run(query); const result = await this.db.get("PRAGMA journal_mode;"); } getDbPath() { return this.dbPath; } getDbFilename() { return this.dbFilename; } getTableName() { return this.tableName; } async getJournalMode() { if (!this.db) { throw new Error("Database not initialized"); } const query = "PRAGMA journal_mode;"; const result = await this.db.get(query); return result?.["journal_mode"]; } async set(key, value, oneTime = false) { return this.setWithExpiry(key, value, null, oneTime); } async setex(key, seconds, value, oneTime = false) { const expiry = Date.now() + seconds * 1000; return this.setWithExpiry(key, value, expiry, oneTime); } async setWithExpiry(key, value, expiry, oneTime) { if (!this.db) { throw new Error("Database not initialized"); } const jsonValue = JSON.stringify(value); const query = `INSERT OR REPLACE INTO ${this.tableName} (key, value, expiry, one_time) VALUES (?, ?, ?, ?)`; const params = [key, jsonValue, expiry, oneTime ? 1 : 0]; this.logQuery(query, params); await this.db.run(query, params); if (this.autoCommit && !this.inTransaction) { await this.commitTransaction(); } return true; } async get(key) { if (!this.db) { return null; } const query = `SELECT value, expiry, one_time FROM ${this.tableName} WHERE key = ?`; const params = [key]; this.logQuery(query, params); const result = await this.db.get(query, params); if (!result || !result.value) { return null; } if (result.expiry && typeof result.expiry === "number" && result.expiry < Date.now()) { await this.delete(key); return null; } if (result.one_time && result.one_time === 1) { await this.delete(key); } return JSON.parse(result.value); } async delete(key) { if (!this.db) { throw new Error("Database not initialized"); } const query = `DELETE FROM ${this.tableName} WHERE key = ?`; const params = [key]; this.logQuery(query, params); const result = await this.db.run(query, params); if (this.autoCommit && !this.inTransaction) { await this.commitTransaction(); } return result && result.changes !== undefined ? result.changes > 0 : false; } async exists(key) { if (!this.db) { throw new Error("Database not initialized"); } const query = `SELECT 1 FROM ${this.tableName} WHERE key = ?`; const params = [key]; this.logQuery(query, params); const result = await this.db.get(query, params); return result !== undefined; } async keys(pattern) { if (!this.db) { throw new Error("Database not initialized"); } let query = `SELECT key FROM ${this.tableName}`; const params = []; if (pattern) { query += ` WHERE key LIKE ?`; params.push(pattern.replace(/%/g, "\\%").replace(/_/g, "\\_")); } const result = await this.db.all(query, params); return result.map((row) => row.key); } async convertToJson(jsonFilePath) { if (!this.db) { throw new Error("Database not initialized"); } const query = `SELECT key, value FROM ${this.tableName}`; const result = await this.db.all(query); const jsonObject = {}; result.forEach((row) => { const key = row.key; const value = JSON.parse(row.value); jsonObject[key] = value; }); const jsonString = JSON.stringify(jsonObject, null, 2); const filePath = jsonFilePath || path.join(process.cwd(), "database_export.json"); try { await fs.writeFile(filePath, jsonString); return true; } catch (error) { return false; } } async close() { if (!this.db) { throw new Error("Database not initialized"); } this.logQuery("CLOSE DATABASE", []); await this.db.close(); this.db = null; } async ensureDirectoryExists(dirPath) { try { await fs.mkdir(dirPath, { recursive: true }); } catch (err) { if (err.code !== "EEXIST") { throw err; } } } async beginTransaction() { if (!this.db) { throw new Error("Database not initialized"); } const query = "BEGIN TRANSACTION"; this.logQuery(query, []); await this.db.run(query); this.inTransaction = true; } async commitTransaction() { if (!this.db) { throw new Error("Database not initialized"); } if (this.inTransaction) { const query = "COMMIT"; this.logQuery(query, []); await this.db.run(query); this.inTransaction = false; } } async ttl(key) { if (!this.db) { return null; } const query = `SELECT expiry FROM ${this.tableName} WHERE key = ?`; const params = [key]; this.logQuery(query, params); const result = await this.db.get(query, params); if (!result || !result.expiry) { return null; } if (typeof result.expiry === "number") { const timeLeft = result.expiry - Date.now(); return Math.max(timeLeft, 0); } else { return null; } } async clear() { if (!this.db) { throw new Error("Database not initialized"); } const query = `DELETE FROM ${this.tableName}`; this.logQuery(query, []); await this.db.run(query); } }