Skip to content

Commit

Permalink
fix: Fix transformers (#1106)
Browse files Browse the repository at this point in the history
* fix: Fix transformers

The transformers implementation was never compatible with ioredis, this
fixes it and also adds much-needed tests. Also, this includes a fix for
hgetall with transformers.

Fixes: #1051

* Update test/command.js

Co-authored-by: Cody Olsen <stipsan@gmail.com>

Co-authored-by: Cody Olsen <stipsan@gmail.com>
  • Loading branch information
silverwind and stipsan committed Jan 21, 2022
1 parent 2f3418c commit cb17f5a
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 21 deletions.
34 changes: 27 additions & 7 deletions src/command.js
@@ -1,5 +1,6 @@
import _ from 'lodash';
import asCallback from 'standard-as-callback';
import { Command as IoredisCommand } from 'ioredis';
import promiseContainer from './promise-container';

export function isInSubscriberMode(RedisMock) {
Expand Down Expand Up @@ -52,6 +53,18 @@ export function throwIfCommandIsNotAllowed(commandName, RedisMock) {
throwIfNotConnected(commandName, RedisMock);
}

export const Command = {
// eslint-disable-next-line no-underscore-dangle
transformers: IoredisCommand._transformer,
setArgumentTransformer: (name, func) => {
Command.transformers.argument[name] = func;
},

setReplyTransformer: (name, func) => {
Command.transformers.reply[name] = func;
},
};

/**
* Transform non-buffer arguments to strings to simulate real ioredis behavior
* @param {any} arg the argument to transform
Expand All @@ -61,21 +74,28 @@ const argMapper = (arg) => {
return arg instanceof Buffer ? arg : arg.toString();
};

export function processArguments(args, commandName, RedisMock) {
export function processArguments(args, commandName) {
// fast return, the defineCommand command requires NO transformation of args
if (commandName === 'defineCommand') return args;

let commandArgs = args ? _.flatten(args) : [];
if (RedisMock.Command.transformers.argument[commandName]) {
commandArgs = RedisMock.Command.transformers.argument[commandName](args);
if (Command.transformers.argument[commandName]) {
commandArgs = Command.transformers.argument[commandName](args);
}
commandArgs = commandArgs.map(argMapper);
return commandArgs;
}

export function processReply(result, commandName, RedisMock) {
if (RedisMock.Command.transformers.reply[commandName]) {
return RedisMock.Command.transformers.reply[commandName](result);
export function processReply(result, commandName) {
if (Command.transformers.reply[commandName]) {
// ioredis' reply transformer seems to receive a flat array of key/value
// pairs for the hgetall command, emulate this
let newResult = result;
if (commandName === 'hgetall') {
newResult = _.flatten(Object.entries(result));
}

return Command.transformers.reply[commandName](newResult);
}
return result;
}
Expand All @@ -88,7 +108,7 @@ export function safelyExecuteCommand(
) {
throwIfCommandIsNotAllowed(commandName, RedisMock);
const result = commandEmulator(...commandArgs);
return processReply(result, commandName, RedisMock);
return processReply(result, commandName);
}

export default function command(commandEmulator, commandName, RedisMock) {
Expand Down
16 changes: 3 additions & 13 deletions src/index.js
@@ -1,10 +1,9 @@
/* eslint-disable max-classes-per-file */
import { EventEmitter } from 'events';
import { Command } from 'ioredis';
import redisCommands from 'redis-commands';
import * as commands from './commands';
import * as commandsStream from './commands-stream';
import createCommand from './command';
import createCommand, { Command } from './command';
import createData from './data';
import createExpires from './expires';
import emitConnectEvent from './commands-utils/emitConnectEvent';
Expand Down Expand Up @@ -167,17 +166,8 @@ class RedisMock extends EventEmitter {
});
}
}
RedisMock.prototype.Command = {
// eslint-disable-next-line no-underscore-dangle
transformers: Command._transformer,
setArgumentTransformer: (name, func) => {
RedisMock.prototype.Command.transformers.argument[name] = func;
},

setReplyTransformer: (name, func) => {
RedisMock.prototype.Command.transformers.reply[name] = func;
},
};

RedisMock.Command = Command;

RedisMock.Cluster = class RedisClusterMock extends RedisMock {
constructor(nodesOptions) {
Expand Down
2 changes: 1 addition & 1 deletion src/pipeline.js
Expand Up @@ -36,7 +36,7 @@ class Pipeline {
args.length = lastArgIndex;
}
const commandEmulator = command.bind(this.redis);
const commandArgs = processArguments(args, commandName, this.redis);
const commandArgs = processArguments(args, commandName);

this._addTransaction(commandEmulator, commandName, commandArgs, callback);
return this;
Expand Down
56 changes: 56 additions & 0 deletions test/command.js
@@ -1,5 +1,19 @@
import _ from 'lodash';
import Redis from 'ioredis';
import command from '../src/command';

// Ensure that we're getting the correct instance of Command when running in test:jest.js, as jest.js isn't designed to test code directly imported private functions like src/command
jest.mock('ioredis', () => {
const { Command } = jest.requireActual('ioredis');
const RedisMock = jest.requireActual('../src/index');

return {
__esModule: true,
Command,
default: RedisMock,
};
});

describe('basic command', () => {
const stub = command((...args) => args, 'testCommandName', {
Command: { transformers: { argument: {}, reply: {} } },
Expand Down Expand Up @@ -30,3 +44,45 @@ describe('basic command', () => {
'should reject the promise if the first argument is bool false to allow simulating failures'
);
});

describe('transformers', () => {
it('should support setReplyTransformer', async () => {
const redis = new Redis();

Redis.Command.setReplyTransformer('hgetall', result => {
expect(Array.isArray(result)).toBeTruthy();
expect(result.length).toEqual(4);

const arr = [];
for (let i = 0; i < result.length; i += 2) {
arr.push([String(result[i]), String(result[i + 1])]);
}
return arr;
});

await redis.hset('replytest', 'bar', 'baz');
await redis.hset('replytest', 'baz', 'quz');
expect(await redis.hgetall('replytest')).toEqual([['bar', 'baz'], ['baz', 'quz']]);
delete Redis.Command.transformers.reply.hgetall;
});

it('should support setArgumentTransformer', async () => {
const redis = new Redis();

Redis.Command.setArgumentTransformer('hmset', args => {
if (args.length === 2) {
if (typeof Map !== 'undefined' && args[1] instanceof Map) {
return [args[0]].concat(_.flatten(Object.entries(args[1])));
}
if (typeof args[1] === 'object' && args[1] !== null) {
return [args[0]].concat(_.flatten(Object.entries(args[1])));
}
}
return args;
})

await redis.hmset('argtest', { k1: 'v1', k2: 'v2' });
expect(await redis.hgetall('argtest')).toEqual({ k1: 'v1', k2: 'v2' });
delete Redis.Command.transformers.argument.hmset;
})
});

0 comments on commit cb17f5a

Please sign in to comment.