Skip to content

Commit

Permalink
Node: add LPOS command (valkey-io#1927)
Browse files Browse the repository at this point in the history
* Added LPOS node command

---------

Signed-off-by: Guian Gumpac <[email protected]>
  • Loading branch information
GumpacG authored Jul 18, 2024
1 parent 4ddb11d commit ebb04c1
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* Node: Added SMISMEMBER command ([#1955](https://github.com/valkey-io/valkey-glide/pull/1955))
* Node: Added SDIFF command ([#1924](https://github.com/valkey-io/valkey-glide/pull/1924))
* Node: Added LOLWUT command ([#1934](https://github.com/valkey-io/valkey-glide/pull/1934))
* Node: Added LPOS command ([#1927](https://github.com/valkey-io/valkey-glide/pull/1927))

## 1.0.0 (2024-07-09)

Expand Down
2 changes: 2 additions & 0 deletions node/npm/glide/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ function initialize() {
PeriodicChecksManualInterval,
PeriodicChecks,
Logger,
LPosOptions,
ExpireOptions,
InfoOptions,
InsertPosition,
Expand Down Expand Up @@ -128,6 +129,7 @@ function initialize() {
PeriodicChecksManualInterval,
PeriodicChecks,
Logger,
LPosOptions,
ExpireOptions,
InfoOptions,
InsertPosition,
Expand Down
32 changes: 32 additions & 0 deletions node/src/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "glide-rs";
import * as net from "net";
import { Buffer, BufferWriter, Reader, Writer } from "protobufjs";
import { LPosOptions } from "./command-options/LPosOptions";
import {
AggregationType,
ExpireOptions,
Expand Down Expand Up @@ -51,6 +52,7 @@ import {
createLInsert,
createLLen,
createLPop,
createLPos,
createLPush,
createLPushX,
createLRange,
Expand Down Expand Up @@ -2845,6 +2847,36 @@ export class BaseClient {
return this.createWritePromise(createObjectRefcount(key));
}

/**
* Returns the index of the first occurrence of `element` inside the list specified by `key`. If no
* match is found, `null` is returned. If the `count` option is specified, then the function returns
* an `array` of indices of matching elements within the list.
*
* See https://valkey.io/commands/lpos/ for more details.
*
* @param key - The name of the list.
* @param element - The value to search for within the list.
* @param options - The LPOS options.
* @returns The index of `element`, or `null` if `element` is not in the list. If the `count` option
* is specified, then the function returns an `array` of indices of matching elements within the list.
*
* since - Valkey version 6.0.6.
*
* @example
* ```typescript
* await client.rpush("myList", ["a", "b", "c", "d", "e", "e"]);
* console.log(await client.lpos("myList", "e", new LPosOptions({ rank: 2 }))); // Output: 5 - the second occurrence of "e" is at index 5.
* console.log(await client.lpos("myList", "e", new LPosOptions({ count: 3 }))); // Output: [ 4, 5 ] - indices for the occurrences of "e" in list "myList".
* ```
*/
public lpos(
key: string,
element: string,
options?: LPosOptions,
): Promise<number | number[] | null> {
return this.createWritePromise(createLPos(key, element, options));
}

/**
* @internal
*/
Expand Down
18 changes: 18 additions & 0 deletions node/src/Commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { createLeakedStringVec, MAX_REQUEST_ARGS_LEN } from "glide-rs";
import Long from "long";
import { LPosOptions } from "./command-options/LPosOptions";

import { command_request } from "./ProtobufMessage";

Expand Down Expand Up @@ -1678,3 +1679,20 @@ export function createFlushAll(mode?: FlushMode): command_request.Command {
return createCommand(RequestType.FlushAll, []);
}
}

/**
* @internal
*/
export function createLPos(
key: string,
element: string,
options?: LPosOptions,
): command_request.Command {
let args: string[] = [key, element];

if (options) {
args = args.concat(options.toArgs());
}

return createCommand(RequestType.LPos, args);
}
22 changes: 22 additions & 0 deletions node/src/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

import { LPosOptions } from "./command-options/LPosOptions";
import {
AggregationType,
ExpireOptions,
Expand Down Expand Up @@ -56,6 +57,7 @@ import {
createLInsert,
createLLen,
createLPop,
createLPos,
createLPush,
createLPushX,
createLRange,
Expand Down Expand Up @@ -1695,6 +1697,26 @@ export class BaseTransaction<T extends BaseTransaction<T>> {
public flushall(mode?: FlushMode): T {
return this.addAndReturn(createFlushAll(mode));
}

/**
* Returns the index of the first occurrence of `element` inside the list specified by `key`. If no
* match is found, `null` is returned. If the `count` option is specified, then the function returns
* an `array` of indices of matching elements within the list.
*
* See https://valkey.io/commands/lpos/ for more details.
*
* @param key - The name of the list.
* @param element - The value to search for within the list.
* @param options - The LPOS options.
*
* Command Response - The index of `element`, or `null` if `element` is not in the list. If the `count`
* option is specified, then the function returns an `array` of indices of matching elements within the list.
*
* since - Valkey version 6.0.6.
*/
public lpos(key: string, element: string, options?: LPosOptions): T {
return this.addAndReturn(createLPos(key, element, options));
}
}

/**
Expand Down
64 changes: 64 additions & 0 deletions node/src/command-options/LPosOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
*/

/**
* Optional arguments to LPOS command.
*
* See https://valkey.io/commands/lpos/ for more details.
*/
export class LPosOptions {
/** Redis API keyword use to determine the rank of the match to return. */
public static RANK_REDIS_API = "RANK";
/** Redis API keyword used to extract specific number of matching indices from a list. */
public static COUNT_REDIS_API = "COUNT";
/** Redis API keyword used to determine the maximum number of list items to compare. */
public static MAXLEN_REDIS_API = "MAXLEN";
/** The rank of the match to return. */
private rank?: number;
/** The specific number of matching indices from a list. */
private count?: number;
/** The maximum number of comparisons to make between the element and the items in the list. */
private maxLength?: number;

constructor({
rank,
count,
maxLength,
}: {
rank?: number;
count?: number;
maxLength?: number;
}) {
this.rank = rank;
this.count = count;
this.maxLength = maxLength;
}

/**
*
* Converts LPosOptions into a string[].
*
* @returns string[]
*/
public toArgs(): string[] {
const args: string[] = [];

if (this.rank !== undefined) {
args.push(LPosOptions.RANK_REDIS_API);
args.push(this.rank.toString());
}

if (this.count !== undefined) {
args.push(LPosOptions.COUNT_REDIS_API);
args.push(this.count.toString());
}

if (this.maxLength !== undefined) {
args.push(LPosOptions.MAXLEN_REDIS_API);
args.push(this.maxLength.toString());
}

return args;
}
}
109 changes: 109 additions & 0 deletions node/tests/SharedTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
intoString,
} from "./TestUtilities";
import { SingleNodeRoute } from "../build-ts/src/GlideClusterClient";
import { LPosOptions } from "../build-ts/src/command-options/LPosOptions";

async function getVersion(): Promise<[number, number, number]> {
const versionString = await new Promise<string>((resolve, reject) => {
Expand Down Expand Up @@ -3863,6 +3864,114 @@ export function runBaseTests<Context>(config: {
},
config.timeout,
);

it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])(
`lpos test_%p`,
async (protocol) => {
await runTest(async (client: BaseClient) => {
const key = `{key}:${uuidv4()}`;
const valueArray = ["a", "a", "b", "c", "a", "b"];
expect(await client.rpush(key, valueArray)).toEqual(6);

// simplest case
expect(await client.lpos(key, "a")).toEqual(0);
expect(
await client.lpos(key, "b", new LPosOptions({ rank: 2 })),
).toEqual(5);

// element doesn't exist
expect(await client.lpos(key, "e")).toBeNull();

// reverse traversal
expect(
await client.lpos(key, "b", new LPosOptions({ rank: -2 })),
).toEqual(2);

// unlimited comparisons
expect(
await client.lpos(
key,
"a",
new LPosOptions({ rank: 1, maxLength: 0 }),
),
).toEqual(0);

// limited comparisons
expect(
await client.lpos(
key,
"c",
new LPosOptions({ rank: 1, maxLength: 2 }),
),
).toBeNull();

// invalid rank value
await expect(
client.lpos(key, "a", new LPosOptions({ rank: 0 })),
).rejects.toThrow(RequestError);

// invalid maxlen value
await expect(
client.lpos(key, "a", new LPosOptions({ maxLength: -1 })),
).rejects.toThrow(RequestError);

// non-existent key
expect(await client.lpos("non-existent_key", "e")).toBeNull();

// wrong key data type
const wrongDataType = `{key}:${uuidv4()}`;
expect(await client.sadd(wrongDataType, ["a", "b"])).toEqual(2);

await expect(client.lpos(wrongDataType, "a")).rejects.toThrow(
RequestError,
);

// invalid count value
await expect(
client.lpos(key, "a", new LPosOptions({ count: -1 })),
).rejects.toThrow(RequestError);

// with count
expect(
await client.lpos(key, "a", new LPosOptions({ count: 2 })),
).toEqual([0, 1]);
expect(
await client.lpos(key, "a", new LPosOptions({ count: 0 })),
).toEqual([0, 1, 4]);
expect(
await client.lpos(
key,
"a",
new LPosOptions({ rank: 1, count: 0 }),
),
).toEqual([0, 1, 4]);
expect(
await client.lpos(
key,
"a",
new LPosOptions({ rank: 2, count: 0 }),
),
).toEqual([1, 4]);
expect(
await client.lpos(
key,
"a",
new LPosOptions({ rank: 3, count: 0 }),
),
).toEqual([4]);

// reverse traversal
expect(
await client.lpos(
key,
"a",
new LPosOptions({ rank: -1, count: 0 }),
),
).toEqual([4, 1, 0]);
}, protocol);
},
config.timeout,
);
}

export function runCommonTests<Context>(config: {
Expand Down
18 changes: 18 additions & 0 deletions node/tests/TestUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Transaction,
} from "..";
import { checkIfServerVersionLessThan } from "./SharedTests";
import { LPosOptions } from "../build-ts/src/command-options/LPosOptions";

beforeAll(() => {
Logger.init("info");
Expand Down Expand Up @@ -310,6 +311,7 @@ export async function transactionTest(
const key13 = "{key}" + uuidv4();
const key14 = "{key}" + uuidv4(); // sorted set
const key15 = "{key}" + uuidv4(); // list
const key16 = "{key}" + uuidv4(); // list
const field = uuidv4();
const value = uuidv4();
const args: ReturnType[] = [];
Expand Down Expand Up @@ -397,6 +399,22 @@ export async function transactionTest(
args.push(0);
baseTransaction.lpushx(key15, ["_"]);
args.push(0);
baseTransaction.rpush(key16, [
field + "1",
field + "1",
field + "2",
field + "3",
field + "3",
]);
args.push(5);
baseTransaction.lpos(key16, field + "1", new LPosOptions({ rank: 2 }));
args.push(1);
baseTransaction.lpos(
key16,
field + "1",
new LPosOptions({ rank: 2, count: 0 }),
);
args.push([1]);
baseTransaction.sadd(key7, ["bar", "foo"]);
args.push(2);
baseTransaction.sunionstore(key7, [key7, key7]);
Expand Down

0 comments on commit ebb04c1

Please sign in to comment.