1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-10 11:43:01 +03:00

Merge pull request #838 from fintura/consistent-commands

Consistent commands arguments. All commands are from now on behaving the same no matter if they are on multi or no and they all take an array as either the first or second argument.

Fixes #686 #369 #422 #390 and Closes #634
This commit is contained in:
Ruben Bridgewater
2015-09-16 05:17:22 +02:00
23 changed files with 332 additions and 87 deletions

View File

@@ -16,6 +16,9 @@
"node": true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"mocha": true,
// Relaxing options
"boss": true, // Accept things like `while (command = keys.shift()) { ... }`
"overrides": {
"examples/*.js": {
"unused": false

127
index.js
View File

@@ -863,8 +863,16 @@ RedisClient.prototype.end = function () {
function Multi(client, args) {
this._client = client;
this.queue = [["multi"]];
var command, tmp_args;
if (Array.isArray(args)) {
this.queue = this.queue.concat(args);
while (tmp_args = args.shift()) {
command = tmp_args.shift();
if (Array.isArray(command)) {
this[command[0]].apply(this, command.slice(1).concat(tmp_args));
} else {
this[command].apply(this, tmp_args);
}
}
}
}
@@ -878,16 +886,32 @@ commands.forEach(function (fullCommand) {
return;
}
RedisClient.prototype[command] = function (args, callback) {
if (Array.isArray(args)) {
return this.send_command(command, args, callback);
RedisClient.prototype[command] = function (key, arg, callback) {
if (Array.isArray(key)) {
return this.send_command(command, key, arg);
}
if (Array.isArray(arg)) {
arg.unshift(key);
return this.send_command(command, arg, callback);
}
return this.send_command(command, to_array(arguments));
};
RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command];
Multi.prototype[command] = function () {
this.queue.push([command].concat(to_array(arguments)));
Multi.prototype[command] = function (key, arg, callback) {
if (Array.isArray(key)) {
if (arg) {
key.push(arg);
}
this.queue.push([command].concat(key));
} else if (Array.isArray(arg)) {
if (callback) {
arg.push(callback);
}
this.queue.push([command, key].concat(arg));
} else {
this.queue.push([command].concat(to_array(arguments)));
}
return this;
};
Multi.prototype[command.toUpperCase()] = Multi.prototype[command];
@@ -919,70 +943,63 @@ RedisClient.prototype.auth = RedisClient.prototype.AUTH = function (pass, callba
}
};
RedisClient.prototype.hmget = RedisClient.prototype.HMGET = function (arg1, arg2, arg3) {
if (Array.isArray(arg2) && typeof arg3 === "function") {
return this.send_command("hmget", [arg1].concat(arg2), arg3);
} else if (Array.isArray(arg1) && typeof arg2 === "function") {
return this.send_command("hmget", arg1, arg2);
} else {
return this.send_command("hmget", to_array(arguments));
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function (key, args, callback) {
var field, tmp_args;
if (Array.isArray(key)) {
return this.send_command("hmset", key, args);
}
};
RedisClient.prototype.hmset = RedisClient.prototype.HMSET = function (args, callback) {
var tmp_args, tmp_keys, i, il, key;
if (Array.isArray(args)) {
return this.send_command("hmset", args, callback);
return this.send_command("hmset", [key].concat(args), callback);
}
args = to_array(arguments);
if (typeof args[args.length - 1] === "function") {
callback = args[args.length - 1];
args.length -= 1;
} else {
callback = null;
}
if (args.length === 2 && (typeof args[0] === "string" || typeof args[0] === "number") && typeof args[1] === "object") {
if (typeof args === "object") {
// User does: client.hmset(key, {key1: val1, key2: val2})
// assuming key is a string, i.e. email address
// if key is a number, i.e. timestamp, convert to string
if (typeof args[0] === "number") {
args[0] = args[0].toString();
// TODO: This seems random and no other command get's the key converted => either all or none should behave like this
if (typeof key !== "string") {
key = key.toString();
}
tmp_args = [ args[0] ];
tmp_keys = Object.keys(args[1]);
for (i = 0, il = tmp_keys.length; i < il ; i++) {
key = tmp_keys[i];
tmp_args.push(key);
tmp_args.push(args[1][key]);
tmp_args = [key];
var fields = Object.keys(args);
while (field = fields.shift()) {
tmp_args.push(field, args[field]);
}
args = tmp_args;
return this.send_command("hmset", tmp_args, callback);
}
return this.send_command("hmset", args, callback);
return this.send_command("hmset", to_array(arguments));
};
Multi.prototype.hmset = Multi.prototype.HMSET = function () {
var args = to_array(arguments), tmp_args;
if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") {
tmp_args = [ "hmset", args[0] ];
Object.keys(args[1]).map(function (key) {
tmp_args.push(key);
tmp_args.push(args[1][key]);
});
if (args[2]) {
tmp_args.push(args[2]);
Multi.prototype.hmset = Multi.prototype.HMSET = function (key, args, callback) {
var tmp_args, field;
if (Array.isArray(key)) {
if (args) {
key.push(args);
}
tmp_args = ['hmset'].concat(key);
} else if (Array.isArray(args)) {
if (callback) {
args.push(callback);
}
tmp_args = ['hmset', key].concat(args);
} else if (typeof args === "object") {
tmp_args = ["hmset", key];
if (typeof key !== "string") {
key = key.toString();
}
var fields = Object.keys(args);
while (field = fields.shift()) {
tmp_args.push(field);
tmp_args.push(args[field]);
}
if (callback) {
tmp_args.push(callback);
}
args = tmp_args;
} else {
args.unshift("hmset");
tmp_args = to_array(arguments);
tmp_args.unshift("hmset");
}
this.queue.push(args);
this.queue.push(tmp_args);
return this;
};

View File

@@ -33,6 +33,16 @@ describe("The 'blpop' method", function () {
});
});
it('pops value immediately if list contains values using array notation', function (done) {
bclient = redis.createClient.apply(redis.createClient, args);
client.rpush(["blocking list", "initial value"], helper.isNumber(1));
bclient.blpop(["blocking list", 0], function (err, value) {
assert.strictEqual(value[0], "blocking list");
assert.strictEqual(value[1], "initial value");
return done(err);
});
});
it('waits for value if list is not yet populated', function (done) {
bclient = redis.createClient.apply(redis.createClient, args);
bclient.blpop("blocking list 2", 5, function (err, value) {

View File

@@ -37,8 +37,17 @@ describe("The 'client' method", function () {
});
});
it("lists connected clients when invoked with array syntax on client", function (done) {
client.multi().client(["list"]).exec(function(err, results) {
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
return done();
});
});
it("lists connected clients when invoked with multi's array syntax", function (done) {
client.multi().client("list").exec(function(err, results) {
client.multi([
['client', 'list']
]).exec(function(err, results) {
assert(pattern.test(results[0]), "expected string '" + results + "' to match " + pattern.toString());
return done();
});

View File

@@ -71,7 +71,7 @@ describe("The 'dbsize' method", function () {
var oldSize;
beforeEach(function (done) {
client.dbsize([], function (err, res) {
client.dbsize(function (err, res) {
helper.isType.number()(err, res);
assert.strictEqual(res, 0, "Initial db size should be 0");

View File

@@ -36,6 +36,20 @@ describe("The 'del' method", function () {
client.get('apple', helper.isNull(done));
});
it('allows multiple keys to be deleted with the array syntax', function (done) {
client.mset('foo', 'bar', 'apple', 'banana');
client.del(['foo', 'apple'], helper.isNumber(2));
client.get('foo', helper.isNull());
client.get('apple', helper.isNull(done));
});
it('allows multiple keys to be deleted with the array syntax and no callback', function (done) {
client.mset('foo', 'bar', 'apple', 'banana');
client.del(['foo', 'apple']);
client.get('foo', helper.isNull());
client.get('apple', helper.isNull(done));
});
afterEach(function () {
client.end();
});

View File

@@ -24,6 +24,11 @@ describe("The 'exits' method", function () {
client.EXISTS('foo', helper.isNumber(1, done));
});
it('returns 1 if the key exists with array syntax', function (done) {
client.set('foo', 'bar');
client.EXISTS(['foo'], helper.isNumber(1, done));
});
it('returns 0 if the key does not exist', function (done) {
client.exists('bar', helper.isNumber(0, done));
});

View File

@@ -20,6 +20,14 @@ describe("The 'expire' method", function () {
});
it('expires key after timeout', function (done) {
client.set(['expiry key', 'bar'], helper.isString("OK"));
client.EXPIRE("expiry key", "1", helper.isNumber(1));
setTimeout(function () {
client.exists(["expiry key"], helper.isNumber(0, done));
}, 1100);
});
it('expires key after timeout with array syntax', function (done) {
client.set(['expiry key', 'bar'], helper.isString("OK"));
client.EXPIRE(["expiry key", "1"], helper.isNumber(1));
setTimeout(function () {

View File

@@ -58,36 +58,50 @@ describe("The 'flushdb' method", function () {
describe("when there is data in Redis", function () {
beforeEach(function (done) {
var end = helper.callFuncAfter(function () {
client.flushdb(helper.isString("OK", done));
}, 2);
client.mset(key, uuid.v4(), key2, uuid.v4(), helper.isNotError(end));
client.mset(key, uuid.v4(), key2, uuid.v4(), helper.isNotError());
client.dbsize([], function (err, res) {
helper.isType.positiveNumber()(err, res);
assert.equal(res, 2, 'Two keys should have been inserted');
end();
done();
});
});
it("deletes all the keys", function (done) {
client.mget(key, key2, function (err, res) {
assert.strictEqual(null, err, "Unexpected error returned");
assert.strictEqual(true, Array.isArray(res), "Results object should be an array.");
assert.strictEqual(2, res.length, "Results array should have length 2.");
assert.strictEqual(null, res[0], "Redis key should have been flushed.");
assert.strictEqual(null, res[1], "Redis key should have been flushed.");
done(err);
client.flushdb(function(err, res) {
assert.equal(res, 'OK');
client.mget(key, key2, function (err, res) {
assert.strictEqual(null, err, "Unexpected error returned");
assert.strictEqual(true, Array.isArray(res), "Results object should be an array.");
assert.strictEqual(2, res.length, "Results array should have length 2.");
assert.strictEqual(null, res[0], "Redis key should have been flushed.");
assert.strictEqual(null, res[1], "Redis key should have been flushed.");
done(err);
});
});
});
it("results in a db size of zero", function (done) {
client.dbsize([], function (err, res) {
helper.isNotError()(err, res);
helper.isType.number()(err, res);
assert.strictEqual(0, res, "Flushing db should result in db size 0");
done();
client.flushdb(function(err, res) {
client.dbsize([], function (err, res) {
helper.isNotError()(err, res);
helper.isType.number()(err, res);
assert.strictEqual(0, res, "Flushing db should result in db size 0");
done();
});
});
});
it("results in a db size of zero without a callback", function (done) {
client.flushdb();
setTimeout(function(err, res) {
client.dbsize([], function (err, res) {
helper.isNotError()(err, res);
helper.isType.number()(err, res);
assert.strictEqual(0, res, "Flushing db should result in db size 0");
done();
});
}, 25);
});
});
});
});

View File

@@ -70,6 +70,21 @@ describe("The 'get' method", function () {
done(err);
});
});
it("gets the value correctly with array syntax and the callback being in the array", function (done) {
client.GET([key, function (err, res) {
helper.isString(value)(err, res);
done(err);
}]);
});
it("should not throw on a get without callback (even if it's not useful", function (done) {
client.GET(key);
client.on('error', function(err) {
throw err;
});
setTimeout(done, 50);
});
});
describe("when the key does not exist in Redis", function () {

View File

@@ -74,6 +74,26 @@ describe("The 'getset' method", function () {
});
});
});
it("gets the value correctly with array syntax", function (done) {
client.GETSET([key, value2], function (err, res) {
helper.isString(value)(err, res);
client.get(key, function (err, res) {
helper.isString(value2)(err, res);
done(err);
});
});
});
it("gets the value correctly with array syntax style 2", function (done) {
client.GETSET(key, [value2], function (err, res) {
helper.isString(value)(err, res);
client.get(key, function (err, res) {
helper.isString(value2)(err, res);
done(err);
});
});
});
});
describe("when the key does not exist in Redis", function () {

View File

@@ -67,7 +67,7 @@ describe("The 'hgetall' method", function () {
it('returns binary results', function (done) {
client.hmset(["bhosts", "mjr", "1", "another", "23", "home", "1234", new Buffer([0xAA, 0xBB, 0x00, 0xF0]), new Buffer([0xCC, 0xDD, 0x00, 0xF0])], helper.isString("OK"));
client.HGETALL(["bhosts"], function (err, obj) {
client.HGETALL(["bhosts", function (err, obj) {
assert.strictEqual(4, Object.keys(obj).length);
assert.strictEqual("1", obj.mjr.toString());
assert.strictEqual("23", obj.another.toString());
@@ -75,7 +75,7 @@ describe("The 'hgetall' method", function () {
assert.strictEqual((new Buffer([0xAA, 0xBB, 0x00, 0xF0])).toString('binary'), Object.keys(obj)[3]);
assert.strictEqual((new Buffer([0xCC, 0xDD, 0x00, 0xF0])).toString('binary'), obj[(new Buffer([0xAA, 0xBB, 0x00, 0xF0])).toString('binary')].toString('binary'));
return done(err);
});
}]);
});
});

View File

@@ -83,6 +83,24 @@ describe("The 'hmset' method", function () {
});
});
it('allows a key plus array without callback', function (done) {
client.HMSET(hash, [99, 'banana', 'test', 25]);
client.HGETALL(hash, function (err, obj) {
assert.equal(obj['99'], 'banana');
assert.equal(obj.test, '25');
return done(err);
});
});
it('allows a key plus array and a callback', function (done) {
client.HMSET(hash, [99, 'banana', 'test', 25], helper.isString('OK'));
client.HGETALL(hash, function (err, obj) {
assert.equal(obj['99'], 'banana');
assert.equal(obj.test, '25');
return done(err);
});
});
it('handles object-style syntax without callback', function (done) {
client.HMSET(hash, {"0123456789": "abcdefghij", "some manner of key": "a type of value"});
client.HGETALL(hash, function (err, obj) {

View File

@@ -23,7 +23,7 @@ describe("The 'keys' method", function () {
it('returns matching keys', function (done) {
client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], helper.isString("OK"));
client.KEYS(["test keys*"], function (err, results) {
client.KEYS("test keys*", function (err, results) {
assert.strictEqual(2, results.length);
assert.ok(~results.indexOf("test keys 1"));
assert.ok(~results.indexOf("test keys 2"));

View File

@@ -42,7 +42,7 @@ describe("The 'mget' method", function () {
});
it('handles fetching multiple keys, when some keys do not exist', function (done) {
client.MGET(["mget keys 1", "some random shit", "mget keys 2", "mget keys 3"], function (err, results) {
client.MGET("mget keys 1", ["some random shit", "mget keys 2", "mget keys 3"], function (err, results) {
assert.strictEqual(4, results.length);
assert.strictEqual("mget val 1", results[0].toString());
assert.strictEqual(null, results[1]);

View File

@@ -88,6 +88,12 @@ describe("The 'mset' method", function () {
client.get(key, helper.isString(value2));
client.get(key2, helper.isString(value, done));
});
it("sets the value correctly with array syntax", function (done) {
client.mset([key, value2, key2, value]);
client.get([key, helper.isString(value2)]);
client.get(key2, helper.isString(value, done));
});
});
describe("with undefined 'key' and missing 'value' parameter", function () {

View File

@@ -123,7 +123,7 @@ describe("The 'multi' method", function () {
it('handles multiple operations being applied to a set', function (done) {
client.sadd("some set", "mem 1");
client.sadd("some set", "mem 2");
client.sadd(["some set", "mem 2"]);
client.sadd("some set", "mem 3");
client.sadd("some set", "mem 4");
@@ -136,7 +136,7 @@ describe("The 'multi' method", function () {
// test nested multi-bulk replies with empty mb elements.
client.multi([
["smembers", "some set"],
["smembers", ["some set"]],
["del", "some set"],
["smembers", "some set"]
])
@@ -148,6 +148,40 @@ describe("The 'multi' method", function () {
});
});
it('allows multiple operations to be performed using constructor with all kinds of syntax', function (done) {
var now = Date.now();
client.multi([
["mset", [578, "multibar"], helper.isString('OK')],
[["mset", "multifoo2", "multibar2", "multifoo3", "multibar3"], helper.isString('OK')],
["hmset", ["multihmset", "multibar", "multibaz"]],
[["hmset", "multihmset2", "multibar2", "multifoo3", "multibar3", "test", helper.isString('OK')]],
["hmset", ["multihmset", "multibar", "multifoo", helper.isString('OK')]],
["hmset", [5768, "multibarx", "multifoox"], helper.isString('OK')],
['hmset', now, {123456789: "abcdefghij", "some manner of key": "a type of value", "otherTypes": 555}],
['hmset', 'key2', {"0123456789": "abcdefghij", "some manner of key": "a type of value", "otherTypes": 999}, helper.isString('OK')],
["hmset", "multihmset", ["multibar", "multibaz"]],
["hmset", "multihmset", ["multibar", "multibaz"], helper.isString('OK')],
])
.hmget(now, 123456789, 'otherTypes')
.hmget('key2', ['some manner of key', 'otherTypes'])
.hmget(['multihmset2', 'some manner of key', 'multibar3'])
.mget('multifoo2', ['multifoo3', 'multifoo'], function(err, res) {
assert(res[0], 'multifoo3');
assert(res[1], 'multifoo');
})
.exec(function (err, replies) {
assert.strictEqual(null, err);
assert.equal(replies[10][1], '555');
assert.equal(replies[11][0], 'a type of value');
assert.strictEqual(replies[12][0], null);
assert.equal(replies[12][1], 'test');
assert.equal(replies[13][0], 'multibar2');
assert.equal(replies[13].length, 3);
assert.equal(replies.length, 14);
return done();
});
});
it('allows multiple operations to be performed using a chaining API', function (done) {
client.multi()
.mset('some', '10', 'keys', '20')
@@ -165,6 +199,23 @@ describe("The 'multi' method", function () {
});
});
it('allows multiple commands to work the same as normal to be performed using a chaining API', function (done) {
client.multi()
.mset(['some', '10', 'keys', '20'])
.incr(['some', helper.isNumber(11)])
.incr(['keys'], helper.isNumber(21))
.mget('some', 'keys')
.exec(function (err, replies) {
assert.strictEqual(null, err);
assert.equal('OK', replies[0]);
assert.equal(11, replies[1]);
assert.equal(21, replies[2]);
assert.equal(11, replies[3][0].toString());
assert.equal(21, replies[3][1].toString());
return done();
});
});
it('allows an array to be provided indicating multiple operations to perform', function (done) {
// test nested multi-bulk replies with nulls.
client.multi([

View File

@@ -0,0 +1,37 @@
'use strict';
var config = require("../lib/config");
var helper = require("../helper");
var redis = config.redis;
var assert = require('assert');
describe("The 'rpush' command", function () {
helper.allTests(function(parser, ip, args) {
describe("using " + parser + " and " + ip, function () {
var client;
beforeEach(function (done) {
client = redis.createClient.apply(redis.createClient, args);
client.once("error", done);
client.once("connect", function () {
client.flushdb(done);
});
});
it('inserts multiple values at a time into a list', function (done) {
client.rpush('test', ['list key', 'should be a list']);
client.lrange('test', 0, -1, function(err, res) {
assert.equal(res[0], 'list key');
assert.equal(res[1], 'should be a list');
done(err);
});
});
afterEach(function () {
client.end();
});
});
});
});

View File

@@ -44,6 +44,17 @@ describe("The 'sadd' method", function () {
});
});
it('allows multiple values to be added to the set with a different syntax', function (done) {
client.sadd(["set0", "member0", "member1", "member2"], helper.isNumber(3));
client.smembers("set0", function (err, res) {
assert.strictEqual(res.length, 3);
assert.ok(~res.indexOf("member0"));
assert.ok(~res.indexOf("member1"));
assert.ok(~res.indexOf("member2"));
return done(err);
});
});
afterEach(function () {
client.end();
});

View File

@@ -22,11 +22,11 @@ describe("The 'sdiff' method", function () {
it('returns set difference', function (done) {
client.sadd('foo', 'x', helper.isNumber(1));
client.sadd('foo', 'a', helper.isNumber(1));
client.sadd('foo', ['a'], helper.isNumber(1));
client.sadd('foo', 'b', helper.isNumber(1));
client.sadd('foo', 'c', helper.isNumber(1));
client.sadd(['foo', 'c'], helper.isNumber(1));
client.sadd('bar', 'c', helper.isNumber(1));
client.sadd(['bar', 'c', helper.isNumber(1)]);
client.sadd('baz', 'a', helper.isNumber(1));
client.sadd('baz', 'd', helper.isNumber(1));

View File

@@ -75,6 +75,13 @@ describe("The 'sort' method", function () {
});
});
it("handles sorting with a 'by' pattern and 2 'get' patterns with the array syntax", function (done) {
client.sort(['x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*'], function (err, sorted) {
assert.deepEqual(sorted, ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux']);
return done(err);
});
});
it("sorting with a 'by' pattern and 2 'get' patterns and stores results", function (done) {
client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', 'store', 'bacon', function (err) {
if (err) return done(err);

View File

@@ -41,8 +41,8 @@ describe("The 'srem' method", function () {
});
it('handles a value missing from the set of values being removed', function (done) {
client.sadd("set0", ["member0", "member1", "member2"], helper.isNumber(3));
client.SREM("set0", ["member3", "member4"], helper.isNumber(0));
client.sadd(["set0", "member0", "member1", "member2"], helper.isNumber(3));
client.SREM(["set0", "member3", "member4"], helper.isNumber(0));
client.smembers("set0", function (err, res) {
assert.strictEqual(res.length, 3);
assert.ok(~res.indexOf("member0"));

View File

@@ -35,12 +35,12 @@ describe("The 'type' method", function () {
});
it('reports zset type', function (done) {
client.zadd(["zset key", "10.0", "should be a zset"], helper.isNumber(1));
client.zadd("zset key", ["10.0", "should be a zset"], helper.isNumber(1));
client.TYPE(["zset key"], helper.isString("zset", done));
});
it('reports hash type', function (done) {
client.hset(["hash key", "hashtest", "should be a hash"], helper.isNumber(1));
client.hset("hash key", "hashtest", "should be a hash", helper.isNumber(1));
client.TYPE(["hash key"], helper.isString("hash", done));
});