106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import { decodeExtendedJson, encodeExtendedJson } from '../utils/ext-json.js';
|
|
|
|
export class Storage {
|
|
static async create(dbPath: string): Promise<Storage> {
|
|
// 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<void> {
|
|
// 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<any> {
|
|
// 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<void> {
|
|
// Delete using full key
|
|
const fullKey = this.getFullKey(key);
|
|
this.database.prepare('DELETE FROM storage WHERE key = ?').run(fullKey);
|
|
}
|
|
|
|
async clear(): Promise<void> {
|
|
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));
|
|
}
|
|
} |