import Database from 'better-sqlite3'; import { decodeExtendedJson, encodeExtendedJson } from '../utils/ext-json.js'; export class Storage { static async create(dbPath: string): Promise { // Create the database const database = new Database(dbPath); // Create the storage table if it doesn't exist database.prepare('CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT)').run(); return new Storage(database, ''); } constructor( private readonly database: Database.Database, private readonly basePath: string, ) {} /** * Get the full key with basePath prefix */ private getFullKey(key: string): string { return this.basePath ? `${this.basePath}.${key}` : key; } /** * Strip the basePath prefix from a key */ private stripBasePath(fullKey: string): string { if (!this.basePath) return fullKey; const prefix = `${this.basePath}.`; return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey; } async set(key: string, value: any): Promise { // Encode the extended json object const encodedValue = encodeExtendedJson(value); // Insert or replace the value into the database with full key (including basePath) const fullKey = this.getFullKey(key); this.database.prepare('INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)').run(fullKey, encodedValue); } /** * Get all key-value pairs from this storage namespace (shallow only - no nested children) */ async all(): Promise<{ key: string; value: any }[]> { let query = 'SELECT key, value FROM storage'; const params: any[] = []; if (this.basePath) { // Filter by basePath prefix query += ' WHERE key LIKE ?'; params.push(`${this.basePath}.%`); } // Get all the rows from the database const rows = await this.database.prepare(query).all(...params) as { key: string; value: any }[]; // Filter for shallow results (only direct children) const filteredRows = rows.filter(row => { const strippedKey = this.stripBasePath(row.key); // Only include keys that don't have additional dots (no deeper nesting) return !strippedKey.includes('.'); }); // Decode the extended json objects and strip basePath from keys return filteredRows.map(row => ({ key: this.stripBasePath(row.key), value: decodeExtendedJson(row.value) })); } async get(key: string): Promise { // Get the row from the database using full key const fullKey = this.getFullKey(key); const row = await this.database.prepare('SELECT value FROM storage WHERE key = ?').get(fullKey) as { value: any }; // Return null if not found if (!row) return null; // Decode the extended json object return decodeExtendedJson(row.value); } async remove(key: string): Promise { // Delete using full key const fullKey = this.getFullKey(key); this.database.prepare('DELETE FROM storage WHERE key = ?').run(fullKey); } async clear(): Promise { if (this.basePath) { // Clear only items under this namespace this.database.prepare('DELETE FROM storage WHERE key LIKE ?').run(`${this.basePath}.%`); } else { // Clear everything this.database.prepare('DELETE FROM storage').run(); } } child(key: string): Storage { return new Storage(this.database, this.getFullKey(key)); } }