From 11c6c24881b784038597e23bfead3cbdfbea021c Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Mon, 25 Apr 2022 09:09:23 -0400 Subject: [PATCH] Add support for redis functions (#2020) * fix #1906 - implement BITFIELD_RO * initial support for redis functions * fix test utils * redis functions commands and tests * upgrade deps * fix "Property 'uninstall' does not exist on type 'SinonFakeTimers'" * upgrade dockers version * Merge branch 'master' of github.com:redis/node-redis into functions * fix FUNCTION LIST WITHCODE and FUNCTION STATS * upgrade deps * set minimum version for FCALL and FCALL_RO * fix FUNCTION LOAD * FUNCTION LOAD * fix FUNCTION LOAD & FUNCTION LIST & FUNCTION LOAD WITHCODE * fix FUNCTION_LIST_WITHCODE test --- .github/workflows/tests.yml | 2 +- index.ts | 27 ++- package-lock.json | 45 +++- packages/client/index.ts | 2 +- packages/client/lib/client/commands.ts | 27 +++ packages/client/lib/client/index.spec.ts | 121 ++++++++--- packages/client/lib/client/index.ts | 193 ++++++++++-------- packages/client/lib/client/multi-command.ts | 90 +++++--- packages/client/lib/cluster/cluster-slots.ts | 69 ++++--- packages/client/lib/cluster/commands.ts | 12 ++ packages/client/lib/cluster/index.ts | 112 +++++++--- packages/client/lib/cluster/multi-command.ts | 111 ++++++---- packages/client/lib/commander.ts | 138 +++++++++---- packages/client/lib/commands/EVAL.ts | 4 +- packages/client/lib/commands/EVALSHA.ts | 4 +- .../client/lib/commands/EVALSHA_RO.spec.ts | 17 ++ packages/client/lib/commands/EVALSHA_RO.ts | 9 + packages/client/lib/commands/EVAL_RO.spec.ts | 31 +++ packages/client/lib/commands/EVAL_RO.ts | 9 + packages/client/lib/commands/FCALL.spec.ts | 29 +++ packages/client/lib/commands/FCALL.ts | 7 + packages/client/lib/commands/FCALL_RO.spec.ts | 29 +++ packages/client/lib/commands/FCALL_RO.ts | 9 + .../lib/commands/FUNCTION_DELETE.spec.ts | 24 +++ .../client/lib/commands/FUNCTION_DELETE.ts | 7 + .../client/lib/commands/FUNCTION_DUMP.spec.ts | 21 ++ packages/client/lib/commands/FUNCTION_DUMP.ts | 7 + .../lib/commands/FUNCTION_FLUSH.spec.ts | 30 +++ .../client/lib/commands/FUNCTION_FLUSH.ts | 13 ++ .../client/lib/commands/FUNCTION_KILL.spec.ts | 14 ++ packages/client/lib/commands/FUNCTION_KILL.ts | 7 + .../client/lib/commands/FUNCTION_LIST.spec.ts | 41 ++++ packages/client/lib/commands/FUNCTION_LIST.ts | 16 ++ .../commands/FUNCTION_LIST_WITHCODE.spec.ts | 42 ++++ .../lib/commands/FUNCTION_LIST_WITHCODE.ts | 26 +++ .../client/lib/commands/FUNCTION_LOAD.spec.ts | 36 ++++ packages/client/lib/commands/FUNCTION_LOAD.ts | 22 ++ .../lib/commands/FUNCTION_RESTORE.spec.ts | 37 ++++ .../client/lib/commands/FUNCTION_RESTORE.ts | 16 ++ .../lib/commands/FUNCTION_STATS.spec.ts | 25 +++ .../client/lib/commands/FUNCTION_STATS.ts | 56 +++++ .../lib/commands/generic-transformers.ts | 49 +++++ packages/client/lib/commands/index.ts | 63 +++++- packages/client/lib/lua-script.ts | 2 +- packages/client/lib/multi-command.ts | 19 +- packages/client/lib/test-utils.ts | 2 +- packages/graph/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/test-utils/lib/dockers.ts | 8 +- packages/test-utils/lib/index.ts | 44 ++-- packages/time-series/lib/test-utils.ts | 2 +- 51 files changed, 1406 insertions(+), 324 deletions(-) create mode 100644 packages/client/lib/commands/EVALSHA_RO.spec.ts create mode 100644 packages/client/lib/commands/EVALSHA_RO.ts create mode 100644 packages/client/lib/commands/EVAL_RO.spec.ts create mode 100644 packages/client/lib/commands/EVAL_RO.ts create mode 100644 packages/client/lib/commands/FCALL.spec.ts create mode 100644 packages/client/lib/commands/FCALL.ts create mode 100644 packages/client/lib/commands/FCALL_RO.spec.ts create mode 100644 packages/client/lib/commands/FCALL_RO.ts create mode 100644 packages/client/lib/commands/FUNCTION_DELETE.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_DELETE.ts create mode 100644 packages/client/lib/commands/FUNCTION_DUMP.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_DUMP.ts create mode 100644 packages/client/lib/commands/FUNCTION_FLUSH.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_FLUSH.ts create mode 100644 packages/client/lib/commands/FUNCTION_KILL.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_KILL.ts create mode 100644 packages/client/lib/commands/FUNCTION_LIST.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_LIST.ts create mode 100644 packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts create mode 100644 packages/client/lib/commands/FUNCTION_LOAD.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_LOAD.ts create mode 100644 packages/client/lib/commands/FUNCTION_RESTORE.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_RESTORE.ts create mode 100644 packages/client/lib/commands/FUNCTION_STATS.spec.ts create mode 100644 packages/client/lib/commands/FUNCTION_STATS.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0c8223d66..ba11eaa296 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: node-version: ['12', '14', '16'] - redis-version: ['5', '6.0', '6.2', '7.0-rc2'] + redis-version: ['5', '6.0', '6.2', '7.0-rc3'] steps: - uses: actions/checkout@v2.3.4 with: diff --git a/index.ts b/index.ts index c948e5eac3..cb49e6f7f4 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ import { RedisModules, + RedisFunctions, RedisScripts, createClient as _createClient, RedisClientOptions, @@ -33,12 +34,17 @@ export type RedisDefaultModules = typeof modules; export type RedisClientType< M extends RedisModules = RedisDefaultModules, + F extends RedisFunctions = Record, S extends RedisScripts = Record -> = _RedisClientType; +> = _RedisClientType; -export function createClient( - options?: RedisClientOptions -): _RedisClientType { +export function createClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +>( + options?: RedisClientOptions +): _RedisClientType { return _createClient({ ...options, modules: { @@ -50,12 +56,17 @@ export function createClient( export type RedisClusterType< M extends RedisModules = RedisDefaultModules, + F extends RedisFunctions = Record, S extends RedisScripts = Record -> = _RedisClusterType; +> = _RedisClusterType; -export function createCluster( - options: RedisClusterOptions -): RedisClusterType { +export function createCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +>( + options: RedisClusterOptions +): RedisClusterType { return _createCluster({ ...options, modules: { diff --git a/package-lock.json b/package-lock.json index 2d3f22c5b5..9152f212fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1865,6 +1865,19 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "node_modules/compress-brotli": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", + "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "dev": true, + "dependencies": { + "@types/json-buffer": "~3.0.0", + "json-buffer": "~3.0.1" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3332,9 +3345,9 @@ } }, "node_modules/inquirer": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", - "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz", + "integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -3347,13 +3360,13 @@ "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", - "rxjs": "^7.2.0", + "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" }, "engines": { - "node": ">=8.0.0" + "node": ">=12.0.0" } }, "node_modules/internal-slot": { @@ -5453,7 +5466,7 @@ "globby": "11.0.4", "got": "9.6.0", "import-cwd": "3.0.0", - "inquirer": "8.2.0", + "inquirer": "8.2.2", "is-ci": "3.0.1", "lodash": "4.17.21", "mime-types": "2.1.35", @@ -8382,6 +8395,16 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "compress-brotli": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", + "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "dev": true, + "requires": { + "@types/json-buffer": "~3.0.0", + "json-buffer": "~3.0.1" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -9474,9 +9497,9 @@ "dev": true }, "inquirer": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", - "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.2.tgz", + "integrity": "sha512-pG7I/si6K/0X7p1qU+rfWnpTE1UIkTONN1wxtzh0d+dHXtT/JG6qBgLxoyHVsQa8cFABxAPh0pD6uUUHiAoaow==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -9489,7 +9512,7 @@ "mute-stream": "0.0.8", "ora": "^5.4.1", "run-async": "^2.4.0", - "rxjs": "^7.2.0", + "rxjs": "^7.5.5", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6" @@ -11034,7 +11057,7 @@ "globby": "11.0.4", "got": "9.6.0", "import-cwd": "3.0.0", - "inquirer": "8.2.0", + "inquirer": "8.2.2", "is-ci": "3.0.1", "lodash": "4.17.21", "mime-types": "2.1.35", diff --git a/packages/client/index.ts b/packages/client/index.ts index ac138a935e..734c15664d 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -3,7 +3,7 @@ import RedisCluster from './lib/cluster'; export { RedisClientType, RedisClientOptions } from './lib/client'; -export { RedisModules, RedisScripts } from './lib/commands'; +export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands'; export const createClient = RedisClient.create; diff --git a/packages/client/lib/client/commands.ts b/packages/client/lib/client/commands.ts index eefa134658..94653e476b 100644 --- a/packages/client/lib/client/commands.ts +++ b/packages/client/lib/client/commands.ts @@ -62,6 +62,15 @@ import * as ECHO from '../commands/ECHO'; import * as FAILOVER from '../commands/FAILOVER'; import * as FLUSHALL from '../commands/FLUSHALL'; import * as FLUSHDB from '../commands/FLUSHDB'; +import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE'; +import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP'; +import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH'; +import * as FUNCTION_KILL from '../commands/FUNCTION_KILL'; +import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE'; +import * as FUNCTION_LIST from '../commands/FUNCTION_LIST'; +import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD'; +import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE'; +import * as FUNCTION_STATS from '../commands/FUNCTION_STATS'; import * as HELLO from '../commands/HELLO'; import * as INFO from '../commands/INFO'; import * as KEYS from '../commands/KEYS'; @@ -228,6 +237,24 @@ export default { flushAll: FLUSHALL, FLUSHDB, flushDb: FLUSHDB, + FUNCTION_DELETE, + functionDelete: FUNCTION_DELETE, + FUNCTION_DUMP, + functionDump: FUNCTION_DUMP, + FUNCTION_FLUSH, + functionFlush: FUNCTION_FLUSH, + FUNCTION_KILL, + functionKill: FUNCTION_KILL, + FUNCTION_LIST_WITHCODE, + functionListWithCode: FUNCTION_LIST_WITHCODE, + FUNCTION_LIST, + functionList: FUNCTION_LIST, + FUNCTION_LOAD, + functionLoad: FUNCTION_LOAD, + FUNCTION_RESTORE, + functionRestore: FUNCTION_RESTORE, + FUNCTION_STATS, + functionStats: FUNCTION_STATS, HELLO, hello: HELLO, INFO, diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index a6b924d42a..349a567a30 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -2,7 +2,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisClient, { RedisClientType } from '.'; import { RedisClientMultiCommandType } from './multi-command'; -import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisScripts } from '../commands'; +import { RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands'; import { AbortError, ClientClosedError, ConnectionTimeoutError, DisconnectsClientError, SocketClosedUnexpectedlyError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; import { spy } from 'sinon'; @@ -10,16 +10,42 @@ import { once } from 'events'; import { ClientKillFilters } from '../commands/CLIENT_KILL'; export const SQUARE_SCRIPT = defineScript({ - NUMBER_OF_KEYS: 0, SCRIPT: 'return ARGV[1] * ARGV[1];', + NUMBER_OF_KEYS: 0, transformArguments(number: number): Array { return [number.toString()]; - }, - transformReply(reply: number): number { - return reply; } }); +export const MATH_FUNCTION = { + name: 'math', + engine: 'LUA', + code: `#!LUA name=math + redis.register_function{ + function_name = "square", + callback = function(keys, args) return args[1] * args[1] end, + flags = { "no-writes" } + }`, + library: { + square: { + NAME: 'square', + NUMBER_OF_KEYS: 0, + transformArguments(number: number): Array { + return [number.toString()]; + } + } + } +}; + +export async function loadMathFunction( + client: RedisClientType +): Promise { + await client.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); +} + describe('Client', () => { describe('parseURL', () => { it('redis://user:secret@localhost:6379/0', () => { @@ -115,7 +141,14 @@ describe('Client', () => { }); describe('legacyMode', () => { - function sendCommandAsync(client: RedisClientType, args: RedisCommandArguments): Promise { + function sendCommandAsync< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >( + client: RedisClientType, + args: RedisCommandArguments + ): Promise { return new Promise((resolve, reject) => { (client as any).sendCommand(args, (err: Error | undefined, reply: RedisCommandRawReply) => { if (err) return reject(err); @@ -159,7 +192,14 @@ describe('Client', () => { } }); - function setAsync(client: RedisClientType, ...args: Array): Promise { + function setAsync< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >( + client: RedisClientType, + ...args: Array + ): Promise { return new Promise((resolve, reject) => { (client as any).set(...args, (err: Error | undefined, reply: RedisCommandRawReply) => { if (err) return reject(err); @@ -205,7 +245,11 @@ describe('Client', () => { } }); - function multiExecAsync(multi: RedisClientMultiCommandType): Promise> { + function multiExecAsync< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(multi: RedisClientMultiCommandType): Promise> { return new Promise((resolve, reject) => { (multi as any).exec((err: Error | undefined, replies: Array) => { if (err) return reject(err); @@ -439,25 +483,44 @@ describe('Client', () => { } }); + const module = { + echo: { + transformArguments(message: string): Array { + return ['ECHO', message]; + }, + transformReply(reply: string): string { + return reply; + } + } + }; + testUtils.testWithClient('modules', async client => { - // assert.equal( - // await client.module.echo('message'), - // 'message' - // ); + assert.equal( + await client.module.echo('message'), + 'message' + ); }, { ...GLOBAL.SERVERS.OPEN, clientOptions: { modules: { - module: { - echo: { - transformArguments(message: string): Array { - return ['ECHO', message]; - }, - transformReply(reply: string): string { - return reply; - } - } - } + module + } + } + }); + + testUtils.testWithClient('functions', async client => { + await loadMathFunction(client); + + assert.equal( + await client.math.square(2), + 4 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [7, 0], + clientOptions: { + functions: { + math: MATH_FUNCTION.library } } }); @@ -468,9 +531,13 @@ describe('Client', () => { assert.ok(id !== isolatedId); }, GLOBAL.SERVERS.OPEN); - async function killClient( - client: RedisClientType, - errorClient: RedisClientType = client + async function killClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >( + client: RedisClientType, + errorClient: RedisClientType = client ): Promise { const onceErrorPromise = once(errorClient, 'error'); await client.sendCommand(['QUIT']); @@ -684,7 +751,9 @@ describe('Client', () => { try { await assert.doesNotReject(Promise.all([ - subscriber.subscribe('channel', () => {}), + subscriber.subscribe('channel', () => { + // noop + }), publisher.publish('channel', 'message') ])); } finally { diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 242c590cc8..e78a491cc1 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,5 +1,5 @@ import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands'; +import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString } from '../commands'; import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket'; import RedisCommandsQueue, { PubSubListener, PubSubSubscribeCommands, PubSubUnsubscribeCommands, QueueCommandOptions } from './commands-queue'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; @@ -9,16 +9,17 @@ import { CommandOptions, commandOptions, isCommandOptions } from '../command-opt import { ScanOptions, ZMember } from '../commands/generic-transformers'; import { ScanCommandOptions } from '../commands/SCAN'; import { HScanTuple } from '../commands/HSCAN'; -import { extendWithCommands, extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander'; +import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander'; import { Pool, Options as PoolOptions, createPool } from 'generic-pool'; import { ClientClosedError, DisconnectsClientError } from '../errors'; import { URL } from 'url'; import { TcpSocketConnectOpts } from 'net'; export interface RedisClientOptions< - M extends RedisModules = Record, - S extends RedisScripts = Record -> extends RedisPlugins { + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts +> extends RedisExtensions { url?: string; socket?: RedisSocketOptions; username?: string; @@ -32,58 +33,37 @@ export interface RedisClientOptions< isolationPoolOptions?: PoolOptions; } -type ConvertArgumentType = - Type extends RedisCommandArgument ? ( - Type extends (string & ToType) ? Type : ToType - ) : ( - Type extends Set ? Set> : ( - Type extends Map ? Map> : ( - Type extends Array ? Array> : ( - Type extends Date ? Type : ( - Type extends Record ? { - [Property in keyof Type]: ConvertArgumentType - } : Type - ) - ) - ) - ) - ); - -export type RedisClientCommandSignature< - Command extends RedisCommand, - Params extends Array = Parameters -> = >( - ...args: Params | [options: Options, ...rest: Params] -) => Promise< - ConvertArgumentType< - RedisCommandReply, - Options['returnBuffers'] extends true ? Buffer : string - > ->; - type WithCommands = { - [P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>; + [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>; }; -export type ExcludeMappedString = string extends S ? never : S; - export type WithModules = { [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: RedisClientCommandSignature; + [C in keyof M[P] as ExcludeMappedString]: RedisCommandSignature; + }; +}; + +export type WithFunctions = { + [P in keyof F as ExcludeMappedString

]: { + [FF in keyof F[P] as ExcludeMappedString]: RedisCommandSignature; }; }; export type WithScripts = { - [P in keyof S as ExcludeMappedString

]: RedisClientCommandSignature; + [P in keyof S as ExcludeMappedString

]: RedisCommandSignature; }; export type RedisClientType< M extends RedisModules = Record, + F extends RedisFunctions = Record, S extends RedisScripts = Record -> = RedisClient & WithCommands & WithModules & WithScripts; +> = RedisClient & WithCommands & WithModules & WithFunctions & WithScripts; -export type InstantiableRedisClient = - new (options?: RedisClientOptions) => RedisClientType; +export type InstantiableRedisClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = new (options?: RedisClientOptions) => RedisClientType; export interface ClientCommandOptions extends QueueCommandOptions { isolated?: boolean; @@ -91,30 +71,44 @@ export interface ClientCommandOptions extends QueueCommandOptions { type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void; -export default class RedisClient extends EventEmitter { +export default class RedisClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> extends EventEmitter { static commandOptions(options: T): CommandOptions { return commandOptions(options); } commandOptions = RedisClient.commandOptions; - static extend(plugins?: RedisPlugins): InstantiableRedisClient { - const Client = extendWithModulesAndScripts({ + static extend< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(extensions?: RedisExtensions): InstantiableRedisClient { + const Client = attachExtensions({ BaseClass: RedisClient, - modules: plugins?.modules, - modulesCommandsExecutor: RedisClient.prototype.commandsExecutor, - scripts: plugins?.scripts, - scriptsExecutor: RedisClient.prototype.scriptsExecutor + modulesExecutor: RedisClient.prototype.commandsExecutor, + modules: extensions?.modules, + functionsExecutor: RedisClient.prototype.functionsExecuter, + functions: extensions?.functions, + scriptsExecutor: RedisClient.prototype.scriptsExecuter, + scripts: extensions?.scripts }); if (Client !== RedisClient) { - Client.prototype.Multi = RedisClientMultiCommand.extend(plugins); + Client.prototype.Multi = RedisClientMultiCommand.extend(extensions); } return Client; } - static create(options?: RedisClientOptions): RedisClientType { + static create< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(options?: RedisClientOptions): RedisClientType { return new (RedisClient.extend(options))(options); } @@ -157,14 +151,14 @@ export default class RedisClient return parsed; } - readonly #options?: RedisClientOptions; - readonly #queue: RedisCommandsQueue; + readonly #options?: RedisClientOptions; readonly #socket: RedisSocket; - readonly #isolationPool: Pool>; + readonly #queue: RedisCommandsQueue; + readonly #isolationPool: Pool>; readonly #v4: Record = {}; #selectedDB = 0; - get options(): RedisClientOptions | undefined { + get options(): RedisClientOptions | undefined { return this.#options; } @@ -180,7 +174,7 @@ export default class RedisClient return this.#v4; } - constructor(options?: RedisClientOptions) { + constructor(options?: RedisClientOptions) { super(); this.#options = this.#initiateOptions(options); this.#queue = this.#initiateQueue(); @@ -198,7 +192,7 @@ export default class RedisClient this.#legacyMode(); } - #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { + #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { if (options?.url) { const parsed = RedisClient.parseURL(options.url); if (options.socket) { @@ -350,7 +344,7 @@ export default class RedisClient (...args: Array): void => (this as any).sendCommand(name, ...args); } - duplicate(overrides?: Partial>): RedisClientType { + duplicate(overrides?: Partial>): RedisClientType { return new (Object.getPrototypeOf(this).constructor)({ ...this.#options, ...overrides @@ -361,9 +355,11 @@ export default class RedisClient await this.#socket.connect(); } - async commandsExecutor(command: RedisCommand, args: Array): Promise> { - const { args: redisArgs, options } = transformCommandArguments(command, args); - + async commandsExecutor( + command: C, + args: Array + ): Promise> { + const { args: redisArgs, options } = transformCommandArguments(command, args); return transformCommandReply( command, await this.#sendCommand(redisArgs, options), @@ -371,12 +367,18 @@ export default class RedisClient ); } - sendCommand(args: RedisCommandArguments, options?: ClientCommandOptions): Promise { + sendCommand( + args: RedisCommandArguments, + options?: ClientCommandOptions + ): Promise { return this.#sendCommand(args, options); } // using `#sendCommand` cause `sendCommand` is overwritten in legacy mode - #sendCommand(args: RedisCommandArguments, options?: ClientCommandOptions): Promise { + #sendCommand( + args: RedisCommandArguments, + options?: ClientCommandOptions + ): Promise { if (!this.#socket.isOpen) { return Promise.reject(new ClientClosedError()); } @@ -395,9 +397,34 @@ export default class RedisClient return promise; } - async scriptsExecutor(script: RedisScript, args: Array): Promise> { - const { args: redisArgs, options } = transformCommandArguments(script, args); + async functionsExecuter( + fn: F, + args: Array + ): Promise> { + const { args: redisArgs, options } = transformCommandArguments(fn, args); + return transformCommandReply( + fn, + await this.executeFunction(fn, redisArgs, options), + redisArgs.preserve + ); + } + executeFunction( + fn: RedisFunction, + args: RedisCommandArguments, + options?: ClientCommandOptions + ): Promise { + return this.#sendCommand( + fCallArguments(fn, args), + options + ); + } + + async scriptsExecuter( + script: S, + args: Array + ): Promise> { + const { args: redisArgs, options } = transformCommandArguments(script, args); return transformCommandReply( script, await this.executeScript(script, redisArgs, options), @@ -405,25 +432,29 @@ export default class RedisClient ); } - async executeScript(script: RedisScript, args: RedisCommandArguments, options?: ClientCommandOptions): Promise> { + async executeScript( + script: RedisScript, + args: RedisCommandArguments, + options?: ClientCommandOptions + ): Promise { + const redisArgs: RedisCommandArguments = ['EVALSHA', script.SHA1]; + + if (script.NUMBER_OF_KEYS !== undefined) { + redisArgs.push(script.NUMBER_OF_KEYS.toString()); + } + + redisArgs.push(...args); + try { - return await this.#sendCommand([ - 'EVALSHA', - script.SHA1, - script.NUMBER_OF_KEYS.toString(), - ...args - ], options); + return await this.#sendCommand(redisArgs, options); } catch (err: any) { if (!err?.message?.startsWith?.('NOSCRIPT')) { throw err; } - return await this.#sendCommand([ - 'EVAL', - script.SCRIPT, - script.NUMBER_OF_KEYS.toString(), - ...args - ], options); + redisArgs[0] = 'EVAL'; + redisArgs[1] = script.SCRIPT; + return this.#sendCommand(redisArgs, options); } } @@ -558,11 +589,11 @@ export default class RedisClient } } - executeIsolated(fn: (client: RedisClientType) => T | Promise): Promise { + executeIsolated(fn: (client: RedisClientType) => T | Promise): Promise { return this.#isolationPool.use(fn); } - multi(): RedisClientMultiCommandType { + multi(): RedisClientMultiCommandType { return new (this as any).Multi( this.multiExecutor.bind(this), this.#options?.legacyMode @@ -639,7 +670,7 @@ export default class RedisClient } } -extendWithCommands({ +attachCommands({ BaseClass: RedisClient, commands: COMMANDS, executor: RedisClient.prototype.commandsExecutor diff --git a/packages/client/lib/client/multi-command.ts b/packages/client/lib/client/multi-command.ts index a9409075ca..8fda702a31 100644 --- a/packages/client/lib/client/multi-command.ts +++ b/packages/client/lib/client/multi-command.ts @@ -1,28 +1,63 @@ import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands'; +import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands'; import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command'; -import { extendWithCommands, extendWithModulesAndScripts, transformLegacyCommandArguments } from '../commander'; -import { ExcludeMappedString } from '.'; +import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander'; -type RedisClientMultiCommandSignature = - (...args: Parameters) => RedisClientMultiCommandType; +type CommandSignature< + C extends RedisCommand, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = (...args: Parameters) => RedisClientMultiCommandType; -type WithCommands = { - [P in keyof typeof COMMANDS]: RedisClientMultiCommandSignature<(typeof COMMANDS)[P], M, S>; +type WithCommands< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>; }; -type WithModules = { +type WithModules< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: RedisClientMultiCommandSignature; + [C in keyof M[P] as ExcludeMappedString]: CommandSignature; }; }; -type WithScripts = { - [P in keyof S as ExcludeMappedString

]: RedisClientMultiCommandSignature; +type WithFunctions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof F as ExcludeMappedString

]: { + [FF in keyof F[P] as ExcludeMappedString]: CommandSignature; + }; }; -export type RedisClientMultiCommandType = - RedisClientMultiCommand & WithCommands & WithModules & WithScripts; +type WithScripts< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof S as ExcludeMappedString

]: CommandSignature; +}; + +export type RedisClientMultiCommandType< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = RedisClientMultiCommand & WithCommands & WithModules & WithFunctions & WithScripts; + +type InstantiableRedisMultiCommand< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = new (...args: ConstructorParameters) => RedisClientMultiCommandType; + export type RedisClientMultiExecutor = (queue: Array, chainId?: symbol) => Promise>; @@ -30,15 +65,19 @@ export default class RedisClientMultiCommand { readonly #multi = new RedisMultiCommand(); readonly #executor: RedisClientMultiExecutor; - static extend( - plugins?: RedisPlugins - ): new (...args: ConstructorParameters) => RedisClientMultiCommandType { - return extendWithModulesAndScripts({ + static extend< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(extensions?: RedisExtensions): InstantiableRedisMultiCommand { + return attachExtensions({ BaseClass: RedisClientMultiCommand, - modules: plugins?.modules, - modulesCommandsExecutor: RedisClientMultiCommand.prototype.commandsExecutor, - scripts: plugins?.scripts, - scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor + modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor, + modules: extensions?.modules, + functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor, + functions: extensions?.functions, + scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor, + scripts: extensions?.scripts }); } @@ -102,6 +141,11 @@ export default class RedisClientMultiCommand { return this; } + functionsExecutor(fn: RedisFunction, args: Array): this { + this.#multi.addFunction(fn, args); + return this; + } + scriptsExecutor(script: RedisScript, args: Array): this { this.#multi.addScript(script, args); return this; @@ -123,15 +167,13 @@ export default class RedisClientMultiCommand { EXEC = this.exec; async execAsPipeline(): Promise> { - if (!this.#multi.queue.length) return []; - return this.#multi.transformReplies( await this.#executor(this.#multi.queue) ); } } -extendWithCommands({ +attachCommands({ BaseClass: RedisClientMultiCommand, commands: COMMANDS, executor: RedisClientMultiCommand.prototype.commandsExecutor diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 872b5462ac..d23ef569f3 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -1,7 +1,7 @@ import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client'; import { RedisClusterMasterNode, RedisClusterReplicaNode } from '../commands/CLUSTER_NODES'; import { RedisClusterClientOptions, RedisClusterOptions } from '.'; -import { RedisCommandArgument, RedisModules, RedisScripts } from '../commands'; +import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands'; import { RootNodesUnavailableError } from '../errors'; // We need to use 'require', because it's not possible with Typescript to import @@ -9,9 +9,13 @@ import { RootNodesUnavailableError } from '../errors'; // set to true. const calculateSlot = require('cluster-key-slot'); -export interface ClusterNode { +export interface ClusterNode< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> { id: string; - client: RedisClientType; + client: RedisClientType; } interface NodeAddress { @@ -23,22 +27,30 @@ export type NodeAddressMap = { [address: string]: NodeAddress; } | ((address: string) => NodeAddress | undefined); -interface SlotNodes { - master: ClusterNode; - replicas: Array>; - clientIterator: IterableIterator> | undefined; +interface SlotNodes< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> { + master: ClusterNode; + replicas: Array>; + clientIterator: IterableIterator> | undefined; } type OnError = (err: unknown) => void; -export default class RedisClusterSlots { - readonly #options: RedisClusterOptions; - readonly #Client: InstantiableRedisClient; +export default class RedisClusterSlots< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> { + readonly #options: RedisClusterOptions; + readonly #Client: InstantiableRedisClient; readonly #onError: OnError; - readonly #nodeByAddress = new Map>(); - readonly #slots: Array> = []; + readonly #nodeByAddress = new Map>(); + readonly #slots: Array> = []; - constructor(options: RedisClusterOptions, onError: OnError) { + constructor(options: RedisClusterOptions, onError: OnError) { this.#options = options; this.#Client = RedisClient.extend(options); this.#onError = onError; @@ -72,7 +84,7 @@ export default class RedisClusterSlots; - async rediscover(startWith: RedisClientType): Promise { + async rediscover(startWith: RedisClientType): Promise { if (!this.#runningRediscoverPromise) { this.#runningRediscoverPromise = this.#rediscover(startWith) .finally(() => this.#runningRediscoverPromise = undefined); @@ -81,7 +93,7 @@ export default class RedisClusterSlots): Promise { + async #rediscover(startWith: RedisClientType): Promise { if (await this.#discoverNodes(startWith.options)) return; for (const { client } of this.#nodeByAddress.values()) { @@ -137,7 +149,7 @@ export default class RedisClusterSlots { + #initiateClient(options?: RedisClusterClientOptions): RedisClientType { return new this.#Client(this.#clientOptionsDefaults(options)) .on('error', this.#onError); } @@ -152,7 +164,12 @@ export default class RedisClusterSlots, promises: Array>): ClusterNode { + #initiateClientForNode( + nodeData: RedisClusterMasterNode | RedisClusterReplicaNode, + readonly: boolean, + clientsInUse: Set, + promises: Array> + ): ClusterNode { const address = `${nodeData.host}:${nodeData.port}`; clientsInUse.add(address); @@ -175,11 +192,11 @@ export default class RedisClusterSlots { + getSlotMaster(slot: number): ClusterNode { return this.#slots[slot].master; } - *#slotClientIterator(slotNumber: number): IterableIterator> { + *#slotClientIterator(slotNumber: number): IterableIterator> { const slot = this.#slots[slotNumber]; yield slot.master.client; @@ -188,7 +205,7 @@ export default class RedisClusterSlots { + #getSlotClient(slotNumber: number): RedisClientType { const slot = this.#slots[slotNumber]; if (!slot.clientIterator) { slot.clientIterator = this.#slotClientIterator(slotNumber); @@ -203,9 +220,9 @@ export default class RedisClusterSlots>; + #randomClientIterator?: IterableIterator>; - #getRandomClient(): RedisClientType { + #getRandomClient(): RedisClientType { if (!this.#nodeByAddress.size) { throw new Error('Cluster is not connected'); } @@ -223,7 +240,7 @@ export default class RedisClusterSlots { + getClient(firstKey?: RedisCommandArgument, isReadonly?: boolean): RedisClientType { if (!firstKey) { return this.#getRandomClient(); } @@ -236,7 +253,7 @@ export default class RedisClusterSlots> { + getMasters(): Array> { const masters = []; for (const node of this.#nodeByAddress.values()) { if (node.client.options?.readonly) continue; @@ -247,7 +264,7 @@ export default class RedisClusterSlots | undefined { + getNodeByAddress(address: string): ClusterNode | undefined { const mappedAddress = this.#getNodeAddress(address); return this.#nodeByAddress.get( mappedAddress ? `${mappedAddress.host}:${mappedAddress.port}` : address @@ -262,7 +279,7 @@ export default class RedisClusterSlots client.disconnect()); } - async #destroy(fn: (client: RedisClientType) => Promise): Promise { + async #destroy(fn: (client: RedisClientType) => Promise): Promise { const promises = []; for (const { client } of this.#nodeByAddress.values()) { promises.push(fn(client)); diff --git a/packages/client/lib/cluster/commands.ts b/packages/client/lib/cluster/commands.ts index 91e589e127..a8527cf235 100644 --- a/packages/client/lib/cluster/commands.ts +++ b/packages/client/lib/cluster/commands.ts @@ -18,12 +18,16 @@ import * as DECR from '../commands/DECR'; import * as DECRBY from '../commands/DECRBY'; import * as DEL from '../commands/DEL'; import * as DUMP from '../commands/DUMP'; +import * as EVAL_RO from '../commands/EVAL_RO'; import * as EVAL from '../commands/EVAL'; +import * as EVALSHA_RO from '../commands/EVALSHA_RO'; import * as EVALSHA from '../commands/EVALSHA'; import * as EXISTS from '../commands/EXISTS'; import * as EXPIRE from '../commands/EXPIRE'; import * as EXPIREAT from '../commands/EXPIREAT'; import * as EXPIRETIME from '../commands/EXPIRETIME'; +import * as FCALL_RO from '../commands/FCALL_RO'; +import * as FCALL from '../commands/FCALL'; import * as GEOADD from '../commands/GEOADD'; import * as GEODIST from '../commands/GEODIST'; import * as GEOHASH from '../commands/GEOHASH'; @@ -230,10 +234,14 @@ export default { del: DEL, DUMP, dump: DUMP, + EVAL_RO, + evalRo: EVAL_RO, EVAL, eval: EVAL, EVALSHA, evalSha: EVALSHA, + EVALSHA_RO, + evalShaRo: EVALSHA_RO, EXISTS, exists: EXISTS, EXPIRE, @@ -242,6 +250,10 @@ export default { expireAt: EXPIREAT, EXPIRETIME, expireTime: EXPIRETIME, + FCALL_RO, + fCallRo: FCALL_RO, + FCALL, + fCall: FCALL, GEOADD, geoAdd: GEOADD, GEODIST, diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 404d13b051..a4e16f3671 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -1,18 +1,22 @@ import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisModules, RedisPlugins, RedisScript, RedisScripts } from '../commands'; -import { ClientCommandOptions, RedisClientCommandSignature, RedisClientOptions, RedisClientType, WithModules, WithScripts } from '../client'; +import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands'; +import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client'; import RedisClusterSlots, { ClusterNode, NodeAddressMap } from './cluster-slots'; -import { extendWithModulesAndScripts, transformCommandArguments, transformCommandReply, extendWithCommands } from '../commander'; +import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander'; import { EventEmitter } from 'events'; -import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command'; +import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command'; import { RedisMultiQueuedCommand } from '../multi-command'; -export type RedisClusterClientOptions = Omit; +export type RedisClusterClientOptions = Omit< + RedisClientOptions, + 'modules' | 'functions' | 'scripts' | 'database' +>; export interface RedisClusterOptions< M extends RedisModules = Record, + F extends RedisFunctions = Record, S extends RedisScripts = Record -> extends RedisPlugins { +> extends RedisExtensions { rootNodes: Array; defaults?: Partial; useReplicas?: boolean; @@ -21,16 +25,25 @@ export interface RedisClusterOptions< } type WithCommands = { - [P in keyof typeof COMMANDS]: RedisClientCommandSignature<(typeof COMMANDS)[P]>; + [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>; }; export type RedisClusterType< M extends RedisModules = Record, + F extends RedisFunctions = Record, S extends RedisScripts = Record -> = RedisCluster & WithCommands & WithModules & WithScripts; +> = RedisCluster & WithCommands & WithModules & WithFunctions & WithScripts; -export default class RedisCluster extends EventEmitter { - static extractFirstKey(command: RedisCommand, originalArgs: Array, redisArgs: RedisCommandArguments): RedisCommandArgument | undefined { +export default class RedisCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> extends EventEmitter { + static extractFirstKey( + command: RedisCommand, + originalArgs: Array, + redisArgs: RedisCommandArguments + ): RedisCommandArgument | undefined { if (command.FIRST_KEY_INDEX === undefined) { return undefined; } else if (typeof command.FIRST_KEY_INDEX === 'number') { @@ -40,21 +53,27 @@ export default class RedisCluster(options?: RedisClusterOptions): RedisClusterType { - return new (extendWithModulesAndScripts({ + static create< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(options?: RedisClusterOptions): RedisClusterType { + return new (attachExtensions({ BaseClass: RedisCluster, + modulesExecutor: RedisCluster.prototype.commandsExecutor, modules: options?.modules, - modulesCommandsExecutor: RedisCluster.prototype.commandsExecutor, - scripts: options?.scripts, - scriptsExecutor: RedisCluster.prototype.scriptsExecutor + functionsExecutor: RedisCluster.prototype.functionsExecutor, + functions: options?.functions, + scriptsExecutor: RedisCluster.prototype.scriptsExecutor, + scripts: options?.scripts }))(options); } - readonly #options: RedisClusterOptions; - readonly #slots: RedisClusterSlots; - readonly #Multi: new (...args: ConstructorParameters) => RedisClusterMultiCommandType; + readonly #options: RedisClusterOptions; + readonly #slots: RedisClusterSlots; + readonly #Multi: InstantiableRedisClusterMultiCommandType; - constructor(options: RedisClusterOptions) { + constructor(options: RedisClusterOptions) { super(); this.#options = options; @@ -62,7 +81,7 @@ export default class RedisCluster>): RedisClusterType { + duplicate(overrides?: Partial>): RedisClusterType { return new (Object.getPrototypeOf(this).constructor)({ ...this.#options, ...overrides @@ -73,9 +92,11 @@ export default class RedisCluster): Promise> { - const { args: redisArgs, options } = transformCommandArguments(command, args); - + async commandsExecutor( + command: C, + args: Array + ): Promise> { + const { args: redisArgs, options } = transformCommandArguments(command, args); return transformCommandReply( command, await this.sendCommand( @@ -101,9 +122,38 @@ export default class RedisCluster): Promise> { - const { args: redisArgs, options } = transformCommandArguments(script, args); + async functionsExecutor( + fn: F, + args: Array + ): Promise> { + const { args: redisArgs, options } = transformCommandArguments(fn, args); + return transformCommandReply( + fn, + await this.executeFunction( + fn, + args, + redisArgs, + options + ), + redisArgs.preserve + ); + } + async executeFunction( + fn: RedisFunction, + originalArgs: Array, + redisArgs: RedisCommandArguments, + options?: ClientCommandOptions + ): Promise { + return this.#execute( + RedisCluster.extractFirstKey(fn, originalArgs, redisArgs), + fn.IS_READ_ONLY, + client => client.executeFunction(fn, redisArgs, options) + ); + } + + async scriptsExecutor(script: S, args: Array): Promise> { + const { args: redisArgs, options } = transformCommandArguments(script, args); return transformCommandReply( script, await this.executeScript( @@ -121,7 +171,7 @@ export default class RedisCluster, redisArgs: RedisCommandArguments, options?: ClientCommandOptions - ): Promise> { + ): Promise { return this.#execute( RedisCluster.extractFirstKey(script, originalArgs, redisArgs), script.IS_READ_ONLY, @@ -132,7 +182,7 @@ export default class RedisCluster( firstKey: RedisCommandArgument | undefined, isReadonly: boolean | undefined, - executor: (client: RedisClientType) => Promise + executor: (client: RedisClientType) => Promise ): Promise { const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; let client = this.#slots.getClient(firstKey, isReadonly); @@ -171,7 +221,7 @@ export default class RedisCluster { + multi(routing?: RedisCommandArgument): RedisClusterMultiCommandType { return new this.#Multi( (commands: Array, firstKey?: RedisCommandArgument, chainId?: symbol) => { return this.#execute( @@ -184,11 +234,11 @@ export default class RedisCluster> { + getMasters(): Array> { return this.#slots.getMasters(); } - getSlotMaster(slot: number): ClusterNode { + getSlotMaster(slot: number): ClusterNode { return this.#slots.getSlotMaster(slot); } @@ -201,7 +251,7 @@ export default class RedisCluster = - (...args: Parameters) => RedisClusterMultiCommandType; +type RedisClusterMultiCommandSignature< + C extends RedisCommand, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = (...args: Parameters) => RedisClusterMultiCommandType; -type WithCommands = { - [P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, S> +type WithCommands< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>; }; -type WithModules = { +type WithModules< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: RedisClusterMultiCommandSignature; + [C in keyof M[P] as ExcludeMappedString]: RedisClusterMultiCommandSignature; }; }; -type WithScripts = { - [P in keyof S as ExcludeMappedString

]: RedisClusterMultiCommandSignature +type WithFunctions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof F as ExcludeMappedString

]: { + [FF in keyof F[P] as ExcludeMappedString]: RedisClusterMultiCommandSignature; + }; }; -export type RedisClusterMultiCommandType = - RedisClusterMultiCommand & WithCommands & WithModules & WithScripts; +type WithScripts< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = { + [P in keyof S as ExcludeMappedString

]: RedisClusterMultiCommandSignature; +}; + +export type RedisClusterMultiCommandType< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = RedisClusterMultiCommand & WithCommands & WithModules & WithFunctions & WithScripts; + +export type InstantiableRedisClusterMultiCommandType< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> = new (...args: ConstructorParameters) => RedisClusterMultiCommandType; export type RedisClusterMultiExecutor = (queue: Array, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise>; @@ -32,15 +66,19 @@ export default class RedisClusterMultiCommand { readonly #executor: RedisClusterMultiExecutor; #firstKey: RedisCommandArgument | undefined; - static extend( - plugins?: RedisPlugins - ): new (...args: ConstructorParameters) => RedisClusterMultiCommandType { - return extendWithModulesAndScripts({ + static extend< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(extensions?: RedisExtensions): InstantiableRedisClusterMultiCommandType { + return attachExtensions({ BaseClass: RedisClusterMultiCommand, - modules: plugins?.modules, - modulesCommandsExecutor: RedisClusterMultiCommand.prototype.commandsExecutor, - scripts: plugins?.scripts, - scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor + modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor, + modules: extensions?.modules, + functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor, + functions: extensions?.functions, + scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor, + scripts: extensions?.scripts }); } @@ -51,15 +89,8 @@ export default class RedisClusterMultiCommand { commandsExecutor(command: RedisCommand, args: Array): this { const transformedArguments = command.transformArguments(...args); - if (!this.#firstKey) { - this.#firstKey = RedisCluster.extractFirstKey(command, args, transformedArguments); - } - - return this.addCommand( - undefined, - transformedArguments, - command.transformReply - ); + this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments); + return this.addCommand(undefined, transformedArguments, command.transformReply); } addCommand( @@ -67,21 +98,21 @@ export default class RedisClusterMultiCommand { args: RedisCommandArguments, transformReply?: RedisCommand['transformReply'] ): this { - if (!this.#firstKey) { - this.#firstKey = firstKey; - } - + this.#firstKey ??= firstKey; this.#multi.addCommand(args, transformReply); return this; } + functionsExecutor(fn: RedisFunction, args: Array): this { + const transformedArguments = this.#multi.addFunction(fn, args); + this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments); + return this; + } + scriptsExecutor(script: RedisScript, args: Array): this { const transformedArguments = this.#multi.addScript(script, args); - if (!this.#firstKey) { - this.#firstKey = RedisCluster.extractFirstKey(script, args, transformedArguments); - } - - return this.addCommand(undefined, transformedArguments); + this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments); + return this; } async exec(execAsPipeline = false): Promise> { @@ -106,7 +137,7 @@ export default class RedisClusterMultiCommand { } } -extendWithCommands({ +attachCommands({ BaseClass: RedisClusterMultiCommand, commands: COMMANDS, executor: RedisClusterMultiCommand.prototype.commandsExecutor diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index d70435e14a..f8cb74c313 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -1,16 +1,23 @@ import { CommandOptions, isCommandOptions } from './command-options'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisCommands, RedisModules, RedisScript, RedisScripts } from './commands'; +import { RedisCommand, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands'; -type Instantiable = new(...args: Array) => T; +type Instantiable = new (...args: Array) => T; -interface ExtendWithCommandsConfig { - BaseClass: T; - commands: RedisCommands; - executor(command: RedisCommand, args: Array): unknown; +type CommandsExecutor = + (command: C, args: Array) => unknown; + +interface AttachCommandsConfig { + BaseClass: Instantiable; + commands: Record; + executor: CommandsExecutor; } -export function extendWithCommands({ BaseClass, commands, executor }: ExtendWithCommandsConfig): void { +export function attachCommands({ + BaseClass, + commands, + executor +}: AttachCommandsConfig): void { for (const [name, command] of Object.entries(commands)) { BaseClass.prototype[name] = function (...args: Array): unknown { return executor.call(this, command, args); @@ -18,56 +25,82 @@ export function extendWithCommands({ BaseClass, commands } } -interface ExtendWithModulesAndScriptsConfig { +interface AttachExtensionsConfig { BaseClass: T; + modulesExecutor: CommandsExecutor; modules?: RedisModules; - modulesCommandsExecutor(this: InstanceType, command: RedisCommand, args: Array): unknown; + functionsExecutor: CommandsExecutor; + functions?: RedisFunctions; + scriptsExecutor: CommandsExecutor; scripts?: RedisScripts; - scriptsExecutor(this: InstanceType, script: RedisScript, args: Array): unknown; } -export function extendWithModulesAndScripts(config: ExtendWithModulesAndScriptsConfig): T { - let Commander: T | undefined; +export function attachExtensions(config: AttachExtensionsConfig): any { + let Commander; if (config.modules) { - Commander = class extends config.BaseClass { - constructor(...args: Array) { - super(...args); + Commander = attachWithNamespaces({ + BaseClass: config.BaseClass, + namespaces: config.modules, + executor: config.modulesExecutor + }); + } - for (const module of Object.keys(config.modules!)) { - this[module] = new this[module](this); - } - } - }; - - for (const [moduleName, module] of Object.entries(config.modules)) { - Commander.prototype[moduleName] = class { - readonly self: T; - - constructor(self: InstanceType) { - this.self = self; - } - }; - - for (const [commandName, command] of Object.entries(module)) { - Commander.prototype[moduleName].prototype[commandName] = function (...args: Array): unknown { - return config.modulesCommandsExecutor.call(this.self, command, args); - }; - } - } + if (config.functions) { + Commander = attachWithNamespaces({ + BaseClass: Commander ?? config.BaseClass, + namespaces: config.functions, + executor: config.functionsExecutor + }); } if (config.scripts) { Commander ??= class extends config.BaseClass {}; + attachCommands({ + BaseClass: Commander, + commands: config.scripts, + executor: config.scriptsExecutor + }); + } - for (const [name, script] of Object.entries(config.scripts)) { - Commander.prototype[name] = function (...args: Array): unknown { - return config.scriptsExecutor.call(this, script, args); + return Commander ?? config.BaseClass; +} + +interface AttachWithNamespacesConfig { + BaseClass: Instantiable; + namespaces: Record>; + executor: CommandsExecutor; +} + +function attachWithNamespaces({ + BaseClass, + namespaces, + executor +}: AttachWithNamespacesConfig): any { + const Commander = class extends BaseClass { + constructor(...args: Array) { + super(...args); + + for (const namespace of Object.keys(namespaces)) { + this[namespace] = Object.create(this[namespace], { + self: { + value: this + } + }); + } + } + }; + + for (const [namespace, commands] of Object.entries(namespaces)) { + Commander.prototype[namespace] = {}; + for (const [name, command] of Object.entries(commands)) { + Commander.prototype[namespace][name] = function (...args: Array): unknown { + return executor.call(this.self, command, args); }; } } - return (Commander ?? config.BaseClass) as any; + return Commander; } export function transformCommandArguments( @@ -93,14 +126,29 @@ export function transformLegacyCommandArguments(args: Array): Array { return args.flat().map(x => x?.toString?.()); } -export function transformCommandReply( - command: RedisCommand, - rawReply: RedisCommandRawReply, +export function transformCommandReply( + command: C, + rawReply: unknown, preserved: unknown -): RedisCommandReply { +): RedisCommandReply { if (!command.transformReply) { - return rawReply; + return rawReply as RedisCommandReply; } return command.transformReply(rawReply, preserved); } + +export function fCallArguments(fn: RedisFunction, args: RedisCommandArguments): RedisCommandArguments { + const actualArgs: RedisCommandArguments = [ + fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL', + fn.NAME + ]; + + if (fn.NUMBER_OF_KEYS !== undefined) { + actualArgs.push(fn.NUMBER_OF_KEYS.toString()); + } + + actualArgs.push(...args); + + return actualArgs; +} diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts index f269815b7e..a82f8bf0aa 100644 --- a/packages/client/lib/commands/EVAL.ts +++ b/packages/client/lib/commands/EVAL.ts @@ -1,4 +1,6 @@ -import { EvalOptions, pushEvalArguments } from './generic-transformers'; +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; export function transformArguments(script: string, options?: EvalOptions): Array { return pushEvalArguments(['EVAL', script], options); diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts index 105784cf5f..24f7060a05 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -1,4 +1,6 @@ -import { EvalOptions, pushEvalArguments } from './generic-transformers'; +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; export function transformArguments(sha1: string, options?: EvalOptions): Array { return pushEvalArguments(['EVALSHA', sha1], options); diff --git a/packages/client/lib/commands/EVALSHA_RO.spec.ts b/packages/client/lib/commands/EVALSHA_RO.spec.ts new file mode 100644 index 0000000000..6711f24fd8 --- /dev/null +++ b/packages/client/lib/commands/EVALSHA_RO.spec.ts @@ -0,0 +1,17 @@ +import { strict as assert } from 'assert'; +import testUtils from '../test-utils'; +import { transformArguments } from './EVALSHA_RO'; + +describe('EVALSHA_RO', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('sha1', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVALSHA_RO', 'sha1', '1', 'key', 'argument'] + ); + }); +}); diff --git a/packages/client/lib/commands/EVALSHA_RO.ts b/packages/client/lib/commands/EVALSHA_RO.ts new file mode 100644 index 0000000000..c3fcc3dc9c --- /dev/null +++ b/packages/client/lib/commands/EVALSHA_RO.ts @@ -0,0 +1,9 @@ +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; + +export const IS_READ_ONLY = true; + +export function transformArguments(sha1: string, options?: EvalOptions): Array { + return pushEvalArguments(['EVALSHA_RO', sha1], options); +} diff --git a/packages/client/lib/commands/EVAL_RO.spec.ts b/packages/client/lib/commands/EVAL_RO.spec.ts new file mode 100644 index 0000000000..e39fe82dd3 --- /dev/null +++ b/packages/client/lib/commands/EVAL_RO.spec.ts @@ -0,0 +1,31 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './EVAL_RO'; + +describe('EVAL_RO', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('return KEYS[1] + ARGV[1]', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument'] + ); + }); + + testUtils.testWithClient('client.evalRo', async client => { + assert.equal( + await client.evalRo('return 1'), + 1 + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithCluster('cluster.evalRo', async cluster => { + assert.equal( + await cluster.evalRo('return 1'), + 1 + ); + }, GLOBAL.CLUSTERS.OPEN); +}); diff --git a/packages/client/lib/commands/EVAL_RO.ts b/packages/client/lib/commands/EVAL_RO.ts new file mode 100644 index 0000000000..590c3af04f --- /dev/null +++ b/packages/client/lib/commands/EVAL_RO.ts @@ -0,0 +1,9 @@ +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; + +export const IS_READ_ONLY = true; + +export function transformArguments(script: string, options?: EvalOptions): Array { + return pushEvalArguments(['EVAL_RO', script], options); +} diff --git a/packages/client/lib/commands/FCALL.spec.ts b/packages/client/lib/commands/FCALL.spec.ts new file mode 100644 index 0000000000..25fbd51399 --- /dev/null +++ b/packages/client/lib/commands/FCALL.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import { transformArguments } from './FCALL'; + +describe('FCALL', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('function', { + keys: ['key'], + arguments: ['argument'] + }), + ['FCALL', 'function', '1', 'key', 'argument'] + ); + }); + + testUtils.testWithClient('client.fCall', async client => { + await loadMathFunction(client); + + assert.equal( + await client.fCall(MATH_FUNCTION.library.square.NAME, { + arguments: ['2'] + }), + 4 + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts new file mode 100644 index 0000000000..a4cadedb6f --- /dev/null +++ b/packages/client/lib/commands/FCALL.ts @@ -0,0 +1,7 @@ +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; + +export function transformArguments(fn: string, options?: EvalOptions): Array { + return pushEvalArguments(['FCALL', fn], options); +} diff --git a/packages/client/lib/commands/FCALL_RO.spec.ts b/packages/client/lib/commands/FCALL_RO.spec.ts new file mode 100644 index 0000000000..44332b9426 --- /dev/null +++ b/packages/client/lib/commands/FCALL_RO.spec.ts @@ -0,0 +1,29 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import { transformArguments } from './FCALL_RO'; + +describe('FCALL_RO', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('function', { + keys: ['key'], + arguments: ['argument'] + }), + ['FCALL_RO', 'function', '1', 'key', 'argument'] + ); + }); + + testUtils.testWithClient('client.fCallRo', async client => { + await loadMathFunction(client); + + assert.equal( + await client.fCallRo(MATH_FUNCTION.library.square.NAME, { + arguments: ['2'] + }), + 4 + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts new file mode 100644 index 0000000000..66b79aa883 --- /dev/null +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -0,0 +1,9 @@ +import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; + +export const FIRST_KEY_INDEX = evalFirstKeyIndex; + +export const IS_READ_ONLY = true; + +export function transformArguments(fn: string, options?: EvalOptions): Array { + return pushEvalArguments(['FCALL_RO', fn], options); +} diff --git a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts new file mode 100644 index 0000000000..fd7dca2b07 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts @@ -0,0 +1,24 @@ +import { strict as assert } from 'assert'; +import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './FUNCTION_DELETE'; + +describe('FUNCTION DELETE', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments('library'), + ['FUNCTION', 'DELETE', 'library'] + ); + }); + + testUtils.testWithClient('client.functionDelete', async client => { + await loadMathFunction(client); + + assert.equal( + await client.functionDelete(MATH_FUNCTION.name), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts new file mode 100644 index 0000000000..4aa6be40e1 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -0,0 +1,7 @@ +import { RedisCommandArguments } from '.'; + +export function transformArguments(library: string): RedisCommandArguments { + return ['FUNCTION', 'DELETE', library]; +} + +export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts new file mode 100644 index 0000000000..e3befa3dc5 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './FUNCTION_DUMP'; + +describe('FUNCTION DUMP', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'DUMP'] + ); + }); + + testUtils.testWithClient('client.functionDump', async client => { + assert.equal( + typeof await client.functionDump(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts new file mode 100644 index 0000000000..f608e078c2 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -0,0 +1,7 @@ +import { RedisCommandArgument, RedisCommandArguments } from '.'; + +export function transformArguments(): RedisCommandArguments { + return ['FUNCTION', 'DUMP']; +} + +export declare function transformReply(): RedisCommandArgument; diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts new file mode 100644 index 0000000000..8447216baa --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts @@ -0,0 +1,30 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './FUNCTION_FLUSH'; + +describe('FUNCTION FLUSH', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'FLUSH'] + ); + }); + + it('with mode', () => { + assert.deepEqual( + transformArguments('SYNC'), + ['FUNCTION', 'FLUSH', 'SYNC'] + ); + }); + }); + + testUtils.testWithClient('client.functionFlush', async client => { + assert.equal( + await client.functionFlush(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts new file mode 100644 index 0000000000..143282de97 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -0,0 +1,13 @@ +import { RedisCommandArguments } from '.'; + +export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments { + const args = ['FUNCTION', 'FLUSH']; + + if (mode) { + args.push(mode); + } + + return args; +} + +export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/FUNCTION_KILL.spec.ts b/packages/client/lib/commands/FUNCTION_KILL.spec.ts new file mode 100644 index 0000000000..cf489a8022 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_KILL.spec.ts @@ -0,0 +1,14 @@ +import { strict as assert } from 'assert'; +import testUtils from '../test-utils'; +import { transformArguments } from './FUNCTION_KILL'; + +describe('FUNCTION KILL', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'KILL'] + ); + }); +}); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts new file mode 100644 index 0000000000..517272e837 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -0,0 +1,7 @@ +import { RedisCommandArguments } from '.'; + +export function transformArguments(): RedisCommandArguments { + return ['FUNCTION', 'KILL']; +} + +export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/FUNCTION_LIST.spec.ts b/packages/client/lib/commands/FUNCTION_LIST.spec.ts new file mode 100644 index 0000000000..41de2f40f4 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LIST.spec.ts @@ -0,0 +1,41 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import { transformArguments } from './FUNCTION_LIST'; + +describe('FUNCTION LIST', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'LIST'] + ); + }); + + it('with pattern', () => { + assert.deepEqual( + transformArguments('patter*'), + ['FUNCTION', 'LIST', 'patter*'] + ); + }); + }); + + testUtils.testWithClient('client.functionList', async client => { + await loadMathFunction(client); + + assert.deepEqual( + await client.functionList(), + [{ + libraryName: MATH_FUNCTION.name, + engine: MATH_FUNCTION.engine, + functions: [{ + name: MATH_FUNCTION.library.square.NAME, + description: null, + flags: ['no-writes'] + }] + }] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts new file mode 100644 index 0000000000..d6a39dc726 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -0,0 +1,16 @@ +import { RedisCommandArguments } from '.'; +import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers'; + +export function transformArguments(pattern?: string): RedisCommandArguments { + const args = ['FUNCTION', 'LIST']; + + if (pattern) { + args.push(pattern); + } + + return args; +} + +export function transformReply(reply: Array): Array { + return reply.map(transformFunctionListItemReply); +} diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts new file mode 100644 index 0000000000..33c2ca6d91 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts @@ -0,0 +1,42 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import { transformArguments } from './FUNCTION_LIST_WITHCODE'; + +describe('FUNCTION LIST WITHCODE', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'LIST', 'WITHCODE'] + ); + }); + + it('with pattern', () => { + assert.deepEqual( + transformArguments('patter*'), + ['FUNCTION', 'LIST', 'patter*', 'WITHCODE'] + ); + }); + }); + + testUtils.testWithClient('client.functionListWithCode', async client => { + await loadMathFunction(client); + + assert.deepEqual( + await client.functionListWithCode(), + [{ + libraryName: MATH_FUNCTION.name, + engine: MATH_FUNCTION.engine, + functions: [{ + name: MATH_FUNCTION.library.square.NAME, + description: null, + flags: ['no-writes'] + }], + libraryCode: MATH_FUNCTION.code + }] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts new file mode 100644 index 0000000000..0d763301e8 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts @@ -0,0 +1,26 @@ +import { RedisCommandArguments } from '.'; +import { transformArguments as transformFunctionListArguments } from './FUNCTION_LIST'; +import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers'; + +export function transformArguments(pattern?: string): RedisCommandArguments { + const args = transformFunctionListArguments(pattern); + args.push('WITHCODE'); + return args; +} + +type FunctionListWithCodeRawItemReply = [ + ...FunctionListRawItemReply, + 'library_code', + string +]; + +interface FunctionListWithCodeItemReply extends FunctionListItemReply { + libraryCode: string; +} + +export function transformReply(reply: Array): Array { + return reply.map(library => ({ + ...transformFunctionListItemReply(library as unknown as FunctionListRawItemReply), + libraryCode: library[7] + })); +} diff --git a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts new file mode 100644 index 0000000000..6beac45d0a --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts @@ -0,0 +1,36 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { MATH_FUNCTION } from '../client/index.spec'; +import { transformArguments } from './FUNCTION_LOAD'; + +describe('FUNCTION LOAD', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments( 'code'), + ['FUNCTION', 'LOAD', 'code'] + ); + }); + + it('with REPLACE', () => { + assert.deepEqual( + transformArguments('code', { + REPLACE: true + }), + ['FUNCTION', 'LOAD', 'REPLACE', 'code'] + ); + }); + }); + + testUtils.testWithClient('client.functionLoad', async client => { + assert.equal( + await client.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ), + MATH_FUNCTION.name + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts new file mode 100644 index 0000000000..7ab58d5859 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -0,0 +1,22 @@ +import { RedisCommandArguments } from '.'; + +interface FunctionLoadOptions { + REPLACE?: boolean; +} + +export function transformArguments( + code: string, + options?: FunctionLoadOptions +): RedisCommandArguments { + const args = ['FUNCTION', 'LOAD']; + + if (options?.REPLACE) { + args.push('REPLACE'); + } + + args.push(code); + + return args; +} + +export declare function transformReply(): string; diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts new file mode 100644 index 0000000000..3ed4c2b329 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts @@ -0,0 +1,37 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './FUNCTION_RESTORE'; + +describe('FUNCTION RESTORE', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + transformArguments('dump'), + ['FUNCTION', 'RESTORE', 'dump'] + ); + }); + + it('with mode', () => { + assert.deepEqual( + transformArguments('dump', 'APPEND'), + ['FUNCTION', 'RESTORE', 'dump', 'APPEND'] + ); + }); + }); + + testUtils.testWithClient('client.functionRestore', async client => { + assert.equal( + await client.functionRestore( + await client.functionDump( + client.commandOptions({ + returnBuffers: true + }) + ), + 'FLUSH' + ), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts new file mode 100644 index 0000000000..bc9c41e262 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -0,0 +1,16 @@ +import { RedisCommandArgument, RedisCommandArguments } from '.'; + +export function transformArguments( + dump: RedisCommandArgument, + mode?: 'FLUSH' | 'APPEND' | 'REPLACE' +): RedisCommandArguments { + const args = ['FUNCTION', 'RESTORE', dump]; + + if (mode) { + args.push(mode); + } + + return args; +} + +export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/FUNCTION_STATS.spec.ts b/packages/client/lib/commands/FUNCTION_STATS.spec.ts new file mode 100644 index 0000000000..c9f648fa5e --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_STATS.spec.ts @@ -0,0 +1,25 @@ +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { transformArguments } from './FUNCTION_STATS'; + +describe('FUNCTION STATS', () => { + testUtils.isVersionGreaterThanHook([7, 0]); + + it('transformArguments', () => { + assert.deepEqual( + transformArguments(), + ['FUNCTION', 'STATS'] + ); + }); + + testUtils.testWithClient('client.functionStats', async client => { + const stats = await client.functionStats(); + assert.equal(stats.runningScript, null); + assert.equal(typeof stats.engines, 'object'); + for (const [engine, { librariesCount, functionsCount }] of Object.entries(stats.engines)) { + assert.equal(typeof engine, 'string'); + assert.equal(typeof librariesCount, 'number'); + assert.equal(typeof functionsCount, 'number'); + } + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts new file mode 100644 index 0000000000..bb5c957e78 --- /dev/null +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -0,0 +1,56 @@ +import { RedisCommandArguments } from '.'; + +export function transformArguments(): RedisCommandArguments { + return ['FUNCTION', 'STATS']; +} + +type FunctionStatsRawReply = [ + 'running_script', + null | [ + 'name', + string, + 'command', + string, + 'duration_ms', + number + ], + 'engines', + Array // "flat tuples" (there is no way to type that) + // ...[string, [ + // 'libraries_count', + // number, + // 'functions_count', + // number + // ]] +]; + +interface FunctionStatsReply { + runningScript: null | { + name: string; + command: string; + durationMs: number; + }; + engines: Record; +} + +export function transformReply(reply: FunctionStatsRawReply): FunctionStatsReply { + const engines = Object.create(null); + for (let i = 0; i < reply[3].length; i++) { + engines[reply[3][i]] = { + librariesCount: reply[3][++i][1], + functionsCount: reply[3][i][3] + }; + } + + return { + runningScript: reply[1] === null ? null : { + name: reply[1][1], + command: reply[1][3], + durationMs: reply[1][5] + }, + engines + }; +} diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index e881822fb4..0477caffd4 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -348,6 +348,10 @@ export interface EvalOptions { arguments?: Array; } +export function evalFirstKeyIndex(options?: EvalOptions): string | undefined { + return options?.keys?.[0]; +} + export function pushEvalArguments(args: Array, options?: EvalOptions): Array { if (options?.keys) { args.push( @@ -491,6 +495,51 @@ export function transformCommandReply( }; } +export enum RedisFunctionFlags { + NO_WRITES = 'no-writes', + ALLOW_OOM = 'allow-oom', + ALLOW_STALE = 'allow-stale', + NO_CLUSTER = 'no-cluster' +} + +export type FunctionListRawItemReply = [ + 'library_name', + string, + 'engine', + string, + 'functions', + Array<[ + 'name', + string, + 'description', + string | null, + 'flags', + Array + ]> +]; + +export interface FunctionListItemReply { + libraryName: string; + engine: string; + functions: Array<{ + name: string; + description: string | null; + flags: Array; + }>; +} + +export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply { + return { + libraryName: reply[1], + engine: reply[3], + functions: reply[5].map(fn => ({ + name: fn[1], + description: fn[3], + flags: fn[5] + })) + }; +} + export interface SortOptions { BY?: string; LIMIT?: { diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index d8cfe5332d..84043bcd63 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -1,22 +1,51 @@ +import { ClientCommandOptions } from '../client'; +import { CommandOptions } from '../command-options'; import { RedisScriptConfig, SHA1 } from '../lua-script'; -// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RedisCommandRawReplyArray extends Array {} -export type RedisCommandRawReply = string | number | Buffer | null | undefined | RedisCommandRawReplyArray; +export type RedisCommandRawReply = string | number | Buffer | null | undefined | Array; export type RedisCommandArgument = string | Buffer; export type RedisCommandArguments = Array & { preserve?: unknown }; export interface RedisCommand { - FIRST_KEY_INDEX?: number | ((...args: Array) => RedisCommandArgument); + FIRST_KEY_INDEX?: number | ((...args: Array) => RedisCommandArgument | undefined); IS_READ_ONLY?: boolean; transformArguments(this: void, ...args: Array): RedisCommandArguments; transformReply?(this: void, reply: any, preserved?: any): any; } -export type RedisCommandReply = C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply; +export type RedisCommandReply = + C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply; + +export type ConvertArgumentType = + Type extends RedisCommandArgument ? ( + Type extends (string & ToType) ? Type : ToType + ) : ( + Type extends Set ? Set> : ( + Type extends Map ? Map> : ( + Type extends Array ? Array> : ( + Type extends Date ? Type : ( + Type extends Record ? { + [Property in keyof Type]: ConvertArgumentType + } : Type + ) + ) + ) + ) + ); + +export type RedisCommandSignature< + Command extends RedisCommand, + Params extends Array = Parameters +> = >( + ...args: Params | [options: Options, ...rest: Params] +) => Promise< + ConvertArgumentType< + RedisCommandReply, + Options['returnBuffers'] extends true ? Buffer : string + > +>; export interface RedisCommands { [command: string]: RedisCommand; @@ -30,13 +59,33 @@ export interface RedisModules { [module: string]: RedisModule; } +export interface RedisFunction extends RedisCommand { + NAME: string; + NUMBER_OF_KEYS?: number; +} + +export interface RedisFunctionLibrary { + [fn: string]: RedisFunction; +} + +export interface RedisFunctions { + [library: string]: RedisFunctionLibrary; +} + export type RedisScript = RedisScriptConfig & SHA1; export interface RedisScripts { [script: string]: RedisScript; } -export interface RedisPlugins { +export interface RedisExtensions< + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts +> { modules?: M; + functions?: F; scripts?: S; } + +export type ExcludeMappedString = string extends S ? never : S; diff --git a/packages/client/lib/lua-script.ts b/packages/client/lib/lua-script.ts index 8a0481364c..da19417ec2 100644 --- a/packages/client/lib/lua-script.ts +++ b/packages/client/lib/lua-script.ts @@ -3,7 +3,7 @@ import { RedisCommand } from './commands'; export interface RedisScriptConfig extends RedisCommand { SCRIPT: string; - NUMBER_OF_KEYS: number; + NUMBER_OF_KEYS?: number; } export interface SHA1 { diff --git a/packages/client/lib/multi-command.ts b/packages/client/lib/multi-command.ts index d66974a5a2..10ab77ad19 100644 --- a/packages/client/lib/multi-command.ts +++ b/packages/client/lib/multi-command.ts @@ -1,4 +1,5 @@ -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisScript } from './commands'; +import { fCallArguments } from './commander'; +import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands'; import { WatchError } from './errors'; export interface RedisMultiQueuedCommand { @@ -22,6 +23,18 @@ export default class RedisMultiCommand { }); } + addFunction(fn: RedisFunction, args: Array): RedisCommandArguments { + const transformedArguments = fCallArguments( + fn, + fn.transformArguments(...args) + ); + this.queue.push({ + args: transformedArguments, + transformReply: fn.transformReply + }); + return transformedArguments; + } + addScript(script: RedisScript, args: Array): RedisCommandArguments { const transformedArguments: RedisCommandArguments = []; if (this.scriptsInUse.has(script.SHA1)) { @@ -37,7 +50,9 @@ export default class RedisMultiCommand { ); } - transformedArguments.push(script.NUMBER_OF_KEYS.toString()); + if (script.NUMBER_OF_KEYS !== undefined) { + transformedArguments.push(script.NUMBER_OF_KEYS.toString()); + } const scriptArguments = script.transformArguments(...args); transformedArguments.push(...scriptArguments); diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index fbed769889..1ea7b59007 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -3,7 +3,7 @@ import { SinonSpy } from 'sinon'; import { promiseTimeout } from './utils'; export default new TestUtils({ - defaultDockerVersion: '6.2', + defaultDockerVersion: '7.0-rc3', dockerImageName: 'redis', dockerImageVersionArgument: 'redis-version' }); diff --git a/packages/graph/lib/test-utils.ts b/packages/graph/lib/test-utils.ts index ad6dc0fd19..da4fb0f78d 100644 --- a/packages/graph/lib/test-utils.ts +++ b/packages/graph/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisGraph from '.'; export default new TestUtils({ dockerImageName: 'redislabs/redisgraph', dockerImageVersionArgument: 'redisgraph-version', - defaultDockerVersion: '2.8.7' + defaultDockerVersion: '2.8.9' }); export const GLOBAL = { diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index c41870567e..a4d5bee7e9 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default new TestUtils({ dockerImageName: 'redislabs/rejson', dockerImageVersionArgument: 'rejson-version', - defaultDockerVersion: '2.0.6' + defaultDockerVersion: '2.0.7' }); export const GLOBAL = { diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index 0bf4f034bf..ad803a1fde 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -1,6 +1,6 @@ import { createConnection } from 'net'; import { once } from 'events'; -import { RedisModules, RedisScripts } from '@node-redis/client/lib/commands'; +import { RedisModules, RedisFunctions, RedisScripts } from '@node-redis/client/lib/commands'; import RedisClient, { RedisClientType } from '@node-redis/client/lib/client'; import { promiseTimeout } from '@node-redis/client/lib/utils'; import * as path from 'path'; @@ -152,7 +152,11 @@ async function spawnRedisClusterNodeDocker( } } -async function waitForClusterState(client: RedisClientType): Promise { +async function waitForClusterState< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +>(client: RedisClientType): Promise { while ((await client.clusterInfo()).state !== 'ok') { await promiseTimeout(500); } diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 9eaed1f8d3..7a83f2ad2e 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -1,4 +1,4 @@ -import { RedisModules, RedisScripts } from '@node-redis/client/lib/commands'; +import { RedisModules, RedisFunctions, RedisScripts } from '@node-redis/client/lib/commands'; import RedisClient, { RedisClientOptions, RedisClientType } from '@node-redis/client/lib/client'; import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@node-redis/client/lib/cluster'; import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; @@ -15,15 +15,23 @@ interface CommonTestOptions { minimumDockerVersion?: Array; } -interface ClientTestOptions extends CommonTestOptions { +interface ClientTestOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> extends CommonTestOptions { serverArguments: Array; - clientOptions?: Partial>; + clientOptions?: Partial>; disableClientSetup?: boolean; } -interface ClusterTestOptions extends CommonTestOptions { +interface ClusterTestOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts +> extends CommonTestOptions { serverArguments: Array; - clusterConfiguration?: Partial>; + clusterConfiguration?: Partial>; numberOfNodes?: number; } @@ -93,10 +101,14 @@ export default class TestUtils { }); } - testWithClient( + testWithClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >( title: string, - fn: (client: RedisClientType) => Promise, - options: ClientTestOptions + fn: (client: RedisClientType) => Promise, + options: ClientTestOptions ): void { let dockerPromise: ReturnType; if (this.isVersionGreaterThan(options.minimumDockerVersion)) { @@ -138,16 +150,24 @@ export default class TestUtils { }); } - static async #clusterFlushAll(cluster: RedisClusterType): Promise { + static async #clusterFlushAll< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >(cluster: RedisClusterType): Promise { await Promise.all( cluster.getMasters().map(({ client }) => client.flushAll()) ); } - testWithCluster( + testWithCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts + >( title: string, - fn: (cluster: RedisClusterType) => Promise, - options: ClusterTestOptions + fn: (cluster: RedisClusterType) => Promise, + options: ClusterTestOptions ): void { let dockersPromise: ReturnType; if (this.isVersionGreaterThan(options.minimumDockerVersion)) { diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index eebb1b416d..c4639a98d4 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default new TestUtils({ dockerImageName: 'redislabs/redistimeseries', dockerImageVersionArgument: 'timeseries-version', - defaultDockerVersion: '1.6.8' + defaultDockerVersion: '1.6.9' }); export const GLOBAL = {