Home

Awesome

dakota-cassandra

A full feature Apache Cassandra ORM built on top of datastax/nodejs-driver

Installation

$ npm install dakota-cassandra

Stability

Dakota was written over a weekend (8/28/2015 - 8/31/2015) by Alexander Wong out of Boost VC in San Mateo, CA, USA to address the lack of a full featured NodeJS compatible Cassandra ORM. It is currently still a work in progress and will be refined in the coming weeks. Please check back often for updates and bug fixes.

Basic Usage Example

var Dakota = require('dakota-cassandra');
var dakota = new Dakota(options);
var User = dakota.addModel('User', schema, validations);
var user = new User({ name: 'Alex' });
user.save(function(err) { ... });
User.where({ name: 'Alex' }).first(function(err, user) { ... });

Features

Missing But Coming

Connection and Options

Minimal Options

var options = {
  connection: {
    contactPoints: [
      '127.0.0.1'
    ],
    keyspace: 'dakota_test'
  },
  keyspace: {
    replication: { 'class': 'SimpleStrategy', 'replication_factor': 1 },
    durableWrites: true
  }
};
var dakota = new Dakota(options);

Full Options (Library Defaults)

var nm_ = require('underscore');
var nm_i = require('underscore.inflections');
var nm_s = require('underscore.string');

var defaultOptions = {
  
  // connection
  connection: {
    contactPoints: [
      '127.0.0.1'
    ],
    keyspace: 'dakota_test'
  },
  
  // keyspace
  keyspace: {
    replication: { 'class': 'SimpleStrategy', 'replication_factor': 1 },
    durableWrites: true,
    ensureExists: {
      run: true, // check if keyspace exists and automaticcaly create it if it doesn't
      alter: false // alter existing keyspace to match replication or durableWrites
    }
  },
  
  // logger
  logger: {
    level: 'debug', // log this level and higher [debug < info < warn < error]
    queries: true // log queries
  },
  
  // model
  model: {
    tableName: function(modelName) {
      return nm_i.pluralize(nm_s.underscored(modelName));
    },
    getterSetterName: function(columnName) {
      return columnName.trim().replace(/\s/g, '_');
    },
    validatorSanitizerName: function(operation, columnName) {
      var name = nm_s.capitalize(columnName.trim().replace(/\s/g, '_'));
      return operation + name;
    },
    typeSpecificSetterName: function(operation, columnName) {
      var name = nm_s.capitalize(columnName.trim().replace(/\s/g, '_'));
      if (operation == 'increment' || operation == 'decrement') {
        return operation + name;
      }
      else {
        return operation + nm_i.singularize(name);
      }
    },
    table: {
      ensureExists: {
        run: true, // check if keyspace exists and automaticcaly create it if it doesn't
        recreate: false, // drop and recreate table on schema mismatch, takes precedence over following options
        recreateColumn: false,  // recreate columns where types don't match schema
        removeExtra: false,  // remove extra columns not in schema
        addMissing: false // add columns in schema that aren't in table
      }
    }
  },
  
  // user defined type
  userDefinedType: {
    ensureExists: {
      run: true,
      recreate: false, // drop and recreate type on schema mismatch, takes precedence over following options
      changeType: false, // change field types to match schema
      addMissing: false // add fields in schema that aren't in type
    }
  }
  
};

Models

var User = dakota.addModel('User', require('./user.schema'), require('./user.validations'), options);

Schema

var Dakota = require('dakota-cassandra');
var schema = {
  
  // columns
  columns: {
    
    // timestamps
    ctime: 'timestamp',
    utime: 'timestamp',
    
    // data
    id: 'uuid',
    name: 'text',
    email: {
      alias: 'emailAddress',
      type: 'text',
      set: function(value) { return value.toLowerCase(); },
      get: function(value) { return value.toUpperCase(); }
    },
    ip: 'inet',
    age: 'int',
    
    // collections
    friends: 'set<uuid>',
    tags: 'list<text>',
    browsers: 'map<text,inet>',
    craziness: 'list<frozen<tuple<text,int,text>>>'
    
  },
  
  // key
  key: [['email', 'name'], 'id'], // excuse the contrived example
  
  // callbacks
  callbacks: {
    
    // new
    afterNew: [
      function() { console.log('after new callback'); }
    ],
    
    // create
    beforeCreate: [
      Dakota.Recipes.Callbacks.setTimestampToNow('ctime')
    ],
    afterCreate: [],
    
    // validate
    beforeValidate: [],
    afterValidate: [],
    
    // save
    beforeSave: [
      Dakota.Recipes.Callbacks.setTimestampToNow('utime')
    ],
    afterSave: [],
    
    // delete
    beforeDelete: [],
    afterDelete: []
  },
  
  // methods
  method: {
    greet: function () { console.log('Hello, my name is ' + this.name + '.'); };
  },
  
  // static methods: {
    plusPlusAge: function () {
      User.eachRow(function(n, user) {
        user.age += 1;
        user.save(function(err) { ... });
      }, function(err) {
        console.log('All users .age++ complete!');
      });
    }
  }
};

