Project Files
dist / db.js
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.KvDatabase = exports.MAX_VALUE_LENGTH_CHARS = exports.DEFAULT_DB_STORAGE_FILENAME = void 0;
exports.getDefaultStoragePath = getDefaultStoragePath;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
exports.DEFAULT_DB_STORAGE_FILENAME = "kv-store.db";
const MAX_FILENAME_LENGTH = 128;
exports.MAX_VALUE_LENGTH_CHARS = 10 * 1024;
const SAFE_FILENAME_PATTERN = /^[A-Za-z0-9._-]+$/;
const WINDOWS_RESERVED_BASENAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\..*)?$/i;
function escapeLikePattern(input) {
return input.replace(/[\\%_]/g, "\\$&");
}
function getDefaultStoragePath() {
const home = os.homedir();
return path.join(home, ".lmstudio", "plugin-data", "student489", "kv-store");
}
/** Return the default storage path and ensure it exists. */
function defaultStoragePath() {
const dir = getDefaultStoragePath();
fs.mkdirSync(dir, { recursive: true });
return dir;
}
function sanitizeStorageFilename(input) {
const value = input?.trim();
if (!value)
return undefined;
if (value.length > MAX_FILENAME_LENGTH) {
throw new Error(`dbStorageFilename must be at most ${MAX_FILENAME_LENGTH} characters`);
}
if (value !== path.basename(value)) {
throw new Error("dbStorageFilename must be a file name only, without any path segments");
}
if (value === "." || value === ".." || value.includes("..")) {
throw new Error("dbStorageFilename cannot contain '..'");
}
if (!SAFE_FILENAME_PATTERN.test(value)) {
throw new Error("dbStorageFilename contains invalid characters; allowed: letters, numbers, dot, dash, underscore");
}
if (/[. ]$/.test(value)) {
throw new Error("dbStorageFilename cannot end with dot or space");
}
if (WINDOWS_RESERVED_BASENAME.test(value)) {
throw new Error("dbStorageFilename uses a reserved Windows device name");
}
return value;
}
class KvDatabase {
db;
dbPath;
dbDir;
initialized = false;
constructor(storageFilename) {
const normalizedFilename = sanitizeStorageFilename(storageFilename);
this.dbDir = defaultStoragePath();
fs.mkdirSync(this.dbDir, { recursive: true });
this.dbPath = path.join(this.dbDir, normalizedFilename || exports.DEFAULT_DB_STORAGE_FILENAME);
}
/** Must be called before any other method. Loads WASM + opens/creates DB. */
async init() {
if (this.initialized)
return;
const initSqlJs = require("sql.js");
const wasmPath = path.join(path.dirname(require.resolve("sql.js")), "sql-wasm.wasm");
const SQL = await initSqlJs({
locateFile: () => wasmPath,
});
this.db = fs.existsSync(this.dbPath) ? new SQL.Database(fs.readFileSync(this.dbPath)) : new SQL.Database();
this.setupSchema();
this.initialized = true;
}
setupSchema() {
this.db.run(`
CREATE TABLE IF NOT EXISTS kv_store (
key TEXT PRIMARY KEY,
value TEXT,
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
)
`);
this.persist();
}
/** Write in-memory DB to disk. Called after every write operation. */
persist() {
try {
const data = this.db.export();
const buffer = Buffer.from(data);
fs.writeFileSync(this.dbPath, buffer);
}
catch { }
}
/**
* Insert or replace a key-value pair.
* Updates `updated_at` on conflict; preserves `created_at`.
*/
set(key, value) {
if (key.length < 1 || key.length > 255) {
throw new Error("Key length must be between 1 and 255 characters");
}
if (typeof value !== "string") {
throw new Error("Value must be a string");
}
if (value.length > exports.MAX_VALUE_LENGTH_CHARS) {
throw new Error(`Value length must be at most ${exports.MAX_VALUE_LENGTH_CHARS} characters`);
}
this.db.run(`INSERT INTO kv_store (key, value, created_at, updated_at)
VALUES (?, ?, datetime('now'), datetime('now'))
ON CONFLICT(key) DO UPDATE SET
value = excluded.value,
updated_at = datetime('now')`, [key, value]);
this.persist();
}
/**
* Append text to an existing value, or create the key if it does not exist.
* Returns the resulting stored value.
*/
append(key, value) {
if (key.length < 1 || key.length > 255) {
throw new Error("Key length must be between 1 and 255 characters");
}
if (typeof value !== "string") {
throw new Error("Value must be a string");
}
const currentValue = this.get(key) ?? "";
const nextValue = currentValue + value;
if (nextValue.length > exports.MAX_VALUE_LENGTH_CHARS) {
throw new Error(`Value length must be at most ${exports.MAX_VALUE_LENGTH_CHARS} characters`);
}
this.set(key, nextValue);
return nextValue;
}
/**
* Retrieve the value for a key, or null if the key does not exist.
* Returns the raw stored text string.
*/
get(key) {
const result = this.db.exec(`SELECT value FROM kv_store WHERE key = ?`, [key]);
const raw = result[0]?.values[0]?.[0];
if (raw === null || raw === undefined)
return null;
return raw;
}
/**
* Retrieve values for multiple keys using a single SQL query.
* Returns only keys that exist and preserves input order, including duplicates.
*/
getMany(keys) {
if (keys.length === 0)
return [];
const uniqueKeys = Array.from(new Set(keys));
const placeholders = uniqueKeys.map(() => "?").join(", ");
const result = this.db.exec(`SELECT key, value FROM kv_store WHERE key IN (${placeholders})`, uniqueKeys);
const valueByKey = new Map();
for (const row of result[0]?.values ?? []) {
const key = row[0];
const value = row[1] ?? null;
valueByKey.set(key, value);
}
const pairs = [];
for (const key of keys) {
if (!valueByKey.has(key))
continue;
pairs.push({
key,
value: valueByKey.get(key) ?? null,
});
}
return pairs;
}
/** Delete a key-value pair. No-op if the key does not exist. */
delete(key) {
this.db.run(`DELETE FROM kv_store WHERE key = ?`, [key]);
this.persist();
}
/**
* Delete multiple key-value pairs in a single SQL operation.
* No-op for keys that do not exist.
*/
deleteMany(keys) {
if (keys.length === 0)
return;
const uniqueKeys = Array.from(new Set(keys));
const predicates = uniqueKeys.map(() => `key = ?`).join(" OR ");
this.db.run(`DELETE FROM kv_store WHERE ${predicates}`, uniqueKeys);
this.persist();
}
/**
* Rename a key while preserving its value and created_at timestamp.
* Fails if the source key does not exist or the target key already exists.
*/
rename(oldKey, newKey) {
if (oldKey.length < 1 || oldKey.length > 255) {
throw new Error("Old key length must be between 1 and 255 characters");
}
if (newKey.length < 1 || newKey.length > 255) {
throw new Error("New key length must be between 1 and 255 characters");
}
if (oldKey === newKey) {
throw new Error("Old key and new key must be different");
}
const existingSource = this.db.exec(`SELECT 1 FROM kv_store WHERE key = ?`, [oldKey]);
if (existingSource.length === 0 || existingSource[0].values.length === 0) {
throw new Error("Source key does not exist");
}
const existingTarget = this.db.exec(`SELECT 1 FROM kv_store WHERE key = ?`, [newKey]);
if (existingTarget.length > 0 && existingTarget[0].values.length > 0) {
throw new Error("Target key already exists");
}
this.db.run("BEGIN TRANSACTION");
try {
this.db.run(`UPDATE kv_store
SET key = ?, updated_at = datetime('now')
WHERE key = ?`, [newKey, oldKey]);
this.db.run("COMMIT");
this.persist();
}
catch (error) {
try {
this.db.run("ROLLBACK");
}
catch { }
throw error;
}
}
/**
* List all keys in the store, optionally filtering by a match string.
* The match string is used as a SQL LIKE pattern (case-insensitive).
*/
listKeys(match, limit, offset) {
const normalizedLimit = Number.isFinite(limit) ? Math.trunc(limit) : 200;
const normalizedOffset = Number.isFinite(offset) ? Math.trunc(offset) : 0;
const effectiveLimit = Math.min(Math.max(normalizedLimit, 0), 200);
const effectiveOffset = Math.max(normalizedOffset, 0);
const params = [effectiveLimit, effectiveOffset];
let query = `SELECT key FROM kv_store`;
if (match) {
const escapedMatch = escapeLikePattern(match);
query += `\n WHERE key LIKE ? ESCAPE '\\'`;
params.unshift(`%${escapedMatch}%`);
}
query += `\n ORDER BY key COLLATE NOCASE, key\n LIMIT ? OFFSET ?`;
const result = this.db.exec(query, params);
if (result.length === 0)
return [];
return result[0].values.map((row) => row[0]);
}
/** Count keys in the store, optionally filtering by a case-insensitive substring. */
countKeys(match) {
let query = `SELECT COUNT(*) AS count FROM kv_store`;
const params = [];
if (match) {
const escapedMatch = escapeLikePattern(match);
query += `\n WHERE key LIKE ? ESCAPE '\\'`;
params.push(`%${escapedMatch}%`);
}
const result = this.db.exec(query, params);
if (result.length === 0 || result[0].values.length === 0)
return 0;
const rawCount = result[0].values[0][0];
return typeof rawCount === "number" ? rawCount : Number(rawCount) || 0;
}
close() {
if (this.db) {
this.persist();
this.db.close();
}
}
}
exports.KvDatabase = KvDatabase;
//# sourceMappingURL=db.js.map