diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc0a14c09..a82ea59b12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ #### Changes +* Node: Added `invokeScript` API with routing for cluster client ([#2284](https://github.com/valkey-io/valkey-glide/pull/2284)) * Python: Replace instances of Redis with Valkey ([#2266](https://github.com/valkey-io/valkey-glide/pull/2266)) * Java: Replace instances of Redis with Valkey ([#2268](https://github.com/valkey-io/valkey-glide/pull/2268)) * Node: Replace instances of Redis with Valkey ([#2260](https://github.com/valkey-io/valkey-glide/pull/2260)) diff --git a/node/npm/glide/index.ts b/node/npm/glide/index.ts index 0f8d9d4f69..546f3ebe83 100644 --- a/node/npm/glide/index.ts +++ b/node/npm/glide/index.ts @@ -168,7 +168,6 @@ function initialize() { StreamReadOptions, StreamClaimOptions, StreamPendingOptions, - ScriptOptions, ClosingError, ConfigurationError, ExecAbortError, @@ -284,7 +283,6 @@ function initialize() { StreamReadGroupOptions, StreamReadOptions, StreamPendingOptions, - ScriptOptions, ClosingError, ConfigurationError, ExecAbortError, diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index bc389beafe..c6e38a08ac 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -565,17 +565,6 @@ export interface BaseClientConfiguration { defaultDecoder?: Decoder; } -export interface ScriptOptions { - /** - * The keys that are used in the script. - */ - keys?: GlideString[]; - /** - * The arguments for the script. - */ - args?: GlideString[]; -} - /** * Enum of Valkey data types * `STRING` @@ -3695,9 +3684,13 @@ export class BaseClient { * it will be loaded automatically using the `SCRIPT LOAD` command. After that, it will be invoked using the `EVALSHA` command. * * @see {@link https://valkey.io/commands/script-load/|SCRIPT LOAD} and {@link https://valkey.io/commands/evalsha/|EVALSHA} on valkey.io for details. + * @remarks When in cluster mode, all `keys` must map to the same hash slot. * * @param script - The Lua script to execute. - * @param options - (Optional) See {@link ScriptOptions} and {@link DecoderOption}. + * @param options - (Optional) Additional parameters: + * - (Optional) `keys` : the keys that are used in the script. + * - (Optional) `args`: the arguments for the script. + * - (Optional) `decoder`: see {@link DecoderOption}. * @returns A value that depends on the script that was executed. * * @example @@ -3713,28 +3706,15 @@ export class BaseClient { */ public async invokeScript( script: Script, - options?: ScriptOptions & DecoderOption, + options?: { + keys?: GlideString[]; + args?: GlideString[]; + } & DecoderOption, ): Promise { const scriptInvocation = command_request.ScriptInvocation.create({ hash: script.getHash(), - keys: options?.keys?.map((item) => { - if (typeof item === "string") { - // Convert the string to a Buffer - return Buffer.from(item); - } else { - // If it's already a Buffer, just return it - return item; - } - }), - args: options?.args?.map((item) => { - if (typeof item === "string") { - // Convert the string to a Buffer - return Buffer.from(item); - } else { - // If it's already a Buffer, just return it - return item; - } - }), + keys: options?.keys?.map(Buffer.from), + args: options?.args?.map(Buffer.from), }); return this.createWritePromise(scriptInvocation, options); } diff --git a/node/src/GlideClient.ts b/node/src/GlideClient.ts index 58f301ecdf..1b9014de7a 100644 --- a/node/src/GlideClient.ts +++ b/node/src/GlideClient.ts @@ -923,7 +923,7 @@ export class GlideClient extends BaseClient { } /** - * Check existence of scripts in the script cache by their SHA1 digest. + * Checks existence of scripts in the script cache by their SHA1 digest. * * @see {@link https://valkey.io/commands/script-exists/|valkey.io} for more details. * @@ -941,7 +941,7 @@ export class GlideClient extends BaseClient { } /** - * Flush the Lua scripts cache. + * Flushes the Lua scripts cache. * * @see {@link https://valkey.io/commands/script-flush/|valkey.io} for more details. * @@ -961,7 +961,7 @@ export class GlideClient extends BaseClient { } /** - * Kill the currently executing Lua script, assuming no write operation was yet performed by the script. + * Kills the currently executing Lua script, assuming no write operation was yet performed by the script. * * @see {@link https://valkey.io/commands/script-kill/|valkey.io} for more details. * diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index 9d0a9af478..55337a519b 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -3,6 +3,7 @@ */ import { ClusterScanCursor } from "glide-rs"; +import { Script } from "index"; import * as net from "net"; import { BaseClient, @@ -1442,7 +1443,46 @@ export class GlideClusterClient extends BaseClient { } /** - * Check existence of scripts in the script cache by their SHA1 digest. + * Invokes a Lua script with arguments. + * This method simplifies the process of invoking scripts on a Valkey server by using an object that represents a Lua script. + * The script loading, argument preparation, and execution will all be handled internally. If the script has not already been loaded, + * it will be loaded automatically using the `SCRIPT LOAD` command. After that, it will be invoked using the `EVALSHA` command. + * + * The command will be routed to a random node, unless `route` is provided. + * + * @see {@link https://valkey.io/commands/script-load/|SCRIPT LOAD} and {@link https://valkey.io/commands/evalsha/|EVALSHA} on valkey.io for details. + * + * @param script - The Lua script to execute. + * @param options - (Optional) Additional parameters: + * - (Optional) `args`: the arguments for the script. + * - (Optional) `decoder`: see {@link DecoderOption}. + * - (Optional) `route`: see {@link RouteOption}. + * @returns A value that depends on the script that was executed. + * + * @example + * ```typescript + * const luaScript = new Script("return { ARGV[1] }"); + * const result = await invokeScript(luaScript, { args: ["bar"] }); + * console.log(result); // Output: ['bar'] + * ``` + */ + public async invokeScriptWithRoute( + script: Script, + options?: { args?: GlideString[] } & DecoderOption & RouteOption, + ): Promise> { + const scriptInvocation = command_request.ScriptInvocation.create({ + hash: script.getHash(), + keys: [], + args: options?.args?.map(Buffer.from), + }); + return this.createWritePromise>( + scriptInvocation, + options, + ).then((res) => convertClusterGlideRecord(res, true, options?.route)); + } + + /** + * Checks existence of scripts in the script cache by their SHA1 digest. * * @see {@link https://valkey.io/commands/script-exists/|valkey.io} for more details. * @@ -1464,7 +1504,7 @@ export class GlideClusterClient extends BaseClient { } /** - * Flush the Lua scripts cache. + * Flushes the Lua scripts cache. * * @see {@link https://valkey.io/commands/script-flush/|valkey.io} for more details. * @@ -1491,7 +1531,7 @@ export class GlideClusterClient extends BaseClient { } /** - * Kill the currently executing Lua script, assuming no write operation was yet performed by the script. + * Kills the currently executing Lua script, assuming no write operation was yet performed by the script. * * @see {@link https://valkey.io/commands/script-kill/|valkey.io} for more details. * @remarks The command is routed to all nodes, and aggregates the response to a single array. diff --git a/node/tests/GlideClusterClient.test.ts b/node/tests/GlideClusterClient.test.ts index f5f080bffb..6603aa4308 100644 --- a/node/tests/GlideClusterClient.test.ts +++ b/node/tests/GlideClusterClient.test.ts @@ -1488,6 +1488,67 @@ describe("GlideClusterClient", () => { client.close(); } }); + + it( + "invoke script with route invokeScriptWithRoute %p", + async () => { + const client = + await GlideClusterClient.createClient( + getClientConfigurationOption( + cluster.getAddresses(), + protocol, + ), + ); + const route: Routes = singleNodeRoute + ? { type: "primarySlotKey", key: "1" } + : "allPrimaries"; + + try { + const arg = uuidv4(); + const script = new Script( + Buffer.from("return {ARGV[1]}"), + ); + let res = await client.invokeScriptWithRoute( + script, + { args: [Buffer.from(arg)], route }, + ); + + if (singleNodeRoute) { + expect(res).toEqual([arg]); + } else { + Object.values( + res as Record, + ).forEach((value) => + expect(value).toEqual([arg]), + ); + } + + res = await client.invokeScriptWithRoute( + script, + { + args: [arg], + route, + decoder: Decoder.Bytes, + }, + ); + + if (singleNodeRoute) { + expect(res).toEqual([Buffer.from(arg)]); + } else { + Object.values( + res as Record, + ).forEach((value) => + expect(value).toEqual([ + Buffer.from(arg), + ]), + ); + } + } finally { + client.close(); + } + }, + TIMEOUT, + ); }, ); it(