Skip to content

Commit 1c70afb

Browse files
authored
Node: add LMOVE (#2002)
* implement lmove Signed-off-by: Chloe Yip <chloe.yip@improving.com>
1 parent a37fe95 commit 1c70afb

7 files changed

Lines changed: 239 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#### Changes
2+
* Node: Added LMOVE command ([#2002](https://github.com/valkey-io/valkey-glide/pull/2002))
23
* Node: Added GEOPOS command ([#1991](https://github.com/valkey-io/valkey-glide/pull/1991))
34
* Node: Added BITCOUNT command ([#1982](https://github.com/valkey-io/valkey-glide/pull/1982))
45
* Node: Added FLUSHDB command ([#1986](https://github.com/valkey-io/valkey-glide/pull/1986))

node/npm/glide/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ function initialize() {
9292
PeriodicChecks,
9393
Logger,
9494
LPosOptions,
95+
ListDirection,
9596
ExpireOptions,
9697
FlushMode,
9798
GeoUnit,
@@ -147,6 +148,7 @@ function initialize() {
147148
PeriodicChecks,
148149
Logger,
149150
LPosOptions,
151+
ListDirection,
150152
ExpireOptions,
151153
FlushMode,
152154
GeoUnit,

node/src/BaseClient.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
GeoUnit,
1818
InsertPosition,
1919
KeyWeight,
20+
ListDirection,
2021
RangeByIndex,
2122
RangeByLex,
2223
RangeByScore,
@@ -63,6 +64,7 @@ import {
6364
createLIndex,
6465
createLInsert,
6566
createLLen,
67+
createLMove,
6668
createLPop,
6769
createLPos,
6870
createLPush,
@@ -1465,6 +1467,47 @@ export class BaseClient {
14651467
return this.createWritePromise(createLLen(key));
14661468
}
14671469

1470+
/**
1471+
* Atomically pops and removes the left/right-most element to the list stored at `source`
1472+
* depending on `whereTo`, and pushes the element at the first/last element of the list
1473+
* stored at `destination` depending on `whereFrom`, see {@link ListDirection}.
1474+
*
1475+
* See https://valkey.io/commands/lmove/ for details.
1476+
*
1477+
* @param source - The key to the source list.
1478+
* @param destination - The key to the destination list.
1479+
* @param whereFrom - The {@link ListDirection} to remove the element from.
1480+
* @param whereTo - The {@link ListDirection} to add the element to.
1481+
* @returns The popped element, or `null` if `source` does not exist.
1482+
*
1483+
* since Valkey version 6.2.0.
1484+
*
1485+
* @example
1486+
* ```typescript
1487+
* await client.lpush("testKey1", ["two", "one"]);
1488+
* await client.lpush("testKey2", ["four", "three"]);
1489+
*
1490+
* const result1 = await client.lmove("testKey1", "testKey2", ListDirection.LEFT, ListDirection.LEFT);
1491+
* console.log(result1); // Output: "one".
1492+
*
1493+
* const updated_array_key1 = await client.lrange("testKey1", 0, -1);
1494+
* console.log(updated_array); // Output: "two".
1495+
*
1496+
* const updated_array_key2 = await client.lrange("testKey2", 0, -1);
1497+
* console.log(updated_array_key2); // Output: ["one", "three", "four"].
1498+
* ```
1499+
*/
1500+
public async lmove(
1501+
source: string,
1502+
destination: string,
1503+
whereFrom: ListDirection,
1504+
whereTo: ListDirection,
1505+
): Promise<string | null> {
1506+
return this.createWritePromise(
1507+
createLMove(source, destination, whereFrom, whereTo),
1508+
);
1509+
}
1510+
14681511
/**
14691512
* Sets the list element at `index` to `element`.
14701513
* The index is zero-based, so `0` means the first element, `1` the second element and so on.

node/src/Commands.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,37 @@ export function createLLen(key: string): command_request.Command {
583583
return createCommand(RequestType.LLen, [key]);
584584
}
585585

586+
/**
587+
* Enumeration representing element popping or adding direction for the List Based Commands.
588+
*/
589+
export enum ListDirection {
590+
/**
591+
* Represents the option that elements should be popped from or added to the left side of a list.
592+
*/
593+
LEFT = "LEFT",
594+
/**
595+
* Represents the option that elements should be popped from or added to the right side of a list.
596+
*/
597+
RIGHT = "RIGHT",
598+
}
599+
600+
/**
601+
* @internal
602+
*/
603+
export function createLMove(
604+
source: string,
605+
destination: string,
606+
whereFrom: ListDirection,
607+
whereTo: ListDirection,
608+
): command_request.Command {
609+
return createCommand(RequestType.LMove, [
610+
source,
611+
destination,
612+
whereFrom,
613+
whereTo,
614+
]);
615+
}
616+
586617
/**
587618
* @internal
588619
*/

node/src/Transaction.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
InfoOptions,
1111
InsertPosition,
1212
KeyWeight,
13+
ListDirection,
1314
LolwutOptions,
1415
RangeByIndex,
1516
RangeByLex,
@@ -72,6 +73,7 @@ import {
7273
createLIndex,
7374
createLInsert,
7475
createLLen,
76+
createLMove,
7577
createLPop,
7678
createLPos,
7779
createLPush,
@@ -707,6 +709,33 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
707709
return this.addAndReturn(createLLen(key));
708710
}
709711

712+
/**
713+
* Atomically pops and removes the left/right-most element to the list stored at `source`
714+
* depending on `whereFrom`, and pushes the element at the first/last element of the list
715+
* stored at `destination` depending on `whereTo`, see {@link ListDirection}.
716+
*
717+
* See https://valkey.io/commands/lmove/ for details.
718+
*
719+
* @param source - The key to the source list.
720+
* @param destination - The key to the destination list.
721+
* @param whereFrom - The {@link ListDirection} to remove the element from.
722+
* @param whereTo - The {@link ListDirection} to add the element to.
723+
*
724+
* Command Response - The popped element, or `null` if `source` does not exist.
725+
*
726+
* since Valkey version 6.2.0.
727+
*/
728+
public lmove(
729+
source: string,
730+
destination: string,
731+
whereFrom: ListDirection,
732+
whereTo: ListDirection,
733+
): T {
734+
return this.addAndReturn(
735+
createLMove(source, destination, whereFrom, whereTo),
736+
);
737+
}
738+
710739
/**
711740
* Sets the list element at `index` to `element`.
712741
* The index is zero-based, so `0` means the first element, `1` the second element and so on.

node/tests/SharedTests.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
BitwiseOperation,
1414
ClosingError,
1515
ExpireOptions,
16+
ListDirection,
1617
GlideClient,
1718
GlideClusterClient,
1819
InfoOptions,
@@ -1104,6 +1105,116 @@ export function runBaseTests<Context>(config: {
11041105
config.timeout,
11051106
);
11061107

1108+
it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
1109+
`lmove list_%p`,
1110+
async (protocol) => {
1111+
await runTest(async (client: BaseClient, cluster) => {
1112+
if (cluster.checkIfServerVersionLessThan("6.2.0")) {
1113+
return;
1114+
}
1115+
1116+
const key1 = "{key}-1" + uuidv4();
1117+
const key2 = "{key}-2" + uuidv4();
1118+
const lpushArgs1 = ["2", "1"];
1119+
const lpushArgs2 = ["4", "3"];
1120+
1121+
// Initialize the tests
1122+
expect(await client.lpush(key1, lpushArgs1)).toEqual(2);
1123+
expect(await client.lpush(key2, lpushArgs2)).toEqual(2);
1124+
1125+
// Move from LEFT to LEFT
1126+
checkSimple(
1127+
await client.lmove(
1128+
key1,
1129+
key2,
1130+
ListDirection.LEFT,
1131+
ListDirection.LEFT,
1132+
),
1133+
).toEqual("1");
1134+
1135+
// Move from LEFT to RIGHT
1136+
checkSimple(
1137+
await client.lmove(
1138+
key1,
1139+
key2,
1140+
ListDirection.LEFT,
1141+
ListDirection.RIGHT,
1142+
),
1143+
).toEqual("2");
1144+
1145+
checkSimple(await client.lrange(key2, 0, -1)).toEqual([
1146+
"1",
1147+
"3",
1148+
"4",
1149+
"2",
1150+
]);
1151+
checkSimple(await client.lrange(key1, 0, -1)).toEqual([]);
1152+
1153+
// Move from RIGHT to LEFT - non-existing destination key
1154+
checkSimple(
1155+
await client.lmove(
1156+
key2,
1157+
key1,
1158+
ListDirection.RIGHT,
1159+
ListDirection.LEFT,
1160+
),
1161+
).toEqual("2");
1162+
1163+
// Move from RIGHT to RIGHT
1164+
checkSimple(
1165+
await client.lmove(
1166+
key2,
1167+
key1,
1168+
ListDirection.RIGHT,
1169+
ListDirection.RIGHT,
1170+
),
1171+
).toEqual("4");
1172+
1173+
checkSimple(await client.lrange(key2, 0, -1)).toEqual([
1174+
"1",
1175+
"3",
1176+
]);
1177+
checkSimple(await client.lrange(key1, 0, -1)).toEqual([
1178+
"2",
1179+
"4",
1180+
]);
1181+
1182+
// Non-existing source key
1183+
expect(
1184+
await client.lmove(
1185+
"{key}-non_existing_key" + uuidv4(),
1186+
key1,
1187+
ListDirection.LEFT,
1188+
ListDirection.LEFT,
1189+
),
1190+
).toEqual(null);
1191+
1192+
// Non-list source key
1193+
const key3 = "{key}-3" + uuidv4();
1194+
checkSimple(await client.set(key3, "value")).toEqual("OK");
1195+
await expect(
1196+
client.lmove(
1197+
key3,
1198+
key1,
1199+
ListDirection.LEFT,
1200+
ListDirection.LEFT,
1201+
),
1202+
).rejects.toThrow(RequestError);
1203+
1204+
// Non-list destination key
1205+
await expect(
1206+
client.lmove(
1207+
key1,
1208+
key3,
1209+
ListDirection.LEFT,
1210+
ListDirection.LEFT,
1211+
),
1212+
).rejects.toThrow(RequestError);
1213+
}, protocol);
1214+
},
1215+
config.timeout,
1216+
);
1217+
11071218
it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
11081219
`lset test_%p`,
11091220
async (protocol) => {

node/tests/TestUtilities.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
GlideClusterClient,
1818
InsertPosition,
1919
Logger,
20+
ListDirection,
2021
ProtocolVersion,
2122
ReturnType,
2223
ScoreFilter,
@@ -399,6 +400,7 @@ export async function transactionTest(
399400
const key17 = "{key}" + uuidv4(); // bitmap
400401
const key18 = "{key}" + uuidv4(); // Geospatial Data/ZSET
401402
const key19 = "{key}" + uuidv4(); // bitmap
403+
const key20 = "{key}" + uuidv4(); // list
402404
const field = uuidv4();
403405
const value = uuidv4();
404406
// array of tuples - first element is test name/description, second - expected return value
@@ -476,8 +478,26 @@ export async function transactionTest(
476478
responseData.push(['lset(key5, 0, field + "3")', "OK"]);
477479
baseTransaction.lrange(key5, 0, -1);
478480
responseData.push(["lrange(key5, 0, -1)", [field + "3", field + "2"]]);
479-
baseTransaction.lpopCount(key5, 2);
480-
responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]);
481+
482+
if (gte("6.2.0", version)) {
483+
baseTransaction.lmove(
484+
key5,
485+
key20,
486+
ListDirection.LEFT,
487+
ListDirection.LEFT,
488+
);
489+
responseData.push([
490+
"lmove(key5, key20, ListDirection.LEFT, ListDirection.LEFT)",
491+
field + "3",
492+
]);
493+
494+
baseTransaction.lpopCount(key5, 2);
495+
responseData.push(["lpopCount(key5, 2)", [field + "2"]]);
496+
} else {
497+
baseTransaction.lpopCount(key5, 2);
498+
responseData.push(["lpopCount(key5, 2)", [field + "3", field + "2"]]);
499+
}
500+
481501
baseTransaction.linsert(
482502
key5,
483503
InsertPosition.Before,

0 commit comments

Comments
 (0)