Loading CHANGES.md +4 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ To be released. - Allowed Express 5 in the `express` peer dependency range to support NestJS 11. [[#492], [#493] by Cho Hasang] ### @fedify/sqlite - Implemented `list()` method in `SqliteKvStore`. [[#498]] [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 Loading packages/sqlite/src/kv.test.ts +74 −0 Original line number Diff line number Diff line Loading @@ -310,3 +310,77 @@ test("SqliteKvStore.set() - preserves created timestamp on update", async () => await db.close(); } }); test("SqliteKvStore.list()", async () => { const { db, store } = getStore(); try { await store.set(["prefix", "a"], "value-a"); await store.set(["prefix", "b"], "value-b"); await store.set(["prefix", "nested", "c"], "value-c"); await store.set(["other", "x"], "value-x"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["prefix"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 3); assert( entries.some((e) => e.key[1] === "a" && e.value === "value-a"), ); assert(entries.some((e) => e.key[1] === "b")); assert(entries.some((e) => e.key[1] === "nested")); } finally { await store.drop(); await db.close(); } }); test("SqliteKvStore.list() - excludes expired", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const now = Temporal.Now.instant().epochMilliseconds; // Insert expired entry directly db.prepare(` INSERT INTO "${tableName}" (key, value, created, expires) VALUES (?, ?, ?, ?) `).run( JSON.stringify(["list-test", "expired"]), JSON.stringify("expired-value"), now - 1000, now - 500, ); await store.set(["list-test", "valid"], "valid-value"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["list-test"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 1); assert.deepStrictEqual(entries[0].key, ["list-test", "valid"]); } finally { await store.drop(); await db.close(); } }); test("SqliteKvStore.list() - single element key", async () => { const { db, store } = getStore(); try { await store.set(["a"], "value-a"); await store.set(["b"], "value-b"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["a"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 1); } finally { await store.drop(); await db.close(); } }); packages/sqlite/src/kv.ts +40 −1 Original line number Diff line number Diff line import { type PlatformDatabase, SqliteDatabase } from "#sqlite"; import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify"; import type { KvKey, KvStore, KvStoreListEntry, KvStoreListOptions, KvStoreSetOptions, } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; import { isEqual } from "es-toolkit"; import type { SqliteDatabaseAdapter } from "./adapter.ts"; Loading Loading @@ -212,6 +218,39 @@ export class SqliteKvStore implements KvStore { } } /** * {@inheritDoc KvStore.list} * @since 1.10.0 */ async *list( options: KvStoreListOptions, ): AsyncIterable<KvStoreListEntry> { this.initialize(); const prefix = options.prefix; const now = Temporal.Now.instant().epochMilliseconds; // JSON pattern: '["prefix","' matches keys starting with prefix const pattern = JSON.stringify(prefix).slice(0, -1) + ",%"; const exactKey = JSON.stringify(prefix); const results = this.#db .prepare(` SELECT key, value FROM "${this.#tableName}" WHERE (key LIKE ? ESCAPE '\\' OR key = ?) AND (expires IS NULL OR expires > ?) ORDER BY key `) .all(pattern, exactKey, now) as { key: string; value: string }[]; for (const row of results) { yield { key: this.#decodeKey(row.key), value: this.#decodeValue(row.value), }; } } /** * Creates the table used by the key–value store if it does not already exist. * Does nothing if the table already exists. Loading Loading
CHANGES.md +4 −0 Original line number Diff line number Diff line Loading @@ -54,6 +54,10 @@ To be released. - Allowed Express 5 in the `express` peer dependency range to support NestJS 11. [[#492], [#493] by Cho Hasang] ### @fedify/sqlite - Implemented `list()` method in `SqliteKvStore`. [[#498]] [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 Loading
packages/sqlite/src/kv.test.ts +74 −0 Original line number Diff line number Diff line Loading @@ -310,3 +310,77 @@ test("SqliteKvStore.set() - preserves created timestamp on update", async () => await db.close(); } }); test("SqliteKvStore.list()", async () => { const { db, store } = getStore(); try { await store.set(["prefix", "a"], "value-a"); await store.set(["prefix", "b"], "value-b"); await store.set(["prefix", "nested", "c"], "value-c"); await store.set(["other", "x"], "value-x"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["prefix"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 3); assert( entries.some((e) => e.key[1] === "a" && e.value === "value-a"), ); assert(entries.some((e) => e.key[1] === "b")); assert(entries.some((e) => e.key[1] === "nested")); } finally { await store.drop(); await db.close(); } }); test("SqliteKvStore.list() - excludes expired", async () => { const { db, tableName, store } = getStore(); try { await store.initialize(); const now = Temporal.Now.instant().epochMilliseconds; // Insert expired entry directly db.prepare(` INSERT INTO "${tableName}" (key, value, created, expires) VALUES (?, ?, ?, ?) `).run( JSON.stringify(["list-test", "expired"]), JSON.stringify("expired-value"), now - 1000, now - 500, ); await store.set(["list-test", "valid"], "valid-value"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["list-test"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 1); assert.deepStrictEqual(entries[0].key, ["list-test", "valid"]); } finally { await store.drop(); await db.close(); } }); test("SqliteKvStore.list() - single element key", async () => { const { db, store } = getStore(); try { await store.set(["a"], "value-a"); await store.set(["b"], "value-b"); const entries: { key: readonly string[]; value: unknown }[] = []; for await (const entry of store.list!({ prefix: ["a"] })) { entries.push({ key: entry.key, value: entry.value }); } assert.strictEqual(entries.length, 1); } finally { await store.drop(); await db.close(); } });
packages/sqlite/src/kv.ts +40 −1 Original line number Diff line number Diff line import { type PlatformDatabase, SqliteDatabase } from "#sqlite"; import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify"; import type { KvKey, KvStore, KvStoreListEntry, KvStoreListOptions, KvStoreSetOptions, } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; import { isEqual } from "es-toolkit"; import type { SqliteDatabaseAdapter } from "./adapter.ts"; Loading Loading @@ -212,6 +218,39 @@ export class SqliteKvStore implements KvStore { } } /** * {@inheritDoc KvStore.list} * @since 1.10.0 */ async *list( options: KvStoreListOptions, ): AsyncIterable<KvStoreListEntry> { this.initialize(); const prefix = options.prefix; const now = Temporal.Now.instant().epochMilliseconds; // JSON pattern: '["prefix","' matches keys starting with prefix const pattern = JSON.stringify(prefix).slice(0, -1) + ",%"; const exactKey = JSON.stringify(prefix); const results = this.#db .prepare(` SELECT key, value FROM "${this.#tableName}" WHERE (key LIKE ? ESCAPE '\\' OR key = ?) AND (expires IS NULL OR expires > ?) ORDER BY key `) .all(pattern, exactKey, now) as { key: string; value: string }[]; for (const row of results) { yield { key: this.#decodeKey(row.key), value: this.#decodeValue(row.value), }; } } /** * Creates the table used by the key–value store if it does not already exist. * Does nothing if the table already exists. Loading