Validations

Usage

var user = new User();

user.email = 'dAkOtA@dAKOta.DAkota'; // automatically sanitizes input
user.email; // returns 'dakota@dakota.dakota'

user.password = 'dak';
user.validate(); // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 characters long', ... ], ... } if validation errors

user.save(function(err) {
  if (err) {
    if (err instanceof Dakota.Model.ValidationFailedError) { // Dakota.Model.ValidationFailedError if validation errors
      var invalidColumns = err.message;
    }
  }
  ...
});

user.validate({ only: ['password'] });
user.validate({ except: [email] });
user.save(..., { validate: { only: [...] } });
user.save(..., { validate: { except: [...] } });

user.validatePassword('dak'); // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 characters
user.sanitizeEmail('dAkOtA@dAKOta.DAkota'); // returns 'dakota@dakota.dakota'

User.validatePassword('dak'); // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 characters
User.sanitizeEmail('dAkOtA@dAKOta.DAkota'); // returns 'dakota@dakota.dakota'

Definition

var Dakota = require('dakota-cassandra');
var nm_s = require('underscore.string');

var validations = {
  ctime: {
    validator: {
        validator: function(value, instance) {
            return !nmValidator.isNull(value);
        },
        message: function(displayName) { return displayName + ' is required.'; }
    }
  },
  utime: {
    validator: Dakota.Recipes.Validators.required
  },
  id: {
    validator: Dakota.Recipes.Validators.required
  },
  email: {
    displayName: 'Email',
    validator: Dakota.Recipes.Validators.email,
    sanitizer: Dakota.Recipes.Sanitizers.email
  },
  name: {
    displayName: 'Name',
    validator: [Dakota.Recipes.Validators.required, Dakota.Recipes.Validators.minLength(1)],
    sanitizer: function(value, instance) {
      return nm_s.capitalize(value);
    }
  }
};

Creating and Deleting

var User = dakota.addModel('User', schema, validations);
var user = new User({ name: 'Dakota' });
user.save(function(err) { ... });
user = User.new({ name: 'Alex' });
user.save(function(err) { ... }, {
  ... ,
  validate: {
    only / except ...
  },
  callbacks: {
    skipAll: false, // skips all callbacks, takes precedence over subsequent settings
    skipBeforeValidate: false,
    skipAfterValidate: false,
    skipBeforeCreate: false,
    skipAfterCreate: false,
    skipBeforeSave: false,
    skipAfterSave: false
  }
});
user = User.create({ name: 'Cindy' }, function(err) { ... });

user.if({ name: 'Dakota' }).save(function(err) { ... });
user.ifNotExists(true).ttl(1000).save(function(err) { ... });
user.using({ '$ttl' : 1000, '$timestamp' : 123456789 }).save(function(err) { ... });
user.timestamp(123456789).save(function(err) { ... });

user.delete(function(err) { ... }, {
  ... ,
  callbacks: {
    skipAll: false, // skips all callbacks, takes precedence over subsequent settings
    skipBeforeDelete: false,
    skipAfterDelete: false
  }
});
user.ifExists(true).delete(function(err) { ... });

Upsert and Delete Without Reading

User.upsert({ name: 'Dakota' }, function(err){ ... }); // all callbacks run, except: afterNew, beforeCreate, afterCreate

var user = User.upsert({ id: Dakota.generateUUID(), name: 'Dakota', email: 'dakota@dakota.dakota' });
user.age = 15;
user.appendTag('dog');
user.save(function(err) { ... }); // all callbacks run, except: afterNew, beforeCreate, afterCreate

var user = User.upsert({ id: Dakota.generateUUID(), name: 'Dakota', email: 'dakota@dakota.dakota' });
user.delete(function(err) { ... }); // beforeDelete and afterDelete callbacks run

