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
JavaScript
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);
}
}