scamandrios

node.js bindings for Cassandra with a promises API

npm install scamandrios
1 downloads in the last day
3 downloads in the last week
34 downloads in the last month

Scamandrios

A promises API for Cassandra, forked from helenus. Helenus was Cassandra's twin brother. He was also known as Scamandrios. Like Cassandra, his prophecies were correct, but unlike her he was believed.

NPM

Build Status Dependenciess

To install:

npm install scamandrios

There is a set of integration tests run by mocha. The tests require cassandra to be running on localhost:9160. To run them:

make test && make test-cov

make coverage will generate a full code coverage report in tests/coverage.html.

Usage

CQL

Connection pools are interchangeable with connection objects. You can make all Cassandra API calls against pools or single connections the same way. To create a pool:

var scamandrios = require('scamandrios');

var pool = new scamandrios.ConnectionPool(
{
        hosts      : ['localhost:9160'],
        keyspace   : 'scamandrios_test',
        user       : 'test',
        password   : 'test1233',
        timeout    : 3000
        cqlVersion : '3.0.0', // default
        getHost    : getHostFunc, // optional
});

Specify the cqlVersion parameter if you do not wish to use CQL 3.0.

You can supply a function in the getHost parameter to override the random host selection that the pool will perform when handling a request.

As with most error-emitting objects in node, if you do not listen for error it will bubble up to process.uncaughtException.

pool.on('error', function(err)
{
    console.error(err.name, err.message);
});

All asynchronous operations return promises in lieu of taking callbacks. The promises library used is P, which is Promises/A+ spec compliant. Here's an example of making a CQL query:

pool.connect()
.then(function()
{
    return pool.cql('SELECT col FROM cf_one WHERE key = ?', ['key123']);
})
.then(function(results)
{
    // results can be iterated to get rows
    results.forEach(function(row)
    {
        // rows can be iterated to get column contents
        row.forEach(function(name, value, timestamp, ttl)
        {
            console.log(name, value, timestamp, ttl);
        });
    });
})
.fail(function(err)
{
    console.log(err);
}).done();

The first argument to cql() is the query string. The second is an array of items to interpolate into the query string, which is accomplished using util.format(). The result is an array of Row objects. You can always skip quotes around placeholders. Quotes are added automatically. In CQL3 you cannot use placeholders for ColumnFamily or Column names.

CQL3 queries

CQL can express a set of types more specific than javascript's types. To javascript, a Cassandra set<text> and a list<text> both look like arrays. We found it helpful to have a query-contruction API that accepted type hints, so sets and lists could be interpolated properly into query strings.

The scamandrios.Query constructor exists to help you do this. Here's a somewhat contrived example:

var scamandrios = require('scamandrios');

var conn = new scamandrios.Connection();

var query = new scamandrios.Query('INSERT INTO {table} ({key_col}, {name_col}, {email_col}) VALUES ({key}, {name}, {emails})')
    .types(
    {
        key: 'uuid',
        name: 'text',
        emails: 'map<text, boolean>'
    })
    .params(
    {
        table: 'users',
        key_col: 'uid',
        email_col: 'emails',
        key: '271B6C60-A22D-4D0E-8171-0D344784217E',
        name: 'Mortimer Q. Snerd',
        emails:
        {
            'mortimer@example.com': true,
            'msnerd@hotmail.example.com': false
        }
    });

query.execute(conn);

new Query(str)

Takes a query string and returns a query object.

Think of the query string as being like a handlebars template object. Variables are mentioned inside curly braces:

`SELECT * FROM {table} WHERE {keycolumn} = {keyvalue}` 

There are three variables in that cql statement that need to be interpolated correctly: table, keycolumn, and keyvalue.

query.params(obj)

Builds a dictionary in the query object mapping named parameters to their values. Can be called repeatedly to add fields to the dictionary.

Returns the query object so the function can be chained.

query.types(obj)

Builds a dictionary in the query object mapping named parameters to their types. Can be called repeatedly to add fields to the dictionary. Types not appearing in this mapping are presumed to be Cassandra identifiers. That is, values that do not need to be quoted or escaped in any way.

Returns the query object so the function can be chained.

query.execute(connection)

Interpolate variables into the query string & execute the query. Takes a connection or connection pool parameter. Returns a promise that resolves to the result of the query.

Conveniences

scamandrios.discover(seed)

Given a seed node, return an array of peers in the Cassandra ring. The hosts will be dotted quad addresses and will not include any port information.

scamandrios.discover('10.0.0.1:9160')
.then(function(hosts)
{
    console.log(hosts);
});

Discover can also take an options object:

var opts = 
{
    host: 'loadbalancing.proxy.example.com',
    port: '9160',
    user: 'george',
    password: 'mischief managed',
    lookupSeed: true   // do a second query to discovery the IP of the seed node
};

scamandrios.discover(opts)
.then(function(hosts)
{
    console.log(hosts);
});

scamandrios.discoverPool(seed, connectionOptions)

Given a seed node, return a ConnectionPool configured with the given options. This function assumes that your Cassandra hosts are all using the default port 9160.

scamandrios.discoverPool('10.0.0.1:9160', { user: 'fred', password: 'mischief managed', timeout: 5000 })
.then(function(pool)
{
    return pool.connect();
});

