Merge branch '76-feat-sqlite-create-sqlite-storage-provider' into 'development'
Resolve "feat(SQLite): Create SQLite Storage Provider" Closes #76 See merge request GeneralProtocols/xo/stack!125
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"types": "dist/index.types.d.ts",
|
"types": "dist/index.types.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.browser.d.ts",
|
"types": "./dist/index.types.d.ts",
|
||||||
"browser": {
|
"browser": {
|
||||||
"types": "./dist/index.browser.d.ts",
|
"types": "./dist/index.browser.d.ts",
|
||||||
"default": "./dist/index.browser.js"
|
"default": "./dist/index.browser.js"
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ export * from './index.js';
|
|||||||
|
|
||||||
// These exports will only work in a NodeJS (backend) environment.
|
// These exports will only work in a NodeJS (backend) environment.
|
||||||
// TODO: FIx me!
|
// TODO: FIx me!
|
||||||
// export * from './storage/store-sqlite.js';
|
export * from './storage/store-sqlite.js';
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { type StorageOpts, BaseStorage, BaseStore } from './base-store.js';
|
||||||
|
import sqlite3, { type Database } from 'better-sqlite3';
|
||||||
|
import { pack, unpack } from 'msgpackr';
|
||||||
|
import { StoreInMemorySynced } from './store-inmemory-synced.js';
|
||||||
|
|
||||||
|
export interface SQLiteOptions {
|
||||||
|
wal: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage Adapter that uses SQLite as the storage backend. Everything is stored as a BLOB, to avoid type issues.
|
||||||
|
*
|
||||||
|
* @remarks This implementation is single threaded. It WILL block execution of the main thread.
|
||||||
|
*/
|
||||||
|
export class StorageSQLite extends BaseStorage {
|
||||||
|
/**
|
||||||
|
* Create or open a SQLite database.
|
||||||
|
* @param filepath - The path to the SQLite database file.
|
||||||
|
* @param options - The options for the SQLite database.
|
||||||
|
* @returns A new instance of StorageSQLite.
|
||||||
|
*/
|
||||||
|
static async createOrOpen(
|
||||||
|
filepath: string,
|
||||||
|
options: Partial<SQLiteOptions> = {},
|
||||||
|
) {
|
||||||
|
const db = sqlite3(filepath);
|
||||||
|
|
||||||
|
const opts: SQLiteOptions = {
|
||||||
|
wal: true,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (opts.wal) {
|
||||||
|
db.pragma('journal_mode = WAL');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new StorageSQLite(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(public db: Database) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async listStores() {
|
||||||
|
const result = this.db
|
||||||
|
.prepare(`SELECT * FROM sqlite_master WHERE type='table'`)
|
||||||
|
.all() as { name: string }[];
|
||||||
|
return result.map((row) => row.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createStore(storeName: string, opts: StorageOpts = {}) {
|
||||||
|
// Create table with proper schema for key-value storage
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${storeName}" (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value BLOB
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
|
||||||
|
const store = new StoreSQLite(this.db, storeName, opts);
|
||||||
|
return opts.syncInMemory ? await StoreInMemorySynced.create(store) : store;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStore(storeName: string, opts: StorageOpts) {
|
||||||
|
// Check if table exists
|
||||||
|
const tableExists = this.db
|
||||||
|
.prepare(
|
||||||
|
`
|
||||||
|
SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table' AND name=?
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.get(storeName);
|
||||||
|
|
||||||
|
if (!tableExists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new StoreSQLite(this.db, storeName, opts);
|
||||||
|
return opts.syncInMemory ? await StoreInMemorySynced.create(store) : store;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrGetStore(storeName: string, opts: StorageOpts) {
|
||||||
|
return await this.createStore(storeName, opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStore(storeName: string) {
|
||||||
|
this.db.prepare(`DROP TABLE IF EXISTS "${storeName}"`).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteDatabase() {
|
||||||
|
this.db.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoreSQLite extends BaseStore {
|
||||||
|
constructor(
|
||||||
|
protected db: Database,
|
||||||
|
protected storeName: string,
|
||||||
|
protected opts: StorageOpts = {},
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async keys() {
|
||||||
|
const result = this.db
|
||||||
|
.prepare(`SELECT key FROM "${this.storeName}"`)
|
||||||
|
.all() as { key: string }[];
|
||||||
|
return result.map((row) => row.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get<T>(key: string): Promise<T | undefined> {
|
||||||
|
const result = this.db
|
||||||
|
.prepare(`SELECT value FROM "${this.storeName}" WHERE key = ?`)
|
||||||
|
.get(key) as { value: Buffer } | undefined;
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle decryption if encryption is enabled
|
||||||
|
const decryptedValue = this.opts.encryption
|
||||||
|
? await this.opts.encryption.decrypt(new Uint8Array(result.value))
|
||||||
|
: new Uint8Array(result.value);
|
||||||
|
|
||||||
|
// Deserialize using msgpackr for consistency with other implementations
|
||||||
|
return unpack(decryptedValue) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
async set<T>(key: string, value: T): Promise<void> {
|
||||||
|
// Serialize using msgpackr for consistency with other implementations
|
||||||
|
const serializedValue = pack(value);
|
||||||
|
|
||||||
|
// Handle encryption if encryption is enabled
|
||||||
|
const finalValue = this.opts.encryption
|
||||||
|
? await this.opts.encryption.encrypt(serializedValue)
|
||||||
|
: serializedValue;
|
||||||
|
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`INSERT OR REPLACE INTO "${this.storeName}" (key, value) VALUES (?, ?)`,
|
||||||
|
)
|
||||||
|
.run(key, Buffer.from(finalValue));
|
||||||
|
|
||||||
|
this.emit('set', { key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(key: string): Promise<void> {
|
||||||
|
this.db.prepare(`DELETE FROM "${this.storeName}" WHERE key = ?`).run(key);
|
||||||
|
this.emit('delete', { key });
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
this.db.prepare(`DELETE FROM "${this.storeName}"`).run();
|
||||||
|
this.emit('clear', undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user