3logic / apollo-cassandra

A basic ORM for Cassandra
GNU Lesser General Public License v3.0
48 stars 14 forks source link

Apollo

Build Status Coverage Status

Deprecated

Attention!!!
This module is deprecated due to lack of maintenance. Please consider some valid alternatives as:

Description

Apollo is a Cassandra object modeling for node.js

Notes

Apollo is in early develeopment stage. Code and documentation are incomplete!

Installation

npm install --save apollo-cassandra

Usage

Include Apollo and start creating your models

var Apollo = require('apollo-cassandra');

var connection = {
    "hosts": [
        "127.0.0.1"
    ],
    "keyspace": "my_keyspace"
};

var apollo = new Apollo(connection);
apollo.connect(function(err){
    if(err) throw err;
    /* do amazing things! */
})

Connection

Apollo constructor takes two arguments: connection and options. Let's see what they are in depth:

Here is a complete example:

var apollo = new Apollo(
    {
       hosts: ['1.2.3.4', '12.3.6.5', 'cassandra.me.com:1212'],
       keyspace: 'mykeyspace',
       username: 'username',
       password: 'password'
    },
    {
        replication_strategy: {'class' : 'NetworkTopologyStrategy', 'dc1': 2 }
    }
);

Schema

Now that apollo is connected, create a model describing it through a schema

var personSchema = {
    { 
        fields:{
            name    : "text",
            surname : "text",
            age     : "int"
        }, 
        key:["name"] 
    }
};

Now create a new Model based on your schema. The function add_model uses the table name and schema as parameters.

var Person = apollo.add_model('person',personSchema);

From your model you can query cassandra or save your instances

/*Quesry your db*/
Person.find({name: 'jhon'}, function(err, people){
    if(err) throw err;
    console.log('Found ', people);
});

/*Save your instances*/
var alex = new Person({name: "Alex", surname: "Rubiks", age: 32});
alex.save(function(err){
    if(!err)
        console.log('Yuppiie!');
});

Schema in detail

A schema can be a complex object. Take a look at this example

personSchema = {
    "fields": {
        "id"     : { "type": "uuid", "default": {"$db_function": "uuid()"} },
        "name"   : { "type": "varchar", "default": "no name provided"},
        "surname"   : { "type": "varchar", "default": "no surname provided"},
        "complete_name" : { "type": "varchar", "default": function(){ return this.name + ' ' + this.surname;}},
        "age"    :  { "type": "int" },
        "created"     : {"type": "timestamp", "default" : {"$db_function": "now()"} }
    },
    "key" : [["id"],"created"],
    "indexes": ["name"]
}

What does the above code means?

Generating your model

A model is an object representing your cassandra table. Your application interact with cassandra through your models. An instance of the model represents a row of your table.

Let's create our first model

var Person = apollo.add_model('person',personSchema);

now instantiate a person

var john = new Person({name: "John", surname: "Doe"});

When you instantiate a model, every field you defined in schema is automatically a property of your instances. So, you can write:

john.age = 25;
console.log(john.name); //John
console.log(john.complete_name); // undefined.

note: john.complete_name is undefined in the newly created instance but will be populated when the instance is saved because it has a default value in schema definition

John is a well defined person but he is not still persisted on cassandra. To persist it we need to save it. So simply:

john.save(function(err){
    if(err)
        return 'Houston we have a problem';
    else
        return 'all ok, saved :)';

});

When you save an instance all internal validators will check you provided correct values and finally will try to save the instance on cassandra.

Ok, we are done with John, let's delete it:

john.delete(function(err){
    //...
});

ok, goodbye John.

A few handy tools for your model

Apollo instances provide some utility methods. To generate uuids e.g. in field defaults:

Virtual fields

Your model could have some fields which are not saved on database. You can define them as virtual

