Loading CHANGES.md +6 −4 Original line number Diff line number Diff line Loading @@ -36,8 +36,6 @@ To be released. key ownership verification, recording actor ID, key ID, verification result, and the verification method used. [#323]: https://github.com/fedify-dev/fedify/issues/323 - Added optional `list()` method to the `KvStore` interface for enumerating entries by key prefix. This enables efficient prefix scanning which is useful for implementing features like distributed trace storage, cache Loading @@ -47,6 +45,7 @@ To be released. - Added `KvStoreListEntry` interface. - Implemented in `MemoryKvStore`. [#323]: https://github.com/fedify-dev/fedify/issues/323 [#498]: https://github.com/fedify-dev/fedify/issues/498 ### @fedify/nestjs Loading @@ -54,13 +53,16 @@ To be released. - Allowed Express 5 in the `express` peer dependency range to support NestJS 11. [[#492], [#493] by Cho Hasang] [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 ### @fedify/sqlite - Implemented `list()` method in `SqliteKvStore`. [[#498]] ### @fedify/postgres [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 - Implemented `list()` method in `PostgresKvStore`. [[#498]] Version 1.9.2 Loading packages/postgres/src/kv.test.ts +82 −0 Original line number Diff line number Diff line Loading @@ -139,4 +139,86 @@ test("PostgresKvStore.drop()", { skip: dbUrl == null }, async () => { } }); test("PostgresKvStore.list()", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, 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 sql.end(); } }); test( "PostgresKvStore.list() - excludes expired", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, tableName, store } = getStore(); try { await store.initialize(); // Insert expired entry directly await sql` INSERT INTO ${sql(tableName)} (key, value, created, ttl) VALUES ( ${["list-test", "expired"]}, ${"expired-value"}, CURRENT_TIMESTAMP - INTERVAL '1 hour', ${"30 minutes"} ) `; 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 sql.end(); } }, ); test( "PostgresKvStore.list() - single element key", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, 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 sql.end(); } }, ); // cSpell: ignore regclass packages/postgres/src/kv.ts +36 −1 Original line number Diff line number Diff line import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify"; import type { KvKey, KvStore, KvStoreListEntry, KvStoreListOptions, KvStoreSetOptions, } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; import type { JSONValue, Parameter, Sql } from "postgres"; import { driverSerializesJson } from "./utils.ts"; Loading Loading @@ -107,6 +113,35 @@ export class PostgresKvStore implements KvStore { await this.#expire(); } /** * {@inheritDoc KvStore.list} * @since 1.10.0 */ async *list( options: KvStoreListOptions, ): AsyncIterable<KvStoreListEntry> { await this.initialize(); const prefix = options.prefix; const prefixLength = prefix.length; const results = await this.#sql` SELECT key, value FROM ${this.#sql(this.#tableName)} WHERE array_length(key, 1) >= ${prefixLength} AND key[1:${prefixLength}] = ${prefix}::text[] AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP) ORDER BY key `; for (const row of results) { yield { key: row.key as KvKey, value: 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 +6 −4 Original line number Diff line number Diff line Loading @@ -36,8 +36,6 @@ To be released. key ownership verification, recording actor ID, key ID, verification result, and the verification method used. [#323]: https://github.com/fedify-dev/fedify/issues/323 - Added optional `list()` method to the `KvStore` interface for enumerating entries by key prefix. This enables efficient prefix scanning which is useful for implementing features like distributed trace storage, cache Loading @@ -47,6 +45,7 @@ To be released. - Added `KvStoreListEntry` interface. - Implemented in `MemoryKvStore`. [#323]: https://github.com/fedify-dev/fedify/issues/323 [#498]: https://github.com/fedify-dev/fedify/issues/498 ### @fedify/nestjs Loading @@ -54,13 +53,16 @@ To be released. - Allowed Express 5 in the `express` peer dependency range to support NestJS 11. [[#492], [#493] by Cho Hasang] [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 ### @fedify/sqlite - Implemented `list()` method in `SqliteKvStore`. [[#498]] ### @fedify/postgres [#492]: https://github.com/fedify-dev/fedify/issues/492 [#493]: https://github.com/fedify-dev/fedify/pull/493 - Implemented `list()` method in `PostgresKvStore`. [[#498]] Version 1.9.2 Loading
packages/postgres/src/kv.test.ts +82 −0 Original line number Diff line number Diff line Loading @@ -139,4 +139,86 @@ test("PostgresKvStore.drop()", { skip: dbUrl == null }, async () => { } }); test("PostgresKvStore.list()", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, 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 sql.end(); } }); test( "PostgresKvStore.list() - excludes expired", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, tableName, store } = getStore(); try { await store.initialize(); // Insert expired entry directly await sql` INSERT INTO ${sql(tableName)} (key, value, created, ttl) VALUES ( ${["list-test", "expired"]}, ${"expired-value"}, CURRENT_TIMESTAMP - INTERVAL '1 hour', ${"30 minutes"} ) `; 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 sql.end(); } }, ); test( "PostgresKvStore.list() - single element key", { skip: dbUrl == null }, async () => { if (dbUrl == null) return; // Bun does not support skip option const { sql, 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 sql.end(); } }, ); // cSpell: ignore regclass
packages/postgres/src/kv.ts +36 −1 Original line number Diff line number Diff line import type { KvKey, KvStore, KvStoreSetOptions } from "@fedify/fedify"; import type { KvKey, KvStore, KvStoreListEntry, KvStoreListOptions, KvStoreSetOptions, } from "@fedify/fedify"; import { getLogger } from "@logtape/logtape"; import type { JSONValue, Parameter, Sql } from "postgres"; import { driverSerializesJson } from "./utils.ts"; Loading Loading @@ -107,6 +113,35 @@ export class PostgresKvStore implements KvStore { await this.#expire(); } /** * {@inheritDoc KvStore.list} * @since 1.10.0 */ async *list( options: KvStoreListOptions, ): AsyncIterable<KvStoreListEntry> { await this.initialize(); const prefix = options.prefix; const prefixLength = prefix.length; const results = await this.#sql` SELECT key, value FROM ${this.#sql(this.#tableName)} WHERE array_length(key, 1) >= ${prefixLength} AND key[1:${prefixLength}] = ${prefix}::text[] AND (ttl IS NULL OR created + ttl > CURRENT_TIMESTAMP) ORDER BY key `; for (const row of results) { yield { key: row.key as KvKey, value: 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