import { ClientCommandOptions } from './client'; import { CommandOptions, isCommandOptions } from './command-options'; import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands'; type Instantiable = new (...args: Array) => T; type CommandsExecutor = (command: C, args: Array, name: string) => unknown; interface AttachCommandsConfig { BaseClass: Instantiable; commands: Record; executor: CommandsExecutor; } 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, name); }; } } interface AttachExtensionsConfig { BaseClass: T; modulesExecutor: CommandsExecutor; modules?: RedisModules; functionsExecutor: CommandsExecutor; functions?: RedisFunctions; scriptsExecutor: CommandsExecutor; scripts?: RedisScripts; } export function attachExtensions(config: AttachExtensionsConfig): any { let Commander; if (config.modules) { Commander = attachWithNamespaces({ BaseClass: config.BaseClass, namespaces: config.modules, executor: config.modulesExecutor }); } 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 }); } 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, name); }; } } return Commander; } export function transformCommandArguments( command: RedisCommand, args: Array ): { jsArgs: Array; args: RedisCommandArguments; options: CommandOptions | undefined; } { let options; if (isCommandOptions(args[0])) { options = args[0]; args = args.slice(1); } return { jsArgs: args, args: command.transformArguments(...args), options }; } export function transformLegacyCommandArguments(args: Array): Array { return args.flat().map(arg => { return typeof arg === 'number' || arg instanceof Date ? arg.toString() : arg; }); } export function transformCommandReply( command: C, rawReply: unknown, preserved: unknown ): RedisCommandReply { if (!command.transformReply) { return rawReply as RedisCommandReply; } return command.transformReply(rawReply, preserved); } export function fCallArguments( name: RedisCommandArgument, fn: RedisFunction, args: RedisCommandArguments ): RedisCommandArguments { const actualArgs: RedisCommandArguments = [ fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL', name ]; if (fn.NUMBER_OF_KEYS !== undefined) { actualArgs.push(fn.NUMBER_OF_KEYS.toString()); } actualArgs.push(...args); return actualArgs; }