personSchema = {
    "fields": {
        "id"     : { "type": "uuid", "default": {"$db_function": "uuid()"} },
        "name"   : { "type": "varchar", "default": "no name provided"},
        "surname"   : { "type": "varchar", "default": "no surname provided"},
        "complete_name" : {
            "type": "varchar",
            "virtual" : {
                get: function(){return this.name + ' ' +this.surname;},
                set: function(value){
                    value = value.split(' ');
                    this.name = value[0];
                    this.surname = value[1];
                }
            }
        }
    }
}

A virtual field is simply defined adding a virtual key in field description. Virtuals can have a get and a set function, both optional (you should define at least one of them!). this inside get and set functions is bound to current instance of your model.

Validators

Every time you set a property for an instance of your model, an internal type validator checks that the value is valid. If not an error is thrown. But how to add a custom validator? You need to provide your custom validator in the schema definition. For example, if you want to check age to be a number greater than zero:

var personSchema = {
    //... other properties hidden for clarity
    age: {
        type : "int",
        rule : function(value){ return value > 0; }
    }
}

your validator must return a boolean. If someone will try to assign john.age = -15; an error will be thrown. You can also provide a message for validation error in this way

var personSchema = {
    //... other properties hidden for clarity
    age: {
        type : "int",
        rule : {
            validator : function(value){ return value > 0; },
            message   : 'Age must be greater than 0'
        }
    }
}

then the error will have your message. Message can also be a function; in that case it must return a string:

var personSchema = {
    //... other properties hidden for clarity
    age: {
        type : "int",
        rule : {
            validator : function(value){ return value > 0; },
            message   : function(value){ return 'Age must be greater than 0. You provided '+ value; }
        }
    }
}

The error message will be Age must be greater than 0. You provided -15

Note that default values are validated if defined either by value or as a javascript function. Defaults defined as DB functions, on the other hand, are never validated in the model as they are retrieved after the corresponding data has entered the DB. If you need to exclude defaults from being checked you can pass an extra flag:

var blogUserSchema = {
    //... other properties hidden for clarity
    email: {
        type : "text",
        default : "<enter your email here>",
        rule : {
            validator : function(value){ /* code to check that value matches an email pattern*/ },
            ignore_default: true
        }
    }
}

Querying your data

Ok, now you have a bunch of people on db. How do I retrieve them?

Find

Person.find({name: 'John'}, function(err, people){
    if(err) throw err;
    console.log('Found ', people);
});

In the above example it will perform the query SELECT * FROM person WHERE name='john' but find() allows you to perform even more complex queries on cassandra. You should be aware of how to query cassandra. Every error will be reported to you in the err argument, while in people you'll find instances of Person. If you don't want apollo to cast results to instances of your model you can use the raw option as in the following example:

Person.find({name: 'John'}, { raw: true }, function(err, people){
    //people is an array of plain objects
});

Let's see a complex query

var query = {
    name: 'John', // stays for name='john' 
    age : { '$gt':10 }, // stays for age>10 You can also use $gte, $lt, $lte
    surname : { '$in': ['Doe','Smith'] }, //This is an IN clause
    $orderby:{'$asc' :'age'} }, //Order results by age in ascending order. Also allowed $desc and complex order like $orderby:{'$asc' : ['k1','k2'] } }
    $limit: 10 //limit result set

}

Note that all query clauses must be Cassandra compliant. You cannot, for example, use $in operator for a key which is not the partition key. Querying in Cassandra is very basic but could be confusing at first. Take a look at this post and, obvsiouly, at the documentation

API

Complete API definition is available on the 3logic website. Anyway, you can generate the documentation by cloning this project and launching grunt doc

Test

To test Apollo create a file named local_conf.json in test directory with your connection configuration as below

{
    "contactPoints": [
       "127.0.0.1",
       "192.168.100.65",
       "my.cassandra.com:9845"
    ],
    "keyspace": "tests"
}

About

Apollo is brought to you by

Thanks to Gianni Cossu and Massimiliano Atzori for helping.

Thanks to 3logic too!