Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading