diff --git a/apps/server/src/__tests__/keys-browser.test.ts b/apps/server/src/__tests__/keys-browser.test.ts index 03b237dc..f5ea8f8e 100644 --- a/apps/server/src/__tests__/keys-browser.test.ts +++ b/apps/server/src/__tests__/keys-browser.test.ts @@ -58,6 +58,7 @@ describe("getKeyInfo", () => { assert.strictEqual(result.size, 0) }) + }) describe("hash keys", () => { @@ -82,10 +83,11 @@ describe("getKeyInfo", () => { { key: "field2", value: "value2" }, ]) }) + }) describe("list keys", () => { - it("should get list key info", async () => { + it("should get list key info with readable elements", async () => { const mockClient = createMockClient({ TYPE: "list", TTL: 0, @@ -99,8 +101,79 @@ describe("getKeyInfo", () => { assert.strictEqual(result.name, "mylist") assert.strictEqual(result.type, "list") assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) assert.deepStrictEqual(result.elements, ["item1", "item2"]) }) + + }) + + describe("zset keys", () => { + it("should get zset key info with readable members", async () => { + const mockClient = createMockClient({ + TYPE: "zset", + TTL: -1, + MEMORY: 200, + ZCARD: 2, + ZRANGE: [{ key: "member1", value: "1.5" }, { key: "member2", value: "2.5" }], + }) + + const result = await getKeyInfo(mockClient as any, "myzset") + + assert.strictEqual(result.name, "myzset") + assert.strictEqual(result.type, "zset") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + // RESP3 ZRANGE with WITHSCORES returns [{key: member, value: score}, ...] + assert.deepStrictEqual(result.elements, [{ key: "member1", value: "1.5" }, { key: "member2", value: "2.5" }]) + }) + + }) + + describe("stream keys", () => { + it("should get stream key info with readable entries", async () => { + const mockClient = createMockClient({ + TYPE: "stream", + TTL: -1, + MEMORY: 300, + XLEN: 2, + XRANGE: [ + ["1234567890-0", ["field1", "value1", "field2", "value2"]], + ["1234567891-0", ["field3", "value3"]], + ], + }) + + const result = await getKeyInfo(mockClient as any, "mystream") + + assert.strictEqual(result.name, "mystream") + assert.strictEqual(result.type, "stream") + assert.strictEqual(result.collectionSize, 2) + assert.ok(Array.isArray(result.elements)) + // Stream entries are nested arrays: [[id, [field, value, ...]], ...] + const elements = result.elements as string[][] + assert.strictEqual(elements.length, 2) + assert.ok(Array.isArray(elements[0])) + assert.ok(Array.isArray(elements[1])) + }) + + }) + + describe("json keys", () => { + it("should get json key info with readable JSON string", async () => { + const jsonValue = "{\"name\":\"test\",\"value\":123}" + const mockClient = createMockClient({ + TYPE: "rejson-rl", + TTL: -1, + MEMORY: 100, + "JSON.GET": jsonValue, + }) + + const result = await getKeyInfo(mockClient as any, "myjson") + + assert.strictEqual(result.name, "myjson") + assert.strictEqual(result.type, "rejson-rl") + assert.strictEqual(result.elements, jsonValue) + }) + }) describe("error handling", () => { diff --git a/apps/server/src/__tests__/utils.test.ts b/apps/server/src/__tests__/utils.test.ts index b45649af..0692e383 100644 --- a/apps/server/src/__tests__/utils.test.ts +++ b/apps/server/src/__tests__/utils.test.ts @@ -178,3 +178,4 @@ valkey_version:8.0.0`, assert.throws(() => parseClusterInfo(123 as any)) }) }) + diff --git a/apps/server/src/keys-browser.ts b/apps/server/src/keys-browser.ts index 3fd9570e..93319dc9 100644 --- a/apps/server/src/keys-browser.ts +++ b/apps/server/src/keys-browser.ts @@ -6,7 +6,8 @@ import { RouteOption, ConnectionError, TimeoutError, - ClosingError + ClosingError, + GlideReturnType } from "@valkey/valkey-glide" import pLimit from "p-limit" import { VALKEY, VALKEY_CLIENT } from "../../../common/src/constants.ts" @@ -30,8 +31,7 @@ async function getScanKeyInfo( commands: { sizeCmd: string; elementsCmd: string[] }, ): Promise { try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const results = new Set() + const results = new Set() const isHash = keyInfo.type.toLowerCase() === "hash" let cursor = "0" @@ -41,17 +41,19 @@ async function getScanKeyInfo( client.customCommand([commands.sizeCmd, keyInfo.name]), (async () => { do { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [newCursor, elements] = await client.customCommand([...commands.elementsCmd, cursor]) as [string, any[]] + const [newCursor, elements] = await client.customCommand([...commands.elementsCmd, cursor]) as [string, GlideReturnType[]] if (isHash) { // Hash key types require constructing an object from a flat array. // i.e. converting [key1, value1...] to [{key: key1, value}] for (let i = 0; i < elements.length; i += 2){ - results.add({ key: elements[i], value: elements[i + 1] }) + results.add({ + key: elements[i] as string, + value: elements[i + 1] as string, + }) } } else { - elements.forEach((element) => results.add(element)) + elements.forEach((element) => results.add(element as string)) } cursor = newCursor } while (cursor !== "0") @@ -65,7 +67,10 @@ async function getScanKeyInfo( } } catch (err) { console.log(`Could not get elements for key ${keyInfo.name}:`, err) - return keyInfo + return { + ...keyInfo, + elementsWarning: VALKEY_CLIENT.MESSAGES.NOT_READABLE, + } } } @@ -98,6 +103,10 @@ async function getFullKeyInfo( } } catch (err) { console.log(`Could not get elements for key ${keyInfo.name}:`, err) + // Valkey client uses String decoder, which throws this error when it encounters non-UTF-8 bytes + if (err instanceof Error && err.message.includes("Decoding error")) { + return { ...keyInfo, elementsWarning: VALKEY_CLIENT.MESSAGES.NOT_READABLE } + } return keyInfo } } diff --git a/common/src/constants.ts b/common/src/constants.ts index ac274931..d82ec70a 100644 --- a/common/src/constants.ts +++ b/common/src/constants.ts @@ -131,6 +131,9 @@ export const VALKEY_CLIENT = { defaultCount: 50, } , KEY_VALUE_SIZE_LIMIT: 2048, // 2KiB + MESSAGES: { + NOT_READABLE: "Not human readable.", + }, } export const COMMANDLOG_TYPE = { SLOW: "slow",