From 82615ab72bc68b1eb8dffd1ea062fe3c7662e0b3 Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Tue, 22 Nov 2022 13:22:30 +0100 Subject: [PATCH 1/2] feat: hrandfield --- examples/nextjs/package.json | 2 +- pkg/commands/hgetall.ts | 4 --- pkg/commands/hrandfield.test.ts | 52 +++++++++++++++++++++++++++++++++ pkg/commands/hrandfield.ts | 46 +++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 pkg/commands/hrandfield.test.ts create mode 100644 pkg/commands/hrandfield.ts diff --git a/examples/nextjs/package.json b/examples/nextjs/package.json index 9f2078b2..eb0525d9 100644 --- a/examples/nextjs/package.json +++ b/examples/nextjs/package.json @@ -7,7 +7,7 @@ "start": "next start" }, "dependencies": { - "@upstash/redis": "0.0.0-ci.1882836b-20221026", + "@upstash/redis": "1.16.1", "next": "^12.3.1", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/pkg/commands/hgetall.ts b/pkg/commands/hgetall.ts index 7de97860..e889a8e9 100644 --- a/pkg/commands/hgetall.ts +++ b/pkg/commands/hgetall.ts @@ -1,9 +1,5 @@ import { Command, CommandOptions } from "./command.ts"; -/** - * @param result De - * @returns - */ function deserialize>( result: string[], ): TData | null { diff --git a/pkg/commands/hrandfield.test.ts b/pkg/commands/hrandfield.test.ts new file mode 100644 index 00000000..102aa9c9 --- /dev/null +++ b/pkg/commands/hrandfield.test.ts @@ -0,0 +1,52 @@ +import { keygen, newHttpClient, randomID } from "../test-utils.ts"; +import { assert, assertEquals } from "https://deno.land/std@0.152.0/testing/asserts.ts"; +import { afterAll } from "https://deno.land/std@0.152.0/testing/bdd.ts"; +import { HSetCommand } from "./hset.ts"; +import { HRandFieldCommand } from "./hrandfield.ts"; + +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); +Deno.test("with single field present", async (t) => { + + await t.step("returns the field", async () => { + + const key = newKey(); + const field1 = randomID(); + const value1 = randomID(); + await new HSetCommand([key, { [field1]: value1 }]).exec( + client, + ); + + const res = await new HRandFieldCommand([key]).exec(client); + + assertEquals(res, field1); + }) +}) + + +Deno.test("with multiple fields present", async (t) => { + + await t.step("returns a random field", async () => { + + const key = newKey(); + const fields: Record = {} + for (let i = 0; i < 10; i++) { + fields[randomID()] = randomID() + } + await new HSetCommand([key, fields]).exec( + client, + ); + + const res = await new HRandFieldCommand([key]).exec(client); + + assert(res in fields); + }) +}) +Deno.test("when hash does not exist", async (t) => { + await t.step("it returns null", async () => { + const res = await new HRandFieldCommand([randomID()]).exec(client); + assertEquals(res, null); + }); +}); diff --git a/pkg/commands/hrandfield.ts b/pkg/commands/hrandfield.ts new file mode 100644 index 00000000..4820c2d7 --- /dev/null +++ b/pkg/commands/hrandfield.ts @@ -0,0 +1,46 @@ +import { Command, CommandOptions } from "./command.ts"; + + +function deserialize>( + result: string[], +): TData | null { + if (result.length === 0) { + return null; + } + const obj: Record = {}; + while (result.length >= 2) { + const key = result.shift()!; + const value = result.shift()!; + try { + obj[key] = JSON.parse(value); + } catch { + obj[key] = value; + } + } + return obj as TData; +} + +/** + * @see https://redis.io/commands/hrandfield + */ +export class HRandFieldCommand> extends Command< + string | string[], + TData +> { + constructor(cmd: [key: string], opts?: CommandOptions) + constructor(cmd: [key: string, count: number], opts?: CommandOptions) + constructor(cmd: [key: string, count: number, withValues: true], opts?: CommandOptions>) + constructor(cmd: [key: string, count?: number, withValues?: boolean], opts?: CommandOptions) { + const command = ["hrandfield", cmd[0]] as unknown[] + if (typeof cmd[1] === "number") { + command.push(cmd[1]) + } + if (cmd[2]) { + command.push("WITHVALUES") + } + super(command, { + // @ts-ignore TODO: + deserialize: cmd[2] ? (result) => deserialize(result as string[]) : opts?.deserialize, ...opts + }); + } +} From 2dc1722518ce577174f4a312a045b6e1365c4e82 Mon Sep 17 00:00:00 2001 From: Andreas Thomas Date: Thu, 24 Nov 2022 14:30:13 +0100 Subject: [PATCH 2/2] feat: hrandfield types --- pkg/commands/hrandfield.test.ts | 47 ++++++++++++++++++++++++--------- pkg/commands/hrandfield.ts | 35 ++++++++++++++++-------- pkg/commands/mod.ts | 3 ++- pkg/pipeline.test.ts | 5 +++- pkg/pipeline.ts | 16 +++++++++++ pkg/redis.ts | 20 ++++++++++++++ 6 files changed, 100 insertions(+), 26 deletions(-) diff --git a/pkg/commands/hrandfield.test.ts b/pkg/commands/hrandfield.test.ts index 102aa9c9..595a9a3c 100644 --- a/pkg/commands/hrandfield.test.ts +++ b/pkg/commands/hrandfield.test.ts @@ -1,5 +1,8 @@ import { keygen, newHttpClient, randomID } from "../test-utils.ts"; -import { assert, assertEquals } from "https://deno.land/std@0.152.0/testing/asserts.ts"; +import { + assert, + assertEquals, +} from "https://deno.land/std@0.152.0/testing/asserts.ts"; import { afterAll } from "https://deno.land/std@0.152.0/testing/bdd.ts"; import { HSetCommand } from "./hset.ts"; import { HRandFieldCommand } from "./hrandfield.ts"; @@ -9,9 +12,7 @@ const client = newHttpClient(); const { newKey, cleanup } = keygen(); afterAll(cleanup); Deno.test("with single field present", async (t) => { - await t.step("returns the field", async () => { - const key = newKey(); const field1 = randomID(); const value1 = randomID(); @@ -22,28 +23,48 @@ Deno.test("with single field present", async (t) => { const res = await new HRandFieldCommand([key]).exec(client); assertEquals(res, field1); - }) -}) - + }); +}); Deno.test("with multiple fields present", async (t) => { - await t.step("returns a random field", async () => { - const key = newKey(); - const fields: Record = {} + const fields: Record = {}; for (let i = 0; i < 10; i++) { - fields[randomID()] = randomID() + fields[randomID()] = randomID(); } await new HSetCommand([key, fields]).exec( client, ); - const res = await new HRandFieldCommand([key]).exec(client); + const res = await new HRandFieldCommand([key]).exec(client); assert(res in fields); - }) -}) + }); +}); + +Deno.test("with withvalues", async (t) => { + await t.step("returns a subset with values", async () => { + const key = newKey(); + const fields: Record = {}; + for (let i = 0; i < 10; i++) { + fields[randomID()] = randomID(); + } + await new HSetCommand([key, fields]).exec( + client, + ); + + const res = await new HRandFieldCommand>([ + key, + 2, + true, + ]).exec(client); + for (const [k, v] of Object.entries(res)) { + assert(k in fields); + assert(fields[k] === v); + } + }); +}); Deno.test("when hash does not exist", async (t) => { await t.step("it returns null", async () => { const res = await new HRandFieldCommand([randomID()]).exec(client); diff --git a/pkg/commands/hrandfield.ts b/pkg/commands/hrandfield.ts index 4820c2d7..c56dbf0f 100644 --- a/pkg/commands/hrandfield.ts +++ b/pkg/commands/hrandfield.ts @@ -1,6 +1,5 @@ import { Command, CommandOptions } from "./command.ts"; - function deserialize>( result: string[], ): TData | null { @@ -23,24 +22,38 @@ function deserialize>( /** * @see https://redis.io/commands/hrandfield */ -export class HRandFieldCommand> extends Command< +export class HRandFieldCommand< + TData extends string | string[] | Record, +> extends Command< string | string[], - TData + TData > { - constructor(cmd: [key: string], opts?: CommandOptions) - constructor(cmd: [key: string, count: number], opts?: CommandOptions) - constructor(cmd: [key: string, count: number, withValues: true], opts?: CommandOptions>) - constructor(cmd: [key: string, count?: number, withValues?: boolean], opts?: CommandOptions) { - const command = ["hrandfield", cmd[0]] as unknown[] + constructor(cmd: [key: string], opts?: CommandOptions); + constructor( + cmd: [key: string, count: number], + opts?: CommandOptions, + ); + constructor( + cmd: [key: string, count: number, withValues: boolean], + opts?: CommandOptions>, + ); + constructor( + cmd: [key: string, count?: number, withValues?: boolean], + opts?: CommandOptions>, + ) { + const command = ["hrandfield", cmd[0]] as unknown[]; if (typeof cmd[1] === "number") { - command.push(cmd[1]) + command.push(cmd[1]); } if (cmd[2]) { - command.push("WITHVALUES") + command.push("WITHVALUES"); } super(command, { // @ts-ignore TODO: - deserialize: cmd[2] ? (result) => deserialize(result as string[]) : opts?.deserialize, ...opts + deserialize: cmd[2] + ? (result) => deserialize(result as string[]) + : opts?.deserialize, + ...opts, }); } } diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index fd80825e..d032df34 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -17,9 +17,9 @@ export * from "./flushall.ts"; export * from "./flushdb.ts"; export * from "./get.ts"; export * from "./getbit.ts"; +export * from "./getdel.ts"; export * from "./getrange.ts"; export * from "./getset.ts"; -export * from "./getdel.ts"; export * from "./hdel.ts"; export * from "./hexists.ts"; export * from "./hget.ts"; @@ -30,6 +30,7 @@ export * from "./hkeys.ts"; export * from "./hlen.ts"; export * from "./hmget.ts"; export * from "./hmset.ts"; +export * from "./hrandfield.ts"; export * from "./hscan.ts"; export * from "./hset.ts"; export * from "./hsetnx.ts"; diff --git a/pkg/pipeline.test.ts b/pkg/pipeline.test.ts index 2272026b..ce73ea2b 100644 --- a/pkg/pipeline.test.ts +++ b/pkg/pipeline.test.ts @@ -149,6 +149,9 @@ Deno.test("use all the things", async (t) => { .lrem(newKey(), 1, "value") .lset(persistentKey, 0, "value") .ltrim(newKey(), 0, 1) + .hrandfield(newKey()) + .hrandfield(newKey(), 2) + .hrandfield(newKey(), 3, true) .mget<[string, string]>(newKey(), newKey()) .mset({ key1: "value", key2: "value" }) .msetnx({ key3: "value", key4: "value" }) @@ -215,6 +218,6 @@ Deno.test("use all the things", async (t) => { .zunionstore(newKey(), 1, [newKey()]); const res = await p.exec(); - assertEquals(res.length, 115); + assertEquals(res.length, 118); }); }); diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index ba81f7d9..311842f6 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -126,6 +126,7 @@ import { Requester } from "./http.ts"; import { UpstashResponse } from "./http.ts"; import { CommandArgs } from "./types.ts"; import { ZMScoreCommand } from "./commands/zmscore.ts"; +import { HRandFieldCommand } from "./commands/hrandfield.ts"; /** * Upstash REST API supports command pipelining to send multiple commands in @@ -431,6 +432,21 @@ export class Pipeline { hmset = (key: string, kv: { [field: string]: TData }) => this.chain(new HMSetCommand([key, kv], this.commandOptions)); + /** + * @see https://redis.io/commands/hrandfield + */ + hrandfield = >( + key: string, + count?: number, + withValues?: boolean, + ) => + this.chain( + new HRandFieldCommand( + [key, count, withValues] as any, + this.commandOptions, + ), + ); + /** * @see https://redis.io/commands/hscan */ diff --git a/pkg/redis.ts b/pkg/redis.ts index 0e1cdf51..a64d3d88 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -31,6 +31,7 @@ import { HLenCommand, HMGetCommand, HMSetCommand, + HRandFieldCommand, HScanCommand, HSetCommand, HSetNXCommand, @@ -410,6 +411,25 @@ export class Redis { hmset = (key: string, kv: { [field: string]: TData }) => new HMSetCommand([key, kv], this.opts).exec(this.client); + /** + * @see https://redis.io/commands/hrandfield + */ + hrandfield: { + (key: string): Promise; + (key: string, count: number): Promise; + >( + key: string, + count: number, + withValues: boolean, + ): Promise>; + } = >( + key: string, + count?: number, + withValues?: boolean, + ) => + new HRandFieldCommand([key, count, withValues] as any, this.opts) + .exec(this.client); + /** * @see https://redis.io/commands/hscan */