From 1c70afbeef1e640eb276c1958dc2797bf1da5728 Mon Sep 17 00:00:00 2001 From: Chloe Yip <168601573+cyip10@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:38:44 -0700 Subject: [PATCH] Node: add LMOVE (#2002) * implement lmove Signed-off-by: Chloe Yip --- CHANGELOG.md | 1 + node/npm/glide/index.ts | 2 + node/src/BaseClient.ts | 43 ++++++++++++++ node/src/Commands.ts | 31 ++++++++++ node/src/Transaction.ts | 29 ++++++++++ node/tests/SharedTests.ts | 111 ++++++++++++++++++++++++++++++++++++ node/tests/TestUtilities.ts | 24 +++++++- 7 files changed, 239 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 633798767f..8f3d15f0a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002)) * Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991)) * Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982)) * Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index c74dc3bc4c..12df4b5b47 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -92,6 +92,7 @@ function initialize() { PeriodicChecks, Logger, LPosOptions, + ListDirection, ExpireOptions, FlushMode, GeoUnit, @@ -147,6 +148,7 @@ function initialize() { PeriodicChecks, Logger, LPosOptions, + ListDirection, ExpireOptions, FlushMode, GeoUnit, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 429115b4e8..b7d03103f6 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -17,6 +17,7 @@ import { GeoUnit, InsertPosition, KeyWeight, + ListDirection, RangeByIndex, RangeByLex, RangeByScore, @@ -63,6 +64,7 @@ import { createLIndex, createLInsert, createLLen, + createLMove, createLPop, createLPos, createLPush, @@ -1465,6 +1467,47 @@ export class BaseClient { return this.createWritePromise(createLLen(key)); } + /** + * Atomically pops and removes the left/right-most element to the list stored at `source` + * depending on `whereTo`, and pushes the element at the first/last element of the list + * stored at `destination` depending on `whereFrom`, see {@link ListDirection}. + * + * See https://valkey.io/commands/lmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * @returns The popped element, or `null` if `source` does not exist. + * + * since Valkey version 6.2.0. + * + * @example + * ```typescript + * await client.lpush("testKey1", ["two", "one"]); + * await client.lpush("testKey2", ["four", "three"]); + * + * const result1 = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT); + * console.log(result1); // Output: "one". + * + * const updated_array_key1 = await client.lrange("testKey1", 0, -1); + * console.log(updated_array); // Output: "two". + * + * const updated_array_key2 = await client.lrange("testKey2", 0, -1); + * console.log(updated_array_key2); // Output: ["one", "three", "four"]. + * ``` + */ + public async lmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + ): Promise { + return this.createWritePromise( + createLMove(source, destination, whereFrom, whereTo), + ); + } + /** * Sets the list element at `index` to `element`. * The index is zero-based, so `0` means the first element, `1` the second element and so on. diff --git a/node/src/Commands.ts b/node/src/Commands.ts index 7b40948d0f..7b20ae39f4 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -583,6 +583,37 @@ export function createLLen(key: string): command_request.Command { return createCommand(RequestType.LLen, [key]); } +/** + * Enumeration representing element popping or adding direction for the List Based Commands. + */ +export enum ListDirection { + /** + * Represents the option that elements should be popped from or added to the left side of a list. + */ + LEFT = "LEFT", + /** + * Represents the option that elements should be popped from or added to the right side of a list. + */ + RIGHT = "RIGHT", +} + +/** + * @internal + */ +export function createLMove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, +): command_request.Command { + return createCommand(RequestType.LMove, [ + source, + destination, + whereFrom, + whereTo, + ]); +} + /** * @internal */ diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index fb56e7ff93..801b093955 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -10,6 +10,7 @@ import { InfoOptions, InsertPosition, KeyWeight, + ListDirection, LolwutOptions, RangeByIndex, RangeByLex, @@ -72,6 +73,7 @@ import { createLIndex, createLInsert, createLLen, + createLMove, createLPop, createLPos, createLPush, @@ -707,6 +709,33 @@ export class BaseTransaction> { return this.addAndReturn(createLLen(key)); } + /** + * Atomically pops and removes the left/right-most element to the list stored at `source` + * depending on `whereFrom`, and pushes the element at the first/last element of the list + * stored at `destination` depending on `whereTo`, see {@link ListDirection}. + * + * See https://valkey.io/commands/lmove/ for details. + * + * @param source - The key to the source list. + * @param destination - The key to the destination list. + * @param whereFrom - The {@link ListDirection} to remove the element from. + * @param whereTo - The {@link ListDirection} to add the element to. + * + * Command Response - The popped element, or `null` if `source` does not exist. + * + * since Valkey version 6.2.0. + */ + public lmove( + source: string, + destination: string, + whereFrom: ListDirection, + whereTo: ListDirection, + ): T { + return this.addAndReturn( + createLMove(source, destination, whereFrom, whereTo), + ); + } + /** * Sets the list element at `index` to `element`. * The index is zero-based, so `0` means the first element, `1` the second element and so on. diff --git a/node/tests/SharedTests.ts b/node/tests/SharedTests.ts index cc215cd164..5b74140add 100644 --- a/node/tests/SharedTests.ts +++ b/node/tests/SharedTests.ts @@ -13,6 +13,7 @@ import { BitwiseOperation, ClosingError, ExpireOptions, + ListDirection, GlideClient, GlideClusterClient, InfoOptions, @@ -1104,6 +1105,116 @@ export function runBaseTests(config: { config.timeout, ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `lmove list_%p`, + async (protocol) => { + await runTest(async (client: BaseClient, cluster) => { + if (cluster.checkIfServerVersionLessThan("6.2.0")) { + return; + } + + const key1 = "{key}-1" + uuidv4(); + const key2 = "{key}-2" + uuidv4(); + const lpushArgs1 = ["2", "1"]; + const lpushArgs2 = ["4", "3"]; + + // Initialize the tests + expect(await client.lpush(key1, lpushArgs1)).toEqual(2); + expect(await client.lpush(key2, lpushArgs2)).toEqual(2); + + // Move from LEFT to LEFT + checkSimple( + await client.lmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).toEqual("1"); + + // Move from LEFT to RIGHT + checkSimple( + await client.lmove( + key1, + key2, + ListDirection.LEFT, + ListDirection.RIGHT, + ), + ).toEqual("2"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + "4", + "2", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([]); + + // Move from RIGHT to LEFT - non-existing destination key + checkSimple( + await client.lmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.LEFT, + ), + ).toEqual("2"); + + // Move from RIGHT to RIGHT + checkSimple( + await client.lmove( + key2, + key1, + ListDirection.RIGHT, + ListDirection.RIGHT, + ), + ).toEqual("4"); + + checkSimple(await client.lrange(key2, 0, -1)).toEqual([ + "1", + "3", + ]); + checkSimple(await client.lrange(key1, 0, -1)).toEqual([ + "2", + "4", + ]); + + // Non-existing source key + expect( + await client.lmove( + "{key}-non_existing_key" + uuidv4(), + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).toEqual(null); + + // Non-list source key + const key3 = "{key}-3" + uuidv4(); + checkSimple(await client.set(key3, "value")).toEqual("OK"); + await expect( + client.lmove( + key3, + key1, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).rejects.toThrow(RequestError); + + // Non-list destination key + await expect( + client.lmove( + key1, + key3, + ListDirection.LEFT, + ListDirection.LEFT, + ), + ).rejects.toThrow(RequestError); + }, protocol); + }, + config.timeout, + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `lset test_%p`, async (protocol) => { diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index a230562ced..352f9f3dab 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -17,6 +17,7 @@ import { GlideClusterClient, InsertPosition, Logger, + ListDirection, ProtocolVersion, ReturnType, ScoreFilter, @@ -399,6 +400,7 @@ export async function transactionTest( const key17 = "{key}" + uuidv4(); // bitmap const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET const key19 = "{key}" + uuidv4(); // bitmap + const key20 = "{key}" + uuidv4(); // list const field = uuidv4(); const value = uuidv4(); // array of tuples - first element is test name/description, second - expected return value @@ -476,8 +478,26 @@ export async function transactionTest( responseData.push(['lset(key5, 0, field + "3")', "OK"]); baseTransaction.lrange(key5, 0, -1); responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]); - baseTransaction.lpopCount(key5, 2); - responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + + if (gte("6.2.0", version)) { + baseTransaction.lmove( + key5, + key20, + ListDirection.LEFT, + ListDirection.LEFT, + ); + responseData.push([ + "lmove(key5, key20, ListDirection.LEFT, ListDirection.LEFT)", + field + "3", + ]); + + baseTransaction.lpopCount(key5, 2); + responseData.push(["lpopCount(key5, 2)", [field + "2"]]); + } else { + baseTransaction.lpopCount(key5, 2); + responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]); + } + baseTransaction.linsert( key5, InsertPosition.Before,