From fb11ce3a62b5fac32d108a23d64e892aa70d9606 Mon Sep 17 00:00:00 2001 From: Somajit Dey Date: Fri, 31 Jan 2025 16:38:25 +0530 Subject: [PATCH 1/4] Added command: getex --- pkg/commands/getex.test.ts | 112 +++++++++++++++++++++++++++++++++++++ pkg/commands/getex.ts | 37 ++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 pkg/commands/getex.test.ts create mode 100644 pkg/commands/getex.ts diff --git a/pkg/commands/getex.test.ts b/pkg/commands/getex.test.ts new file mode 100644 index 00000000..56e6b1cb --- /dev/null +++ b/pkg/commands/getex.test.ts @@ -0,0 +1,112 @@ +import { keygen, newHttpClient, randomID } from "../test-utils"; + +import { afterAll, describe, expect, test } from "bun:test"; +import { GetExCommand } from "./getex"; +import { GetCommand } from "./get"; +import { SetCommand } from "./set"; + +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); + +describe("without options", () => { + test("gets value", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([key]).exec(client); + expect(res2).toEqual(value); + }); +}); + +describe("ex", () => { + test("gets value and sets expiry in seconds", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([key, { ex: 1 }]).exec(client); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await new GetCommand([key]).exec(client); + expect(res3).toEqual(null); + }); +}); + +describe("px", () => { + test("gets value and sets expiry in milliseconds", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([key, { px: 1000 }]).exec(client); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await new GetCommand([key]).exec(client); + + expect(res3).toEqual(null); + }); +}); + +describe("exat", () => { + test("gets value and sets expiry in Unix time (seconds)", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([ + key, + { + exat: Math.floor(Date.now() / 1000) + 2, + }, + ]).exec(client); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 3000)); + + const res3 = await new GetCommand([key]).exec(client); + + expect(res3).toEqual(null); + }); +}); + +describe("pxat", () => { + test("gets value and sets expiry in Unix time (milliseconds)", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([key, { pxat: Date.now() + 2000 }]).exec(client); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 3000)); + + const res3 = await new GetCommand([key]).exec(client); + + expect(res3).toEqual(null); + }); +}); + +describe("persist", () => { + test("gets value and removes expiry", async () => { + const key = newKey(); + const value = randomID(); + + const res = await new SetCommand([key, value, { ex: 1 }]).exec(client); + expect(res).toEqual("OK"); + const res2 = await new GetExCommand([key, { persist: true }]).exec(client); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await new GetCommand([key]).exec(client); + + expect(res3).toEqual(value); + }); +}); diff --git a/pkg/commands/getex.ts b/pkg/commands/getex.ts new file mode 100644 index 00000000..deb08b84 --- /dev/null +++ b/pkg/commands/getex.ts @@ -0,0 +1,37 @@ +import type { CommandOptions } from "./command"; +import { Command } from "./command"; + +type GetExCommandOptions = ( + { ex: number; px?: never; exat?: never; pxat?: never; persist?: never } + | { ex?: never; px: number; exat?: never; pxat?: never; persist?: never } + | { ex?: never; px?: never; exat: number; pxat?: never; persist?: never } + | { ex?: never; px?: never; exat?: never; pxat: number; persist?: never } + | { ex?: never; px?: never; exat?: never; pxat?: never; persist: true } + | { ex?: never; px?: never; exat?: never; pxat?: never; persist?: never } +); + +/** + * @see https://redis.io/commands/getex + */ +export class GetExCommand extends Command { + constructor( + [key, opts]: [key: string, opts?: GetExCommandOptions], + cmdOpts?: CommandOptions + ) { + const command: unknown[] = ["getex", key]; + if (opts) { + if ("ex" in opts && typeof opts.ex === "number") { + command.push("ex", opts.ex); + } else if ("px" in opts && typeof opts.px === "number") { + command.push("px", opts.px); + } else if ("exat" in opts && typeof opts.exat === "number") { + command.push("exat", opts.exat); + } else if ("pxat" in opts && typeof opts.pxat === "number") { + command.push("pxat", opts.pxat); + } else if ("persist" in opts && opts.persist) { + command.push("persist"); + } + } + super(command, cmdOpts); + } +} From 15d835877ebd75ed396b08df31df3a1d25dde7c9 Mon Sep 17 00:00:00 2001 From: Somajit Dey Date: Fri, 31 Jan 2025 18:32:39 +0530 Subject: [PATCH 2/4] Updated other modules regarding GetExCommand. Also added an extra test: redis_getex --- pkg/auto-pipeline.test.ts | 1 + pkg/commands/mod.ts | 1 + pkg/commands/redis_getex.test.ts | 120 +++++++++++++++++++++++++++++++ pkg/commands/types.ts | 1 + pkg/pipeline.test.ts | 17 ++--- pkg/pipeline.ts | 6 ++ pkg/redis.ts | 8 +++ 7 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 pkg/commands/redis_getex.test.ts diff --git a/pkg/auto-pipeline.test.ts b/pkg/auto-pipeline.test.ts index dfcb39b0..2631f0cd 100644 --- a/pkg/auto-pipeline.test.ts +++ b/pkg/auto-pipeline.test.ts @@ -44,6 +44,7 @@ describe("Auto pipeline", () => { redis.get(newKey()), redis.getbit(newKey(), 0), redis.getdel(newKey()), + redis.getex(newKey()), redis.getset(newKey(), "hello"), redis.hdel(newKey(), "field"), redis.hexists(newKey(), "field"), diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index 0c98551e..65b71cb0 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -27,6 +27,7 @@ export * from "./geo_search_store"; export * from "./get"; export * from "./getbit"; export * from "./getdel"; +export * from "./getex"; export * from "./getrange"; export * from "./getset"; export * from "./hdel"; diff --git a/pkg/commands/redis_getex.test.ts b/pkg/commands/redis_getex.test.ts new file mode 100644 index 00000000..4fcd2a82 --- /dev/null +++ b/pkg/commands/redis_getex.test.ts @@ -0,0 +1,120 @@ +import { Redis } from "../redis"; +import { keygen, newHttpClient, randomID } from "../test-utils"; + +import { afterAll, describe, expect, test } from "bun:test"; + +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); + +describe("without options", () => { + test("gets value", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value); + expect(res).toEqual("OK"); + const res2 = await redis.getex(key); + expect(res2).toEqual(value); + }); +}); + +describe("ex", () => { + test("gets value and sets expiry in seconds", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value); + expect(res).toEqual("OK"); + const [res1, res2] = await Promise.all([ + redis.exists(key), + redis.getex(key, { ex: 1 }) + ]); + expect(res1).toEqual(1); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await redis.get(key); + expect(res3).toEqual(null); + }); +}); + +describe("px", () => { + test("gets value and sets expiry in milliseconds", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value); + expect(res).toEqual("OK"); + const res2 = await redis.getex(key, { px: 1000 }); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await redis.get(key); + + expect(res3).toEqual(null); + }); +}); + +describe("exat", () => { + test("gets value and sets expiry in Unix time (seconds)", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value); + expect(res).toEqual("OK"); + const res2 = await redis.getex( + key, + { + exat: Math.floor(Date.now() / 1000) + 2, + }, + ); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 3000)); + + const res3 = await redis.get(key); + + expect(res3).toEqual(null); + }); +}); + +describe("pxat", () => { + test("gets value and sets expiry in Unix time (milliseconds)", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value); + expect(res).toEqual("OK"); + const res2 = await redis.getex(key, { pxat: Date.now() + 2000 }); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 3000)); + + const res3 = await redis.get(key); + + expect(res3).toEqual(null); + }); +}); + +describe("persist", () => { + test("gets value and removes expiry", async () => { + const redis = new Redis(client); + const key = newKey(); + const value = randomID(); + + const res = await redis.set(key, value, { ex: 1 }); + expect(res).toEqual("OK"); + const res2 = await redis.getex(key, { persist: true }); + expect(res2).toEqual(value); + await new Promise((res) => setTimeout(res, 2000)); + + const res3 = await redis.get(key); + + expect(res3).toEqual(value); + }); +}); diff --git a/pkg/commands/types.ts b/pkg/commands/types.ts index bbe99801..330da960 100644 --- a/pkg/commands/types.ts +++ b/pkg/commands/types.ts @@ -24,6 +24,7 @@ export { type GeoSearchStoreCommand } from "./geo_search_store"; export { type GetCommand } from "./get"; export { type GetBitCommand } from "./getbit"; export { type GetDelCommand } from "./getdel"; +export { type GetExCommand } from "./getex"; export { type GetRangeCommand } from "./getrange"; export { type GetSetCommand } from "./getset"; export { type HDelCommand } from "./hdel"; diff --git a/pkg/pipeline.test.ts b/pkg/pipeline.test.ts index dc18ad2a..72b15d7c 100644 --- a/pkg/pipeline.test.ts +++ b/pkg/pipeline.test.ts @@ -146,6 +146,7 @@ describe("use all the things", () => { .get(newKey()) .getbit(newKey(), 0) .getdel(newKey()) + .getex(newKey()) .getset(newKey(), "hello") .hdel(newKey(), "field") .hexists(newKey(), "field") @@ -249,7 +250,7 @@ describe("use all the things", () => { .json.set(newKey(), "$", { hello: "world" }); const res = await p.exec(); - expect(res.length).toEqual(121); + expect(res.length).toEqual(122); }); }); describe("keep errors", () => { @@ -257,7 +258,7 @@ describe("keep errors", () => { const p = new Pipeline({ client }); p.set("foo", "1"); p.set("bar", "2"); - p.get("foo"); + p.getex("foo", {ex: 1}); p.get("bar"); const results = await p.exec({ keepErrors: true }); @@ -273,11 +274,11 @@ describe("keep errors", () => { const p = new Pipeline({ client }); p.set("foo", "1"); p.set("bar", "2"); - p.evalsha("wrong-sha1", [], []); - p.get("foo"); + //p.evalsha("wrong-sha1", [], []); + p.getex("foo", {exat: 123}); p.get("bar"); expect(() => p.exec()).toThrow( - "Command 3 [ evalsha ] failed: NOSCRIPT No matching script. Please use EVAL." + "Command 3 [ getex ] failed: ERR invalid expire time" ); }); @@ -286,18 +287,18 @@ describe("keep errors", () => { p.set("foo", "1"); p.set("bar", "2"); p.evalsha("wrong-sha1", [], []); - p.get("foo"); + p.getex("foo", {exat: 123}); p.get("bar"); const results = await p.exec<[string, string, string, number, number]>({ keepErrors: true }); expect(results[0].error).toBeUndefined(); expect(results[1].error).toBeUndefined(); expect(results[2].error).toBe("NOSCRIPT No matching script. Please use EVAL."); - expect(results[3].error).toBeUndefined(); + expect(results[3].error).toBe("ERR invalid expire time"); expect(results[4].error).toBeUndefined(); expect(results[2].result).toBeUndefined(); - expect(results[3].result).toBe(1); + expect(results[3].result).toBeUndefined(); expect(results[4].result).toBe(2); }); }); diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index ce235109..90ab2f02 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -34,6 +34,7 @@ import { GetBitCommand, GetCommand, GetDelCommand, + GetExCommand, GetRangeCommand, GetSetCommand, HDelCommand, @@ -544,6 +545,11 @@ export class Pipeline[] = []> { */ getdel = (...args: CommandArgs) => this.chain(new GetDelCommand(args, this.commandOptions)); + /** + * @see https://redis.io/commands/getex + */ + getex = (...args: CommandArgs) => + this.chain(new GetExCommand(args, this.commandOptions)); /** * @see https://redis.io/commands/getrange */ diff --git a/pkg/redis.ts b/pkg/redis.ts index 8ec32c75..d2bb0fdb 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -3,6 +3,7 @@ import type { CommandOptions, ScoreMember, SetCommandOptions, + GetExCommandOptions, ZAddCommandOptions, ZRangeCommandOptions, } from "./commands/mod"; @@ -35,6 +36,7 @@ import { GetBitCommand, GetCommand, GetDelCommand, + GetExCommand, GetRangeCommand, GetSetCommand, HDelCommand, @@ -621,6 +623,12 @@ export class Redis { getdel = (...args: CommandArgs) => new GetDelCommand(args, this.opts).exec(this.client); + /** + * @see https://redis.io/commands/getex + */ + getex = (...args: CommandArgs) => + new GetExCommand(args, this.opts).exec(this.client); + /** * @see https://redis.io/commands/getrange */ From 721f9cbecfe46fae03a0ac9fab12359aee806c99 Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 31 Jan 2025 19:08:59 +0300 Subject: [PATCH 3/4] fix: tests --- pkg/auto-pipeline.test.ts | 2 +- pkg/commands/redis_getex.test.ts | 120 ------------------------------- pkg/pipeline.test.ts | 10 +-- pkg/redis.ts | 1 - 4 files changed, 6 insertions(+), 127 deletions(-) delete mode 100644 pkg/commands/redis_getex.test.ts diff --git a/pkg/auto-pipeline.test.ts b/pkg/auto-pipeline.test.ts index 7fe9f318..4ec68c07 100644 --- a/pkg/auto-pipeline.test.ts +++ b/pkg/auto-pipeline.test.ts @@ -149,7 +149,7 @@ describe("Auto pipeline", () => { redis.json.arrappend(persistentKey3, "$.log", '"three"'), ]); expect(result).toBeTruthy(); - expect(result.length).toBe(121); // returns + expect(result.length).toBe(122); // returns // @ts-expect-error pipelineCounter is not in type but accessible120 results expect(redis.pipelineCounter).toBe(1); }); diff --git a/pkg/commands/redis_getex.test.ts b/pkg/commands/redis_getex.test.ts deleted file mode 100644 index 4fcd2a82..00000000 --- a/pkg/commands/redis_getex.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Redis } from "../redis"; -import { keygen, newHttpClient, randomID } from "../test-utils"; - -import { afterAll, describe, expect, test } from "bun:test"; - -const client = newHttpClient(); - -const { newKey, cleanup } = keygen(); -afterAll(cleanup); - -describe("without options", () => { - test("gets value", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value); - expect(res).toEqual("OK"); - const res2 = await redis.getex(key); - expect(res2).toEqual(value); - }); -}); - -describe("ex", () => { - test("gets value and sets expiry in seconds", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value); - expect(res).toEqual("OK"); - const [res1, res2] = await Promise.all([ - redis.exists(key), - redis.getex(key, { ex: 1 }) - ]); - expect(res1).toEqual(1); - expect(res2).toEqual(value); - await new Promise((res) => setTimeout(res, 2000)); - - const res3 = await redis.get(key); - expect(res3).toEqual(null); - }); -}); - -describe("px", () => { - test("gets value and sets expiry in milliseconds", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value); - expect(res).toEqual("OK"); - const res2 = await redis.getex(key, { px: 1000 }); - expect(res2).toEqual(value); - await new Promise((res) => setTimeout(res, 2000)); - - const res3 = await redis.get(key); - - expect(res3).toEqual(null); - }); -}); - -describe("exat", () => { - test("gets value and sets expiry in Unix time (seconds)", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value); - expect(res).toEqual("OK"); - const res2 = await redis.getex( - key, - { - exat: Math.floor(Date.now() / 1000) + 2, - }, - ); - expect(res2).toEqual(value); - await new Promise((res) => setTimeout(res, 3000)); - - const res3 = await redis.get(key); - - expect(res3).toEqual(null); - }); -}); - -describe("pxat", () => { - test("gets value and sets expiry in Unix time (milliseconds)", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value); - expect(res).toEqual("OK"); - const res2 = await redis.getex(key, { pxat: Date.now() + 2000 }); - expect(res2).toEqual(value); - await new Promise((res) => setTimeout(res, 3000)); - - const res3 = await redis.get(key); - - expect(res3).toEqual(null); - }); -}); - -describe("persist", () => { - test("gets value and removes expiry", async () => { - const redis = new Redis(client); - const key = newKey(); - const value = randomID(); - - const res = await redis.set(key, value, { ex: 1 }); - expect(res).toEqual("OK"); - const res2 = await redis.getex(key, { persist: true }); - expect(res2).toEqual(value); - await new Promise((res) => setTimeout(res, 2000)); - - const res3 = await redis.get(key); - - expect(res3).toEqual(value); - }); -}); diff --git a/pkg/pipeline.test.ts b/pkg/pipeline.test.ts index 72b15d7c..44f0cbc6 100644 --- a/pkg/pipeline.test.ts +++ b/pkg/pipeline.test.ts @@ -258,7 +258,7 @@ describe("keep errors", () => { const p = new Pipeline({ client }); p.set("foo", "1"); p.set("bar", "2"); - p.getex("foo", {ex: 1}); + p.getex("foo", { ex: 1 }); p.get("bar"); const results = await p.exec({ keepErrors: true }); @@ -274,11 +274,11 @@ describe("keep errors", () => { const p = new Pipeline({ client }); p.set("foo", "1"); p.set("bar", "2"); - //p.evalsha("wrong-sha1", [], []); - p.getex("foo", {exat: 123}); + p.evalsha("wrong-sha1", [], []); + p.get("foo"); p.get("bar"); expect(() => p.exec()).toThrow( - "Command 3 [ getex ] failed: ERR invalid expire time" + "Command 3 [ evalsha ] failed: NOSCRIPT No matching script. Please use EVAL." ); }); @@ -287,7 +287,7 @@ describe("keep errors", () => { p.set("foo", "1"); p.set("bar", "2"); p.evalsha("wrong-sha1", [], []); - p.getex("foo", {exat: 123}); + p.getex("foo", { exat: 123 }); p.get("bar"); const results = await p.exec<[string, string, string, number, number]>({ keepErrors: true }); diff --git a/pkg/redis.ts b/pkg/redis.ts index d2bb0fdb..619659e8 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -3,7 +3,6 @@ import type { CommandOptions, ScoreMember, SetCommandOptions, - GetExCommandOptions, ZAddCommandOptions, ZRangeCommandOptions, } from "./commands/mod"; From 1d8826ecb8b6224400f0372e994e39a3a872bc2a Mon Sep 17 00:00:00 2001 From: CahidArda Date: Fri, 31 Jan 2025 19:09:32 +0300 Subject: [PATCH 4/4] fix: lint --- pkg/commands/exec.ts | 22 +++++++++++----------- pkg/commands/getex.ts | 7 +++---- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/pkg/commands/exec.ts b/pkg/commands/exec.ts index 1d2373e2..36f661e3 100644 --- a/pkg/commands/exec.ts +++ b/pkg/commands/exec.ts @@ -1,23 +1,23 @@ -import { type CommandOptions, Command } from "./command"; +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("MEMORY", "USAGE", "myKey") - * + * * // Execute GET command * await redis.exec("GET", "foo") */ export class ExecCommand extends Command { - constructor( - cmd: [command: string, ...args: (string | number | boolean)[]], - opts?: CommandOptions - ){ - const normalizedCmd = cmd.map(arg => typeof arg === "string" ? arg : String(arg)); - super(normalizedCmd, opts); - } -} \ No newline at end of file + constructor( + cmd: [command: string, ...args: (string | number | boolean)[]], + opts?: CommandOptions + ) { + const normalizedCmd = cmd.map((arg) => (typeof arg === "string" ? arg : String(arg))); + super(normalizedCmd, opts); + } +} diff --git a/pkg/commands/getex.ts b/pkg/commands/getex.ts index deb08b84..d9df3f04 100644 --- a/pkg/commands/getex.ts +++ b/pkg/commands/getex.ts @@ -1,14 +1,13 @@ import type { CommandOptions } from "./command"; import { Command } from "./command"; -type GetExCommandOptions = ( - { ex: number; px?: never; exat?: never; pxat?: never; persist?: never } +type GetExCommandOptions = + | { ex: number; px?: never; exat?: never; pxat?: never; persist?: never } | { ex?: never; px: number; exat?: never; pxat?: never; persist?: never } | { ex?: never; px?: never; exat: number; pxat?: never; persist?: never } | { ex?: never; px?: never; exat?: never; pxat: number; persist?: never } | { ex?: never; px?: never; exat?: never; pxat?: never; persist: true } - | { ex?: never; px?: never; exat?: never; pxat?: never; persist?: never } -); + | { ex?: never; px?: never; exat?: never; pxat?: never; persist?: never }; /** * @see https://redis.io/commands/getex