Skip to content

Commit

Permalink
DX-1382: Generic EXEC command (#1346)
Browse files Browse the repository at this point in the history
* add: exec command

* export exec command
  • Loading branch information
fahreddinozcan authored Nov 11, 2024
1 parent a1b6b4b commit 5558861
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 0 deletions.
118 changes: 118 additions & 0 deletions pkg/commands/exec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { keygen, newHttpClient, randomID } from "../test-utils";
import { afterAll, expect, test, describe } from "bun:test";
import { ExecCommand } from "./exec";
import { SetCommand } from "./set";

const client = newHttpClient();
const { newKey, cleanup } = keygen();

afterAll(cleanup);

describe("ExecCommand", () => {
test("basic string operations", () => {
test("GET and SET", async () => {
const key = newKey();
const value = randomID();

const setRes = await new ExecCommand<"OK">(["SET", key, value]).exec(client);
expect(setRes).toEqual("OK");

const getRes = await new ExecCommand<string | null>(["GET", key]).exec(client);
expect(getRes).toEqual(value);
});
});

describe("numeric operations", () => {
test("INCR", async () => {
const key = newKey();

const incrRes = await new ExecCommand<number>(["INCR", key]).exec(client);
expect(incrRes).toEqual(1);

const incrRes2 = await new ExecCommand<number>(["INCR", key]).exec(client);
expect(incrRes2).toEqual(2);
});

test("MEMORY USAGE", async () => {
const key = newKey();
const value = randomID();

await new SetCommand([key, value]).exec(client);
const memoryRes = await new ExecCommand<number | null>(["MEMORY", "USAGE", key]).exec(client);
expect(typeof memoryRes).toEqual("number");
expect(memoryRes).toBeGreaterThan(0);
});
});

describe("array responses", () => {
test("KEYS", async () => {
const prefix = randomID();
const keys = [`${prefix}:1`, `${prefix}:2`, `${prefix}:3`];

// Set multiple keys
for (const key of keys) {
await new SetCommand([key, randomID()]).exec(client);
}

const keysRes = await new ExecCommand<string[]>(["KEYS", `${prefix}:*`]).exec(client);
expect(keysRes.length).toEqual(3);
expect(keysRes.sort()).toEqual(keys.sort());
});
});

describe("error handling", () => {
test("invalid command", async () => {
const key = newKey();

try {
await new ExecCommand<any>(["INVALID_COMMAND", key]).exec(client);
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});

test("wrong number of arguments", async () => {
try {
await new ExecCommand<any>(["GET"]).exec(client);
expect(true).toBe(false); // Should not reach here
} catch (error) {
expect(error).toBeDefined();
}
});
});

describe("argument type handling", () => {
test("numeric arguments", async () => {
const key = newKey();
const score = 99.5;
const member = randomID();

const res = await new ExecCommand<number>(["ZADD", key, score, member]).exec(client);
expect(res).toEqual(1);

const scoreRes = await new ExecCommand<[string, number]>([
"ZRANGE",
key,
0,
-1,
"WITHSCORES",
]).exec(client);

expect(scoreRes[0]).toEqual(member);
expect(scoreRes[1]).toEqual(score);
});

test("boolean arguments", async () => {
const key = newKey();
const value = randomID();

const res = await new ExecCommand<"OK" | null>(["SET", key, value, "NX"]).exec(client);
expect(res).toEqual("OK");

// Second attempt should return null due to NX flag
const res2 = await new ExecCommand<"OK" | null>(["SET", key, randomID(), "NX"]).exec(client);
expect(res2).toEqual(null);
});
});
});
23 changes: 23 additions & 0 deletions pkg/commands/exec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type CommandOptions, Command } from "./command";

/**
* Generic exec command for executing arbitrary Redis commands
* Allows executing Redis commands that might not be directly supported by the SDK
*
* @example
* // Execute MEMORY USAGE command
* await redis.exec<number>("MEMORY", "USAGE", "myKey")
*
* // Execute GET command
* await redis.exec<string>("GET", "foo")
*/

export class ExecCommand<TResult> extends Command<TResult, TResult> {
constructor(
cmd: [command: string, ...args: (string | number | boolean)[]],
opts?: CommandOptions<TResult, TResult>
){
const normalizedCmd = cmd.map(arg => typeof arg === "string" ? arg : String(arg));
super(normalizedCmd, opts);
}
}
1 change: 1 addition & 0 deletions pkg/commands/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./del";
export * from "./echo";
export * from "./eval";
export * from "./evalsha";
export * from "./exec";
export * from "./exists";
export * from "./expire";
export * from "./expireat";
Expand Down
7 changes: 7 additions & 0 deletions pkg/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
EchoCommand,
EvalCommand,
EvalshaCommand,
ExecCommand,
ExistsCommand,
ExpireAtCommand,
ExpireCommand,
Expand Down Expand Up @@ -531,6 +532,12 @@ export class Redis {
...args: [sha1: string, keys: string[], args: TArgs]
) => new EvalshaCommand<TArgs, TData>(args, this.opts).exec(this.client);

/**
* Generic method to execute any Redis command.
*/
exec = <TResult>(args: [command: string, ...args: (string | number | boolean)[]]) =>
new ExecCommand<TResult>(args, this.opts).exec(this.client);

/**
* @see https://redis.io/commands/exists
*/
Expand Down

0 comments on commit 5558861

Please sign in to comment.