Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added cassandra backend #117

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ module.exports = require("./lib/acl.js");
module.exports.redisBackend = require("./lib/redis-backend.js");
module.exports.memoryBackend = require("./lib/memory-backend.js");
module.exports.mongodbBackend = require("./lib/mongodb-backend.js");
module.exports.cassandraBackend = require("./lib/cassandra-backend.js");
2 changes: 1 addition & 1 deletion lib/acl.js
Original file line number Diff line number Diff line change
Expand Up @@ -585,7 +585,7 @@ Acl.prototype.middleware = function(numPathComponents, userId, actions){
return;
}

url = req.url.split('?')[0];
url = (req.route ? req.url : req.originalUrl).split('?')[0];
if(!numPathComponents){
resource = url;
}else{
Expand Down
191 changes: 191 additions & 0 deletions lib/cassandra-backend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
Cassandra Backend.
Implementation of the storage backend using Cassandra

Attention: The specified keyspace and table must exist beforehand. The 'create table' query:
CREATE TABLE [keyspace].[columnfamily] (
bucketname varchar,
key varchar,
values set<varchar>,
PRIMARY KEY ((bucketname, key))
)
*/
"use strict";

var contract = require('./contract');
var async = require('async');
var _ = require('lodash');

function CassandraBackend(client, keyspace, columnfamily){
this.client = client;
var keyspaceWithTableName = keyspace + "." + columnfamily;
this.queries = {
clean: "TRUNCATE " + keyspaceWithTableName,
get: "SELECT values FROM " + keyspaceWithTableName + " WHERE bucketname = ? AND key = ?",
union: "SELECT values FROM " + keyspaceWithTableName + " WHERE bucketname = ? AND key IN ?",
add: "UPDATE " + keyspaceWithTableName + " SET values = values + ? WHERE bucketname = ? AND key = ?",
del: "DELETE FROM " + keyspaceWithTableName + " WHERE bucketname = ? AND key IN ?",
remove: "UPDATE " + keyspaceWithTableName + " SET values = values - ? WHERE bucketname = ? AND key = ?"
};
}

CassandraBackend.prototype = {
/**
Begins a transaction.
*/
begin : function(){
// returns a transaction object(just an array of functions will do here.)
return [];
},

/**
Ends a transaction (and executes it)
*/
end : function(transaction, cb){
contract(arguments).params('array', 'function').end();
async.series(transaction,function(err){
cb(err instanceof Error? err : undefined);
});
},

/**
Cleans the whole storage.
*/
clean : function(cb){
contract(arguments).params('function').end();
this.client.execute(this.queries.clean, [], cb);
},

/**
Gets the contents at the bucket's key.
*/
get: function(bucket, key, cb) {
contract(arguments)
.params('string', 'string|number', 'function')
.end();
key = encodeText(key);
this.client.execute(this.queries.get, [bucket, key], {hints: ['varchar', 'varchar']}, function(err, result) {
if (err) return cb(err);
if (result.rows.length == 0) return cb(undefined, []);
result = decodeAll(result.rows[0].values);
cb(undefined, result);
});
},

/**
Returns the union of the values in the given keys.
*/
union: function(bucket, keys, cb) {
contract(arguments)
.params('string', 'array', 'function')
.end();
keys = encodeAll(keys);
this.client.execute(this.queries.union, [bucket, keys], {hints: ['varchar', 'set<varchar>']}, function(err, result) {
if (err) return cb(err);
if (result.rows.length == 0) return cb(undefined, []);
result = result.rows.reduce(function(prev, curr) { return prev.concat(decodeAll(curr.values)) }, []);
cb(undefined, _.union(result));
});
},

/**
Adds values to a given key inside a bucket.
*/
add: function(transaction, bucket, key, values) {
contract(arguments)
.params('array', 'string', 'string|number', 'string|array|number')
.end();

if (key == "key") throw new Error("Key name 'key' is not allowed.");
key = encodeText(key);
var self = this;
transaction.push(function (cb) {
values = makeArray(values);
self.client.execute(self.queries.add, [values, bucket, key], {hints: ['set<varchar>', 'varchar', 'varchar']}, function(err) {
if (err) return cb(err);
cb(undefined);
});
});
},

/**
Delete the given key(s) at the bucket
*/
del: function(transaction, bucket, keys) {
contract(arguments)
.params('array', 'string', 'string|array')
.end();
keys = makeArray(keys);
var self = this;
transaction.push(function (cb) {
self.client.execute(self.queries.del, [bucket, keys], {hints: ['varchar', 'set<varchar>']}, function(err) {
if (err) return cb(err);
cb(undefined);
});
});
},

/**
Removes values from a given key inside a bucket.
*/
remove: function(transaction, bucket, key, values) {
contract(arguments)
.params('array', 'string', 'string|number', 'string|array|number')
.end();
key = encodeText(key);
var self = this;
values = makeArray(values);
transaction.push(function (cb) {
self.client.execute(self.queries.remove, [values, bucket, key], {hints: ['set<varchar>', 'varchar', 'varchar']}, function(err) {
if (err) return cb(err);
cb(undefined);
});
});
}
};

function encodeText(text) {
if (typeof text == 'number' || text instanceof Number) text = text.toString();
if (typeof text == 'string' || text instanceof String) {
text = encodeURIComponent(text);
text = text.replace(/\./, '%2E');
}
return text;
}

function decodeText(text) {
if (typeof text == 'string' || text instanceof String) {
text = decodeURIComponent(text);
}
return text;
}

function encodeAll(arr) {
if (Array.isArray(arr)) {
var ret = [];
arr.forEach(function(aval) {
ret.push(encodeText(aval));
});
return ret;
} else {
return arr;
}
}

function decodeAll(arr) {
if (Array.isArray(arr)) {
var ret = [];
arr.forEach(function(aval) {
ret.push(decodeText(aval));
});
return ret;
} else {
return arr;
}
}

function makeArray(arr){
return Array.isArray(arr) ? encodeAll(arr) : [encodeText(arr)];
}

exports = module.exports = CassandraBackend;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"async": "~0.9.0",
"bluebird": "^2.3.11",
"cassandra-driver": "^2.0.1",
"lodash": "~2.4.1",
"mongodb": "^1.4.30",
"redis": ">=0.12.1"
Expand Down
30 changes: 25 additions & 5 deletions test/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,26 @@ describe('MongoDB - useSingle', function () {
run()
});

// Attention: keyspace and columnfamily must exist beforehand
describe('Cassandra', function () {
before(function (done) {
var self = this
, cassandra = require('cassandra-driver');

client = new cassandra.Client({contactPoints: ['127.0.0.1']});
client.connect(function(err) {
if (err) return done(err);
client.execute("TRUNCATE acltest.acl", [], function(err) {
if (err) return done(err);
self.backend = new Acl.cassandraBackend(client, "acltest", "acl");
done()
});
});
});

run()
});

describe('Redis', function () {
before(function (done) {
var self = this
Expand All @@ -43,22 +63,22 @@ describe('Redis', function () {
password: null
}
, Redis = require('redis')


var redis = Redis.createClient(options.port, options.host, {no_ready_check: true} )

function start(){
self.backend = new Acl.redisBackend(redis)
done()
}

if (options.password) {
redis.auth(options.password, start)
} else {
start()
}
})

run()
})

Expand All @@ -68,7 +88,7 @@ describe('Memory', function () {
var self = this
self.backend = new Acl.memoryBackend()
})

run()
})

Expand Down