diff --git a/index.js b/index.js index 778bc45..6274acd 100644 --- a/index.js +++ b/index.js @@ -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"); diff --git a/lib/acl.js b/lib/acl.js index 059e006..f3ec9d2 100644 --- a/lib/acl.js +++ b/lib/acl.js @@ -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{ diff --git a/lib/cassandra-backend.js b/lib/cassandra-backend.js new file mode 100644 index 0000000..e207370 --- /dev/null +++ b/lib/cassandra-backend.js @@ -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, + 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']}, 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']}, 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']}, 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']}, 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; diff --git a/package.json b/package.json index d44a9a4..42b8e29 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/runner.js b/test/runner.js index 7eb6f2a..f1afe06 100644 --- a/test/runner.js +++ b/test/runner.js @@ -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 @@ -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() }) @@ -68,7 +88,7 @@ describe('Memory', function () { var self = this self.backend = new Acl.memoryBackend() }) - + run() })