User.delete({ name: 'Dakota' }, function(err) { ... }); // no callbacks run
User.where(...).delete(function(err) { ... }); // no callbacks run
User.where(...).ifExists(true).delete(function(err) { ... }); // no callbacks run

User.deleteAll(function(err) { ... }); // no callbacks run
User.truncate(function(err) { ... }); // no callbacks run

Querying

User.all(function(err, users) { ... });
User.where({ name: 'Dakota' }).first(function(err, user) { ... });
User.where({ name: 'Dakota' }).limit(1).execute(function(err, user) { ... });
User.count(function(err, count) { ... });
User.select(['email', 'utime']).where({ name: 'Dakota', age: { '$gte' : 5 } }).orderBy('age', '$asc').limit(1).allowFiltering(true).all(function(err, users) { ... });

User.find({ name: 'Dakota', email: 'dakota@dakota.com' }, function(err, users) { ... });
User.findOne({ name: 'Dakota' }, function(err, user) { ... });

User.eachRow(function(n, user) { ... }, function(err) { ... });

var stream = User.stream();
stream.on('readable', function() {
  // readable is emitted as soon a row is received and parsed
  var user;
  while (user = this.read()) {
    ...
  }
});
stream.on('end', function() {
  // stream ended, there aren't any more rows
});
stream.on('error', function(err) {
  // something went wrong: err is a response error from Cassandra
});

Setters and Getters

User.first(function(err, user) {

user.email = 'dakota@dakota.dakota';
user.email; // returns 'dakota@dakota.dakota'

user.set('name', 'Dakota');
user.get('name'); // returns 'Dakota'
user.get(['name', 'email', ... ]); // returns { name: 'Dakota', email: 'dakota@dakota.dakota' }

// assuming schema items: { type: list<text> }
user.appendItem('item 1');
user.appendItem('item 1');
user.prependItem('item 0');
user.appendItem('item 2');
user.items; // returns ['item 0', 'item 1', 'item 1', 'item 2']
user.removeItem('item 1');
user.items; // returns ['item 0', 'item 2']
user.inject(1, 'item 0');
user.items; // returns ['item 0', 'item 0'];
user.removeItem('item 0');
user.items; // returns null
user.items = ['item 0', 'item 1', 'item 2'];
user.items; // returns ['item 0', 'item 1', 'item 2]

// assuming schema friends: { type: set<text> }
user.addFriend('Bob');
user.addFriend('Bob');
user.addFriend('Joe');
user.friends; // returns ['Bob', 'Joe']
user.removeFriend('Joe');
user.friends; // returns ['Bob']
user.friends = ['Jenny', 'Alex', 'Cathy', 'Cathy'];
user.friends; // returns ['Jenny', 'Alex', 'Cathy']

// assuming schema hosts: { type: map<text,inet> }
user.hosts = { localhost: '127.0.0.1', mask: '255.255.255.255' };
user.injectHost('home', '123.456.789.123');
user.hosts; // returns { home: '123.456.789.123', localhost: '127.0.0.1', mask: '255.255.255.255' }
user.removeHost('mask');
user.hosts; // returns { home: '123.456.789.123', localhost: '127.0.0.1' }

});

UserCounter.first(function(err, userCounter) {

// assuming schema cnt: { type: counter }
userCounter.incrementCnt(5);
userCounter.incrementCnt(3);
userCounter.decrementCnt(7);
userCounter.cnt; // returns 1

});

Change Tracking

User.first(function(err, user) {

user.changed(); // returns false, check if any changes to any columns
user.changes(); // returns {}
user.name = 'Dakota';
user.changed('name'); // return true
user.changes(); // returns { name: { from: 'prev name', to: 'Dakota } }
user.changes('name'); // returns { from: 'prev name', to: 'Dakota }

});

User Defined Types

var Dakota = require('dakota-cassandra');

var address = {
  street: 'text',
  city: 'text',
  state: 'text',
  zip: 'int',
  phones: 'frozen<set<text>>',
  tenants: 'frozen<map<int,text>>'
};

var userDefinedTypes = {
  address: address
};
var dakota = new Dakota(options, userDefinedTypes);

Helpers

var Dakota = require('dakota-cassandra');

Dakota.generateUUID();

Dakota.generateTimeUUID();
Dakota.getDateFromTimeUUID(timeUUID);
Dakota.getTimeFromTimeUUID(timeUUID);

Dakota.nowToTimestamp();
Dakota.dateToTimestamp(new Date());

Examples

For an in-depth look at using Dakota, take a look inside the /tests folder.