dakota-cassandra
Cassandra ORM for NodeJS
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 = ;var dakota = options;var User = dakota;var user = name: 'Alex' ;user;User;
Features
- Solid foundation
- Written on top of datastax/nodejs-driver (official Cassandra JavaScript driver)
- Based off of Mongoid and Mongoose design and usability patterns
- Chainable query building interface and full support for CQL
- Build queries by chaining methods like
.select
,.where
,.limit
,.all
, and.first
- Complete access to CQL queries through query builder
- compiles
SELECT
queries with selective columns,FROM
,WHERE
,ORDER BY
,LIMIT
, andALLOW FILTERING
support - compiles
UPDATE
queries withUSING
,SET
,WHERE
,IF
, andIF EXISTS
support - compiles
INSERT
queries withINTO
,IF NOT EXISTS
, andUSING
support - compiles
DELETE
queries withFROM
,USING
,WHERE
,IF
, andIF EXISTS
support - compiles
TRUNCATE
queries
- compiles
- compiles
prepared statements
in all cases - support for
eachRow
andstream
to process large data sets - all queries are buffered until a successful connection is established
- Build queries by chaining methods like
- Full support for Cassandra types
- All basic types (
ascii
,bigint
,blob
,boolean
,decimal
,double
,float
,inet
,int
,text
,timestamp
,timeuuid
,uuid
,varchar
,varint
) - All collection types (
list
,set
,map
) - Support for
user defined types
,counters
,tuples
, andfrozen
fields
- All basic types (
- Schema validation, and sanitization backed models
- Define custom setters, getters, instance, and static methods
- Callback / filter chains on
afterNew
,beforeCreate
,afterCreate
,beforeValidate
,afterValidate
,beforeSave
,afterSave
,beforeDelete
,afterDelete
- Define custom sanitizers and validators for fields
- 'Recipes' for common and chainable validation and sanitization tasks
- ... examples include:
minLength
,maxLength
,required
,email
, and more ... - User definable validation messages that can be output to user
- Set column values with pre-generated setters and getters
Append
,prepend
,add
,remove
,increment
,decrement
, andinject
convenience methods for working with collection types
- Changed column tracking
- Only updates or inserts changed fields
- Automatically combines multiple
append
,prepend
,add
,remove
,increment
,decrement
,inject
actions if they are additive or composes a singleset
action
- Automatic
keyspace
,table
, anduser defined type
schema rectification (configurable)- Detects and alerts on differences between schemas and structures
- Automatically creates structures, adds columns, removes columns, or changes types and replication settings (configurable)
- Keyspaces have options for
ensure exists
, andalter
(to alterreplication
anddurableWrites
) - Tables have options for
ensure exists
,recreate
,recreateColumn
,removeExtra
,addMissing
- User defined types have options for
ensure exists
,recreate
,changeType
,addMissing
- Keyspaces have options for
Missing But Coming
- Indexes on tables
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 = options;
options.connection
is passed directly to the datastax/nodejs-driverClient
object; you can specify additional fields here as necessaryoptions.keyspace
is used to configure your app's keyspace- If a keyspace with the name in
options.connection.keyspace
doesn't exist, it is automatically created. If it does exist, its schema is compared against the options here (see below for automatic discrepancy resolution).
- If a keyspace with the name in
Full Options (Library Defaults)
var nm_ = ;var nm_i = ;var nm_s = ; 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: { return nm_i; } { return columnName; } { var name = nm_s; return operation + name; } { var name = nm_s; if operation == 'increment' || operation == 'decrement' return operation + name; else return operation + nm_i; } 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 ;
keyspace.ensureExists
dictates the keyspace discrepancy rectification policyrun
- check existence of keyspace, create if missing, and compare schema?alter
- alter keyspace to matchreplication
anddurableWrites
options
logger
determines behavior of built in loggerlevel
- can be set to 'debug', 'info', 'warn', or 'error' to only display log messages above the specified levelqueries
- log compiled query statements and params?
model
dictates table discrepancy handling and general setuptableName
- function used to convert from model name to table name- by default, a model named 'UserByEmail' will create a table named 'user_by_emails'
getterSetterName
- function used to name getters and setters for columns- by default, a column named 'email_addresses' will create
.email_addresses
and.email_addresses =
methods
- by default, a column named 'email_addresses' will create
validatorSanitizerName
- function used to name validators and sanitizers for columns- by default, a column named 'email' will create
.validateEmail
,.sanitizeEmail
, and.validateSanitizedEmail
methods
- by default, a column named 'email' will create
typeSpecificSetterName
- function used to name getters and setters specific to certain types- by default, a column named 'friend_uuids' of type list will create
.appendFriend_uuid
,.prependFriend_uuid
,.removeFriend_uuid
, and.injectFriend_uuid
methods - the
operation
argument in this function is passed strings like 'append', 'prepend', etc...
- by default, a column named 'friend_uuids' of type list will create
table.ensureExists
specifies the table discrepancy rectification policyrun
- check existence of table, create if missing, and compare schema?recreate
- drop and recreate table on discrepancyrecreateColumn
- drop and recreate column on type mismatchremoveExtra
- drop columns not in schemaaddExtra
- add columns that are defined in the schema but don't exist in the table
userDefinedType.ensureExists
ditactes the UDT discrepancy rectification policyrun
- check existence of UDT, create if missing, and compare schema?recreate
- drop and recreate UDT on discrepancychangeType
- attempt to change type on field if type mismatches schemaaddMissing
- add fields that are defined in the schema but don't exist in UDT
Models
var User = dakota;
- Models are created via the
.addModel
method onDakota
instances. When they're added, they're immediately validated and compared against existing tables (see options on configuringensureExists
above). - The first argument specifies the name of the model; the second is an
Object
containing the model's schema; the third is anObject
containing sanitizations and validations (null
can be passed if no validations are necessary); the last is an optionsObject
which can be used to overrideoptions.model
passed in to thenew Dakota(options)
constructor.
Schema
var Dakota = ;var schema = // columns columns: // timestamps ctime: 'timestamp' utime: 'timestamp' // data id: 'uuid' name: 'text' email: alias: 'emailAddress' type: 'text' { return value; } { return value; } 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: { console; } // create beforeCreate: DakotaRecipesCallbacks afterCreate: // validate beforeValidate: afterValidate: // save beforeSave: DakotaRecipesCallbacks afterSave: // delete beforeDelete: afterDelete: // methods method: { console; }; // static methods: { { User; } };
schema.columns
defines your model's fields and corresponding types- an
Object
can be set per field for additional configuration (seeemail
above)alias
specifies the name to use for auto generated methods and arguments to those methods- because column names are stored in each record in Cassandra, it is sometimes desirable to have a more user friendly name
- ... for instance:
fids: { alias: 'FriendIDs', type: set<uuid> }
will create.friendIDs
,.friendIDs =
,.addFriendID
, ... methods - aliases are also support mass assignment, for instance:
new User({ FriendIDs: [...], ... })
anduser.set({ FriendIDs: [...], ... })
type
specifies the type of the fieldset
andget
will be invoked when setting or getting the column value- NOTICE they both
return
the value
- NOTICE they both
- an
schema.key
defines the model's primary key- composite keys should be grouped in a nested array
schema.callbacks
defines chainable callbacks that are run in definition order for particular eventsRecipes
for common callbacks are provided in the/lib/recipes
directory and are loaded underDakota.Rescipes.Callbacks
- NOTE there are cases when callbacks are skipped or ignored, please see the Creating and Deleting and Upsert and Delete Without Reading sections below for more details
schema.methods
defines instance methods available on each model instanceschema.staticMethods
defines static methods on the model
Validations
Usage
var user = ; useremail = '[email protected]'; // automatically sanitizes inputuseremail; // returns '[email protected]' userpassword = 'dak';user; // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 characters long', ... ], ... } if validation errors user; user;user;user;user; user; // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 charactersuser; // returns '[email protected]' User; // returns { password: ['Password must contain at least one character and one number.', 'Password must be more than 6 charactersUser; // returns '[email protected]'
sanitizers
are run when a column's value is set- in the example above, our sanitizer downcases the user's email address
validators
are run when the.validate()
methods is explicitly called and on model.save(...)
- if validation errors exist, an
Object
will be produced where the keys correspond to column names and the values are arrays of validation error messages .validate(...)
returns a validationObject
immediately on validation fail, andfalse
on validation pass.save(...)
is interrupted on validation errors and aDakota.Model.ValidationFailedError
is passed as theerr
argument to the callback- both
.validate
and.save
can take an options object that specify validations toonly
run on some columns or run on all columnsexcept
some
- if validation errors exist, an
Definition
var Dakota = ;var nm_s = ; var validations = ctime: validator: { return !nmValidator; } { return displayName + ' is required.'; } utime: validator: DakotaRecipesValidatorsrequired id: validator: DakotaRecipesValidatorsrequired email: displayName: 'Email' validator: DakotaRecipesValidatorsemail sanitizer: DakotaRecipesSanitizersemail name: displayName: 'Name' validator: DakotaRecipesValidatorsrequired DakotaRecipesValidators { return nm_s; } ;
- validation keys correspond to the column names in the schema definition
.displayName
specifies the name of the column to be displayed to users in validation messages.validator
is an array of validator definitions or a single definition.validator.validator
is a a function that must return true or false based on its input value.validator.message
returns a custom message based on a passed indisplayName
.sanitizer
is an array of sanitizer functions or a single sanitization function- Both
.validator
and.sanitizer
are passed an optional argumentinstance
, which refers to the model instance being validated - Recipes for validators and sanitizers can be found under the
/lib/recipes
folder and are loaded underDakota.Rescipes.Validators
andDakota.Rescipes.Sanitizers
Creating and Deleting
var User = dakota;var user = name: 'Dakota' ;user;user = User;user;user = User; user;user;user;user; user;user;
- Instances of models can be created 3 different ways
new User([assignments])
andUser.new([assignments])
are functionally identical and create an instance without immediately persisting it to the databaseUser.create([assignments], callback)
immediately but asynchronously persists the object to the database- NOTE both
.new
and.create
will upsert records- ... to only create new records, manually check if a record exists or use
.new
with.ifNotExists
- ... to only create new records, manually check if a record exists or use
.delete(callback)
deletes the model instance's corresponding row in the database- All callbacks will be run using the methods above since the entire record is presumed to be loaded
.ttl(...)
,.timestamp(...)
,.using(...)
,.ifExists(...)
,.ifNotExists(...)
, and.if()
query chains can modify query parameters before.delete(...)
or.save(...)
compile and run the query on the database
Upsert and Delete Without Reading
User; // all callbacks run, except: afterNew, beforeCreate, afterCreate var user = User;userage = 15;user;user; // all callbacks run, except: afterNew, beforeCreate, afterCreate var user = User;user; // beforeDelete and afterDelete callbacks run User; // no callbacks runUser; // no callbacks runUser; // no callbacks run User; // no callbacks runUser; // no callbacks run
User.upsert([assignments], [callback])
creates a special instance of a user object that prefers idempotent actions- Since the entire record is not presumed to be loaded:
- columns can only be read after they are set
- columns are only validated if they are set
- collection specific operations like
$append
,$prepend
,$remove
will not combine into a$set
on conflict afterNew
,beforeCreate
,afterCreate
callbacks are NOT run
- You should ensure all primary keys are set prior to calling
.save
by defining them in the initial call to.upsert
or setting the corresponding columns via conventional setters
- Since the entire record is not presumed to be loaded:
User.upsert(...)
followed by.delete(...)
will delete the record based on columns set in.upsert
or via setters later- This is the only delete without reading method that runs the
beforeDelete
andafterDelete
callbacks
- This is the only delete without reading method that runs the
User.delete([where], [callback])
andUser.where([conditionals]).delete([callback])
behave identicallyUser.where(...)...
starts a query building object and allows for additional clauses to be appended, like.ifExists
beforeDelete
andafterDelete
callbacks are NOT run
.deleteAll(callback)
and.truncate(callback)
are functionally identical and remove all rows from a tablebeforeDelete
andafterDelete
callbacks are NOT run because the rows are never loaded into memory- ... if callbacks must be run, consider a
User.where(...).eachRow(function(err, user) { user.delete(...) })
Querying
Userall { ... };User;User;User;Userall { ... }; User;User; User; var stream = User;stream;stream;stream;
- There are a multitude of ways to dynamically query your data using the query builder
.all(callback)
,.first(callback)
,.execute(callback)
, and.count(callback)
methods terminate query building, compile your query, and submit it to the database; they should be used at the end of a query chain to execute the query.select([column{String}, ...])
and.select(column{String}, ...)
specifies which columns to return in your results.select
is additive, meaning.select('name').select('email')
will return both name and email in resulting rows
.where(column{String}, value)
,.where({ column{String}: value })
, and.where({ column{String}: { operation[$eq, $gt, $gte, ...]{String}: value }})
specifies the WHERE conditions in your query.where
is additive but overrides conditions on the same column, meaning.where('name', 'Dakota').where({ age: 5}).where('name', 'Dak')
will compiles toWHERE "name" = 'Dak' AND "age" = 5
.orderBy({ partitionKey{String}: order[$asc, $desc]{String} })
and.orderBy(partitionKey{String}, order[$asc, $desc]{String})
order your results by a particular partition key in either$asc
or$desc
order.limit(limit{Integer})
limits the number of rows returned.allowFiltering(allow{Boolean})
adds theALLOW FILTERING
clause to the compiledSELECT
query.find([conditions], callback)
and.findOne([conditions], callback)
are short hand methods for.where(...).all(...)
and.where(...).first(...)
.eachRow(...)
and.stream()
methods invoke the corresponding Cassandra non-buffering row processing methods
Setters and Getters
User; UserCounter;
- Single and multiple compatible calls to collection specific setters will modify collections without setting the whole column value
- ... for example,
.addFriend('Bob')
will compile intofriends = friends + {'Bob'}
- ... likewise,
.addFriend('Bob')
followed by.addFriend('Joe')
will compile intofriends = friends + {'Bob', 'Joe'}
- ... however,
.addFriend('Bob')
followed by.removeFriend('...')
will compile intofriends = {'Bob', ... }
sinceadd
andremove
calls cannot be combined
- ... for example,
- Single and multiple compatible calls to
.remove
onmap
typed columns will generate aDELETE map1[key1], map2[key3] FROM...
query if performed in isolation- ... for example,
.removeHost('mask')
will compile intoDELETE hosts['mask'] FROM users WHERE...
- ... likewise,
.removeHost('mask')
followed by.removeHost('home')
will compile intoDELETE hosts['mask'], hosts['home'] FROM users WHERE...
- ... however,
.removeHost('mask')
followed by.addFriend('Bob')
or.injectHost('home', '123.456.789.123')
will compile intohosts = { 'home' : '123.456.789.123' }
sinceadd
breaks isolation andinject
cannot be combined
- ... for example,
Change Tracking
User;
User Defined Types
var Dakota = ; 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 = options userDefinedTypes;
- User defined types must be passed into the
new Dakota(options, [userDefinedTypes])
constructor because model schemas may depend on their existence - The format of the
userDefinedTypes
argument should be anObject
where eachkey
is thename
of the user defined type you'd like to define - The definition of each user defined type should be an
Object
that maps field names to types
Helpers
var Dakota = ; Dakota; Dakota;Dakota;Dakota; Dakota;Dakota;
- Helpers for generating and manipulating
UUID
andTimeUUID
are available as static methods on Dakota .getDateFromTimeUUID
and.getTimeFromTimeUUID
can be used to extra time and date data from TimeUUID strings and objects
Examples
For an in-depth look at using Dakota, take a look inside the /tests
folder.