diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be482a0cd..f54f490f7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,7 @@ * Node: Added XGROUP CREATE & XGROUP DESTROY commands ([#2084](https://github.com/valkey-io/valkey-glide/pull/2084)) * Node: Added BZPOPMAX & BZPOPMIN command ([#2077]((https://github.com/valkey-io/valkey-glide/pull/2077)) * Node: Added XGROUP CREATECONSUMER & XGROUP DELCONSUMER commands ([#2088](https://github.com/valkey-io/valkey-glide/pull/2088)) +* Node: Added GETEX command ([#2107]((https://github.com/valkey-io/valkey-glide/pull/2107)) #### Breaking Changes * Node: (Refactor) Convert classes to types ([#2005](https://github.com/valkey-io/valkey-glide/pull/2005)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index a4686e774b..d7070015ff 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -109,6 +109,7 @@ function initialize() { FunctionStatsResponse, SlotIdTypes, SlotKeyTypes, + TimeUnit, RouteByAddress, Routes, SingleNodeRoute, @@ -203,6 +204,7 @@ function initialize() { SlotIdTypes, SlotKeyTypes, StreamEntries, + TimeUnit, ReturnTypeXinfoStream, RouteByAddress, Routes, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index d445b60497..4d46a08c75 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -82,6 +82,7 @@ import { createGet, createGetBit, createGetDel, + createGetEx, createGetRange, createHDel, createHExists, @@ -204,6 +205,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + TimeUnit, } from "./Commands"; import { ClosingError, @@ -933,6 +935,32 @@ export class BaseClient { return this.createWritePromise(createGet(key), { decoder: decoder }); } + /** + * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. + * + * See https://valkey.io/commands/getex for more details. + * + * @param key - The key to retrieve from the database. + * @param options - (Optional) Set expiriation to the given key. + * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. + * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * @returns If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. + * + * since - Valkey 6.2.0 and above. + * + * @example + * ```typescript + * const result = await client.getex("key", {expiry: { type: TimeUnit.Seconds, count: 5 }}); + * console.log(result); // Output: 'value' + * ``` + */ + public async getex( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, + ): Promise { + return this.createWritePromise(createGetEx(key, options)); + } + /** * Gets a string value associated with the given `key`and deletes the key. * @@ -1005,7 +1033,7 @@ export class BaseClient { * console.log(result); // Output: 'OK' * * // Example usage of set method with conditional options and expiration - * const result2 = await client.set("key", "new_value", {conditionalSet: "onlyIfExists", expiry: { type: "seconds", count: 5 }}); + * const result2 = await client.set("key", "new_value", {conditionalSet: "onlyIfExists", expiry: { type: TimeUnit.Seconds, count: 5 }}); * console.log(result2); // Output: 'OK' - Set "new_value" to "key" only if "key" already exists, and set the key expiration to 5 seconds. * * // Example usage of set method with conditional options and returning old value diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 47094c43b3..d98d683748 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -142,26 +142,7 @@ export type SetOptions = { */ | "keepExisting" | { - type: /** - * Set the specified expire time, in seconds. Equivalent to - * `EX` in the Redis API. - */ - | "seconds" - /** - * Set the specified expire time, in milliseconds. Equivalent - * to `PX` in the Redis API. - */ - | "milliseconds" - /** - * Set the specified Unix time at which the key will expire, - * in seconds. Equivalent to `EXAT` in the Redis API. - */ - | "unixSeconds" - /** - * Set the specified Unix time at which the key will expire, - * in milliseconds. Equivalent to `PXAT` in the Redis API. - */ - | "unixMilliseconds"; + type: TimeUnit; count: number; }; }; @@ -187,28 +168,23 @@ export function createSet( args.push("GET"); } - if ( - options.expiry && - options.expiry !== "keepExisting" && - !Number.isInteger(options.expiry.count) - ) { - throw new Error( - `Received expiry '${JSON.stringify( - options.expiry, - )}'. Count must be an integer`, - ); - } + if (options.expiry) { + if ( + options.expiry !== "keepExisting" && + !Number.isInteger(options.expiry.count) + ) { + throw new Error( + `Received expiry '${JSON.stringify( + options.expiry, + )}'. Count must be an integer`, + ); + } - if (options.expiry === "keepExisting") { - args.push("KEEPTTL"); - } else if (options.expiry?.type === "seconds") { - args.push("EX", options.expiry.count.toString()); - } else if (options.expiry?.type === "milliseconds") { - args.push("PX", options.expiry.count.toString()); - } else if (options.expiry?.type === "unixSeconds") { - args.push("EXAT", options.expiry.count.toString()); - } else if (options.expiry?.type === "unixMilliseconds") { - args.push("PXAT", options.expiry.count.toString()); + if (options.expiry === "keepExisting") { + args.push("KEEPTTL"); + } else { + args.push(options.expiry.type, options.expiry.count.toString()); + } } } @@ -3640,3 +3616,57 @@ export function createBZPopMin( ): command_request.Command { return createCommand(RequestType.BZPopMin, [...keys, timeout.toString()]); } + +/** + * Time unit representation which is used in optional arguments for {@link BaseClient.getex|getex} and {@link BaseClient.set|set} command. + */ +export enum TimeUnit { + /** + * Set the specified expire time, in seconds. Equivalent to + * `EX` in the VALKEY API. + */ + Seconds = "EX", + /** + * Set the specified expire time, in milliseconds. Equivalent + * to `PX` in the VALKEY API. + */ + Milliseconds = "PX", + /** + * Set the specified Unix time at which the key will expire, + * in seconds. Equivalent to `EXAT` in the VALKEY API. + */ + UnixSeconds = "EXAT", + /** + * Set the specified Unix time at which the key will expire, + * in milliseconds. Equivalent to `PXAT` in the VALKEY API. + */ + UnixMilliseconds = "PXAT", +} + +/** + * @internal + */ +export function createGetEx( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, +): command_request.Command { + const args = [key]; + + if (options) { + if (options !== "persist" && !Number.isInteger(options.duration)) { + throw new Error( + `Received expiry '${JSON.stringify( + options.duration, + )}'. Count must be an integer`, + ); + } + + if (options === "persist") { + args.push("PERSIST"); + } else { + args.push(options.type, options.duration.toString()); + } + } + + return createCommand(RequestType.GetEx, args); +} diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index b1441ea566..3da471684c 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -106,6 +106,7 @@ import { createGet, createGetBit, createGetDel, + createGetEx, createGetRange, createHDel, createHExists, @@ -240,6 +241,7 @@ import { createZRevRankWithScore, createZScan, createZScore, + TimeUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; @@ -293,6 +295,7 @@ export class BaseTransaction> { } /** Get the value associated with the given key, or null if no such value exists. + * * See https://valkey.io/commands/get/ for details. * * @param key - The key to retrieve from the database. @@ -303,6 +306,26 @@ export class BaseTransaction> { return this.addAndReturn(createGet(key)); } + /** + * Get the value of `key` and optionally set its expiration. `GETEX` is similar to {@link get}. + * See https://valkey.io/commands/getex for more details. + * + * @param key - The key to retrieve from the database. + * @param options - (Optional) set expiriation to the given key. + * "persist" will retain the time to live associated with the key. Equivalent to `PERSIST` in the VALKEY API. + * Otherwise, a {@link TimeUnit} and duration of the expire time should be specified. + * + * since - Valkey 6.2.0 and above. + * + * Command Response - If `key` exists, returns the value of `key` as a `string`. Otherwise, return `null`. + */ + public getex( + key: string, + options?: "persist" | { type: TimeUnit; duration: number }, + ): T { + return this.addAndReturn(createGetEx(key, options)); + } + /** * Gets a string value associated with the given `key`and deletes the key. * diff --git a/node/tests/GlideClientInternals.test.ts b/node/tests/GlideClientInternals.test.ts index de3af5e4d4..72ee0fe8f1 100644 --- a/node/tests/GlideClientInternals.test.ts +++ b/node/tests/GlideClientInternals.test.ts @@ -32,6 +32,7 @@ import { RequestError, ReturnType, SlotKeyTypes, + TimeUnit, } from ".."; import { command_request, @@ -545,7 +546,7 @@ describe("SocketConnectionInternals", () => { const request1 = connection.set("foo", "bar", { conditionalSet: "onlyIfExists", returnOldValue: true, - expiry: { type: "seconds", count: 10 }, + expiry: { type: TimeUnit.Seconds, count: 10 }, }); expect(await request1).toMatch("OK"); diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index 5c24fd6f78..19228485c3 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -39,6 +39,7 @@ import { Script, SignedEncoding, SortOrder, + TimeUnit, Transaction, UnsignedEncoding, UpdateByScore, @@ -5672,7 +5673,7 @@ export function runBaseTests(config: { const value = uuidv4(); const setResWithExpirySetMilli = await client.set(key, value, { expiry: { - type: "milliseconds", + type: TimeUnit.Milliseconds, count: 500, }, }); @@ -5682,7 +5683,7 @@ export function runBaseTests(config: { const setResWithExpirySec = await client.set(key, value, { expiry: { - type: "seconds", + type: TimeUnit.Seconds, count: 1, }, }); @@ -5692,7 +5693,7 @@ export function runBaseTests(config: { const setWithUnixSec = await client.set(key, value, { expiry: { - type: "unixSeconds", + type: TimeUnit.UnixSeconds, count: Math.floor(Date.now() / 1000) + 1, }, }); @@ -5714,7 +5715,7 @@ export function runBaseTests(config: { expect(getResExpire).toEqual(null); const setResWithExpiryWithUmilli = await client.set(key, value, { expiry: { - type: "unixMilliseconds", + type: TimeUnit.UnixMilliseconds, count: Date.now() + 1000, }, }); @@ -5806,7 +5807,7 @@ export function runBaseTests(config: { // * returns the old value const setResWithAllOptions = await client.set(key, value, { expiry: { - type: "unixSeconds", + type: TimeUnit.UnixSeconds, count: Math.floor(Date.now() / 1000) + 1, }, conditionalSet: "onlyIfExists", @@ -5823,10 +5824,10 @@ export function runBaseTests(config: { const value = uuidv4(); const count = 2; const expiryCombination = [ - { type: "seconds", count }, - { type: "unixSeconds", count }, - { type: "unixMilliseconds", count }, - { type: "milliseconds", count }, + { type: TimeUnit.Seconds, count }, + { type: TimeUnit.Milliseconds, count }, + { type: TimeUnit.UnixSeconds, count }, + { type: TimeUnit.UnixMilliseconds, count }, "keepExisting", ]; let exist = false; @@ -5837,10 +5838,10 @@ export function runBaseTests(config: { | "keepExisting" | { type: - | "seconds" - | "milliseconds" - | "unixSeconds" - | "unixMilliseconds"; + | TimeUnit.Seconds + | TimeUnit.Milliseconds + | TimeUnit.UnixSeconds + | TimeUnit.UnixMilliseconds; count: number; }, conditionalSet: "onlyIfDoesNotExist", @@ -5863,10 +5864,10 @@ export function runBaseTests(config: { | "keepExisting" | { type: - | "seconds" - | "milliseconds" - | "unixSeconds" - | "unixMilliseconds"; + | TimeUnit.Seconds + | TimeUnit.Milliseconds + | TimeUnit.UnixSeconds + | TimeUnit.UnixMilliseconds; count: number; }, @@ -8584,6 +8585,51 @@ export function runBaseTests(config: { }, config.timeout, ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `getex test_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster: RedisCluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = "{key}" + uuidv4(); + const key2 = "{key}" + uuidv4(); + const value = uuidv4(); + + expect(await client.set(key1, value)).toBe("OK"); + expect(await client.getex(key1)).toEqual(value); + expect(await client.ttl(key1)).toBe(-1); + + expect( + await client.getex(key1, { + type: TimeUnit.Seconds, + duration: 15, + }), + ).toEqual(value); + expect(await client.ttl(key1)).toBeGreaterThan(0); + expect(await client.getex(key1, "persist")).toEqual(value); + expect(await client.ttl(key1)).toBe(-1); + + // non existent key + expect(await client.getex(key2)).toBeNull(); + + // invalid time measurement + await expect( + client.getex(key1, { + type: TimeUnit.Seconds, + duration: -10, + }), + ).rejects.toThrow(RequestError); + + // Key exists, but is not a string + expect(await client.sadd(key2, ["a"])).toBe(1); + await expect(client.getex(key2)).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); } export function runCommonTests(config: { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 25f9c77d24..a55ec380e6 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -33,6 +33,7 @@ import { ScoreFilter, SignedEncoding, SortOrder, + TimeUnit, Transaction, UnsignedEncoding, } from ".."; @@ -659,8 +660,21 @@ export async function transactionTest( responseData.push(["flushdb(FlushMode.SYNC)", "OK"]); baseTransaction.dbsize(); responseData.push(["dbsize()", 0]); - baseTransaction.set(key1, "bar"); + baseTransaction.set(key1, "foo"); responseData.push(['set(key1, "bar")', "OK"]); + baseTransaction.set(key1, "bar", { returnOldValue: true }); + responseData.push(['set(key1, "bar", {returnOldValue: true})', "foo"]); + + if (gte(version, "6.2.0")) { + baseTransaction.getex(key1); + responseData.push(["getex(key1)", "bar"]); + baseTransaction.getex(key1, { type: TimeUnit.Seconds, duration: 1 }); + responseData.push([ + 'getex(key1, {expiry: { type: "seconds", count: 1 }})', + "bar", + ]); + } + baseTransaction.randomKey(); responseData.push(["randomKey()", key1]); baseTransaction.getrange(key1, 0, -1);