DiscoveryPool

If you would like your pool to automatically rediscover its nodes on a timer, use a DiscoveryPool.


var DiscoveryPool = require(scamandrios).DiscoveryPool;

var opts =
{
    user: 'george',
    password: 'mischief managed',
    lookupSeed: true
};
var pool = new DiscoveryPool('loadbalancing.proxy.example.com', opts);
pool.connect()
.then(function()
{
    // make queries etc
}).done();

The seed node will be queried to discover the current state of the ring periodically. As nodes enter & leave, clients will be created & destroyed. The interval isn't yet configurable. It's 30 seconds aka the same as the monitor interval.

pool.health()

Returns an object with two fields:

{
    healthy: [ 'happyHost1:port', 'happyHost2:port' ],
    unhealthy: [ 'unhappyHost1:port', 'unhappyHost2:port']
}

pool.executeCQLAllClients(buffer)

Executes the given CQL query on all active clients. Very useful if you want to point your entire connection pool at a single keyspace, for instance, or if you're executing a health-check query on all of them.

connection.assignKeyspace(keyspaceName)

assignKeyspace creates the named keyspace if it doesn't exist and then calls connection.use on the keyspace. It is safe to call more than once on a connection object. We use it in the following pattern. Suppose we have an object holding onto a connection to a cassandra instance. We want to make sure that this connection is set up & pointing to the right keyspace before we use it.

var self = this;

this.withKeyspace = this.connection.connect().then(function()
{
    return self.connection.assignKeyspace('my_keyspace');
});

This promise turns into a value for the keyspace. You can then preceed other function calls with obj.withKeyspace.then(). For instance,

obj.withKeyspace
.then(function() { return obj.connection.cql('SELECT * FROM ? ', [obj.colfamily1]); })
.then(function(rows)
{
    // do something with rows;
}).done();

keyspace.createTableAs(tableName, propertyToStoreAs, createOptions)

Sugar for fetching a column family/table object, creating it if necessary. Caches the column family directly on the keyspace object as propertyToStoreAs.

Thrift

If you do not want to use CQL, you can make calls using the thrift driver

pool.connect.then(function(keyspace)
{
    return keyspace.get('my_cf');
})
.then(function(cf)
{
    return cf.insert('foo', { bar: 'baz'});
})
.then(function()
{
    return cf.get('foo', { consistency: scamandrios.ConsistencyLevel.ONE });
})
.then(function(row)
{
    console.log(row.get('bar').value);
})
.fail(function(err)
{
    // handle any error for the entire chain
})
.done();

Thrift Support

Currently scamandrios supports the following command for the thrift side of the driver:

  • connection.createKeyspace()
  • connection.dropKeyspace()
  • keyspace.createColumnFamily()
  • keyspace.dropColumnFamily()
  • columnFamily.insert()
  • columnFamily.get()
  • columnFamily.getIndexed()
  • columnFamily.remove()
  • columnFamily.truncate()
  • columnfamily.incr()

The focus of this fork of the driver is CQL 3.0 and its data structures. No further Thrift support is planned.

Row

The scamandrios Row object acts like an array but contains some helper methods to make your life a bit easier when dealing with dynamic columns in Cassandra.

row.count

Returns the number of columns in the row

row[N]

Returns the column at index N.

results.forEach(function(row)
{
    // gets the 5th column of each row
    console.log(row[5]);
});

row.get(name)

Returns the column with that specific name.

results.forEach(function(row)
{
    //gets the column with the name 'foo' of each row
    console.log(row.get('foo'));
});

row.forEach()

This is a wrapper function for Array.forEach() that returns name, value, ts, ttl of each column in the row as callback params.

// for every row in a result
results.forEach(function(row)
{
    // for every column in the row
    row.forEach(function(name, value, ts, ttl)
    {
        console.log(name, value, ts, ttl);
    });
});

row.slice(start, finish)

Slices columns in the row based on their numeric index, this allows you to get columns x through y, it returns a scamandrios row object of columns that match the slice.

results.forEach(function(row)
{
    var firstFive = row.slice(0, 5);
    console.log(firstFive);
});

row.nameSlice(start, finish)

Slices the columns based on part of their column name. returns a scamandrios row of columns that match the slice

results.forEach(function(row)
{
        // gets all columns that start with a, b, c, or d
        console.log(row.nameSlice('a','e'));
});

Column

Columns are returned as objects with the following structure:

{
    name:      'Foo',    // the column name
    value:     'bar',    // the column value
    timestamp: Date(),   // a date object containing the timestamp for the column
    ttl:       123456    // the ttl (in milliseconds) for the columns
}

ConsistencyLevel

scamandrios supports using a custom consistency level. By default, when using the thrift client reads and writes will both use QUORUM. When using the thrift driver, you simply pass a custom level in the options:

cf.insert(key, values, {consistency : scamandrios.ConsistencyLevel.ANY});

Contributors

  • Russell Bradberry - @devdazed
  • Matthias Eder - @matthiase
  • Christoph Tavan - @ctavan
  • C J Silverio - @ceejbot
  • Kit Cambridge - @kitcambridge

License

MIT; see provided license file for copyright information.

npm loves you