Loading sqlite/README.md +34 −0 Original line number Diff line number Diff line Loading @@ -2,3 +2,37 @@ ============== This package provides a SQLite-based key-value store implementation. ## Usage ### Node.js ```typescript import { DatabaseSync } from 'node:sqlite'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new DatabaseSync('./data.db'); const store = new SqliteKvStore(db); ``` ### Bun ```typescript import { Database } from 'bun:sqlite'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new Database('./data.db'); const store = new SqliteKvStore(db); ``` ### Deno For Deno, you can directly import from `@fedify/sqlite` when using the import map in `deno.json`: ```typescript import { DB } from 'https://deno.land/x/sqlite@v3.7.0/mod.ts'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new DB('./data.db'); const store = new SqliteKvStore(db); ``` sqlite/adapter.ts +5 −3 Original line number Diff line number Diff line /** * SQLite adapter * SQLite database adapter. * * An abstract interface for SQLite database for different runtime environments. */ export interface SQLiteDatabase { /** * Prepares a SQL statement. Loading Loading @@ -31,8 +32,9 @@ export interface SQLiteStatement { /** * Executes a SQL statement and returns the first row of the result set. * @param params - The parameters to bind to the SQL statement. * @returns The first row of the result set, or `undefined` if the result set is empty. */ get(...params: unknown[]): unknown; get(...params: unknown[]): unknown | undefined; /** * Executes a SQL statement and returns all rows of the result set. Loading sqlite/bun.test.ts 0 → 100644 +288 −0 Original line number Diff line number Diff line import * as temporal from "@js-temporal/polyfill"; import { delay } from "@std/async/delay"; import { Database } from "bun:sqlite"; import { expect, test } from "bun:test"; import { SqliteKvStore } from "./bun.ts"; let Temporal: typeof temporal.Temporal; if ("Temporal" in globalThis) { Temporal = globalThis.Temporal; } else { Temporal = temporal.Temporal; } function getStore(): { db: Database; tableName: string; store: SqliteKvStore; } { const db = new Database(":memory:"); const tableName = `fedify_kv_test_${Math.random().toString(36).slice(5)}`; return { db, tableName, store: new SqliteKvStore(db, { tableName }), }; } test("SqliteKvStore.initialize()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const result = db.query(` SELECT name FROM sqlite_master WHERE type='table' AND name=? `).get(tableName); expect(result).toBeDefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.get()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const now = Temporal.Now.instant().epochMilliseconds; db.prepare(` INSERT INTO ${tableName} (key, value, created) VALUES (?, ?, ?) `).run(JSON.stringify(["foo", "bar"]), JSON.stringify(["foobar"]), now); expect(await store.get(["foo", "bar"])).toEqual(["foobar"]); db.prepare(` INSERT INTO ${tableName} (key, value, expires, created) VALUES (?, ?, ?, ?) `).run( JSON.stringify(["foo", "bar", "ttl"]), JSON.stringify(["foobar"]), now + 500, Temporal.Now.instant().epochMilliseconds, ); await delay(500); expect(await store.get(["foo", "bar", "ttl"])).toBeUndefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set()", async () => { const { db, tableName, store } = getStore(); try { await store.set(["foo", "baz"], "baz"); const result = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "baz"])); expect(result.length).toBe(1); expect(JSON.parse(result[0].key as string)).toEqual(["foo", "baz"]); expect(JSON.parse(result[0].value as string)).toBe("baz"); expect(result[0].expires).toBeNull(); await store.set(["foo", "qux"], "qux", { ttl: Temporal.Duration.from({ days: 1 }), }); const result2 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result2.length).toBe(1); expect(JSON.parse(result2[0].key as string)).toEqual(["foo", "qux"]); expect(JSON.parse(result2[0].value as string)).toBe("qux"); expect( (result2[0].expires as number) >= (result2[0].created as number) + 86400000, ).toBe(true); await store.set(["foo", "quux"], true); const result3 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "quux"])); expect(result3.length).toBe(1); expect(JSON.parse(result3[0].key as string)).toEqual(["foo", "quux"]); expect(JSON.parse(result3[0].value as string)).toBe(true); expect(result3[0].expires).toBeNull(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set() - upsert functionality", async () => { const { db, tableName, store } = getStore(); try { await store.set(["upsert"], "initial"); expect(await store.get(["upsert"])).toBe("initial"); await store.set(["upsert"], "updated"); expect(await store.get(["upsert"])).toBe("updated"); const result = db.prepare(` SELECT COUNT(*) as count FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["upsert"])); expect((result as { count: number }).count).toBe(1); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.delete()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); await store.delete(["foo", "qux"]); const result = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result.length).toBe(0); db.prepare(` INSERT INTO ${tableName} (key, value, created) VALUES (?, ?, ?) `).run( JSON.stringify(["foo", "qux"]), JSON.stringify(["qux"]), Temporal.Now.instant().epochMilliseconds, ); await store.delete(["foo", "qux"]); const result2 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result2.length).toBe(0); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.drop()", async () => { const { db, tableName, store } = getStore(); try { await store.drop(); const result = db.query(` SELECT name FROM sqlite_master WHERE type='table' AND name=? `).get(tableName); expect(result).toBeNull(); } finally { db.close(); } }); test("SqliteKvStore.cas()", async () => { const { db, tableName, store } = getStore(); try { await store.set(["foo", "bar"], "foobar"); expect(await store.cas(["foo", "bar"], "bar", "baz")).toBe(false); expect(await store.get(["foo", "bar"])).toBe("foobar"); expect(await store.cas(["foo", "bar"], "foobar", "baz")).toBe(true); expect(await store.get(["foo", "bar"])).toBe("baz"); await store.delete(["foo", "bar"]); expect(await store.cas(["foo", "bar"], "foobar", "baz")).toBe(false); expect(await store.get(["foo", "bar"])).toBeUndefined(); expect(await store.cas(["foo", "bar"], undefined, "baz")).toBe(true); expect(await store.get(["foo", "bar"])).toBe("baz"); expect(await store.cas(["foo", "bar"], "baz", undefined)).toBe(true); expect(await store.get(["foo", "bar"])).toBeUndefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore - complex values", async () => { const { db, tableName, store } = getStore(); try { await store.set(["complex"], { nested: { value: "test", }, }); expect(await store.get(["complex"])).toEqual({ nested: { value: "test", }, }); await store.set(["undefined"], undefined); expect(await store.get(["undefined"])).toBeUndefined(); expect(await store.cas(["undefined"], undefined, "baz")).toBe(true); expect(await store.get(["undefined"])).toBe("baz"); await store.set(["null"], null); expect(await store.get(["null"])).toBeNull(); expect(await store.cas(["null"], null, "baz")).toBe(true); expect(await store.get(["null"])).toBe("baz"); await store.set(["empty string"], ""); expect(await store.get(["empty string"])).toBe(""); expect(await store.cas(["empty string"], "", "baz")).toBe(true); expect(await store.get(["empty string"])).toBe("baz"); await store.set(["array"], [1, 2, 3]); expect(await store.get(["array"])).toEqual([1, 2, 3]); expect( await store.cas(["array"], [1, 2, 3], [1, 2, 3, 4]), ).toBe(true); expect(await store.get(["array"])).toEqual([1, 2, 3, 4]); await store.set(["object"], { a: 1, b: 2 }); expect(await store.get(["object"])).toEqual({ a: 1, b: 2 }); expect( await store.cas(["object"], { a: 1, b: 2 }, { a: 1, b: 2, c: 3 }), ).toBe(true); expect(await store.get(["object"])).toEqual({ a: 1, b: 2, c: 3 }); await store.set(["falsy", "false"], false); expect(await store.get(["falsy", "false"])).toBe(false); expect(await store.cas(["falsy", "false"], false, true)).toBe(true); expect(await store.get(["falsy", "false"])).toBe(true); await store.set(["falsy", "0"], 0); expect(await store.get(["falsy", "0"])).toBe(0); expect(await store.cas(["falsy", "0"], 0, 1)).toBe(true); expect(await store.get(["falsy", "0"])).toBe(1); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set() - preserves created timestamp on update", async () => { const { db, tableName, store } = getStore(); try { await store.set(["timestamp-test"], "initial"); const initialResult = db.prepare(` SELECT created, expires FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["timestamp-test"])); const initialCreated = (initialResult as { created: number }).created; expect(initialCreated).toBeDefined(); expect((initialResult as { expires: number | null }).expires).toBeNull(); await delay(100); const ttl = Temporal.Duration.from({ seconds: 30 }); await store.set(["timestamp-test"], "updated", { ttl }); const updatedResult = db.prepare(` SELECT created, expires FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["timestamp-test"])); expect((updatedResult as { created: number }).created).toBe( initialCreated, "Created timestamp should remain unchanged after update", ); } finally { await store.drop(); db.close(); } }); sqlite/bun.ts 0 → 100644 +46 −0 Original line number Diff line number Diff line import type { Database, Statement } from "bun:sqlite"; import type { SQLiteDatabase, SQLiteStatement } from "./adapter.ts"; import type { SqliteKvStoreOptions } from "./kv.ts"; import { SqliteKvStore as BaseSqliteKvStore } from "./kv.ts"; class BunSqliteDatabase implements SQLiteDatabase { constructor(private readonly db: Database) {} prepare(sql: string): SQLiteStatement { return new BunSqliteStatement(this.db.query(sql)); } exec(sql: string): void { this.db.exec(sql); } close(): void { this.db.close(false); } } class BunSqliteStatement implements SQLiteStatement { constructor(private readonly stmt: Statement) {} run(...params: unknown[]): { changes: number; lastInsertRowid: number } { return this.stmt.run(...params); } get(...params: unknown[]): unknown { const result = this.stmt.get(...params); if (result === null) { return undefined; } return result; } all(...params: unknown[]): unknown[] { return this.stmt.all(...params); } } export class SqliteKvStore extends BaseSqliteKvStore { constructor(db: Database, options: SqliteKvStoreOptions = {}) { super(new BunSqliteDatabase(db), options); } } sqlite/deno.json +13 −2 Original line number Diff line number Diff line Loading @@ -3,8 +3,11 @@ "version": "1.8.0", "license": "MIT", "exports": { ".": "./mod.ts", "./kv": "./kv.ts" ".": "./mod.ts" }, "imports": { "@fedify/sqlite": "./mod.ts", "@fedify/sqlite/": "./" }, "nodeModulesDir": "none", "unstable": [ Loading @@ -17,5 +20,13 @@ "tasks": { "check": "deno fmt --check && deno lint && deno check *.ts", "test": "deno test --allow-net --allow-env --doc --no-check=leaks" }, "test": { "include": [ "deno.test.ts" ], "exclude": [ "node.test.ts" ] } } Loading
sqlite/README.md +34 −0 Original line number Diff line number Diff line Loading @@ -2,3 +2,37 @@ ============== This package provides a SQLite-based key-value store implementation. ## Usage ### Node.js ```typescript import { DatabaseSync } from 'node:sqlite'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new DatabaseSync('./data.db'); const store = new SqliteKvStore(db); ``` ### Bun ```typescript import { Database } from 'bun:sqlite'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new Database('./data.db'); const store = new SqliteKvStore(db); ``` ### Deno For Deno, you can directly import from `@fedify/sqlite` when using the import map in `deno.json`: ```typescript import { DB } from 'https://deno.land/x/sqlite@v3.7.0/mod.ts'; import { SqliteKvStore } from '@fedify/sqlite'; const db = new DB('./data.db'); const store = new SqliteKvStore(db); ```
sqlite/adapter.ts +5 −3 Original line number Diff line number Diff line /** * SQLite adapter * SQLite database adapter. * * An abstract interface for SQLite database for different runtime environments. */ export interface SQLiteDatabase { /** * Prepares a SQL statement. Loading Loading @@ -31,8 +32,9 @@ export interface SQLiteStatement { /** * Executes a SQL statement and returns the first row of the result set. * @param params - The parameters to bind to the SQL statement. * @returns The first row of the result set, or `undefined` if the result set is empty. */ get(...params: unknown[]): unknown; get(...params: unknown[]): unknown | undefined; /** * Executes a SQL statement and returns all rows of the result set. Loading
sqlite/bun.test.ts 0 → 100644 +288 −0 Original line number Diff line number Diff line import * as temporal from "@js-temporal/polyfill"; import { delay } from "@std/async/delay"; import { Database } from "bun:sqlite"; import { expect, test } from "bun:test"; import { SqliteKvStore } from "./bun.ts"; let Temporal: typeof temporal.Temporal; if ("Temporal" in globalThis) { Temporal = globalThis.Temporal; } else { Temporal = temporal.Temporal; } function getStore(): { db: Database; tableName: string; store: SqliteKvStore; } { const db = new Database(":memory:"); const tableName = `fedify_kv_test_${Math.random().toString(36).slice(5)}`; return { db, tableName, store: new SqliteKvStore(db, { tableName }), }; } test("SqliteKvStore.initialize()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const result = db.query(` SELECT name FROM sqlite_master WHERE type='table' AND name=? `).get(tableName); expect(result).toBeDefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.get()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const now = Temporal.Now.instant().epochMilliseconds; db.prepare(` INSERT INTO ${tableName} (key, value, created) VALUES (?, ?, ?) `).run(JSON.stringify(["foo", "bar"]), JSON.stringify(["foobar"]), now); expect(await store.get(["foo", "bar"])).toEqual(["foobar"]); db.prepare(` INSERT INTO ${tableName} (key, value, expires, created) VALUES (?, ?, ?, ?) `).run( JSON.stringify(["foo", "bar", "ttl"]), JSON.stringify(["foobar"]), now + 500, Temporal.Now.instant().epochMilliseconds, ); await delay(500); expect(await store.get(["foo", "bar", "ttl"])).toBeUndefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set()", async () => { const { db, tableName, store } = getStore(); try { await store.set(["foo", "baz"], "baz"); const result = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "baz"])); expect(result.length).toBe(1); expect(JSON.parse(result[0].key as string)).toEqual(["foo", "baz"]); expect(JSON.parse(result[0].value as string)).toBe("baz"); expect(result[0].expires).toBeNull(); await store.set(["foo", "qux"], "qux", { ttl: Temporal.Duration.from({ days: 1 }), }); const result2 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result2.length).toBe(1); expect(JSON.parse(result2[0].key as string)).toEqual(["foo", "qux"]); expect(JSON.parse(result2[0].value as string)).toBe("qux"); expect( (result2[0].expires as number) >= (result2[0].created as number) + 86400000, ).toBe(true); await store.set(["foo", "quux"], true); const result3 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "quux"])); expect(result3.length).toBe(1); expect(JSON.parse(result3[0].key as string)).toEqual(["foo", "quux"]); expect(JSON.parse(result3[0].value as string)).toBe(true); expect(result3[0].expires).toBeNull(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set() - upsert functionality", async () => { const { db, tableName, store } = getStore(); try { await store.set(["upsert"], "initial"); expect(await store.get(["upsert"])).toBe("initial"); await store.set(["upsert"], "updated"); expect(await store.get(["upsert"])).toBe("updated"); const result = db.prepare(` SELECT COUNT(*) as count FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["upsert"])); expect((result as { count: number }).count).toBe(1); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.delete()", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); await store.delete(["foo", "qux"]); const result = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result.length).toBe(0); db.prepare(` INSERT INTO ${tableName} (key, value, created) VALUES (?, ?, ?) `).run( JSON.stringify(["foo", "qux"]), JSON.stringify(["qux"]), Temporal.Now.instant().epochMilliseconds, ); await store.delete(["foo", "qux"]); const result2 = db.prepare(` SELECT * FROM ${tableName} WHERE key = ? `).all(JSON.stringify(["foo", "qux"])); expect(result2.length).toBe(0); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.drop()", async () => { const { db, tableName, store } = getStore(); try { await store.drop(); const result = db.query(` SELECT name FROM sqlite_master WHERE type='table' AND name=? `).get(tableName); expect(result).toBeNull(); } finally { db.close(); } }); test("SqliteKvStore.cas()", async () => { const { db, tableName, store } = getStore(); try { await store.set(["foo", "bar"], "foobar"); expect(await store.cas(["foo", "bar"], "bar", "baz")).toBe(false); expect(await store.get(["foo", "bar"])).toBe("foobar"); expect(await store.cas(["foo", "bar"], "foobar", "baz")).toBe(true); expect(await store.get(["foo", "bar"])).toBe("baz"); await store.delete(["foo", "bar"]); expect(await store.cas(["foo", "bar"], "foobar", "baz")).toBe(false); expect(await store.get(["foo", "bar"])).toBeUndefined(); expect(await store.cas(["foo", "bar"], undefined, "baz")).toBe(true); expect(await store.get(["foo", "bar"])).toBe("baz"); expect(await store.cas(["foo", "bar"], "baz", undefined)).toBe(true); expect(await store.get(["foo", "bar"])).toBeUndefined(); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore - complex values", async () => { const { db, tableName, store } = getStore(); try { await store.set(["complex"], { nested: { value: "test", }, }); expect(await store.get(["complex"])).toEqual({ nested: { value: "test", }, }); await store.set(["undefined"], undefined); expect(await store.get(["undefined"])).toBeUndefined(); expect(await store.cas(["undefined"], undefined, "baz")).toBe(true); expect(await store.get(["undefined"])).toBe("baz"); await store.set(["null"], null); expect(await store.get(["null"])).toBeNull(); expect(await store.cas(["null"], null, "baz")).toBe(true); expect(await store.get(["null"])).toBe("baz"); await store.set(["empty string"], ""); expect(await store.get(["empty string"])).toBe(""); expect(await store.cas(["empty string"], "", "baz")).toBe(true); expect(await store.get(["empty string"])).toBe("baz"); await store.set(["array"], [1, 2, 3]); expect(await store.get(["array"])).toEqual([1, 2, 3]); expect( await store.cas(["array"], [1, 2, 3], [1, 2, 3, 4]), ).toBe(true); expect(await store.get(["array"])).toEqual([1, 2, 3, 4]); await store.set(["object"], { a: 1, b: 2 }); expect(await store.get(["object"])).toEqual({ a: 1, b: 2 }); expect( await store.cas(["object"], { a: 1, b: 2 }, { a: 1, b: 2, c: 3 }), ).toBe(true); expect(await store.get(["object"])).toEqual({ a: 1, b: 2, c: 3 }); await store.set(["falsy", "false"], false); expect(await store.get(["falsy", "false"])).toBe(false); expect(await store.cas(["falsy", "false"], false, true)).toBe(true); expect(await store.get(["falsy", "false"])).toBe(true); await store.set(["falsy", "0"], 0); expect(await store.get(["falsy", "0"])).toBe(0); expect(await store.cas(["falsy", "0"], 0, 1)).toBe(true); expect(await store.get(["falsy", "0"])).toBe(1); } finally { await store.drop(); db.close(); } }); test("SqliteKvStore.set() - preserves created timestamp on update", async () => { const { db, tableName, store } = getStore(); try { await store.set(["timestamp-test"], "initial"); const initialResult = db.prepare(` SELECT created, expires FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["timestamp-test"])); const initialCreated = (initialResult as { created: number }).created; expect(initialCreated).toBeDefined(); expect((initialResult as { expires: number | null }).expires).toBeNull(); await delay(100); const ttl = Temporal.Duration.from({ seconds: 30 }); await store.set(["timestamp-test"], "updated", { ttl }); const updatedResult = db.prepare(` SELECT created, expires FROM ${tableName} WHERE key = ? `).get(JSON.stringify(["timestamp-test"])); expect((updatedResult as { created: number }).created).toBe( initialCreated, "Created timestamp should remain unchanged after update", ); } finally { await store.drop(); db.close(); } });
sqlite/bun.ts 0 → 100644 +46 −0 Original line number Diff line number Diff line import type { Database, Statement } from "bun:sqlite"; import type { SQLiteDatabase, SQLiteStatement } from "./adapter.ts"; import type { SqliteKvStoreOptions } from "./kv.ts"; import { SqliteKvStore as BaseSqliteKvStore } from "./kv.ts"; class BunSqliteDatabase implements SQLiteDatabase { constructor(private readonly db: Database) {} prepare(sql: string): SQLiteStatement { return new BunSqliteStatement(this.db.query(sql)); } exec(sql: string): void { this.db.exec(sql); } close(): void { this.db.close(false); } } class BunSqliteStatement implements SQLiteStatement { constructor(private readonly stmt: Statement) {} run(...params: unknown[]): { changes: number; lastInsertRowid: number } { return this.stmt.run(...params); } get(...params: unknown[]): unknown { const result = this.stmt.get(...params); if (result === null) { return undefined; } return result; } all(...params: unknown[]): unknown[] { return this.stmt.all(...params); } } export class SqliteKvStore extends BaseSqliteKvStore { constructor(db: Database, options: SqliteKvStoreOptions = {}) { super(new BunSqliteDatabase(db), options); } }
sqlite/deno.json +13 −2 Original line number Diff line number Diff line Loading @@ -3,8 +3,11 @@ "version": "1.8.0", "license": "MIT", "exports": { ".": "./mod.ts", "./kv": "./kv.ts" ".": "./mod.ts" }, "imports": { "@fedify/sqlite": "./mod.ts", "@fedify/sqlite/": "./" }, "nodeModulesDir": "none", "unstable": [ Loading @@ -17,5 +20,13 @@ "tasks": { "check": "deno fmt --check && deno lint && deno check *.ts", "test": "deno test --allow-net --allow-env --doc --no-check=leaks" }, "test": { "include": [ "deno.test.ts" ], "exclude": [ "node.test.ts" ] } }