Skip to content

Commit

Permalink
Add SMOVE command for node
Browse files Browse the repository at this point in the history
  • Loading branch information
ort-bot authored and Ubuntu committed May 28, 2024
1 parent fbbf797 commit 0814cd7
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 57 deletions.
47 changes: 33 additions & 14 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
createSCard,
createSIsMember,
createSMembers,
createSMove,
createSPop,
createSRem,
createSet,
Expand Down Expand Up @@ -385,17 +386,17 @@ export class BaseClient {
) {
const message = Array.isArray(command)
? redis_request.RedisRequest.create({
callbackIdx,
transaction: redis_request.Transaction.create({
commands: command,
}),
})
callbackIdx,
transaction: redis_request.Transaction.create({
commands: command,
}),
})
: command instanceof redis_request.Command
? redis_request.RedisRequest.create({
? redis_request.RedisRequest.create({
callbackIdx,
singleCommand: command,
})
: redis_request.RedisRequest.create({
: redis_request.RedisRequest.create({
callbackIdx,
scriptInvocation: command,
});
Expand Down Expand Up @@ -1224,6 +1225,24 @@ export class BaseClient {
);
}

/** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set.
* Creates a new destination set if needed. The operation is atomic.
* See https://valkey.io/commands/smove for more details.
*
* @param source - The key of the set to remove the element from.
* @param destination - The key of the set to add the element to.
* @param member - The set element to move.
* @returns True on success, or False if the `source` set does not exist or the element is not a member of the source set.
*
* @example
* ```typescript
* const result == await client.smove("set1", "set2", "member1");
* console.log(result); // Output: True # "member1" was moved from "set1" to "set2".
*/
public smove(source: string, destination: string, member: string): Promise<boolean> {
return this.createWritePromise(createSMove(source, destination, member));
}

/** Returns the set cardinality (number of elements) of the set stored at `key`.
* See https://redis.io/commands/scard/ for details.
*
Expand Down Expand Up @@ -2137,9 +2156,9 @@ export class BaseClient {
ReadFrom,
connection_request.ReadFrom
> = {
primary: connection_request.ReadFrom.Primary,
preferReplica: connection_request.ReadFrom.PreferReplica,
};
primary: connection_request.ReadFrom.Primary,
preferReplica: connection_request.ReadFrom.PreferReplica,
};

/** Returns the element at index `index` in the list stored at `key`.
* The index is zero-based, so 0 means the first element, 1 the second element and so on.
Expand Down Expand Up @@ -2298,11 +2317,11 @@ export class BaseClient {
: undefined;
const authenticationInfo =
options.credentials !== undefined &&
"password" in options.credentials
"password" in options.credentials
? {
password: options.credentials.password,
username: options.credentials.username,
}
password: options.credentials.password,
username: options.credentials.username,
}
: undefined;
const protocol = options.protocol as
| connection_request.ProtocolVersion
Expand Down
88 changes: 48 additions & 40 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,25 @@ export type SetOptions = {
* Retain the time to live associated with the key. Equivalent to `KEEPTTL` in the Redis API.
*/
| "keepExisting"
| {
type: /**
| {
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";
count: number;
};
| "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";
count: number;
};
};

/**
Expand Down Expand Up @@ -549,6 +549,14 @@ export function createSMembers(key: string): redis_request.Command {
return createCommand(RequestType.SMembers, [key]);
}

/**
*
* @internal
*/
export function createSMove(source: string, destination: string, member: string): redis_request.Command {
return createCommand(RequestType.SMove, [source, destination, member]);
}

/**
* @internal
*/
Expand Down Expand Up @@ -820,15 +828,15 @@ export type ScoreBoundary<T> =
* Represents a specific numeric score boundary in a sorted set.
*/
| {
/**
* The score value.
*/
value: T;
/**
* Whether the score value is inclusive. Defaults to True.
*/
isInclusive?: boolean;
};
/**
* The score value.
*/
value: T;
/**
* Whether the score value is inclusive. Defaults to True.
*/
isInclusive?: boolean;
};

/**
* Represents a range by index (rank) in a sorted set.
Expand Down Expand Up @@ -1091,21 +1099,21 @@ export function createZRank(

export type StreamTrimOptions = (
| {
/**
* Trim the stream according to entry ID.
* Equivalent to `MINID` in the Redis API.
*/
method: "minid";
threshold: string;
}
/**
* Trim the stream according to entry ID.
* Equivalent to `MINID` in the Redis API.
*/
method: "minid";
threshold: string;
}
| {
/**
* Trim the stream according to length.
* Equivalent to `MAXLEN` in the Redis API.
*/
method: "maxlen";
threshold: number;
}
/**
* Trim the stream according to length.
* Equivalent to `MAXLEN` in the Redis API.
*/
method: "maxlen";
threshold: number;
}
) & {
/**
* If `true`, the stream will be trimmed exactly. Equivalent to `=` in the Redis API. Otherwise the stream will be trimmed in a near-exact manner, which is more efficient, equivalent to `~` in the Redis API.
Expand Down
15 changes: 15 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
createSCard,
createSIsMember,
createSMembers,
createSMove,
createSPop,
createSRem,
createSelect,
Expand Down Expand Up @@ -678,6 +679,20 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
return this.addAndReturn(createSMembers(key), true);
}

/** Moves `member` from the set at `source` to the set at `destination`, removing it from the source set.
* Creates a new destination set if needed. The operation is atomic.
* See https://valkey.io/commands/smove for more details.
*
* @param source - The key of the set to remove the element from.
* @param destination - The key of the set to add the element to.
* @param member - The set element to move.
*
* Command Response - True on success, or False if the `source` set does not exist or the element is not a member of the source set.
*/
public smove(source: string, destination: string, member: string): T {
return this.addAndReturn(createSMove(source, destination, member));
}

/** Returns the set cardinality (number of elements) of the set stored at `key`.
* See https://redis.io/commands/scard/ for details.
*
Expand Down
51 changes: 51 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,57 @@ export function runBaseTests<Context>(config: {
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`smove test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key1 = "{key}" + uuidv4();
const key2 = "{key}" + uuidv4();
const key3 = "{key}" + uuidv4();
const string_key = "{key}" + uuidv4();
const non_existing_key = "{key}" + uuidv4();

expect(await client.sadd(key1, ["1", "2", "3"])).toEqual(3);
expect(await client.sadd(key2, ["2", "3"])).toEqual(2);

// move an element
expect(await client.smove(key1, key2, "1"));
expect(await client.smembers(key1)).toEqual(new Set(["2", "3"]));
expect(await client.smembers(key2)).toEqual(new Set(["1", "2", "3"]));

// moved element already exists in the destination set
expect(await client.smove(key2, key1, "2"));
expect(await client.smembers(key1)).toEqual(new Set(["2", "3"]));
expect(await client.smembers(key2)).toEqual(new Set(["1", "3"]));

// attempt to move from a non-existing key
expect(await client.smove(non_existing_key, key1, "4")).toEqual(false);
expect(await client.smembers(key1)).toEqual(new Set(["2", "3"]));

// move to a new set
expect(await client.smove(key1, key3, "2"));
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.smembers(key3)).toEqual(new Set(["2"]));

// attempt to move a missing element
expect(await client.smove(key1, key3, "42")).toEqual(false);
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.smembers(key3)).toEqual(new Set(["2"]));

// move missing element to missing key
expect(await client.smove(key1, non_existing_key, "42")).toEqual(false);
expect(await client.smembers(key1)).toEqual(new Set(["3"]));
expect(await client.type(non_existing_key)).toEqual("none");

// key exists, but it is not a set
expect(await client.set(string_key, "value")).toEqual("OK");
await expect(client.smove(string_key, key1, "_")).rejects.toThrow();

}, protocol);
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`srem, scard and smembers with non existing key_%p`,
async (protocol) => {
Expand Down
8 changes: 5 additions & 3 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ export async function transactionTest(
args.push("bar");
baseTransaction.spopCount(key7, 2);
args.push(new Set());
baseTransaction.smove(key7, key7, "non_existing_member");
args.push(false);
baseTransaction.scard(key7);
args.push(0);
baseTransaction.zadd(key8, {
Expand Down Expand Up @@ -401,9 +403,9 @@ export class RedisCluster {
.split(",")
.map((address) => address.split(":"))
.map((address) => [address[0], Number(address[1])]) as [
string,
number,
][];
string,
number,
][];

if (clusterFolder === undefined || ports === undefined) {
throw new Error(`Insufficient data in input: ${input}`);
Expand Down

0 comments on commit 0814cd7

Please sign in to comment.