diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4afea615..4175ad6af9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * Node: Added FUNCTION LOAD command ([#1969](https://github.com/valkey-io/valkey-glide/pull/1969)) * Node: Added FUNCTION DELETE command ([#1990](https://github.com/valkey-io/valkey-glide/pull/1990)) * Node: Added FUNCTION FLUSH command ([#1984](https://github.com/valkey-io/valkey-glide/pull/1984)) +* Node: Added FCALL and FCALL_RO commands ([#2011](https://github.com/valkey-io/valkey-glide/pull/2011)) * Node: Added ZMPOP command ([#1994](https://github.com/valkey-io/valkey-glide/pull/1994)) #### Fixes diff --git a/node/src/BaseClient.ts b/node/src/BaseClient.ts index 9119493f3c..2f3b00c40e 100644 --- a/node/src/BaseClient.ts +++ b/node/src/BaseClient.ts @@ -13,6 +13,7 @@ import { Buffer, BufferWriter, Reader, Writer } from "protobufjs"; import { AggregationType, ExpireOptions, + GeoUnit, InsertPosition, KeyWeight, RangeByIndex, @@ -34,10 +35,12 @@ import { createExists, createExpire, createExpireAt, + createFCall, + createFCallReadOnly, createGeoAdd, createGeoDist, - createGeoPos, createGeoHash, + createGeoPos, createGet, createGetBit, createGetDel, @@ -129,7 +132,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - GeoUnit, } from "./Commands"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; import { GeoAddOptions } from "./commands/geospatial/GeoAddOptions"; @@ -3330,6 +3332,63 @@ export class BaseClient { return this.createWritePromise(createObjectRefcount(key)); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcall("Deep_Thought", [], []); + * console.log(response); // Output: Returns the function's return value. + * ``` + */ + public fcall( + func: string, + keys: string[], + args: string[], + ): Promise { + return this.createWritePromise(createFCall(func, keys, args)); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @remarks When in cluster mode, all `keys` must map to the same hash slot. + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallReadOnly("Deep_Thought", ["key1"], ["Answer", "to", "the", + * "Ultimate", "Question", "of", "Life,", "the", "Universe,", "and", "Everything"]); + * console.log(response); // Output: 42 # The return value on the function that was executed. + * ``` + */ + public fcallReadonly( + func: string, + keys: string[], + args: string[], + ): Promise { + return this.createWritePromise(createFCallReadOnly(func, keys, args)); + } + /** * 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 diff --git a/node/src/Commands.ts b/node/src/Commands.ts index b34e7be2b1..4b6fcbb73d 100644 --- a/node/src/Commands.ts +++ b/node/src/Commands.ts @@ -1584,6 +1584,32 @@ export function createBLPop( return createCommand(RequestType.BLPop, args); } +/** + * @internal + */ +export function createFCall( + func: string, + keys: string[], + args: string[], +): command_request.Command { + let params: string[] = []; + params = params.concat(func, keys.length.toString(), keys, args); + return createCommand(RequestType.FCall, params); +} + +/** + * @internal + */ +export function createFCallReadOnly( + func: string, + keys: string[], + args: string[], +): command_request.Command { + let params: string[] = []; + params = params.concat(func, keys.length.toString(), keys, args); + return createCommand(RequestType.FCallReadOnly, params); +} + /** * @internal */ @@ -1593,6 +1619,17 @@ export function createFunctionDelete( return createCommand(RequestType.FunctionDelete, [libraryCode]); } +/** + * @internal + */ +export function createFunctionFlush(mode?: FlushMode): command_request.Command { + if (mode) { + return createCommand(RequestType.FunctionFlush, [mode.toString()]); + } else { + return createCommand(RequestType.FunctionFlush, []); + } +} + /** * @internal */ @@ -1616,17 +1653,6 @@ export function createBitCount( return createCommand(RequestType.BitCount, args); } -/** - * @internal - */ -export function createFunctionFlush(mode?: FlushMode): command_request.Command { - if (mode) { - return createCommand(RequestType.FunctionFlush, [mode.toString()]); - } else { - return createCommand(RequestType.FunctionFlush, []); - } -} - export type StreamReadOptions = { /** * If set, the read request will block for the set amount of milliseconds or diff --git a/node/src/GlideClusterClient.ts b/node/src/GlideClusterClient.ts index d552469812..0cddacdb0f 100644 --- a/node/src/GlideClusterClient.ts +++ b/node/src/GlideClusterClient.ts @@ -21,6 +21,8 @@ import { createCustomCommand, createDBSize, createEcho, + createFCall, + createFCallReadOnly, createFlushAll, createFlushDB, createFunctionDelete, @@ -659,6 +661,67 @@ export class GlideClusterClient extends BaseClient { ); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @param route - The command will be routed to a random node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallWithRoute("Deep_Thought", [], "randomNode"); + * console.log(response); // Output: Returns the function's return value. + * ``` + */ + public fcallWithRoute( + func: string, + args: string[], + route?: Routes, + ): Promise { + return this.createWritePromise( + createFCall(func, [], args), + toProtobufRoute(route), + ); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param args - A list of `function` arguments and it should not represent names of keys. + * @param route - The command will be routed to a random node, unless `route` is provided, in which + * case the client will route the command to the nodes defined by `route`. + * @returns The invoked function's return value. + * + * @example + * ```typescript + * const response = await client.fcallReadonlyWithRoute("Deep_Thought", ["Answer", "to", "the", "Ultimate", + * "Question", "of", "Life,", "the", "Universe,", "and", "Everything"], "randomNode"); + * console.log(response); // Output: 42 # The return value on the function that was execute. + * ``` + */ + public fcallReadonlyWithRoute( + func: string, + args: string[], + route?: Routes, + ): Promise { + return this.createWritePromise( + createFCallReadOnly(func, [], args), + toProtobufRoute(route), + ); + } + /** * Deletes a library and all its functions. * diff --git a/node/src/Transaction.ts b/node/src/Transaction.ts index 8a9b1b61e0..2cfdaae4dc 100644 --- a/node/src/Transaction.ts +++ b/node/src/Transaction.ts @@ -5,6 +5,7 @@ import { AggregationType, ExpireOptions, + GeoUnit, InfoOptions, InsertPosition, KeyWeight, @@ -37,6 +38,8 @@ import { createExists, createExpire, createExpireAt, + createFCall, + createFCallReadOnly, createFlushAll, createFlushDB, createFunctionDelete, @@ -142,7 +145,6 @@ import { createZRevRank, createZRevRankWithScore, createZScore, - GeoUnit, } from "./Commands"; import { command_request } from "./ProtobufMessage"; import { BitOffsetOptions } from "./commands/BitOffsetOptions"; @@ -1854,6 +1856,42 @@ export class BaseTransaction> { return this.addAndReturn(createLolwut(options)); } + /** + * Invokes a previously loaded function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * + * Command Response - The invoked function's return value. + */ + public fcall(func: string, keys: string[], args: string[]): T { + return this.addAndReturn(createFCall(func, keys, args)); + } + + /** + * Invokes a previously loaded read-only function. + * + * See https://valkey.io/commands/fcall/ for more details. + * + * since Valkey version 7.0.0. + * + * @param func - The function name. + * @param keys - A list of `keys` accessed by the function. To ensure the correct execution of functions, + * all names of keys that a function accesses must be explicitly provided as `keys`. + * @param args - A list of `function` arguments and it should not represent names of keys. + * + * Command Response - The invoked function's return value. + */ + public fcallReadonly(func: string, keys: string[], args: string[]): T { + return this.addAndReturn(createFCallReadOnly(func, keys, args)); + } + /** * Deletes a library and all its functions. * diff --git a/node/tests/RedisClient.test.ts b/node/tests/RedisClient.test.ts index 853a8aa769..3a6a83c117 100644 --- a/node/tests/RedisClient.test.ts +++ b/node/tests/RedisClient.test.ts @@ -397,22 +397,10 @@ describe("GlideClient", () => { checkSimple(await client.functionLoad(code)).toEqual(libName); checkSimple( - await client.customCommand([ - "FCALL", - funcName, - "0", - "one", - "two", - ]), + await client.fcall(funcName, [], ["one", "two"]), ).toEqual("one"); checkSimple( - await client.customCommand([ - "FCALL_RO", - funcName, - "0", - "one", - "two", - ]), + await client.fcallReadonly(funcName, [], ["one", "two"]), ).toEqual("one"); // TODO verify with FUNCTION LIST @@ -441,23 +429,11 @@ describe("GlideClient", () => { libName, ); - expect( - await client.customCommand([ - "FCALL", - func2Name, - "0", - "one", - "two", - ]), + checkSimple( + await client.fcall(func2Name, [], ["one", "two"]), ).toEqual(2); - expect( - await client.customCommand([ - "FCALL_RO", - func2Name, - "0", - "one", - "two", - ]), + checkSimple( + await client.fcallReadonly(func2Name, [], ["one", "two"]), ).toEqual(2); } finally { expect(await client.functionFlush()).toEqual("OK"); diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index e039c96ad5..064b155a28 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -610,8 +610,9 @@ describe("GlideClusterClient", () => { await client.functionLoad(code), ).toEqual(libName); // call functions from that library to confirm that it works - let fcall = await client.customCommand( - ["FCALL", funcName, "0", "one", "two"], + let fcall = await client.fcallWithRoute( + funcName, + ["one", "two"], route, ); checkClusterResponse( @@ -620,9 +621,9 @@ describe("GlideClusterClient", () => { (value) => checkSimple(value).toEqual("one"), ); - - fcall = await client.customCommand( - ["FCALL_RO", funcName, "0", "one", "two"], + fcall = await client.fcallReadonlyWithRoute( + funcName, + ["one", "two"], route, ); checkClusterResponse( @@ -659,8 +660,9 @@ describe("GlideClusterClient", () => { await client.functionLoad(newCode, true), ).toEqual(libName); - fcall = await client.customCommand( - ["FCALL", func2Name, "0", "one", "two"], + fcall = await client.fcallWithRoute( + func2Name, + ["one", "two"], route, ); checkClusterResponse( @@ -669,8 +671,9 @@ describe("GlideClusterClient", () => { (value) => expect(value).toEqual(2), ); - fcall = await client.customCommand( - ["FCALL_RO", func2Name, "0", "one", "two"], + fcall = await client.fcallReadonlyWithRoute( + func2Name, + ["one", "two"], route, ); checkClusterResponse( diff --git a/node/tests/TestUtilities.ts b/node/tests/TestUtilities.ts index 4a009cef85..03978c0cb7 100644 --- a/node/tests/TestUtilities.ts +++ b/node/tests/TestUtilities.ts @@ -693,6 +693,10 @@ export async function transactionTest( args.push(libName); baseTransaction.functionLoad(code, true); args.push(libName); + baseTransaction.fcall(funcName, [], ["one", "two"]); + args.push("one"); + baseTransaction.fcallReadonly(funcName, [], ["one", "two"]); + args.push("one"); baseTransaction.functionDelete(libName); args.push("OK"); baseTransaction.functionFlush();