├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── benchmark ├── collection.js └── qb.js ├── lib ├── connection.js ├── cql │ ├── builders.js │ ├── column │ │ ├── basic.js │ │ ├── collection.js │ │ ├── custom.js │ │ └── state │ │ │ ├── base.js │ │ │ ├── index.js │ │ │ ├── order.js │ │ │ └── select.js │ ├── constants.js │ ├── cson.js │ ├── parse │ │ └── table.js │ ├── queries │ │ ├── alterTable.js │ │ ├── base.js │ │ ├── delete.js │ │ ├── insert.js │ │ ├── select.js │ │ ├── truncate.js │ │ └── update.js │ ├── stmt │ │ ├── assignment.js │ │ ├── columns.js │ │ ├── conditionals.js │ │ ├── options.js │ │ ├── order.js │ │ ├── raw.js │ │ ├── termTuple.js │ │ └── where.js │ ├── table.js │ ├── typecast │ │ └── cast.js │ └── types.js ├── errors.js ├── model │ ├── collection.js │ ├── diff.js │ └── model.js └── util │ ├── casing.js │ ├── index.js │ ├── stmt.js │ └── wrapper.js ├── package.json ├── readme.md └── spec ├── ConnectionSpec.js ├── cql ├── AlterTableSpec.js ├── ColumnSpec.js ├── DeleteSpec.js ├── InsertSpec.js ├── SelectSpec.js ├── StmtSpec.js ├── TableSpec.js ├── TypecastSpec.js └── UpdateSpec.js ├── fake-connection.js ├── helper.js └── model ├── CollectionSpec.js ├── DiffSpec.js └── ModelSpec.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | node_modules 27 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 MCProHosting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /benchmark/collection.js: -------------------------------------------------------------------------------- 1 | var collection = require('../lib/model/collection'); 2 | var connection = require('../spec/fake-connection'); 3 | var t = require('../lib/cql/types'); 4 | 5 | collection = connection().Collection('UsersStuff'); 6 | collection.columns([t.Text('a'), t.List('b')]); 7 | 8 | new (require('benchmark').Suite)() 9 | .add('Select builder wrappings', function () { 10 | collection.select().then(function () {}); 11 | }) 12 | .add('Checkout new', function () { 13 | collection.new(); 14 | }) 15 | .add('Checkout new with property', function () { 16 | collection.define('a', 'b').new(); 17 | }) 18 | .add('Save', function () { 19 | var model = collection.new(); 20 | model.a = 1; 21 | model.b = [2, 3]; 22 | model.save(); 23 | }) 24 | .add('Update with diff', function () { 25 | var model = collection.new(); 26 | model.sync({ a: 1, b: [3, 4] }); 27 | model.a = 1; 28 | model.b = [2, 3]; 29 | model.save(); 30 | }) 31 | .on('cycle', function(event) { 32 | console.log(String(event.target)); 33 | }) 34 | .run(); 35 | -------------------------------------------------------------------------------- /benchmark/qb.js: -------------------------------------------------------------------------------- 1 | // To use, `npm install benchmark microtime` 2 | 3 | var builders = require('../lib/cql/builders'); 4 | var t = require('../lib/cql/types'); 5 | var Raw = require('../lib/cql/stmt/raw'); 6 | var Bluebird = require('bluebird'); 7 | 8 | new (require('benchmark').Suite)() 9 | .add('Just for kicks: bluebird', function () { 10 | Bluebird.resolve(true).then(function () { return "hi"; }); 11 | }) 12 | .add('Select statement builder', function () { 13 | builders.select() 14 | .columns('a', 'b') 15 | .from('tbl') 16 | .where(t.Text('a'), '<', 3) 17 | .andWhere('c', '>', 2) 18 | .parameterize(); 19 | }) 20 | .add('Delete statement builder', function () { 21 | builders.delete() 22 | .columns('a', 'b') 23 | .from('tbl') 24 | .where(t.Text('a'), '<', 3) 25 | .andWhere('c', '>', 2) 26 | .parameterize(); 27 | }) 28 | .add('Insert statement builder', function () { 29 | builders.insert() 30 | .data({ 'a': 'b' }) 31 | .into('tbl') 32 | .ifNotExists() 33 | .parameterize(); 34 | }) 35 | .add('Update statement builder', function () { 36 | builders.update() 37 | .table('tbl') 38 | .where(t.Text('a'), '<', 3) 39 | .andWhere('c', '>', 2) 40 | .set('foo = \'bar\'') 41 | .set('a', 2) 42 | .set('a', 2, 3) 43 | .timestamp(10) 44 | .ttl(10) 45 | .parameterize(); 46 | }) 47 | .on('cycle', function(event) { 48 | console.log(String(event.target)); 49 | }) 50 | .run(); 51 | -------------------------------------------------------------------------------- /lib/connection.js: -------------------------------------------------------------------------------- 1 | var cassandra = require('cassandra-driver'); 2 | var Bluebird = require('bluebird'); 3 | var events = require('events'); 4 | 5 | var _ = require('lodash'); 6 | var util = require('./util'); 7 | 8 | var BaseQuery = require('./cql/queries/base'); 9 | var types = require('./cql/types'); 10 | var builders = require('./cql/builders'); 11 | 12 | var Collection = require('./model/collection'); 13 | 14 | /** 15 | * Represents a Cassandra database connection. This is the primary means 16 | * of interacting with the ORM. 17 | * 18 | * @param {Object=} options If passed, we'll connect immediately. 19 | */ 20 | function Connection (options) { 21 | if (options) { 22 | this.connect(options); 23 | } 24 | 25 | // Add types in 26 | _.extend(this, types); 27 | // And builder methods 28 | _.extend(this, builders); 29 | } 30 | 31 | require('util').inherits(Connection, events.EventEmitter); 32 | 33 | 34 | /** 35 | * Connects to the Cassandra database, binding client methods as needed. 36 | * @param {Object} options 37 | * @return {Connection} 38 | */ 39 | Connection.prototype.connect = function (options) { 40 | var client = this.client = new cassandra.Client(options); 41 | // Promisify batch function 42 | client.batchAsync = Bluebird.promisify(client.batch, client); 43 | client.executeAsync = Bluebird.promisify(client.execute, client); 44 | 45 | // Promisify and bind functions that take a normal callback. 46 | this.shutdown = client.shutdownAsync = Bluebird.promisify(client.shutdown, client); 47 | this.connect = client.connectAsync = Bluebird.promisify(client.connect, client); 48 | 49 | // Bind client functions that don't need callbacks. 50 | var self = this; 51 | ['getReplicas', 'stream', 'eachRow'].forEach(function (method) { 52 | util.proxyMethod(self, 'client.' + method); 53 | }); 54 | }; 55 | 56 | /** 57 | * Batch runs many queries at once. Takes an array of queries, query strings, 58 | * or objects to be passed directory into the Cassandra driver. 59 | * 60 | * @param {[]BaseQuery|[]String|[]Array} queries 61 | * @param {Options=} options 62 | * @return {Promise} 63 | */ 64 | Connection.prototype.batch = function (queries, options) { 65 | return this.client.batchAsync(queries.map(function (query) { 66 | // Pass raw strings right in 67 | if (_.isString(query)) { 68 | return { query: query, params: [] }; 69 | } 70 | // Parameterize queries 71 | else if (query instanceof BaseQuery) { 72 | var params = query.parameterize(); 73 | return { query: params[1], params: params[0] }; 74 | } 75 | // Or just pass anything we don't know right in. 76 | else { 77 | return query; 78 | } 79 | }), options || {}); 80 | }; 81 | 82 | /** 83 | * Executes a CQL query. 84 | * @param {String} stmt 85 | * @param {Array=} params 86 | * @param {Object=} options 87 | * @return {Promise} 88 | */ 89 | Connection.prototype.execute = function (stmt, params, options) { 90 | // Statements should be prepared by default. 91 | _.defaults(typeof options === 'undefined' ? params : options, { prepare: true }); 92 | 93 | this.emit('query', stmt, params, options); 94 | 95 | return this.client.executeAsync(stmt, params, options); 96 | }; 97 | 98 | /** 99 | * Creates and returns a new collection on the connection. 100 | * @param {String} name 101 | * @return {Model} 102 | */ 103 | Connection.prototype.Collection = 104 | Connection.prototype.Model = function (name) { 105 | return new Collection(this, name); 106 | }; 107 | 108 | /** 109 | * Helper if you need to import stuff from the cassandra-driver directly. 110 | * @param {String} path 111 | * @return {*} 112 | */ 113 | Connection.import = function (path) { 114 | return require('cassandra-driver/' + path); 115 | }; 116 | 117 | module.exports = Connection; 118 | -------------------------------------------------------------------------------- /lib/cql/builders.js: -------------------------------------------------------------------------------- 1 | var util = require('../util'); 2 | var builders = module.exports = {}; 3 | 4 | // Bind query types to the exports. Note: when running functions, it's 5 | // expected that the context is the database connection. 6 | require('lodash').forIn({ 7 | select: require('../cql/queries/select'), 8 | delete: require('../cql/queries/delete'), 9 | insert: require('../cql/queries/insert'), 10 | update: require('../cql/queries/update'), 11 | truncate: require('../cql/queries/truncate'), 12 | alterTable: require('../cql/queries/alterTable') 13 | }, function (Builder, name) { 14 | builders[name] = function () { 15 | return new Builder(this); 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /lib/cql/column/basic.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('buffer'); 2 | var cassandra = require('cassandra-driver'); 3 | var Index = require('./state/index'); 4 | 5 | /** 6 | * Represents a column in a CQL table. Note that the name is not necessarily 7 | * required. 8 | * 9 | * @param {String} name Name of the column. 10 | * @param {String} type The type string of the table. 11 | */ 12 | function Column (name, type) { 13 | this.name = name; 14 | this.type = type; 15 | this.isCollection = false; 16 | this.attrs = []; 17 | this.isKey = { partition: false, compound: false }; 18 | this.columnIndex = null; 19 | } 20 | 21 | /** 22 | * Sets the column to be a partition key. 23 | * @return {Column} 24 | */ 25 | Column.prototype.partitionKey = function () { 26 | this.isKey.partition = true; 27 | return this; 28 | }; 29 | 30 | /** 31 | * Sets the column to be a compound key. 32 | * @return {Column} 33 | */ 34 | Column.prototype.compoundKey = function () { 35 | this.isKey.compound = true; 36 | return this; 37 | }; 38 | 39 | /** 40 | * Sets the column to be in an index. Takes a name of an index, 41 | * or creates one automatically. 42 | * @param {String=} name 43 | * @return {Index} 44 | */ 45 | Column.prototype.index = function (name) { 46 | this.columnIndex = new Index(this, name); 47 | return this.columnIndex; 48 | }; 49 | 50 | /** 51 | * Tries to run a generator for the column type, from the 52 | * driver's "types" object. 53 | * @return {*} 54 | */ 55 | Column.prototype.generate = function () { 56 | var generator = cassandra.types[this.type]; 57 | 58 | switch (typeof generator) { 59 | case 'undefined': 60 | throw new Error('Cannot generate for type ' + this.type); 61 | case 'function': 62 | return generator.apply(this, arguments); 63 | default: 64 | return generator; 65 | } 66 | }; 67 | 68 | /** 69 | * Returns the Cassandra type string for the column. 70 | * @return {String} 71 | */ 72 | Column.prototype.getType = function () { 73 | return this.type; 74 | }; 75 | 76 | /** 77 | * Returns the column name. 78 | * @return {String} 79 | */ 80 | Column.prototype.toString = 81 | Column.prototype.getName = function () { 82 | return this.name; 83 | }; 84 | 85 | /** 86 | * Adds a column attribute. 87 | * @param {String} attr 88 | * @return {Column} 89 | */ 90 | Column.prototype.addAttr = function (attr) { 91 | this.attrs.push(attr.toUpperCase()); 92 | return this; 93 | }; 94 | 95 | /** 96 | * Gets the column attributes. 97 | * @return {[]String} 98 | */ 99 | Column.prototype.getAttrs = function () { 100 | return this.attrs; 101 | }; 102 | 103 | /** 104 | * Returns the "name + type" for use in CREATE TABLE. 105 | * @return {String} 106 | */ 107 | Column.prototype.getEntry = function () { 108 | return [this.getName(), this.getType()].concat(this.getAttrs()).join(' '); 109 | }; 110 | 111 | var states = [ 112 | require('./state/select'), 113 | require('./state/order') 114 | ]; 115 | 116 | // Bind all the methods on the state so that, when called, they'll instantiate 117 | // a new state and run the requested method. 118 | states.forEach(function (State) { 119 | for (var key in State.prototype) { 120 | // Don't overwrite the column's own states. 121 | if (typeof Column.prototype[key] !== 'undefined') { 122 | continue; 123 | } 124 | 125 | Column.prototype[key] = bindState(State, key); 126 | } 127 | }); 128 | 129 | function bindState (State, key) { 130 | return function () { 131 | var state = new State(this); 132 | return state[key].apply(state, arguments); 133 | }; 134 | } 135 | 136 | module.exports = Column; 137 | -------------------------------------------------------------------------------- /lib/cql/column/collection.js: -------------------------------------------------------------------------------- 1 | var Column = require('./basic'); 2 | 3 | /** 4 | * Represents a collection-type column in Cassandra. Example: 5 | * 6 | * // Creates a new map of strings to integers. 7 | * new CollectionColumn('high_scores', 'map', [c.String(), c.Int()]) 8 | * 9 | * You can, of course, nest collections with multiple depth. 10 | * 11 | * @param {String} name Name of the collection. 12 | * @param {String} colType Type of the collection (list, map, set, tuple). 13 | * @param {[]Column} nestedTypes The types nested inside the collection. This 14 | * for lists and sets, this will be length 1. 15 | * For maps, it should be length of 2. 16 | */ 17 | function CollectionColumn (name, colType, nestedTypes) { 18 | Column.call(this, name); 19 | this.isCollection = true; 20 | this.colType = colType; 21 | this.nestedTypes = nestedTypes; 22 | } 23 | 24 | CollectionColumn.prototype = new Column(); 25 | 26 | /** 27 | * Returns the Cassandra type string for the column. 28 | * @return {String} 29 | */ 30 | CollectionColumn.prototype.getType = function () { 31 | var nested = this.nestedTypes 32 | .map(function (type) { 33 | return type.getType(); 34 | }) 35 | .join(', '); 36 | 37 | var type = this.colType + '<' + nested + '>'; 38 | 39 | // Tuples must be frozen as of Cassandra 2.1. 40 | if (this.colType === 'tuple') { 41 | type = 'frozen <' + type + '>'; 42 | } 43 | 44 | return type; 45 | }; 46 | 47 | module.exports = CollectionColumn; 48 | -------------------------------------------------------------------------------- /lib/cql/column/custom.js: -------------------------------------------------------------------------------- 1 | var Column = require('./basic'); 2 | 3 | /** 4 | * Represents a custom-type column in Cassandra. 5 | * 6 | * @param {String} name Name of the collection. 7 | * @param {String} type The name of the custom type. 8 | */ 9 | function CustomColumn (name, type) { 10 | Column.call(this, name, 'frozen <' + type + '>'); 11 | } 12 | 13 | CustomColumn.prototype = new Column(); 14 | 15 | module.exports = CustomColumn; 16 | -------------------------------------------------------------------------------- /lib/cql/column/state/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a "transitional" state that allows us to chain methods onto 3 | * a column to adjust how to queries. 4 | */ 5 | function QueryState (column) { 6 | if (column) { 7 | this.name = column.getName(); 8 | this.type = column.getType(); 9 | } 10 | this.modify = {}; 11 | } 12 | 13 | module.exports = QueryState; 14 | -------------------------------------------------------------------------------- /lib/cql/column/state/index.js: -------------------------------------------------------------------------------- 1 | var cson = require('../../cson'); 2 | 3 | function Index (column, name) { 4 | this.name = name; 5 | this.column = column; 6 | this.settings = { using: null, options: null }; 7 | } 8 | 9 | /** 10 | * Sets the Java class to use for the index. 11 | * @param {String} classname 12 | * @return {Index} 13 | */ 14 | Index.prototype.using = function (classname) { 15 | this.settings.using = classname; 16 | return this; 17 | }; 18 | 19 | /** 20 | * Sets the index options. 21 | * @param {Object} options 22 | * @return {Index} 23 | */ 24 | Index.prototype.options = function (options) { 25 | this.settings.options = options; 26 | return this; 27 | }; 28 | 29 | /** 30 | * Returns a string representation of the index funciton. 31 | * @param {String} table name of the table 32 | * @return {String} 33 | */ 34 | Index.prototype.toString = function (table) { 35 | var output = 'CREATE INDEX'; 36 | // If we gave an index name, add that. 37 | if (this.name) { 38 | output += ' ' + this.name; 39 | } 40 | output += ' ON ' + table + ' (' + this.column.name + ')'; 41 | 42 | if (this.settings.using) { 43 | output += ' USING \'' + this.settings.using + '\''; 44 | } 45 | if (this.settings.options) { 46 | output += ' WITH OPTIONS = ' + cson.encode(this.settings.options); 47 | } 48 | 49 | output += ';'; 50 | 51 | return output; 52 | }; 53 | 54 | module.exports = Index; 55 | -------------------------------------------------------------------------------- /lib/cql/column/state/order.js: -------------------------------------------------------------------------------- 1 | var QueryState = require('./base'); 2 | 3 | /** 4 | * Marks ascending/descending column. 5 | */ 6 | function OrderState() { 7 | QueryState.apply(this, arguments); 8 | } 9 | 10 | OrderState.prototype = new QueryState(); 11 | 12 | /** 13 | * Sets the column to order descending. 14 | * @return {OrderState} 15 | */ 16 | OrderState.prototype.desc = function () { 17 | this.modify.direction = 'DESC'; 18 | return this; 19 | }; 20 | 21 | /** 22 | * Sets the column to order ascending. 23 | * @return {OrderState} 24 | */ 25 | OrderState.prototype.asc = function () { 26 | this.modify.direction = 'ASC'; 27 | return this; 28 | }; 29 | 30 | /** 31 | * Returns the column ordered in the direction. 32 | * @return {String} 33 | */ 34 | OrderState.prototype.toString = function () { 35 | return this.name + ' ' + this.modify.direction; 36 | }; 37 | 38 | module.exports = OrderState; 39 | -------------------------------------------------------------------------------- /lib/cql/column/state/select.js: -------------------------------------------------------------------------------- 1 | var QueryState = require('./base'); 2 | var constants = require('../../constants'); 3 | var util = require('../../../util'); 4 | 5 | /** 6 | * State for SELECT "column" functions. Allows aliasing, TTL, counting, and 7 | * WRITETIME. 8 | */ 9 | function SelectState() { 10 | QueryState.apply(this, arguments); 11 | } 12 | 13 | SelectState.prototype = new QueryState(); 14 | 15 | /** 16 | * Sets the column alias. 17 | * @return {String} 18 | * @return {QueryState} 19 | */ 20 | SelectState.prototype.as = function (alias) { 21 | this.modify.alias = alias; 22 | return this; 23 | }; 24 | 25 | /** 26 | * Sets the query to get the TTL of the column 27 | * @return {QueryState} 28 | */ 29 | SelectState.prototype.ttl = function () { 30 | this.modify.fn = 'TTL'; 31 | return this; 32 | }; 33 | 34 | /** 35 | * Sets the query to get the count of the column. 36 | * @return {QueryState} 37 | */ 38 | SelectState.prototype.count = function () { 39 | this.modify.fn = 'COUNT'; 40 | return this; 41 | }; 42 | 43 | /** 44 | * Sets the query to get the count of the column. 45 | * @return {QueryState} 46 | */ 47 | SelectState.prototype.writeTime = function () { 48 | this.modify.fn = 'WRITETIME'; 49 | return this; 50 | }; 51 | 52 | /** 53 | * Sets the query to SELECT DISTINCT 54 | * @return {QueryState} 55 | */ 56 | SelectState.prototype.distinct = function () { 57 | this.modify.distinct = true; 58 | return this; 59 | }; 60 | /** 61 | * dateOf timeuuid function 62 | * @return {SelectState} 63 | */ 64 | SelectState.prototype.dateOf = function () { 65 | this.modify.fn = 'dateOf'; 66 | return this; 67 | }; 68 | 69 | /** 70 | * minTimeuuid timeuuid function 71 | * @return {SelectState} 72 | */ 73 | SelectState.prototype.minTimeuuid = function () { 74 | this.modify.fn = 'minTimeuuid'; 75 | return this; 76 | }; 77 | 78 | /** 79 | * maxTimeuuid timeuuid function 80 | * @return {SelectState} 81 | */ 82 | SelectState.prototype.maxTimeuuid = function () { 83 | this.modify.fn = 'maxTimeuuid'; 84 | return this; 85 | }; 86 | 87 | /** 88 | * unixTimestampOf timeuuid function 89 | * @return {SelectState} 90 | */ 91 | SelectState.prototype.unixTimestampOf = function () { 92 | this.modify.fn = 'unixTimestampOf'; 93 | return this; 94 | }; 95 | 96 | /** 97 | * token function 98 | * @return {SelectState} 99 | */ 100 | SelectState.prototype.token = function () { 101 | this.modify.fn = 'token'; 102 | return this; 103 | }; 104 | 105 | /** 106 | * Makes the correct typeAsBlob function for the column. 107 | * @return {[type]} [description] 108 | */ 109 | SelectState.prototype.asBlob = function () { 110 | this.modify.fn = this.type + 'AsBlob'; 111 | return this; 112 | }; 113 | 114 | // Bind the blob conversion functions. 115 | constants.baseTypes.forEach(function (type) { 116 | var fn = 'blobAs' + util.capFirst(type.toLowerCase()); 117 | SelectState.prototype[fn] = function () { 118 | this.modify.fn = fn; 119 | return this; 120 | }; 121 | }); 122 | 123 | /** 124 | * Returns the parsed column name. 125 | * @return {String} 126 | */ 127 | SelectState.prototype.toString = function () { 128 | var output = this.name; 129 | if (this.modify.fn) { 130 | output = this.modify.fn + '(' + output + ')'; 131 | } 132 | 133 | if (this.modify.alias) { 134 | output += ' as ' + this.modify.alias; 135 | } 136 | if (this.modify.distinct) { 137 | output = 'DISTINCT ' + output; 138 | } 139 | 140 | return output; 141 | }; 142 | 143 | module.exports = SelectState; 144 | -------------------------------------------------------------------------------- /lib/cql/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lineDelimiter: '\r\n', 3 | indent: ' ', 4 | baseTypes: [ 5 | 'ASCII', 6 | 'BigInt', 7 | 'BLOB', 8 | 'Boolean', 9 | 'Counter', 10 | 'Decimal', 11 | 'Double', 12 | 'Float', 13 | 'IP', 14 | 'Int', 15 | 'Text', 16 | 'Timestamp', 17 | 'TimeUUID', 18 | 'UUID', 19 | 'VarChar', 20 | 'VarInt' 21 | ], 22 | setTypes: [ 23 | 'Tuple', 24 | 'List', 25 | 'Map', 26 | 'Set' 27 | ] 28 | }; 29 | -------------------------------------------------------------------------------- /lib/cql/cson.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | /** 4 | * Parses an object into CQL's simple, not-quite-JSON format. 5 | * 6 | * @param {Object} obj 7 | * @return {String} 8 | */ 9 | function encode (obj) { 10 | if (('' + obj).match(/^[0-9\.]*$/) !== null) { 11 | // Numeric items can be returned directly. 12 | return obj; 13 | } else if (_.isString(obj)) { 14 | // Quote strings. 15 | return '\'' + obj + '\''; 16 | } else { 17 | // And turn objects into CSON. 18 | var output = '{ '; 19 | for (var key in obj) { 20 | output += '\'' + key + '\': \'' + obj[key] + '\' '; 21 | } 22 | output += '}'; 23 | 24 | return output; 25 | } 26 | } 27 | 28 | /** 29 | * Decodes an object into CQL's format. This can be JSON for the "caching" 30 | * property, or the simplistic CSON for the compaction and compression 31 | * properties. 32 | * 33 | * @param {String} obj 34 | * @return {Object} 35 | */ 36 | function decode(str) { 37 | if (str[0] === '\'') { 38 | // JSON-style is quoted in single quotes 39 | return JSON.parse(str.slice(1, -1)); 40 | } else { 41 | // Otherwise, turn it into JSON. 42 | str = str.replace('"', '\"'); 43 | str = str.replace('\'', '"'); 44 | 45 | return JSON.parse(str); 46 | } 47 | } 48 | 49 | 50 | module.exports = { 51 | encode: encode, 52 | decode: decode 53 | }; 54 | -------------------------------------------------------------------------------- /lib/cql/parse/table.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCProHosting/cassandra-orm/2f7164f53cf0ac6247be72da5787425732a6fc7f/lib/cql/parse/table.js -------------------------------------------------------------------------------- /lib/cql/queries/alterTable.js: -------------------------------------------------------------------------------- 1 | var Base = require('./base'); 2 | var Table = require('../table'); 3 | var cson = require('../cson'); 4 | 5 | /** 6 | * An alter table query. Valid usage: 7 | * - AlterTable().table('tbl').type('column', type) 8 | * AlterTable().table('tbl').type(new Text('column')) 9 | * - AlterTable().table('tbl').add('names', 'text') 10 | * AlterTable().table('tbl').add(new Text('names')) 11 | * - AlterTable().table('tbl').drop('column') 12 | * - AlterTable().table('tbl').rename('oldname', 'newname') 13 | * - AlterTable().table('tbl').setProperties({ ... }) 14 | */ 15 | function AlterTable () { 16 | Base.apply(this, arguments); 17 | 18 | this.parts = { 19 | table: null, 20 | operation: [] 21 | }; 22 | } 23 | 24 | AlterTable.prototype = new Base(); 25 | 26 | /** 27 | * Sets the table the query is operating on. 28 | * @param {Table} table 29 | * @return {AlterTable} 30 | */ 31 | AlterTable.prototype.table = function (table) { 32 | if (table instanceof Table) { 33 | this.parts.table = table.getName(); 34 | } else { 35 | this.parts.table = table; 36 | } 37 | 38 | return this; 39 | }; 40 | 41 | /** 42 | * Updates a table column type. Can take a column name and the 43 | * type to change to, or a column object from which it will extract 44 | * the name and the type to update to. 45 | * 46 | * @param {String|Column} column 47 | * @param {String} type 48 | * @return {AlterTable} 49 | */ 50 | AlterTable.prototype.type = function (column, type) { 51 | if (typeof type === 'undefined') { 52 | this.parts.operation = ['ALTER', column.getName(), 'TYPE', column.getType()]; 53 | } else { 54 | this.parts.operation = ['ALTER', column, 'TYPE', type]; 55 | } 56 | 57 | return this; 58 | }; 59 | 60 | /** 61 | * Adds a new column. 62 | * @param {Column} column 63 | * @return {AlterTable} 64 | */ 65 | AlterTable.prototype.add = function (column, type) { 66 | if (typeof type === 'undefined') { 67 | this.parts.operation = ['ADD', column.getName(), column.getType()]; 68 | } else { 69 | this.parts.operation = ['ADD', column, type]; 70 | } 71 | 72 | return this; 73 | }; 74 | 75 | /** 76 | * Removes a column from the table. 77 | * @param {String|Column} column 78 | * @return {AlterTable} 79 | */ 80 | AlterTable.prototype.drop = function (column) { 81 | this.parts.operation = ['DROP', column]; 82 | return this; 83 | }; 84 | 85 | /** 86 | * Renames a column. 87 | * @param {String} oldName 88 | * @param {String} newName 89 | * @return {AlterTable} 90 | */ 91 | AlterTable.prototype.rename = function (oldName, newName) { 92 | this.parts.operation = ['RENAME', oldName, 'TO', newName]; 93 | return this; 94 | }; 95 | 96 | /** 97 | * Updates table properties. 98 | * @param {Object} props 99 | * @return {AlterTable} 100 | */ 101 | AlterTable.prototype.setProperties = function (props) { 102 | var operation = this.parts.operation = ['WITH']; 103 | 104 | Object.keys(props).forEach(function (key, index) { 105 | if (index > 0) { 106 | operation.push('AND'); 107 | } 108 | operation.push(key + ' = ' + cson.encode(props[key])); 109 | }); 110 | 111 | return this; 112 | }; 113 | 114 | /** 115 | * Parameterizes the query, returning an array of parameters followed 116 | * by the string representation. 117 | * 118 | * @return {[Array, String]} 119 | */ 120 | AlterTable.prototype.parameterize = function () { 121 | var parts = ['ALTER TABLE', this.parts.table.toString()].concat(this.parts.operation); 122 | return [[], parts.join(' ') + ';']; 123 | }; 124 | 125 | module.exports = AlterTable; 126 | -------------------------------------------------------------------------------- /lib/cql/queries/base.js: -------------------------------------------------------------------------------- 1 | var util = require('../../util'); 2 | 3 | /** 4 | * Base of a SQL query. 5 | * @param {Cassandra.Client} connection 6 | */ 7 | function Query (connection) { 8 | this.connection = connection; 9 | } 10 | 11 | /** 12 | * Adds a part that implements parameterized() to the query. 13 | * @param {Array} params 14 | * @param {String} part 15 | * @param {String} prefix 16 | */ 17 | Query.prototype.addParameterized = function (params, part, prefix) { 18 | var out = this.parts[part].parameterize(); 19 | if (out[1].length) { 20 | params.push.apply(params, out[0]); 21 | return prefix + out[1]; 22 | } else { 23 | return ''; 24 | } 25 | }; 26 | 27 | /** 28 | * Adds the part with the prefix, if it has a non-zero length 29 | * @param {String} part 30 | * @param {String} prefix 31 | */ 32 | Query.prototype.addPart = function (part, prefix) { 33 | var str = this.parts[part].toString(); 34 | if (str.length > 0) { 35 | return prefix + str; 36 | } else { 37 | return ''; 38 | } 39 | }; 40 | 41 | /** 42 | * Parameterizes the query, returning an array of parameters followed 43 | * by the string representation. 44 | * 45 | * @return {[Array, String]} 46 | */ 47 | Query.prototype.parameterize = function () { 48 | throw new Error('Parameterize must be implemented'); 49 | }; 50 | 51 | /** 52 | * Executes the built query. 53 | * 54 | * @param {Options=} options 55 | * @return {Promise} 56 | */ 57 | Query.prototype.execute = function (options) { 58 | var query = this.parameterize(); 59 | return this.connection.execute(query[1], query[0], options || {}); 60 | }; 61 | 62 | /** 63 | * Executes "eachRow" on the built query. todo: after 0.12 comes along 64 | * we should return a generator function. 65 | * 66 | * @param {Options=} options 67 | * @param {Function} callback 68 | * @return {Promise} 69 | */ 70 | Query.prototype.eachRow = function (options, callback) { 71 | var query = this.parameterize(); 72 | return this.connection.eachRow(query[1], query[0], options || {}, callback); 73 | }; 74 | 75 | /** 76 | * Executes, then returns the promise. 77 | * 78 | * @return {Promise} 79 | */ 80 | Query.prototype.then = function () { 81 | var promise = this.execute(); 82 | return promise.then.apply(promise, arguments); 83 | }; 84 | 85 | /** 86 | * Executes, then returns the promise. 87 | * 88 | * @return {Promise} 89 | */ 90 | Query.prototype.catch = function () { 91 | var promise = this.execute(); 92 | return promise.catch.apply(promise, arguments); 93 | }; 94 | 95 | module.exports = Query; 96 | -------------------------------------------------------------------------------- /lib/cql/queries/delete.js: -------------------------------------------------------------------------------- 1 | var Columns = require('../stmt/columns'); 2 | var Where = require('../stmt/where'); 3 | var Conditionals = require('../stmt/conditionals'); 4 | var Options = require('../stmt/options'); 5 | var Base = require('./base'); 6 | var util = require('../../util'); 7 | 8 | function Delete () { 9 | Base.apply(this, arguments); 10 | 11 | this.parts = { 12 | table: null, 13 | columns: new Columns(), 14 | where: new Where(), 15 | options: new Options(), 16 | conditionals: new Conditionals() 17 | }; 18 | } 19 | 20 | Delete.prototype = new Base(); 21 | 22 | util.proxyMethod(Delete.prototype, 'parts.where.where'); 23 | util.proxyMethod(Delete.prototype, 'parts.where.andWhere'); 24 | util.proxyMethod(Delete.prototype, 'parts.where.orWhere'); 25 | util.proxyMethod(Delete.prototype, 'parts.options.timestamp'); 26 | util.proxyMethod(Delete.prototype, 'parts.options.ttl'); 27 | util.proxyMethod(Delete.prototype, 'parts.conditionals.when'); 28 | util.proxyMethod(Delete.prototype, 'parts.columns.columns'); 29 | 30 | /** 31 | * Sets the table to Delete from. 32 | * @param {String|Table} table 33 | * @return {Delete} 34 | */ 35 | Delete.prototype.from = function (table) { 36 | this.parts.table = util.resolveName(table); 37 | return this; 38 | }; 39 | 40 | /** 41 | * Parameterizes the query, returning an array of parameters followed 42 | * by the string representation. 43 | * 44 | * @return {[Array, String]} 45 | */ 46 | Delete.prototype.parameterize = function () { 47 | var str = ''; 48 | var params = []; 49 | 50 | str += ('DELETE ' + this.parts.columns.toString()).trim() + ' '; 51 | str += 'FROM ' + this.parts.table; 52 | str += this.addPart('options', ' USING '); 53 | str += this.addParameterized(params, 'where', ' WHERE '); 54 | str += this.addParameterized(params, 'conditionals', ' IF '); 55 | 56 | return [params, str + ';']; 57 | }; 58 | 59 | module.exports = Delete; 60 | -------------------------------------------------------------------------------- /lib/cql/queries/insert.js: -------------------------------------------------------------------------------- 1 | var Columns = require('../stmt/columns'); 2 | var Tuple = require('../stmt/termTuple'); 3 | var Options = require('../stmt/options'); 4 | var Base = require('./base'); 5 | var util = require('../../util'); 6 | 7 | function Insert () { 8 | Base.apply(this, arguments); 9 | 10 | this.parts = { 11 | table: null, 12 | columns: new Columns(), 13 | values: new Tuple(), 14 | options: new Options(), 15 | ifNotExists: false 16 | }; 17 | } 18 | 19 | Insert.prototype = new Base(); 20 | 21 | util.proxyMethod(Insert.prototype, 'parts.options.timestamp'); 22 | util.proxyMethod(Insert.prototype, 'parts.options.ttl'); 23 | util.proxyMethod(Insert.prototype, 'parts.columns.columns'); 24 | 25 | /** 26 | * Sets the table to select from. 27 | * @param {String|Table} table 28 | * @return {Select} 29 | */ 30 | Insert.prototype.into = function (table) { 31 | this.parts.table = util.resolveName(table); 32 | return this; 33 | }; 34 | 35 | /** 36 | * Sets the insert values. 37 | * @return {Insert} 38 | */ 39 | Insert.prototype.values = function () { 40 | var tup = new Array(arguments.length); 41 | for (var i = 0, l = arguments.length; i < l; i++) { 42 | tup[i] = arguments[i]; 43 | } 44 | 45 | this.parts.values.setTerms(tup); 46 | return this; 47 | }; 48 | 49 | /** 50 | * Inserts an object column > value map. 51 | * @param {Object} map 52 | * @return {Insert} 53 | */ 54 | Insert.prototype.data = function (map) { 55 | var columns = []; 56 | var values = []; 57 | 58 | for (var key in map) { 59 | columns.push(key); 60 | values.push(map[key]); 61 | } 62 | 63 | this.columns.apply(this, columns); 64 | this.values.apply(this, values); 65 | 66 | return this; 67 | }; 68 | 69 | /** 70 | * Adds the IF NOT EXISTS qualifier. 71 | * @param {Boolean=true} set 72 | * @return {Insert} 73 | */ 74 | Insert.prototype.ifNotExists = function (set) { 75 | this.parts.ifNotExists = typeof set === 'undefined' ? true : set; 76 | return this; 77 | }; 78 | 79 | /** 80 | * Parameterizes the query, returning an array of parameters followed 81 | * by the string representation. 82 | * 83 | * @return {[Array, String]} 84 | */ 85 | Insert.prototype.parameterize = function () { 86 | // Get the values and parameters 87 | var values = this.parts.values.parameterize(); 88 | var params = values[0]; 89 | 90 | // Basic INSERT query 91 | var str = 'INSERT INTO ' + this.parts.table + ' ' + 92 | '(' + this.parts.columns + ') ' + 93 | 'VALUES ' + values[1]; 94 | 95 | // Add IF NOT EXISTS if necessary. 96 | if (this.parts.ifNotExists === true) { 97 | str += ' IF NOT EXISTS'; 98 | } 99 | 100 | // Add in options 101 | var options = this.parts.options.toString(); 102 | if (options.length > 0) { 103 | str += ' USING ' + options; 104 | } 105 | 106 | return [params, str + ';']; 107 | }; 108 | 109 | module.exports = Insert; 110 | -------------------------------------------------------------------------------- /lib/cql/queries/select.js: -------------------------------------------------------------------------------- 1 | var Columns = require('../stmt/columns'); 2 | var Where = require('../stmt/where'); 3 | var Order = require('../stmt/order'); 4 | var Base = require('./base'); 5 | var util = require('../../util'); 6 | 7 | function Select () { 8 | Base.apply(this, arguments); 9 | 10 | this.parts = { 11 | table: null, 12 | columns: new Columns(['*']), 13 | where: new Where(), 14 | order: new Order(), 15 | limit: '', 16 | filter: false 17 | }; 18 | } 19 | 20 | Select.prototype = new Base(); 21 | 22 | util.proxyMethod(Select.prototype, 'parts.where.where'); 23 | util.proxyMethod(Select.prototype, 'parts.where.andWhere'); 24 | util.proxyMethod(Select.prototype, 'parts.where.orWhere'); 25 | util.proxyMethod(Select.prototype, 'parts.order.orderBy'); 26 | util.proxyMethod(Select.prototype, 'parts.columns.columns'); 27 | 28 | /** 29 | * Sets the table to select from. 30 | * @param {String|Table} table 31 | * @return {Select} 32 | */ 33 | Select.prototype.from = function (table) { 34 | this.parts.table = util.resolveName(table); 35 | return this; 36 | }; 37 | 38 | /** 39 | * Limits the query to the number of results. 40 | * @param {Number|String} amt 41 | * @return {Select} 42 | */ 43 | Select.prototype.limit = function (amt) { 44 | this.parts.limit = parseInt(amt, 10); 45 | return this; 46 | }; 47 | 48 | /** 49 | * Turns on (or off) filtering. 50 | * @param {Boolean=true} set 51 | * @return {Select} 52 | */ 53 | Select.prototype.filter = function (set) { 54 | this.parts.filter = typeof set === 'undefined' ? true : set; 55 | return this; 56 | }; 57 | 58 | /** 59 | * Parameterizes the query, returning an array of parameters followed 60 | * by the string representation. 61 | * 62 | * @return {[Array, String]} 63 | */ 64 | Select.prototype.parameterize = function () { 65 | var str = ''; 66 | var params = []; 67 | 68 | str += 'SELECT ' + this.parts.columns.toString() + ' '; 69 | str += 'FROM ' + this.parts.table; 70 | 71 | str += this.addParameterized(params, 'where', ' WHERE '); 72 | str += this.addPart('order', ' ORDER BY '); 73 | str += this.addPart('limit', ' LIMIT '); 74 | 75 | if (this.parts.filter) { 76 | str += ' ALLOW FILTERING'; 77 | } 78 | 79 | return [params, str + ';']; 80 | }; 81 | 82 | module.exports = Select; 83 | -------------------------------------------------------------------------------- /lib/cql/queries/truncate.js: -------------------------------------------------------------------------------- 1 | var Columns = require('../stmt/columns'); 2 | var Where = require('../stmt/where'); 3 | var Conditionals = require('../stmt/conditionals'); 4 | var Options = require('../stmt/options'); 5 | var Base = require('./base'); 6 | var util = require('../../util'); 7 | 8 | function Truncate () { 9 | Base.apply(this, arguments); 10 | 11 | this.parts = { table: null }; 12 | } 13 | 14 | Truncate.prototype = new Base(); 15 | 16 | /** 17 | * Sets the table to truncate. 18 | * @param {String|Table} table 19 | * @return {Truncate} 20 | */ 21 | Truncate.prototype.table = function (table) { 22 | this.parts.table = util.resolveName(table); 23 | return this; 24 | }; 25 | 26 | /** 27 | * Parameterizes the query, returning an array of parameters followed 28 | * by the string representation. 29 | * 30 | * @return {[Array, String]} 31 | */ 32 | Truncate.prototype.parameterize = function () { 33 | return [[], 'TRUNCATE ' + this.parts.table + ';']; 34 | }; 35 | 36 | module.exports = Truncate; 37 | -------------------------------------------------------------------------------- /lib/cql/queries/update.js: -------------------------------------------------------------------------------- 1 | var Assignment = require('../stmt/assignment'); 2 | var Options = require('../stmt/options'); 3 | var Where = require('../stmt/where'); 4 | var Conditionals = require('../stmt/conditionals'); 5 | var Base = require('./base'); 6 | var util = require('../../util'); 7 | 8 | function Update () { 9 | Base.apply(this, arguments); 10 | 11 | this.parts = { 12 | table: null, 13 | assignment: new Assignment(), 14 | options: new Options(), 15 | where: new Where(), 16 | conditionals: new Conditionals() 17 | }; 18 | } 19 | 20 | Update.prototype = new Base(); 21 | 22 | util.proxyMethod(Update.prototype, 'parts.assignment.subtract'); 23 | util.proxyMethod(Update.prototype, 'parts.assignment.add'); 24 | util.proxyMethod(Update.prototype, 'parts.assignment.setRaw'); 25 | util.proxyMethod(Update.prototype, 'parts.assignment.setSimple'); 26 | util.proxyMethod(Update.prototype, 'parts.assignment.setIndex'); 27 | util.proxyMethod(Update.prototype, 'parts.assignment.set'); 28 | util.proxyMethod(Update.prototype, 'parts.where.where'); 29 | util.proxyMethod(Update.prototype, 'parts.where.andWhere'); 30 | util.proxyMethod(Update.prototype, 'parts.where.orWhere'); 31 | util.proxyMethod(Update.prototype, 'parts.conditionals.when'); 32 | util.proxyMethod(Update.prototype, 'parts.options.timestamp'); 33 | util.proxyMethod(Update.prototype, 'parts.options.ttl'); 34 | 35 | /** 36 | * Sets the table to update. 37 | * @param {String|Table} table 38 | * @return {Update} 39 | */ 40 | Update.prototype.table = function (table) { 41 | this.parts.table = util.resolveName(table); 42 | return this; 43 | }; 44 | 45 | /** 46 | * Parameterizes the query, returning an array of parameters followed 47 | * by the string representation. 48 | * 49 | * @return {[Array, String]} 50 | */ 51 | Update.prototype.parameterize = function () { 52 | var str = 'UPDATE ' + this.parts.table; 53 | var params = []; 54 | 55 | // Add in options 56 | str += this.addPart('options', ' USING '); 57 | str += this.addParameterized(params, 'assignment', ' SET '); 58 | str += this.addParameterized(params, 'where', ' WHERE '); 59 | str += this.addParameterized(params, 'conditionals', ' IF '); 60 | 61 | return [params, str + ';']; 62 | }; 63 | 64 | module.exports = Update; 65 | -------------------------------------------------------------------------------- /lib/cql/stmt/assignment.js: -------------------------------------------------------------------------------- 1 | var util = require('../../util'); 2 | 3 | function Assignment () { 4 | this.parameters = []; 5 | this.assigments = []; 6 | } 7 | 8 | /** 9 | * Parameterizes the value if it isn't a raw string. 10 | * @param {String|Raw} value 11 | * @return {String} 12 | */ 13 | Assignment.prototype.resolveValue = function (value) { 14 | if (!util.isRaw(value)) { 15 | this.parameters.push(value); 16 | return '?'; 17 | } else { 18 | return value; 19 | } 20 | }; 21 | 22 | /** 23 | * Adds a list or map item to an existing collection, or a number to 24 | * a counter column. 25 | * 26 | * @param {String|Column} column 27 | * @param {*} value 28 | * @return {Assignment} 29 | */ 30 | Assignment.prototype.subtract = function (column, value) { 31 | this.assigments.push(column + ' = ' + column + ' - ' + this.resolveValue(value)); 32 | return this; 33 | }; 34 | 35 | /** 36 | * Subtracts a list or map item to an existing collection, or a number from 37 | * a counter column. 38 | * 39 | * @param {String|Column} column 40 | * @param {*} value 41 | * @return {Assignment} 42 | */ 43 | Assignment.prototype.add = function (column, value) { 44 | this.assigments.push(column + ' = ' + column + ' + ' + this.resolveValue(value)); 45 | return this; 46 | }; 47 | 48 | /** 49 | * Adds an update: 50 | * 51 | * - single argument: passed in as a raw update 52 | * - two arguments passed: column = value format 53 | * - three arguments: column [index] = value 54 | * 55 | * @param {String} column 56 | * @param {String|*} index 57 | * @param {*} value 58 | * @return {Assignment} 59 | */ 60 | Assignment.prototype.set = function (column, index, value) { 61 | // Just passed in a single string. Only take raw strings to prevent error 62 | // if people pass in input and don't check to see if it's defined. 63 | if (typeof index === 'undefined' && util.isRaw(column)) { 64 | return this.setRaw(column); 65 | } 66 | // Setting a value, not a property. 67 | if (typeof value === 'undefined') { 68 | return this.setSimple(column, index); 69 | } 70 | // Setting a property item. 71 | return this.setIndex(column, index, value); 72 | }; 73 | 74 | /** 75 | * Adds a raw set to the assignment. 76 | * @param {String} str 77 | * @return {Assignment} 78 | */ 79 | Assignment.prototype.setRaw = function (str) { 80 | this.assigments.push(str.toString()); 81 | return this; 82 | }; 83 | 84 | /** 85 | * Adds a raw set to the assignment. 86 | * @param {String|Column} column 87 | * @param {*} value 88 | * @return {Assignment} 89 | */ 90 | Assignment.prototype.setSimple = function (column, value) { 91 | this.assigments.push(column + ' = ' + this.resolveValue(value)); 92 | return this; 93 | }; 94 | 95 | /** 96 | * Sets the column [index] = value in the query. 97 | * @param {String} column 98 | * @param {String|*} index 99 | * @param {*} value 100 | * @return {Assignment} 101 | */ 102 | Assignment.prototype.setIndex = function (column, index, value) { 103 | index = this.resolveValue(index); 104 | value = this.resolveValue(value); 105 | this.assigments.push(column + ' [' + index + '] = ' + value); 106 | return this; 107 | }; 108 | 109 | /** 110 | * Resolves the parameters into a string fragment. 111 | * @return {String} 112 | */ 113 | Assignment.prototype.parameterize = function () { 114 | return [this.parameters, this.assigments.join(', ')]; 115 | }; 116 | 117 | module.exports = Assignment; 118 | -------------------------------------------------------------------------------- /lib/cql/stmt/columns.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a collection of columns. 3 | * @param {Array=} columns 4 | */ 5 | function Columns (columns) { 6 | this._columns = columns || []; 7 | } 8 | 9 | /** 10 | * Sets the columns. Takes an array as its first argument, or columns as 11 | * a variadic arguments. 12 | * @param {Array|String...} columns 13 | * @return {Columns} 14 | */ 15 | Columns.prototype.columns = function (columns) { 16 | if (Array.isArray(columns)) { 17 | this._columns = columns; 18 | } else { 19 | this._columns = new Array(arguments.length); 20 | for (var i = 0, l = arguments.length; i < l; i++) { 21 | this._columns[i] = arguments[i]; 22 | } 23 | } 24 | 25 | return this; 26 | }; 27 | 28 | /** 29 | * Returns a comma-delimited listing of columns 30 | */ 31 | Columns.prototype.toString = function () { 32 | return this._columns.join(', '); 33 | }; 34 | 35 | module.exports = Columns; 36 | -------------------------------------------------------------------------------- /lib/cql/stmt/conditionals.js: -------------------------------------------------------------------------------- 1 | function Conditionals () { 2 | this.statments = {}; 3 | } 4 | 5 | /** 6 | * Adds a statement to the conditional. 7 | * @param {String|Column} key 8 | * @param {*} value 9 | * @return {Conditionals} 10 | */ 11 | Conditionals.prototype.when = function (key, value) { 12 | this.statments[key] = value; 13 | return this; 14 | }; 15 | 16 | /** 17 | * Resolves the conditionals into a string fragment. 18 | * @return {String} 19 | */ 20 | Conditionals.prototype.parameterize = function () { 21 | var params = []; 22 | var output = ''; 23 | for (var key in this.statments) { 24 | if (output.length > 0) { 25 | output += ' AND '; 26 | } 27 | 28 | if (typeof this.statments[key] === 'undefined') { 29 | output += 'EXISTS ' + key; 30 | } else { 31 | output += key + ' = ?'; 32 | params.push(this.statments[key]); 33 | } 34 | } 35 | return [params, output]; 36 | }; 37 | 38 | module.exports = Conditionals; 39 | -------------------------------------------------------------------------------- /lib/cql/stmt/options.js: -------------------------------------------------------------------------------- 1 | function Options () { 2 | this.options = []; 3 | } 4 | 5 | /** 6 | * Adds a timestamp option to the options. 7 | * @param {Number} time 8 | * @return {Options} 9 | */ 10 | Options.prototype.timestamp = function (time) { 11 | this.options.push('TIMESTAMP ' + time); 12 | return this; 13 | }; 14 | 15 | /** 16 | * Adds a ttl option to the options. 17 | * @param {Number} time 18 | * @return {Options} 19 | */ 20 | Options.prototype.ttl = function (time) { 21 | this.options.push('TTL ' + time); 22 | return this; 23 | }; 24 | 25 | /** 26 | * Converts the options to a string suitable for injection into 27 | * a query. 28 | * @return {String} 29 | */ 30 | Options.prototype.toString = function () { 31 | return this.options.join(' AND '); 32 | }; 33 | 34 | module.exports = Options; 35 | -------------------------------------------------------------------------------- /lib/cql/stmt/order.js: -------------------------------------------------------------------------------- 1 | function Order () { 2 | this.rules = []; 3 | } 4 | 5 | /** 6 | * Adds an ordering rule. Takes an OrderState, Column, or RawString as the 7 | * first argument. 8 | * 9 | * @param {OrderState|Column|Raw} column 10 | * @param {String=} direction 11 | * @return {Order} 12 | */ 13 | Order.prototype.orderBy = function (column, direction) { 14 | var stmt = column.toString() + ' ' + (direction || '').toUpperCase(); 15 | this.rules.push(stmt.trim()); 16 | 17 | return this; 18 | }; 19 | 20 | /** 21 | * Joins the ordering rules into a comma-delimited string. 22 | * @return {String} 23 | */ 24 | Order.prototype.toString = function () { 25 | return this.rules.join(', '); 26 | }; 27 | 28 | module.exports = Order; 29 | -------------------------------------------------------------------------------- /lib/cql/stmt/raw.js: -------------------------------------------------------------------------------- 1 | function Raw (text) { 2 | this.text = text; 3 | } 4 | 5 | Raw.prototype.toString = function () { 6 | return this.text; 7 | }; 8 | 9 | module.exports = Raw; 10 | -------------------------------------------------------------------------------- /lib/cql/stmt/termTuple.js: -------------------------------------------------------------------------------- 1 | var util = require('../../util'); 2 | 3 | /** 4 | * Represents a tuple of terms for Cassandra. 5 | */ 6 | function TermTuple () { 7 | this.terms = new Array(arguments.length); 8 | for (var i = 0, l = arguments.length; i < l; i++) { 9 | this.terms[i] = arguments[i]; 10 | } 11 | } 12 | 13 | /** 14 | * SEts an array of terms in the tuple. 15 | * @param {[]*} terms 16 | * @return {TermTuple} 17 | */ 18 | TermTuple.prototype.setTerms = function (terms) { 19 | this.terms = terms; 20 | return this; 21 | }; 22 | 23 | /** 24 | * Joins the tuple into a comma-delimited list. 25 | * @return {String} 26 | */ 27 | TermTuple.prototype.toString = function () { 28 | return '(' + this.terms.join(', ') + ')'; 29 | }; 30 | 31 | /** 32 | * Returns a list of "pulled" values in the tuple and replaces 33 | * them with "?" for querying, the resulting string. 34 | * 35 | * @return {[Array, String]} 36 | */ 37 | TermTuple.prototype.parameterize = function () { 38 | var newTerms = []; 39 | var output = []; 40 | 41 | for (var i = 0, l = this.terms.length; i < l; i++) { 42 | var term = this.terms[i]; 43 | 44 | // If the term is null or undefined, insert NULL. 45 | // Loose comparison intentional. 46 | if (term == null) { // jshint ignore:line 47 | newTerms.push('NULL'); 48 | } 49 | // If we have a nested construct, parameterize it in turn 50 | else if (typeof term.parameterize === 'function') { 51 | var out = term.parameterize(); 52 | output = output.concat(out[0]); 53 | newTerms.push(out[1]); 54 | } 55 | // Insert raw things in a raw way! 56 | else if (util.isRaw(term)) { 57 | newTerms.push(term); 58 | } 59 | // Otherwise just parameterize basic terms. 60 | else { 61 | output.push(term); 62 | newTerms.push('?'); 63 | } 64 | } 65 | 66 | return [output, new TermTuple().setTerms(newTerms).toString()]; 67 | }; 68 | 69 | module.exports = TermTuple; 70 | -------------------------------------------------------------------------------- /lib/cql/stmt/where.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var util = require('../../util'); 3 | 4 | function Where () { 5 | this.statements = []; 6 | this.parameters = []; 7 | } 8 | 9 | /** 10 | * [resolveColumn description] 11 | * @param {[type]} column [description] 12 | * @return {[type]} [description] 13 | */ 14 | Where.prototype.resolveColumn = function (column) { 15 | // If the columns is a list, join into a column list. 16 | if (Array.isArray(column)) { 17 | return '(' + column.join(', ') + ')'; 18 | } else { 19 | return column.toString(); 20 | } 21 | }; 22 | 23 | /** 24 | * Adds a where clause to the table, which takes the following forms: 25 | * 26 | * column_name op term 27 | * | ( column_name, column_name, ... ) op term-tuple 28 | * | column_name IN ( term, ( term ... ) ) 29 | * | ( column_name, column_name, ... ) IN ( term-tuple, ( term-tuple ... ) ) 30 | * 31 | * @param {[]Column|Column} column 32 | * @param {String} operation 33 | * @param {String|[]String} term 34 | * @return {String} 35 | */ 36 | Where.prototype.baseWhere = function (column, operation, term) { 37 | if (typeof operation === 'undefined') { 38 | // If we were passed a function in as the first value, run it 39 | // and add its terms to this statement. 40 | if (typeof column === 'function') { 41 | var where = new Where(); 42 | column(where); 43 | this.statements.push(where.statements); 44 | this.parameters = this.parameters.concat(where.parameters); 45 | return; 46 | } else { 47 | this.statements.push(column.toString()); 48 | return; 49 | } 50 | } 51 | 52 | column = this.resolveColumn(column); 53 | 54 | if (typeof term.parameterize === 'function') { 55 | // Parameterize terms using the function, if possible. 56 | // 57 | var output = term.parameterize(); 58 | this.parameters = this.parameters.concat(output[0]); 59 | term = output[1].toString(); 60 | } else if (!util.isRaw(term)) { 61 | // Or just do it straight up. 62 | this.parameters.push(term); 63 | term = '?'; 64 | } 65 | 66 | this.statements.push([column, operation, term].join(' ')); 67 | }; 68 | 69 | /** 70 | * Adds an "AND" statement to the where. 71 | * @see resolveStatement for args 72 | * @return {Where} 73 | */ 74 | Where.prototype.where = 75 | Where.prototype.andWhere = function () { 76 | if (this.statements.length > 0) { 77 | this.statements.push('AND'); 78 | } 79 | this.baseWhere.apply(this, arguments); 80 | return this; 81 | }; 82 | 83 | /** 84 | * Adds a "WHERE" statement to the where. 85 | * @see resolveStatement for args 86 | * @return {Where} 87 | */ 88 | Where.prototype.orWhere = function () { 89 | if (this.statements.length > 0) { 90 | this.statements.push('OR'); 91 | } 92 | this.baseWhere.apply(this, arguments); 93 | return this; 94 | }; 95 | 96 | /** 97 | * Resolves the WHERE into a string fragment. 98 | * @return {String} 99 | */ 100 | Where.prototype.parameterize = function () { 101 | return [this.parameters, this.statements.map(resolveStatement).join(' ')]; 102 | }; 103 | 104 | /** 105 | * If the statment is an array, returns a comma-delimited list 106 | * surrounded by parenthesis. Otherwise, just returns it. 107 | * @param {String|[]String} statement 108 | * @return {String} 109 | */ 110 | function resolveStatement (statement) { 111 | if (Array.isArray(statement)) { 112 | return '(' + statement.join(' ') + ')'; 113 | } else { 114 | return statement; 115 | } 116 | } 117 | 118 | 119 | module.exports = Where; 120 | -------------------------------------------------------------------------------- /lib/cql/table.js: -------------------------------------------------------------------------------- 1 | var constants = require('./constants'); 2 | var cson = require('./cson'); 3 | 4 | function Table (name, connection) { 5 | this.name = name; 6 | this.connection = connection; 7 | 8 | this.columns = []; 9 | this.properties = []; 10 | this.keys = { partition: [], compound: [] }; 11 | } 12 | 13 | /** 14 | * Returns the table name 15 | * 16 | * @return {String} 17 | */ 18 | Table.prototype.getName = function () { 19 | return this.name; 20 | }; 21 | 22 | /** 23 | * Sets the table name 24 | * 25 | * @return {Table} 26 | */ 27 | Table.prototype.setName = function (name) { 28 | this.name = name; 29 | return this; 30 | }; 31 | 32 | /** 33 | * Adds a column to the table. 34 | * 35 | * @param {Column} column 36 | * @return {Table} 37 | */ 38 | Table.prototype.addColumn = function (column) { 39 | this.columns.push(column); 40 | 41 | if (column.isKey.partition) { 42 | this.addPartitionKey(column); 43 | } else if (column.isKey.compound) { 44 | this.addCompoundKey(column); 45 | } 46 | 47 | return this; 48 | }; 49 | 50 | /** 51 | * Gets the primary keys from the table. 52 | * @return {[]String} 53 | */ 54 | Table.prototype.getKeys = function () { 55 | return this.primaryKeys; 56 | }; 57 | 58 | /** 59 | * Adds a new partition key... 60 | * @param {String} key 61 | * @return {Table} 62 | */ 63 | Table.prototype.addPartitionKey = function (key) { 64 | this.keys.partition.push(key); 65 | return this; 66 | }; 67 | 68 | /** 69 | * Adds a new compound key on the table. 70 | * @param {String} key 71 | * @return {Table} 72 | */ 73 | Table.prototype.addCompoundKey = function (key) { 74 | this.keys.compound.push(key); 75 | return this; 76 | }; 77 | 78 | /** 79 | * Adds a property to the table. You can pass in "key" as a plain string, 80 | * or key and value which will be added like `key = JSON.encode(value)` which 81 | * seems to work well for Cassandra's selection of table properties. 82 | * 83 | * @param {String} key 84 | * @param {String=} value 85 | */ 86 | Table.prototype.addProperty = function (key, value) { 87 | if (typeof value === 'undefined') { 88 | this.properties.push(key); 89 | } else if (key === 'caching') { 90 | // Caching seems to use quoted JSON, unlike other properties. 91 | this.properties.push(key + '=\'' + JSON.stringify(value) + '\''); 92 | } else { 93 | this.properties.push(key + '=' + cson.encode(value)); 94 | } 95 | 96 | return this; 97 | }; 98 | 99 | /** 100 | * Creates the CQL representation of the table (CREATE TABLE). 101 | * @return {String} 102 | */ 103 | Table.prototype.toString = function () { 104 | var tableName = this.name; 105 | var output = ['CREATE TABLE ' + tableName + ' (']; 106 | 107 | // Add each column to the statement. 108 | this.columns.forEach(function (column) { 109 | output.push(constants.indent + column.getEntry() + ','); 110 | }); 111 | 112 | var keys = this.keys; 113 | if (keys.partition.length > 0 || keys.compound.length > 0) { 114 | // Add the primary keys if we have any. 115 | 116 | var keyStrs = this.keys.compound; 117 | if (keys.partition.length > 1) { 118 | keyStrs.unshift('(' + keys.partition.join(', ') + ')'); 119 | } else if (keys.partition.length === 1) { 120 | keyStrs.unshift(keys.partition[0]); 121 | } 122 | 123 | output.push( 124 | constants.indent + 125 | 'PRIMARY KEY ' + 126 | '(' + keyStrs.join(', ') + ')' 127 | ); 128 | } else if (this.columns.length > 0) { 129 | // Trim off the trailing comma of the last column. 130 | output[output.length - 1] = output[output.length - 1].slice(0, -1); 131 | } 132 | 133 | // If we defined table properties, add those. 134 | if (this.properties.length > 0) { 135 | output.push(') WITH ' + this.properties.join( 136 | ' AND' + 137 | constants.lineDelimiter + 138 | constants.indent 139 | ) + ';'); 140 | } else { 141 | output.push(');'); 142 | } 143 | 144 | // Check for column indexes 145 | this.columns.forEach(function (column) { 146 | if (column.columnIndex) { 147 | output.push(column.columnIndex.toString(tableName)); 148 | } 149 | }); 150 | 151 | // And we're done! 152 | return output.join(constants.lineDelimiter); 153 | }; 154 | 155 | module.exports = Table; 156 | -------------------------------------------------------------------------------- /lib/cql/typecast/cast.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var InvalidTypeError = require('../../errors').InvalidType; 3 | /** 4 | * Typecasts the single `value` to fit the type according to 5 | * http://www.datastax.com/documentation/developer/nodejs-driver/1.0/nodejs-driver/reference/nodejs2Cql3Datatypes.html 6 | * 7 | * @param {*} value 8 | * @return {*} 9 | */ 10 | function castValue (type, value) { 11 | if (typeof value === 'undefined' || value === null) { 12 | return value; 13 | } 14 | 15 | switch (type) { 16 | case 'ascii': 17 | case 'text': 18 | case 'timeuuid': 19 | case 'uuid': 20 | case 'varchar': 21 | return '' + value; 22 | case 'bigint': 23 | case 'counter': 24 | return new cassandra.types.Long.fromString('' + value); 25 | case 'blob': 26 | case 'decimal': 27 | case 'inet': 28 | case 'varint': 29 | return Buffer.isBuffer(value) ? value : new Buffer(value); 30 | case 'boolean': 31 | return !!value; 32 | case 'double': 33 | case 'float': 34 | return parseFloat(value, 10); 35 | case 'int': 36 | return parseInt(value, 10); 37 | case 'timestamp': 38 | if (_.isDate(value)) { 39 | return value; 40 | } else if (typeof value === 'number') { 41 | return new Date(value); 42 | } else if (typeof value.toDate === 'function') { 43 | return value.toDate(); 44 | } else { 45 | return NaN; 46 | } 47 | break; 48 | default: 49 | return value; 50 | 51 | } 52 | } 53 | 54 | /** 55 | * Tries to case the value to a type, throwing an error if it fails. 56 | * @param {Column} Column 57 | * @param {*} 58 | * @return {*} 59 | */ 60 | function castValueOrFail (column, type, value) { 61 | var out = castValue(type, value); 62 | if (_.isNaN(out)) { 63 | throw new InvalidTypeError(column.getName(), value); 64 | } 65 | 66 | return out; 67 | } 68 | 69 | /** 70 | * Casts all values in a collection. 71 | * @return {*} 72 | */ 73 | function castCollection (column, obj) { 74 | var nest = column.nestedTypes; 75 | var out; 76 | 77 | switch (column.colType) { 78 | case 'list': 79 | case 'set': 80 | // If the value isn't an array, throw an error. 81 | // Otherwise map over the values and cast them. 82 | if (!Array.isArray(obj)) { 83 | throw new InvalidTypeError(column.getName(). obj); 84 | } else { 85 | return obj.map(castValueOrFail.bind(null, column, nest[0])); 86 | } 87 | break; 88 | case 'map': 89 | // For maps (objects), loop over and cast both the object 90 | // keys and its values. 91 | out = {}; 92 | for (var key in obj) { 93 | if (!obj.hasOwnProperty(key)) { 94 | continue; 95 | } 96 | 97 | out[castValueOrFail(column, nest[0], key)] = castValueOrFail(column, nest[1], obj[key]); 98 | } 99 | return out; 100 | 101 | case 'tuple': 102 | // For tuples, make sure the column and target value is 103 | // of equal length, then go and cast each tuple item. 104 | if (!Array.isArray(obj) || obj.length !== nest.length) { 105 | throw new InvalidTypeError(column.getName(), obj); 106 | } 107 | out = new Array(nest.length); 108 | for (var i = 0, l = obj.length; i < l; i++) { 109 | out[i] = castValueOrFail(column, nest[i], obj[i]); 110 | } 111 | 112 | return out; 113 | 114 | default: return obj; 115 | } 116 | } 117 | 118 | /** 119 | * Generic function to case a value to match a column's type. Throws 120 | * an `invalid` error if the value doesn't fit. 121 | * 122 | * @param {Column} column 123 | * @param {*} 124 | * @return {*} 125 | */ 126 | function cast (column, value) { 127 | if (typeof value === 'undefined' || value === null) { 128 | return null; 129 | } 130 | 131 | if (column.isCollection) { 132 | return castCollection(column, value); 133 | } else { 134 | return castValueOrFail(column, column.getType(), value); 135 | } 136 | } 137 | 138 | module.exports = cast; 139 | -------------------------------------------------------------------------------- /lib/cql/types.js: -------------------------------------------------------------------------------- 1 | var BasicColumn = require('./column/basic'); 2 | var CollectionColumn = require('./column/collection'); 3 | var constants = require('./constants'); 4 | 5 | module.exports = {}; 6 | 7 | // Define basic types. 8 | constants.baseTypes.forEach(function (type) { 9 | module.exports[type] = function (name) { 10 | return new BasicColumn(name, type.toLowerCase()); 11 | }; 12 | }); 13 | 14 | // Collection types. 15 | constants.setTypes.forEach(function (type) { 16 | module.exports[type] = function (name, nested) { 17 | return new CollectionColumn(name, type.toLowerCase(), nested); 18 | }; 19 | }); 20 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | function InvalidTypeError (column) { 4 | this.column = column; 5 | } 6 | util.inherits(InvalidTypeError, Error); 7 | 8 | module.exports.InvalidType = InvalidTypeError; 9 | -------------------------------------------------------------------------------- /lib/model/collection.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var _ = require('lodash'); 3 | 4 | var Table = require('../cql/table'); 5 | var Column = require('../cql/column/basic'); 6 | var Model = require('./model'); 7 | var util = require('../util'); 8 | var cast = require('../cql/typecast/cast'); 9 | 10 | /** 11 | * Represents a "table" in the database. Allows you to perform operations 12 | * against the whole of the database set. 13 | * 14 | * @param {Connection} connection 15 | * @param {String} name 16 | */ 17 | function Collection (connection, name) { 18 | this.connection = connection; 19 | this.name = name; 20 | this.table = new Table(util.toSnakeCase(name), connection); 21 | this.props = { private: { getters: {}, setters: {} }, public: {} }; 22 | this.middleware = {}; 23 | } 24 | 25 | /** 26 | * Typecasts one value by its column or name. 27 | * @param {String|Column} column 28 | * @param {*} 29 | * @return {*} 30 | */ 31 | Collection.prototype.typecast = function (column, value) { 32 | if (typeof column === 'string') { 33 | for (var i = 0, l = this.columns.length; i < l; i++) { 34 | var col = this.table.columns[i]; 35 | 36 | if (col.toString() === column) { 37 | return cast(col, value); 38 | } 39 | } 40 | throw new Error('Column ' + column + ' not found on table.'); 41 | } else { 42 | return cast(column, value); 43 | } 44 | }; 45 | 46 | /** 47 | * Sets the models columns. 48 | * 49 | * @param {[]Column} columns 50 | * @return {Collection} 51 | */ 52 | Collection.prototype.columns = function (columns) { 53 | var self = this; 54 | columns.forEach(function (column) { 55 | // If it's not a collumn, we were probably chaining indexes 56 | // or other methods on. Expect there to be a nested column. 57 | if (!(column instanceof Column)) { 58 | // Throw an error if that expectation was wrong. 59 | if (!(column.column instanceof Column)) { 60 | throw new Error( 61 | 'You must either pass columns to the collection, ' + 62 | 'or instances that have a property column.' 63 | ); 64 | } 65 | 66 | column = column.column; 67 | } 68 | 69 | self[util.toStudlyCase(column.getName())] = column; 70 | self.table.addColumn(column); 71 | }); 72 | 73 | return this; 74 | }; 75 | 76 | /** 77 | * Defines a "getter" function on the collection. 78 | * 79 | * @param {String|Column} 80 | * @param {Function} 81 | * @return {Collection} 82 | */ 83 | Collection.prototype.getter = function (column, fn) { 84 | this.props.private.getters[column.toString()] = fn; 85 | return this; 86 | }; 87 | 88 | /** 89 | * Defines a "setter" function on the collection. 90 | * 91 | * @param {String|Column} 92 | * @param {Function} 93 | * @return {Collection} 94 | */ 95 | Collection.prototype.setter = function (column, fn) { 96 | this.props.private.setters[column.toString()] = fn; 97 | return this; 98 | }; 99 | 100 | /** 101 | * Starts a select query on the collection. 102 | * @return {SelectQuery} 103 | */ 104 | Collection.prototype.select = function () { 105 | var self = this; 106 | var query = this.connection.select(); 107 | var oldExecute = query.execute; 108 | 109 | query.execute = function () { 110 | return oldExecute 111 | .apply(query, arguments) 112 | .then(function (results) { 113 | var records; 114 | if (results.rows) { 115 | records = results.rows.map(function (record) { 116 | return self.new().sync(record); 117 | }); 118 | } 119 | 120 | return _.extend(records, results); 121 | }); 122 | }; 123 | 124 | return query.from(this.table.getName()); 125 | }; 126 | 127 | /** 128 | * Starts an insert query on the collection. 129 | * @return {InsertQuery} 130 | */ 131 | Collection.prototype.insert = function () { 132 | return this.connection.insert().into(this.table.getName()); 133 | }; 134 | 135 | /** 136 | * Starts an delete query on the collection. 137 | * @return {DeleteQuery} 138 | */ 139 | Collection.prototype.delete = function () { 140 | return this.connection.delete().from(this.table.getName()); 141 | }; 142 | 143 | /** 144 | * Starts an update query on the collection. 145 | * @return {UpdateQuery} 146 | */ 147 | Collection.prototype.update = function () { 148 | return this.connection.update().table(this.table.getName()); 149 | }; 150 | 151 | /** 152 | * Truncates data in the collection's table. 153 | * @return {Promise} 154 | */ 155 | Collection.prototype.truncate = function () { 156 | return this.connection.truncate().table(this.table.getName()); 157 | }; 158 | 159 | /** 160 | * Checks out a new Model from the collection. 161 | * 162 | * @param {Object=} attributes 163 | * @return {Model} 164 | */ 165 | Collection.prototype.new = function (attributes) { 166 | return new Model(this, attributes, this.props); 167 | }; 168 | 169 | /** 170 | * Adds a new property to be registered on the object. 171 | * @param {String} name 172 | * @param {*} value 173 | * @param {String=public} accessibility 174 | * @return {Collection} 175 | */ 176 | Collection.prototype.define = function (name, value, accessiblity) { 177 | this.props[accessiblity || 'public'][name] = value; 178 | return this; 179 | }; 180 | 181 | /** 182 | * Adds a middleware to the specified stack. 183 | * @param {String|[]String} stack 184 | * @param {Function} handler 185 | * @return {Collection} 186 | */ 187 | Collection.prototype.use = function (stack, handler) { 188 | // If it's an array, loop it over. 189 | if (Array.isArray(stack)) { 190 | for (var i = 0, l = stack.length; i < l; i++) { 191 | this.use(stack[i], handler); 192 | } 193 | return; 194 | } 195 | 196 | var middleware = this.middleware[stack]; 197 | if (typeof middleware === 'undefined') { 198 | middleware = this.middleware[stack] = []; 199 | } 200 | 201 | middleware.push(handler); 202 | }; 203 | 204 | /** 205 | * Runs the middleware stack, calling "then" when it's complete. 206 | * 207 | * @param {String} stack 208 | * @oaram {*} context 209 | * @param {Function} then 210 | * @return {Promise} 211 | */ 212 | Collection.prototype.run = function (stack, context) { 213 | var middleware = this.middleware[stack] || []; 214 | 215 | return new Bluebird(function (resolve, reject) { 216 | runStack(context, middleware, resolve, reject); 217 | }); 218 | }; 219 | 220 | /** 221 | * Helper function to run a middleware stack. 222 | * 223 | * @param {[]Function} middleware 224 | * @param {Object} context 225 | * @param {Function} resolve 226 | * @param {Function} reject 227 | */ 228 | function runStack (context, middleware, resolve, reject, err) { 229 | if (err) { 230 | return reject(err); 231 | } 232 | if (middleware.length === 0) { 233 | return resolve(); 234 | } 235 | 236 | return middleware[0].call(context, function (err) { 237 | runStack(context, middleware.slice(1), resolve, reject, err); 238 | }); 239 | } 240 | 241 | module.exports = Collection; 242 | -------------------------------------------------------------------------------- /lib/model/diff.js: -------------------------------------------------------------------------------- 1 | var eq = require('deep-equal'); 2 | var arrayDiff = require('array-diff')(); 3 | 4 | /** 5 | * Updates a set-type column. 6 | * 7 | * @param {UpdateQuery} builder 8 | * @param {String} key 9 | * @param {Object} left 10 | * @param {Object} right 11 | */ 12 | function diffSet (query, column, left, right) { 13 | // Make a copy of the right items. 14 | var additions = right.slice(); 15 | var subtractions = []; 16 | 17 | // Go through every left item. Mark items that were deleted. 18 | var i, l; 19 | for (i = 0, l = left.length; i < l; i++) { 20 | var value = left[i]; 21 | var index = additions.indexOf(value); 22 | 23 | if (index === -1) { 24 | // Remove the item if it's no longer present. 25 | subtractions.push(value); 26 | } else { 27 | // If it is, remove it from the "additions" 28 | additions.splice(index, 1); 29 | } 30 | } 31 | 32 | // If we updated most every value, just rewrite it. 33 | // todo: actual benchmarks here 34 | if (additions.length + subtractions.length > left.length * 0.75) { 35 | return query.setSimple(column, right); 36 | } 37 | 38 | // Now add/remove the necessary. 39 | if (additions.length) { 40 | query.add(column, additions); 41 | } 42 | if (subtractions.length) { 43 | query.subtract(column, subtractions); 44 | } 45 | } 46 | 47 | /** 48 | * Updates a map-type column. 49 | * 50 | * @param {UpdateQuery} builder 51 | * @param {String} key 52 | * @param {Object} left 53 | * @param {Object} right 54 | */ 55 | function diffMap (query, column, left, right) { 56 | var removed = []; 57 | var updates = {}; 58 | var updated = false; 59 | 60 | var key; 61 | // Look through all the keys in the db. 62 | for (key in left) { 63 | // Remove it if it's no longer present. 64 | if (typeof right[key] === 'undefined') { 65 | removed.push(key); 66 | } 67 | // Or update it if it's different. 68 | else if (!eq(right[key], left[key])) { 69 | updated = true; 70 | updates[key] = right[key]; 71 | } 72 | } 73 | // Then loop back and see what keys we need to add. 74 | for (key in right) { 75 | if (typeof left[key] === 'undefined') { 76 | updated = true; 77 | updates[key] = right[key]; 78 | } 79 | } 80 | 81 | // Update things and remove other things 82 | if (updated) { 83 | query.add(column, updates); 84 | } 85 | if (removed.length) { 86 | query.subtract(column, removed); 87 | } 88 | } 89 | 90 | /** 91 | * Updates a list-type column. This is designed to tend to safeness 92 | * rather than towards highest performance. 93 | * 94 | * That is, internal edits within the list will result in the list being 95 | * rewritten rather than that index being edited, as it is very possible 96 | * that the list order has shifted or it has been rewritten since the 97 | * last update. 98 | * 99 | * @param {UpdateQuery} builder 100 | * @param {String} key 101 | * @param {Object} left 102 | * @param {Object} right 103 | */ 104 | function diffList (query, column, left, right) { 105 | var diff = arrayDiff(left, right); 106 | var i, l; 107 | 108 | // Check out the deletions in the array. If all of one value was 109 | // removed, we can .subtract, but if there were duplicates and 110 | // only one instance was removed, we must rewrite the list. 111 | var deletions = []; 112 | for (i = 0, l = diff.length; i < l; i++) { 113 | var deleted = deletions.indexOf(diff[i][1]); 114 | // If we haven't seen this deleted item before, add it. 115 | if (diff[i][0] === '-' && deleted === -1) { 116 | deletions.push(diff[i][1]); 117 | } 118 | // But if we did see it before and this time it's not deleted, 119 | // rewrite. 120 | else if (diff[i][0] !== '-' && deleted !== -1) { 121 | return query.setSimple(column, right); 122 | } 123 | } 124 | // And look over the array backwards to get deletions at the end. 125 | // Perhaps not incredibly graceful, but it is O(n) rather than O(n^2). 126 | // At the same time, look for elements that were appended to the array. 127 | var appends = []; 128 | var isAppending = true; 129 | for (i = diff.length - 1; i >= 0; i--) { 130 | // If it appears in deleted but is not marked for deletion, it's 131 | // a duplicate and we should rewrite. 132 | if (diff[i][0] !== '-' && deletions.indexOf(diff[i][1]) !== -1) { 133 | return query.setSimple(column, right); 134 | } 135 | 136 | if (isAppending) { 137 | if (diff[i][0] === '+') { 138 | appends.push(diff[i][1]); 139 | } else { 140 | isAppending = false; 141 | } 142 | } 143 | } 144 | // Reverse it (we looked over the list backwards) 145 | appends = appends.reverse(); 146 | 147 | // Now loop for prepends and watch for internal additions within the 148 | // array. If any do occur, we rewrite the array (see docstring) 149 | var prepends = []; 150 | var isPrepending = true; 151 | for (i = 0, l = diff.length - appends.length; i < l; i++) { 152 | if (isPrepending) { 153 | if (diff[i][0] === '+') { 154 | prepends.push(diff[i][1]); 155 | } else { 156 | isPrepending = false; 157 | } 158 | } 159 | // not an "else" statement so we run this check on the iteration 160 | // we switch isPrepending to be false 161 | if (!isPrepending && diff[i][0] === '+') { 162 | return query.setSimple(column, right); 163 | } 164 | } 165 | // Reverse the prepends 166 | prepends = prepends.reverse(); 167 | 168 | // If we updated most every value, just rewrite it. 169 | // todo: actual benchmarks here 170 | if (deletions.length + appends.length + prepends.length > left.length * 0.75) { 171 | return query.setSimple(column, right); 172 | } 173 | 174 | // At this point, we're good, so run all the updates! 175 | if (deletions.length) { 176 | query.subtract(column, deletions); 177 | } 178 | if (appends.length) { 179 | query.add(column, appends); 180 | } 181 | if (prepends.length) { 182 | query.add(prepends, column); 183 | } 184 | } 185 | 186 | /** 187 | * Adds updates to the query builder necessary to update the "old" 188 | * attribute to match the "new". 189 | * 190 | * @param {UpdateQuery} builder 191 | * @param {String} key 192 | * @param {Object} left 193 | * @param {Object} right 194 | */ 195 | module.exports = function (query, column, left, right) { 196 | // If they're deeply equal, do nothing. 197 | if (eq(left, right)) { 198 | return; 199 | } 200 | // If one of the columns is null, it's a simple set. We're 201 | // either setting from null to something (no diff to make) or 202 | // making something equal null. 203 | else if (left === null || right === null) { 204 | query.setSimple(column, right); 205 | } 206 | // For 'set' collections... 207 | else if (column.isCollection && column.colType === 'set') { 208 | diffSet(query, column, left, right); 209 | } 210 | // For 'list' collections... 211 | else if (column.isCollection && column.colType === 'list') { 212 | diffList(query, column, left, right); 213 | } 214 | // For 'map' collections... 215 | else if (column.isCollection && column.colType === 'map') { 216 | diffMap(query, column, left, right); 217 | } 218 | // If they're dates... 219 | else if (left instanceof Date && right instanceof Date) { 220 | query.setSimple(column, right); 221 | } 222 | // Or just set it simply 223 | else { 224 | query.setSimple(column, right); 225 | } 226 | }; 227 | -------------------------------------------------------------------------------- /lib/model/model.js: -------------------------------------------------------------------------------- 1 | var Bluebird = require('bluebird'); 2 | var eq = require('deep-equal'); 3 | var _ = require('lodash'); 4 | 5 | var diff = require('./diff'); 6 | 7 | function Model (collection, attributes, props) { 8 | this._def('_', _.extend({ 9 | collection: collection, 10 | old: {}, 11 | attributes: {}, 12 | columns: collection.table.columns, 13 | isFromDb: false, 14 | accessored: [] 15 | }, props.private)); 16 | 17 | for (var key in props.public) { 18 | this._def(key, props.public[key]); 19 | } 20 | 21 | this.bindAccessors(); 22 | _.extend(this, attributes); 23 | } 24 | 25 | /** 26 | * Binds setters and getters for model columns. We only add accessords 27 | * for columns 28 | */ 29 | Model.prototype.bindAccessors = function () { 30 | var properties = {}; 31 | 32 | this._.accessored = _.union(_.keys(this._.getters), _.keys(this._.setters)); 33 | 34 | _.forEach(this._.accessored, function (key) { 35 | var getter = this._.getters[key]; 36 | var setter = this._.setters[key]; 37 | 38 | properties[key] = { 39 | configurable: true, 40 | enumerable: true, 41 | get: function () { 42 | if (typeof getter === 'undefined') { 43 | return this._.attributes[key]; 44 | } else { 45 | return getter.call(this, this._.attributes[key]); 46 | } 47 | }, 48 | set: function (value) { 49 | if (typeof setter === 'undefined') { 50 | this._.attributes[key] = value; 51 | } else { 52 | this._.attributes[key] = setter.call(this, value); 53 | } 54 | } 55 | }; 56 | }, this); 57 | 58 | if (this._.accessored.length > 0) { 59 | Object.defineProperties(this, properties); 60 | } 61 | }; 62 | 63 | /** 64 | * Defines a new non-enumerable property on the model. 65 | * 66 | * @param {String} name 67 | * @param {*} value 68 | */ 69 | Model.prototype._def = function (name, value) { 70 | Object.defineProperty(this, name, { 71 | configurable: true, 72 | writable: true, 73 | enumerable: false, 74 | value: value 75 | }); 76 | }; 77 | 78 | /** 79 | * Wipes the sync state and attributes. Mainly for testing ease, but meh, 80 | * you might find this useful too. 81 | */ 82 | Model.prototype.reset = function () { 83 | this._.old = {}; 84 | this._.isFromDb = false; 85 | }; 86 | 87 | /** 88 | * Fixes the casing of a column name to match the model definition, since 89 | * Cassandra columns are case-insensitive but attributes are sensitive. 90 | * @param {String} column 91 | * @return {String} 92 | */ 93 | Model.prototype.fixCasing = function (column) { 94 | for (var i = 0, l = this._.columns.length; i < l; i++) { 95 | var name = this._.columns[i].toString(); 96 | if (name.toLowerCase() === column.toLowerCase()) { 97 | return name; 98 | } 99 | } 100 | 101 | return column; 102 | }; 103 | 104 | /** 105 | * Updates the "old" and existing properties. Should be called after the 106 | * model is updated or read from the database. 107 | * 108 | * @param {Object} attributes 109 | * @return {Model} 110 | */ 111 | Model.prototype.sync = function (attributes) { 112 | this._.isFromDb = true; 113 | 114 | var self = this; 115 | Object.keys(attributes).forEach(function (key) { 116 | var value = attributes[key]; 117 | var name = self.fixCasing(key); 118 | 119 | if (self._.accessored.indexOf(name) === -1) { 120 | self[name] = _.cloneDeep(value); 121 | } 122 | 123 | self._.attributes[name] = _.cloneDeep(value); 124 | self._.old[name] = _.cloneDeep(value); 125 | }); 126 | 127 | return this; 128 | }; 129 | 130 | /** 131 | * Extends this model's data with anything passed in the arguments. Functions 132 | * in the same way to lodash's "extend". 133 | * @return {Model} 134 | */ 135 | Model.prototype.extend = function () { 136 | var args = new Array(arguments.length + 1); 137 | var data = args[0] = {}; 138 | 139 | var i, l; 140 | for (i = 0, l = arguments.length; i < l; i++) { 141 | args[i + 1] = arguments[i]; 142 | } 143 | 144 | // Compact all the arguments to our single "data" argument 145 | _.extend.apply(null, args); 146 | 147 | // Then apply those over this model. 148 | for (i = 0, l = this._.columns.length; i < l; i++) { 149 | var column = this._.columns[i].toString(); 150 | var item = data[column]; 151 | if (typeof item !== 'undefined') { 152 | this[column] = item; 153 | } 154 | } 155 | 156 | return this; 157 | }; 158 | 159 | /** 160 | * Iterates over the model's columns, typecasting each one and updating 161 | * the "attributes" for columns without getters/setters. 162 | */ 163 | Model.prototype._fixAttributes = function () { 164 | for (var i = 0, l = this._.columns.length; i < l; i++) { 165 | var column = this._.columns[i]; 166 | var name = column.getName(); 167 | var value; 168 | 169 | if (this._.accessored.indexOf(name) !== -1) { 170 | value = this._.attributes[name]; 171 | this._.attributes[name] = this._.collection.typecast(column, value); 172 | } else { 173 | value = this._.collection.typecast(column, this[name]); 174 | this._.attributes[name] = value; 175 | this[name] = value; 176 | } 177 | } 178 | }; 179 | 180 | /** 181 | * Returns whether the property has changed since it was synced to the 182 | * database. 183 | * @param {String} property 184 | * @return {Boolean} 185 | */ 186 | Model.prototype.isDirty = function (property) { 187 | this._fixAttributes(); 188 | 189 | return !eq(this._.attributes[property], this._.old[property]); 190 | }; 191 | 192 | /** 193 | * Returns true if every property is synced with the db. False if any 194 | * are different. 195 | * 196 | * @return {Boolean} 197 | */ 198 | Model.prototype.isSynced = function () { 199 | for (var i = 0, l = this._.columns.length; i < l; i++) { 200 | if (this.isDirty(this._.columns[i].toString())) { 201 | return false; 202 | } 203 | } 204 | 205 | return true; 206 | }; 207 | 208 | /** 209 | * Converts the model to a plain object 210 | * 211 | * @param {Boolean} useGetters 212 | * @return {Object} 213 | */ 214 | Model.prototype.toObject = function (useGetters) { 215 | this._fixAttributes(); 216 | 217 | var output = {}; 218 | for (var i = 0, l = this._.columns.length; i < l; i++) { 219 | var column = this._.columns[i].toString(); 220 | if (typeof useGetters === 'undefined' || useGetters === true) { 221 | output[column] = this[column]; 222 | } else { 223 | output[column] = this._.attributes[column]; 224 | } 225 | } 226 | 227 | return output; 228 | }; 229 | 230 | /** 231 | * Converts the record to a JSON representation. 232 | * 233 | * @return {String} 234 | */ 235 | Model.prototype.toJson = function () { 236 | return JSON.stringify(this.toObject()); 237 | }; 238 | 239 | /** 240 | * Persists the model to the database. 241 | * 242 | * @param {Object={}} options 243 | * @param {Boolean=false} force 244 | * @return {Promise} 245 | */ 246 | Model.prototype.save = function (options, force) { 247 | if (typeof options === 'boolean') { 248 | force = options; 249 | options = {}; 250 | } 251 | 252 | if (!force && this.isSynced()) { 253 | return Bluebird.resolve(this); 254 | } 255 | 256 | this._fixAttributes(); 257 | if (this._.isFromDb) { 258 | return this._updateExisting(options); 259 | } else { 260 | return this._createNew(options); 261 | } 262 | }; 263 | 264 | /** 265 | * Deletes this model's record from the database. 266 | * 267 | * @param {Boolean=false} force 268 | * @return {Promise} 269 | */ 270 | Model.prototype.delete = wrapAround('beforeDelete', function (force) { 271 | if (!this._.isFromDb && !force) { 272 | return Bluebird.resolve(); 273 | } 274 | 275 | return this._addWhereSelf(this._.collection.delete()).execute(); 276 | }, 'afterDelete'); 277 | 278 | /** 279 | * Applies an options object, calling methods (keys) with their 280 | * values (arrays). 281 | * @param {Query} query 282 | * @param {Object={}} options 283 | * @return {Query} 284 | */ 285 | Model.prototype._applyOptions = function (query, options) { 286 | options = options || {}; 287 | 288 | for (var key in options) { 289 | var args = options[key]; 290 | query[key].apply(query, Array.isArray(args) ? args : [args]); 291 | } 292 | 293 | return query; 294 | }; 295 | 296 | /** 297 | * Adds a "where" to the query so that it targets only this model. 298 | * @param {BaseQuery} query 299 | * @return {BaseQuery} 300 | */ 301 | Model.prototype._addWhereSelf = function (query) { 302 | var keys = this._.collection.table.keys; 303 | var pks = keys.partition.concat(keys.compound); 304 | 305 | if (!pks.length) { 306 | throw new Error('A primary key is required on all models.'); 307 | } 308 | 309 | for (var i = 0, l = pks.length; i < l; i++) { 310 | query.where(pks[i], '=', this._.old[pks[i].toString()]); 311 | } 312 | 313 | return query; 314 | }; 315 | 316 | /** 317 | * INSERTs the model into the database. 318 | * 319 | * @return {Promise} 320 | */ 321 | Model.prototype._createNew = wrapAround('beforeCreate', function (options) { 322 | var properties = this.toObject(false); 323 | var self = this; 324 | 325 | return this._applyOptions( 326 | this._.collection.insert().data(properties), 327 | options 328 | ).then(function () { 329 | self.sync(properties); 330 | return self; 331 | }); 332 | }, 'afterCreate'); 333 | 334 | /** 335 | * UPDATEs a record in the database with the changed properties. 336 | * 337 | * @return {Promise} 338 | */ 339 | Model.prototype._updateExisting = wrapAround('beforeUpdate', function (options) { 340 | var query = this._addWhereSelf(this._.collection.update()); 341 | var self = this; 342 | 343 | for (var i = 0, l = this._.columns.length; i < l; i++) { 344 | var column = this._.columns[i]; 345 | var name = column.getName(); 346 | 347 | diff(query, column, this._.old[name], this._.attributes[name]); 348 | } 349 | 350 | this._applyOptions(query, options); 351 | 352 | return query.then(function () { 353 | self.sync(self.toObject(false)); 354 | return self; 355 | }); 356 | }, 'afterUpdate'); 357 | 358 | /** 359 | * Wraps the function so that the "before" and "after" middleware 360 | * is run "around" it. 361 | * 362 | * @param {String} before 363 | * @param {Function} fn 364 | * @param {String} after 365 | * @return {Promise} 366 | */ 367 | function wrapAround (before, fn, after) { 368 | return function () { 369 | var self = this; 370 | var args = arguments; 371 | 372 | return self._.collection 373 | .run(before, self) 374 | .then(function () { 375 | return fn.apply(self, args); 376 | }) 377 | .then(function () { 378 | var resolved = Bluebird.resolve.apply(Bluebird, arguments); 379 | 380 | return self._.collection.run(after, self).then(function () { 381 | return resolved; 382 | }); 383 | }); 384 | }; 385 | } 386 | 387 | module.exports = Model; 388 | -------------------------------------------------------------------------------- /lib/util/casing.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | 3 | /** 4 | * Capitalizes the first letter in a string. 5 | * @param {String} str 6 | * @return {STring} 7 | */ 8 | var capFirst = module.exports.capFirst = function (str) { 9 | return str.charAt(0).toUpperCase() + str.slice(1); 10 | }; 11 | 12 | /** 13 | * Converts a string from camelCase or StudlyCase to snake_case. Could use 14 | * regex for this, but handling Studly (probably the most common use for 15 | * model names...) is ugly. And this is faster too ;) 16 | * 17 | * @param {String} str 18 | * @return {String} 19 | */ 20 | var toSnakeCase = module.exports.toSnakeCase = function (str) { 21 | var out = ''; 22 | for (var i = 0, l = str.length; i < l; i++) { 23 | var chr = str.charAt(i); 24 | var lower = chr.toLowerCase(); 25 | 26 | if (chr !== lower) { 27 | out += (i > 0 ? '_' : '') + lower; 28 | } else { 29 | out += chr; 30 | } 31 | } 32 | 33 | return out; 34 | }; 35 | 36 | /** 37 | * Converts a string from snake_case to camelCase. 38 | * @param {String} str 39 | * @return {String} 40 | */ 41 | var toCamelCase = module.exports.toCamelCase = function (str) { 42 | return str.replace(/(_\w)/g, function (match) { 43 | return match[1].toUpperCase(); 44 | }); 45 | }; 46 | 47 | /** 48 | * Converts a string from snake_case to StudlyCase. 49 | * 50 | * @param {String} str 51 | * @return {String} 52 | */ 53 | var toStudlyCase = module.exports.toStudlyCase = function (str) { 54 | return capFirst(toCamelCase(str)); 55 | }; 56 | -------------------------------------------------------------------------------- /lib/util/index.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | module.exports = _.extend({ 4 | proxyMethod: proxyMethod 5 | }, 6 | require('./casing'), 7 | require('./stmt'), 8 | require('./wrapper') 9 | ); 10 | 11 | /** 12 | * Pulls an object property from a path list, e.g. ['a', 'b'] => obj.a.b 13 | * @param {Object} obj 14 | * @param {[]String} path 15 | * @return {*} 16 | */ 17 | function lookup (obj, path) { 18 | if (path.length === 0) { 19 | return obj; 20 | } 21 | 22 | return lookup(obj[path.shift()], path); 23 | } 24 | 25 | /** 26 | * Adds a proxy method to the object that calls through to the underlying 27 | * object, and returns the proxy. 28 | * @param {Object} proxy 29 | * @param {Object|Function} obj 30 | * @param {String} name 31 | * @param {String} method 32 | */ 33 | function proxyMethod (proxy, method, name) { 34 | if (typeof method === 'string') { 35 | var parts = method.split('.'); 36 | name = parts.slice(-1)[0]; 37 | } 38 | 39 | proxy[name] = function () { 40 | if (parts) { 41 | var context = lookup(this, parts.slice(0, -1)); 42 | lookup(context, parts.slice(-1)).apply(context, arguments); 43 | } else { 44 | method(); 45 | } 46 | return this; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /lib/util/stmt.js: -------------------------------------------------------------------------------- 1 | var Raw = require('../cql/stmt/raw'); 2 | var _ = require('lodash'); 3 | 4 | module.exports = {}; 5 | 6 | /** 7 | * Joins a nested list together. 8 | * @param {Array} list 9 | * @param {[]String} outer Left and right outer delimiters for lists. 10 | * @param {String} delimiter Separator between list values. 11 | * @return {String} 12 | */ 13 | module.exports.deepJoin = function (list, outer, delimiter) { 14 | var output = []; 15 | 16 | for (var i = 0, l = list.length; i < l; i++) { 17 | var item = list[i]; 18 | if (_.isArray(item)) { 19 | output.push(deepJoin(item, outer, delimiter)); 20 | } else { 21 | output.push(item); 22 | } 23 | } 24 | 25 | return outer[0] + output.join(delimiter) + outer[1]; 26 | }; 27 | 28 | /** 29 | * Resolves the name of the table/column/querystate. 30 | * @param {*} obj 31 | * @return {String} 32 | */ 33 | module.exports.resolveName = function (obj) { 34 | if (typeof obj.getName === 'function') { 35 | return obj.getName(); 36 | } else { 37 | return obj; 38 | } 39 | }; 40 | 41 | /** 42 | * Returns if the "str" is raw CQL. 43 | * @param {String|Object} str 44 | * @return {Boolean} 45 | */ 46 | module.exports.isRaw = function (str) { 47 | return str instanceof Raw; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/util/wrapper.js: -------------------------------------------------------------------------------- 1 | var promiseMethods = ['then', 'spread', 'catch', 'error', 'finally']; 2 | 3 | function Wrapper () { 4 | this._obj = null; 5 | this._chain = []; 6 | } 7 | 8 | /** 9 | * Adds a chained promise method to be run after the wrapped object's 10 | * method and before we return the completed promise. 11 | * @param {String} method 12 | * @param {*...} args 13 | * @return {Wrapper} 14 | */ 15 | Wrapper.prototype.chain = function () { 16 | this._chain.push([].slice.call(arguments)); 17 | return this; 18 | }; 19 | 20 | /** 21 | * Wraps an object in promise methods. Takes an object, and when a promise 22 | * method is called on that object we'll run fn and return fn's output. 23 | * 24 | * @param {Object} obj 25 | * @param {Function} fn 26 | * @param {*...} args 27 | * @return {Wrapper} 28 | */ 29 | Wrapper.prototype.obj = function (obj) { 30 | this._obj = obj; 31 | this.fn = [].slice.call(arguments, 1); 32 | 33 | for (var i = 0, l = promiseMethods.length; i < l; i++) { 34 | var method = promiseMethods[i]; 35 | obj[method] = this.runMethod(method); 36 | } 37 | 38 | return this; 39 | }; 40 | 41 | /** 42 | * Generator function to resolve the wrapped promise. 43 | * @param {String} promiseMethod 44 | * @return {Function} 45 | */ 46 | Wrapper.prototype.runMethod = function (promiseMethod) { 47 | // Get the method we want to run to create the promise initially. 48 | var method = this.fn[0]; 49 | var args = this.fn.slice(1); 50 | 51 | return (function () { 52 | // Run it... 53 | var promise = this._obj[method].apply(this._obj, args); 54 | 55 | // Then run anything we want to chain on top of it. 56 | for (var i = 0, l = this._chain.length; i < l; i++) { 57 | promise = promise[this._chain[i][0]].apply(promise, this._chain[i].slice(1)); 58 | } 59 | 60 | // Finally, resolve the promise as the user requested. 61 | return promise[promiseMethod].apply(promise, arguments); 62 | }).bind(this); 63 | }; 64 | 65 | /** 66 | * Returns the wrapped object. 67 | * @return {Objec} 68 | */ 69 | Wrapper.prototype.getWrapped = function () { 70 | return this._obj; 71 | }; 72 | 73 | module.exports = { 74 | /** 75 | * Starts a wrapper on an object. 76 | * @return {Wrapper} 77 | */ 78 | wrap: function () { 79 | var wrapper = new Wrapper(); 80 | return wrapper.obj.apply(wrapper, arguments); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artisan-cassandra-orm", 3 | "version": "0.0.1", 4 | "description": "Node.js ORM for Cassandra 2.1+. Inspired by SQLAlchemy. WIP.", 5 | "main": "lib/connection.js", 6 | "scripts": { 7 | "test": "node node_modules/jasmine-node/bin/jasmine-node spec && node node_modules/jshint/bin/jshint lib" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/MCProHosting/artisan-cassandra-orm.git" 12 | }, 13 | "keywords": [ 14 | "database", 15 | "db", 16 | "cassandra", 17 | "orm" 18 | ], 19 | "author": "Connor Peet ", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/MCProHosting/artisan-cassandra-orm/issues" 23 | }, 24 | "homepage": "https://github.com/MCProHosting/artisan-cassandra-orm", 25 | "devDependencies": { 26 | "benchmark": "^1.0.0", 27 | "jasmine-node": "~2.0.0", 28 | "jshint": "^2.5.11" 29 | }, 30 | "dependencies": { 31 | "array-diff": "0.0.1", 32 | "bluebird": "~2.3.11", 33 | "cassandra-driver": "^1.0.2", 34 | "deep-equal": "^0.2.1", 35 | "lodash": "~2.4.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Cassandra ORM 2 | [![Build Status](https://travis-ci.org/MCProHosting/artisan-cassandra-orm.svg)](https://travis-ci.org/MCProHosting/artisan-cassandra-orm) 3 | 4 | Node.js ORM for Cassandra 2.1+. Inspired by SQLAlchemy. WIP. 5 | 6 | > **Status**: We're rolling out the ORM in production in the coming days/weeks. We plan to leave it in its current state for now, with some tweaks and bugfixes perhaps. Later on we plan to develop eloquent support for things like relations and denormalization, as we see common patterns that arise as we use this ORM. 7 | 8 | Goals/features: 9 | 10 | * A fluent query builder for interacting directly with the database. 11 | * An ORM tailored to leverage Cassandra's performance boons. 12 | * An ORM that works how you'd expect and gets out of your way. 13 | * Emphasis on providing 100% coverage of common (primarily CRUD) database operations, without having to write any raw query. 14 | * A system which lends itself well to automated migrations. (todo: built-in migrations support?) 15 | * Extremely performant, low overhead. Queries are built faster than Bluebird promises can be resolved. 16 | * Promises. Promises everywhere. 17 | 18 | ```js 19 | var Cassandra = require('artisan-cassandra-orm'); 20 | var c = new Cassandra({ contactPoints: ['127.0.0.1'], keyspace: 'middleEarth' }); 21 | 22 | c.select() 23 | .from('hobbits') 24 | .where('last_name', '=', 'baggins') 25 | .limit(2) 26 | .then(function (results) { 27 | // ... 28 | }) 29 | ``` 30 | 31 | ### Connection 32 | 33 | This is based is [datastax/nodejs-driver](https://github.com/datastax/nodejs-driver). You can create a connection by creating a new Connection object. Options are the same as Datastax's driver. 34 | 35 | ```js 36 | var Cassandra = require('artisan-cassandra-orm'); 37 | var c = new Cassandra({ contactPoints: ['127.0.0.1'], keyspace: 'middleEarth' }) 38 | ``` 39 | 40 | We promisify and provide some additional methods atop the driver. The following method/properties are available: 41 | 42 | * `.execute(query: String[, params: Array[, options: Object]]) -> Promise` 43 | * `.batch(queries: Array[, options: Object]]) -> Promise` 44 | * `.shutdown() -> Promise` 45 | * `.connect([options: Object]) -> Promise` 46 | * `getReplicas`, `stream`, and `eachRow` are passed verbatim to the connection object. 47 | * `.client` is the underlying Datastax client. 48 | 49 | 50 | #### Debugging 51 | 52 | The connection is an EventEmitter, and you can listen for queries on it. For instance, this will log queries to the stdout when the environment isn't production: 53 | 54 | ``` 55 | if (process.NODE_ENV !== 'production') { 56 | connection.on('query', function (query, parameters) { 57 | console.log(query); 58 | console.log(parameters); 59 | }); 60 | } 61 | 62 | // Later ... 63 | connection.execute('select * from hobbits where last_name = ?;', ['baggins']); 64 | 65 | // Console: 66 | // => select * from hobbits where last_name = ?; 67 | // => ['baggins'] 68 | ``` 69 | 70 | ### Query Builder 71 | 72 | The query builder is at the core of the ORM. Although it is preferable to interact via models, you can create a query builder directly by calling `.select()`, `.insert()`, `.delete()`, and `.update()` on a connection. 73 | 74 | These builders are promise-friendly. By calling a promise method, such as `.then` or `.catch` on the builder, it knows to execute the built query. Builders may expose several components: 75 | 76 | #### Common Components 77 | 78 | **Note:** For any of these components, you can pass in a raw string or a Column object for any string argument. 79 | 80 | ##### Raw 81 | 82 | Raw strings (created with `c.Stmt.Raw(String)`) will always be inserted into queries without any additions, modifications, or parameterization. 83 | 84 | ##### Tuples 85 | 86 | To create a tuple, use `c.Stmt.Tuple(elements...)`. Tuples can, of course, be nested. 87 | 88 | ``` 89 | .where('profession', '=', c.Stmt.Tuple('burglar', 'adventurer'); 90 | // => `WHERE profession = ('burglar', 'adventurer')` 91 | ``` 92 | 93 | We use the tuple here to differentiate it from a List or Set, which are the _only_ data represented as an array. 94 | 95 | ##### Table 96 | 97 | Available on Select and Delete as `.from()`, on Insert as `.into()`, and on update as `.table()`. 98 | 99 | It takes either a string, table object, or model object as its first and only parameter, and sets the query to be executed on that table. 100 | 101 | ```js 102 | .table('hobbits') 103 | ``` 104 | 105 | ##### Column 106 | 107 | Available on Select, Insert, and Select. Exposes a method "columns" which takes an array of strings or Columns as its first argument, or many strings or columns as a ternary argument. 108 | 109 | ``` 110 | .columns('first_name', 'last_name') 111 | // or... 112 | .columns(['first_name', 'last_name']) 113 | ``` 114 | 115 | ##### Where 116 | 117 | Available on Select, Insert, Delete, and Update. 118 | 119 | * `.andWhere` alias: `.where`. Adds an "AND" condition to the builder. 120 | * `.orWhere`. Adds an "OR" condition to the builder. 121 | 122 | Both these methods are used in the same way. 123 | 124 | * If passed three arguments, it expects them to be `column, operator, value`. The value will be parameterized _unless_ a Raw string is passed. 125 | * If passed a function as the first an only parameter, it creates a where grouping. For example: 126 | 127 | ```js 128 | c.select() 129 | .from('users') 130 | .where('profession', 'CONTAINS', 'wizard') 131 | .orWhere('beard_length', '=', c.Stmt.Raw('\'long\'')) 132 | .andWhere(function (w) { 133 | w.where('location', '=', 'mordor') 134 | .orWhere('location', '=' 'shire') 135 | }); 136 | ``` 137 | 138 | Outputs: 139 | 140 | ```sql 141 | SELECT * FROM users 142 | WHERE profession CONTAINS ? 143 | OR beard_length = 'long' 144 | AND (location = ? OR location = ?) 145 | 146 | Params: ['wizard', 'mordor', 'shire'] 147 | ``` 148 | 149 | #### Options 150 | 151 | Options are available on insert, delete, and update queries. The following two method are exposed: 152 | 153 | * `.ttl(seconds: Number)` Sets the time to live of the operation. 154 | * `.timestamp(microseconds: Number)` Sets the update time (update), creation time (insert), or tombstone record (delete) of the record. 155 | 156 | #### Conditionals 157 | 158 | Conditionals are available on insert, delete, and update queries. 159 | 160 | > In Cassandra 2.0.7 and later, you can conditionally [CRUD] columns using IF or IF EXISTS. Conditionals incur a non-negligible performance cost and should be used sparingly. 161 | 162 | They expose a method `.when(key[, value])`. If a value is not passed, it's an IF EXISTS statement. Example: 163 | 164 | ```js 165 | .when('ring_of_power') 166 | // => IF EXISTS ring_of_power 167 | .when('ring_owner', 'gollum') 168 | // => IF ring_owner = ? 169 | ``` 170 | 171 | #### Builders 172 | 173 | ##### Select 174 | 175 | The select builder provides the components listed above: 176 | 177 | * table as `.from` 178 | * columns (`.columns`) 179 | * where (`.where`, `.andWhere`, `.orWhere`) 180 | 181 | Additionally, the following two methods: 182 | 183 | * `.limit(amount: Number)` Limits the query to the following number. 184 | * `.filter([filtering: Boolean=true])` Turns `ALLOW FILTERING` on or off. 185 | * `.orderBy(column: String, direction: String)` 186 | 187 | ```js 188 | c.select() 189 | .columns('first_name') 190 | .from('hobbits') 191 | .where('last_name', '=', 'baggins') 192 | .orderBy('first_name', 'desc') 193 | .limit(2) 194 | .filter() 195 | .then(function (results) { 196 | // ... 197 | }) 198 | ``` 199 | 200 | ##### Insert 201 | 202 | The select builder provides the components listed above: 203 | 204 | * table as `.into` 205 | * options (`.ttl` and `.timestamp`) 206 | 207 | Additionally, the following methods: 208 | 209 | * `.data(map: Object)` Inserts a key: value set of data. 210 | * `.columns` When inserting columns/values independently (see: columns component). 211 | * `.values(elements....)` When inserting columns/values independently. 212 | 213 | ```js 214 | c.insert() 215 | .into('hobbits') 216 | .data({ first_name: 'Frodo', last_name: 'Baggins' }) 217 | .ttl(60) 218 | .then(function (results) { 219 | // ... 220 | }) 221 | ``` 222 | 223 | ##### Update 224 | 225 | The select builder provides the components listed above: 226 | 227 | * table as `.table` 228 | * where (`.where`, `.andWhere`, `.orWhere`) 229 | * conditionals (`.when`) 230 | * options (`.ttl` and `.timestamp`) 231 | 232 | Additionally, the following methods: 233 | 234 | * `add(column: String|Column, value)` Appends item(s) to a set, list, or map, or adds to a counter column. 235 | * `subtract(column: String|Column, value)` Removes item(s) from a set, list, or map, or subtracts to a counter column. 236 | * `set` can be used in multiple ways: 237 | * `set(str: String)` Adds a "raw" update. No parameterization or anything. Alias: `setRaw`. 238 | * `set(column: String|Column, value)` Updates a column to equal a value, `column = value`. Alias: `setSimple`. 239 | * `set(column: String|Column, index, value)` Updates an index in a set, `column[index] = value`. Alias: `setIndex`. 240 | 241 | ```js 242 | c.update() 243 | .table('hobbits') 244 | .when('ring_of_power') 245 | .where('location', '=', 'erebor') 246 | .add('victims', 'Smaug') 247 | .set('location', 'Shire') 248 | .ttl(60) 249 | .then(function (results) { 250 | // ... 251 | }) 252 | ``` 253 | 254 | ##### Delete 255 | 256 | The delete builder provides the components listed above: 257 | 258 | * table as `.from` 259 | * where (`.where`, `.andWhere`, `.orWhere`) 260 | * columns (`.columns`) 261 | * conditionals (`.when`) 262 | * options (`.timestamp`) 263 | 264 | ```js 265 | c.delete() 266 | .table('dwarves') 267 | .where('name', '=', 'Thorin Oakenshield') 268 | .then(function (results) { 269 | // ... 270 | }) 271 | ``` 272 | 273 | ### Modeling - Still a WIP, not implemented (fully) 274 | 275 | #### Collections 276 | 277 | ##### Creation and Settings 278 | 279 | Collections are created by calling `.model(name: String)` on the connection object. 280 | 281 | ```js 282 | /** 283 | * Create a new model. Its name will be converted to 284 | * snake_case for the table name (it will be `users_info`) 285 | */ 286 | var User = c.model('UserInfo'); 287 | 288 | // Or we can explicitly set the table name: 289 | User.table.setName('user_info'); 290 | 291 | // To pluralize we normally just add an "s" on the 292 | // end, but you explicitly set its plural form like: 293 | User.plural('UserInfoz'); 294 | ``` 295 | 296 | ##### Adding Columns 297 | 298 | The connection also provides all built-in Cassandra types for you: ASCII, BigInt, BLOB, Boolean, Counter, Decimal, Double, Float, IP, Int, Text, Timestamp, TimeUUID, UUID, VarChar, VarIn, Tuple, List, Map, Set. 299 | 300 | You can create columns with these like so: 301 | 302 | ```js 303 | /** 304 | * Add columns to the user. You can, of course, have 305 | * many partition keys and secondary keys. They'll 306 | * be added in the order that the columns are defined 307 | * in the table. 308 | */ 309 | User.columns([ 310 | c.Column.Text('userid').partitionKey(), 311 | t.Set('emails', [t.Text()]), 312 | t.Text('last_name').compoundKey() 313 | ]); 314 | 315 | /** 316 | * You may also add table properties. 317 | */ 318 | User.table.addProperty('COMPACT STORAGE'); 319 | User.table.addProperty('compression', { sstable_compression: 'LZ4Compressor' }); 320 | ``` 321 | 322 | Table schema output: 323 | 324 | ```sql 325 | CREATE TABLE users_info ( 326 | userid text, 327 | emails set, 328 | last_name text, 329 | PRIMARY KEY (emails, last_name) 330 | ) WITH COMPACT STORAGE AND 331 | compression={ 'sstable_compression': 'LZ4Compressor' } 332 | ``` 333 | 334 | Columns are then converted to StudlyCase and published on the collection, for use in querying later. In the above example, the following columns would be made available: 335 | 336 | ``` 337 | User.Userid 338 | User.Emails 339 | User.LastName 340 | ``` 341 | 342 | ##### Table Creation, Migration 343 | 344 | TBD 345 | 346 | ##### Querying 347 | 348 | Like with connections, you can start a query relative to the model by calling select/update/insert/delete. 349 | 350 | ```js 351 | User.select(); 352 | User.update(); 353 | User.insert(); 354 | User.delete(); 355 | ``` 356 | 357 | ##### Defining and Using Relations 358 | 359 | Relations can be defined very elegantly on any collection. Note: these work, but keep in mind that in many cases it may be better to denormalize your data rather than using relations. 360 | 361 | ```js 362 | Dragon.hasMany(GoldCoin).from(Dragon.Uuid).to(GoldCoins.Owner); 363 | // Says that a Dragon has many GoldCoins, via dragon.uuid <--> goldcoins.owner 364 | 365 | GoldCoins.belongsTo(Dragon).from(GoldCoins.Owner).to(Dragon.Uuid); 366 | // Says the same thing! Note: you only need to do one of these, 367 | // don't define both. We do that automatically for you. 368 | 369 | Dragon.has(Weakness).from(Dragon.Uuid).to(Weakness.Dragon) 370 | // One-to-one relationship 371 | ``` 372 | 373 | After finding a model, you can look up related models. 374 | 375 | ```js 376 | // You can load a model "with" owned models. 377 | Dragon.with(GoldCoins).select().then( ... ); 378 | 379 | // You can then access that property 380 | dragon.goldCoins; // => array of GoldCoins models 381 | 382 | // Likewise, you can go backwards. 383 | GoldCoins.with(Dragon).select().then( ... ); 384 | coin.dragon; // => a Dragon instance 385 | 386 | // You can attach or detach models from each other. 387 | // In "to-many" relations, detach will remove the 388 | // model from the relation list and attach will add. 389 | dragon.goldCoins.detach(coin); 390 | dragon.goldCoins.attach(coin); 391 | // In "to-one", detach will delete the relation and 392 | // attach will set and overwrite an existing relation. 393 | coin.dragon.detach(dragon); // coin.dragon will be undefined. 394 | coin.dragon.attach(dragon1); // Now coin.dragon is dragon1 395 | coin.dragon.attach(dragon2); // We overwrite, so coin.dragon is now dragon2 396 | ``` 397 | 398 | ##### Lifecycle 399 | 400 | Like Express, lifecycle callbacks are done in the form of middleware. The following callbacks are available: 401 | 402 | * beforeCreate 403 | * afterCreate 404 | * beforeDelete 405 | * afterDelete 406 | * beforeUpdate 407 | * afterUpdate 408 | 409 | > Note: these are only called when working with models, not when querying directly on the collection (e.g., it won't run on User.delete().where('a', '=' ,'b')) 410 | 411 | The context, `this` for callbacks will be set to the model object. Methods and attributes on the model (see below) will be available. Example: 412 | 413 | ```js 414 | User.use('beforeCreate', function (next) { 415 | var err = validator.try(this.attributes, rules); 416 | if (err) { 417 | next(err); 418 | // or throw err; 419 | } else { 420 | next(); // We're all good 421 | } 422 | }); 423 | 424 | // You can pass multiple events in as an array. 425 | User.use(['beforeCreate', 'beforeUpdate'], function (next) { 426 | var self = this; 427 | if (this.isDirty('password')) { 428 | bcrypt.hash(this.password, 8, function (err, hashed) { 429 | if (err) { 430 | throw err; 431 | } else { 432 | self.password = hashed; 433 | next(); 434 | } 435 | }); 436 | } else { 437 | next(); 438 | } 439 | }); 440 | ``` 441 | 442 | ##### Creating & Looking up Models 443 | 444 | Models can either be created or looked up. Creating models can be done via `.new()`: 445 | 446 | ```js 447 | // Returns a fresh new model! 448 | var user = User.new(); 449 | // Create a new model already populated with some data. 450 | var user = User.new({ name: 'Thorin Oakenshield' }); 451 | ``` 452 | 453 | To look up models, simply start a "select" query on the collection. It will be resolved to an array of models. 454 | 455 | Neither method takes arguments directly. Rather, they return a select query builder. So, for example: 456 | 457 | ```js 458 | User.select() 459 | .where(User.Profession, 'CONTAINS', 'wizard') 460 | .limit(1) 461 | .then(function (wizards, results) { 462 | // wizards is an array of user models. 463 | // results is the raw output from the datastax driver 464 | }); 465 | ``` 466 | 467 | ##### Custom Methods/Properties 468 | 469 | Static methods can be attached to the collection directly, of course: 470 | 471 | ```js 472 | User.sayHi = function () { 473 | console.log('Hi'); 474 | }; 475 | ``` 476 | 477 | You can also define methods or properties that are present on model instances. 478 | 479 | ```js 480 | User.define('whoAmI', function () { 481 | return this.name; 482 | }); 483 | 484 | User.findOne() 485 | .where(User.Name, '=', 'Frodo Baggins') 486 | .then(function (frodo) { 487 | console.log(frodo.whoAmI()); 488 | // => Frodo Baggins 489 | }); 490 | ``` 491 | 492 | ##### Getters/Setters 493 | 494 | You can defined getters and setters like so: 495 | 496 | ```js 497 | // Say we're storing user points in the database. When we "get" the 498 | // points we'd rather return a string like " Points" 499 | User.getter('points', function (points) { 500 | return points + ' Points'; 501 | }); 502 | 503 | // We'd also need a setter to trim off the " Points" before we 504 | // set the value! 505 | User.setter('points', function (points) { 506 | return parseInt(points.replace(' Points', ''), 10); 507 | }); 508 | 509 | // Note that, when calling toObject, you'll get the data passed 510 | // through the getters 511 | User.toObject(); // => { points: '42 Points' } 512 | // But if you don't want this to happen, pass "false" as its 513 | // first argument. 514 | User.toObject(false); // => { points: 42 } 515 | ``` 516 | 517 | #### Models 518 | 519 | Models provide several useful methods and attributes you may use. 520 | 521 | ##### Attributes 522 | 523 | The _only_ enumerable properties on a model are its attributes. This means you can very easily loop through them, serialize the model to JSON, or what have you. You can likewise set to update: 524 | 525 | ```js 526 | var user = User.new(); 527 | user.name = 'Smaug'; 528 | user.emails = ['root@erebor.com']; 529 | ``` 530 | 531 | ##### Saving/Updating Models 532 | 533 | Models can be updated with a simple "save" call. We will create the model in the database if it does not exist, or update an existing model. 534 | 535 | Unless "true" is passed as the first parameter, we won't update the model if no properties have been changed. 536 | 537 | ```js 538 | user.save().then(function (user) { 539 | // the user model has now been updated! 540 | }); 541 | ``` 542 | 543 | ##### Utility 544 | 545 | * `.isDirty(column: String|Column) -> Boolean` Returns whether the column has changed since the model was created or synced with the database. 546 | * `isSynced() -> Boolean` Returns whether any attributes have changed since the object was last updated from the database. 547 | * `.toObject() -> Object` Returns the current model properties as a plain object. 548 | * `.toJson() -> String` Converts the current model properties to a string. 549 | * `.old` is an object which contains the attributes as they exist in the database. 550 | * `.collection` is a reference to the object's parent collection. 551 | -------------------------------------------------------------------------------- /spec/ConnectionSpec.js: -------------------------------------------------------------------------------- 1 | var Connection = require('../lib/connection'); 2 | var _ = require('lodash'); 3 | 4 | describe('connection', function () { 5 | if (process.env.TEST_INTEGRATION) { 6 | var helper = require('./helper'); 7 | 8 | describe('interaction', function () { 9 | var connection; 10 | 11 | beforeEach(function (ready) { 12 | connection = helper().then(ready); 13 | }); 14 | 15 | afterEach(function (closed) { 16 | connection.then(function () { 17 | return this.shutdown(); 18 | }).then(closed); 19 | }); 20 | 21 | it('basic methods works', function (done) { 22 | connection.then(function () { 23 | // At this point the raw .executes will have been called. Try 24 | // the select binding. 25 | return this.select().from('users'); 26 | }).then(function (results) { 27 | // Make sure it works there. 28 | expect(_.pick(results.rows[0], ['first_name', 'last_name'])) 29 | .toEqual({ first_name: 'Connor', last_name: 'Peet' }); 30 | 31 | // Now make sure shutdown works. Done won't be called and we'll 32 | // get a timeout error if this fails. 33 | return done(); 34 | }); 35 | }); 36 | it('batch works', function (done) { 37 | connection.then(function () { 38 | // At this point the raw .executes will have been called. Try 39 | // the select binding. 40 | return this.batch([ 41 | this.update().table('users').set('last_name', 'bar').where('first_name', '=', 'Connor'), 42 | this.insert().into('users').data({ first_name: 'John', last_name: 'Doe' }) 43 | ]); 44 | }).then(done); 45 | }); 46 | 47 | it('is an eventemitter', function (done) { 48 | var emitted = jasmine.createSpy(); 49 | connection.then(function () { 50 | this.on('query', emitted); 51 | return this.execute('select * from users;', [], {}) 52 | }).then(function () { 53 | expect(emitted).toHaveBeenCalledWith('select * from users;', [], { prepare: true }); 54 | done(); 55 | }); 56 | }); 57 | }); 58 | } 59 | 60 | it('exposes type', function () { 61 | var connection = new Connection(); 62 | expect(connection.Text('foo') instanceof require('../lib/cql/column/basic')).toBe(true); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/cql/AlterTableSpec.js: -------------------------------------------------------------------------------- 1 | var AlterTable = require('../../lib/cql/queries/alterTable'); 2 | var t = require('../../lib/cql/types'); 3 | 4 | describe('alter table', function () { 5 | it('sets column type with strings', function () { 6 | expect(new AlterTable() 7 | .table('tbl') 8 | .type('col', 'text') 9 | .parameterize() 10 | ).toEqual([[], 'ALTER TABLE tbl ALTER col TYPE text;']); 11 | }); 12 | it('sets column type with column object', function () { 13 | expect(new AlterTable() 14 | .table('tbl') 15 | .type(t.Text('col')) 16 | .parameterize() 17 | ).toEqual([[], 'ALTER TABLE tbl ALTER col TYPE text;']); 18 | }); 19 | it('adds columns with strings', function () { 20 | expect(new AlterTable() 21 | .table('tbl') 22 | .add('col', 'text') 23 | .parameterize() 24 | ).toEqual([[], 'ALTER TABLE tbl ADD col text;']); 25 | }); 26 | it('adds columns with object', function () { 27 | expect(new AlterTable() 28 | .table('tbl') 29 | .add(t.Text('col')) 30 | .parameterize() 31 | ).toEqual([[], 'ALTER TABLE tbl ADD col text;']); 32 | }); 33 | it('drops columns', function () { 34 | expect(new AlterTable() 35 | .table('tbl') 36 | .drop(t.Text('col')) 37 | .parameterize() 38 | ).toEqual([[], 'ALTER TABLE tbl DROP col;']); 39 | }); 40 | it('renames columns', function () { 41 | expect(new AlterTable() 42 | .table('tbl') 43 | .rename('col1', 'col2') 44 | .parameterize() 45 | ).toEqual([[], 'ALTER TABLE tbl RENAME col1 TO col2;']); 46 | }); 47 | it('updates column properties single', function () { 48 | expect(new AlterTable() 49 | .table('tbl') 50 | .setProperties({ comment: 'Hello World' }) 51 | .parameterize() 52 | ).toEqual([[], 'ALTER TABLE tbl WITH comment = \'Hello World\';']); 53 | }); 54 | it('updates column properties multiple', function () { 55 | expect(new AlterTable() 56 | .table('tbl') 57 | .setProperties({ comment: 'Hello World', read_repair_chance: 0.2 }) 58 | .parameterize() 59 | ).toEqual([[], 'ALTER TABLE tbl WITH comment = \'Hello World\' AND read_repair_chance = 0.2;']); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/cql/ColumnSpec.js: -------------------------------------------------------------------------------- 1 | var BasicColumn = require('../../lib/cql/column/basic'); 2 | var CollectionColumn = require('../../lib/cql/column/collection'); 3 | var CustomColumn = require('../../lib/cql/column/custom'); 4 | 5 | describe('basic column', function () { 6 | it('should generate entry', function () { 7 | var column = new BasicColumn('name', 'text'); 8 | expect(column.getEntry()).toBe('name text'); 9 | }); 10 | it('should add static types', function () { 11 | var column = new BasicColumn('name', 'text').addAttr('static'); 12 | expect(column.getEntry()).toBe('name text STATIC'); 13 | }); 14 | it('sets partition keying', function () { 15 | var column = new BasicColumn('name', 'text'); 16 | expect(column.isKey).toEqual({ partition: false, compound: false }); 17 | column.partitionKey(); 18 | expect(column.isKey).toEqual({ partition: true, compound: false }); 19 | }); 20 | it('sets compound keying', function () { 21 | var column = new BasicColumn('name', 'text'); 22 | expect(column.isKey).toEqual({ partition: false, compound: false }); 23 | column.compoundKey(); 24 | expect(column.isKey).toEqual({ partition: false, compound: true }); 25 | }); 26 | }); 27 | 28 | describe('indexing', function () { 29 | it('works basically', function () { 30 | expect(new BasicColumn('name', 'text').index().toString('tbl')).toBe('CREATE INDEX ON tbl (name);'); 31 | }); 32 | it('allows naming of indexes', function () { 33 | expect(new BasicColumn('name', 'text').index('a').toString('tbl')).toBe('CREATE INDEX a ON tbl (name);'); 34 | }); 35 | it('allows specifying of class name', function () { 36 | expect( 37 | new BasicColumn('name', 'text').index().using('a').toString('tbl') 38 | ).toBe('CREATE INDEX ON tbl (name) USING \'a\';'); 39 | }); 40 | it('allows specifying of options', function () { 41 | expect( 42 | new BasicColumn('name', 'text').index().options({ a: 'b' }).toString('tbl') 43 | ).toBe('CREATE INDEX ON tbl (name) WITH OPTIONS = { \'a\': \'b\' };'); 44 | }); 45 | }); 46 | 47 | describe('generators', function () { 48 | it('should generate when function', function () { 49 | expect(typeof(new BasicColumn(null, 'timeuuid').generate())).toBe('string'); 50 | }); 51 | it('should throw an error if cant generator', function () { 52 | expect(function () { 53 | new BasicColumn(null, 'Foo').generate(); 54 | }).toThrow(); 55 | }); 56 | }); 57 | 58 | describe('collection column', function () { 59 | it('should work with single basic type', function () { 60 | var column = new CollectionColumn('emails', 'list', [new BasicColumn(null, 'text')]); 61 | expect(column.getEntry()).toBe('emails list'); 62 | }); 63 | it('should work with multiple types', function () { 64 | var column = new CollectionColumn('high_scores', 'map', [ 65 | new BasicColumn(null, 'text'), 66 | new BasicColumn(null, 'int') 67 | ]); 68 | expect(column.getEntry()).toBe('high_scores map'); 69 | }); 70 | it('should freeze tuples', function () { 71 | var column = new CollectionColumn('high_scores', 'tuple', [ 72 | new BasicColumn(null, 'text'), 73 | new BasicColumn(null, 'int') 74 | ]); 75 | expect(column.getEntry()).toBe('high_scores frozen >'); 76 | }); 77 | it('should nest types', function () { 78 | var column = new CollectionColumn('high_scores', 'tuple', [ 79 | new BasicColumn(null, 'text'), 80 | new CollectionColumn(null, 'list', [new BasicColumn(null, 'int')]) 81 | ]); 82 | expect(column.getEntry()).toBe('high_scores frozen >>'); 83 | }); 84 | }); 85 | 86 | describe('custom column', function () { 87 | it('should work correctly', function () { 88 | var column = new CustomColumn('name', 'funType'); 89 | expect(column.getEntry()).toBe('name frozen '); 90 | }); 91 | }); 92 | 93 | describe('column states', function () { 94 | it('select functions', function () { 95 | var column = new BasicColumn('name'); 96 | expect(column.as('foo').toString()).toBe('name as foo'); 97 | expect(column.ttl().toString()).toBe('TTL(name)'); 98 | expect(column.count().toString()).toBe('COUNT(name)'); 99 | expect(column.writeTime().toString()).toBe('WRITETIME(name)'); 100 | expect(column.distinct().toString()).toBe('DISTINCT name'); 101 | expect(column.count().as('foo').toString()).toBe('COUNT(name) as foo'); 102 | expect(column.dateOf().toString()).toBe('dateOf(name)'); 103 | expect(column.minTimeuuid().toString()).toBe('minTimeuuid(name)'); 104 | expect(column.maxTimeuuid().toString()).toBe('maxTimeuuid(name)'); 105 | expect(column.unixTimestampOf().toString()).toBe('unixTimestampOf(name)'); 106 | expect(column.token().toString()).toBe('token(name)'); 107 | }); 108 | it('orders functions', function () { 109 | var column = new BasicColumn('name'); 110 | expect(column.desc().toString()).toBe('name DESC'); 111 | expect(column.asc().toString()).toBe('name ASC'); 112 | }); 113 | it('blob functions', function () { 114 | var column = new BasicColumn('name', 'text'); 115 | expect(column.asBlob().toString()).toBe('textAsBlob(name)'); 116 | expect(column.blobAsText().toString()).toBe('blobAsText(name)'); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /spec/cql/DeleteSpec.js: -------------------------------------------------------------------------------- 1 | var Delete = require('../../lib/cql/queries/delete'); 2 | var Raw = require('../../lib/cql/stmt/raw'); 3 | var Table = require('../../lib/cql/table'); 4 | var t = require('../../lib/cql/types'); 5 | 6 | describe('insert', function () { 7 | var table; 8 | beforeEach(function () { 9 | table = new Table('tbl'); 10 | }); 11 | 12 | it('generates basic no columns', function () { 13 | expect(new Delete() 14 | .from(table) 15 | .parameterize() 16 | ).toEqual([[], 'DELETE FROM tbl;']); 17 | }); 18 | 19 | it('generates basic with columns', function () { 20 | expect(new Delete() 21 | .columns('a', 'b') 22 | .from(table) 23 | .parameterize() 24 | ).toEqual([[], 'DELETE a, b FROM tbl;']); 25 | }); 26 | 27 | it('where', function () { 28 | expect(new Delete() 29 | .from(table) 30 | .where('a', '=', 'b') 31 | .parameterize() 32 | ).toEqual([['b'], 'DELETE FROM tbl WHERE a = ?;']); 33 | }); 34 | 35 | it('options', function () { 36 | expect(new Delete() 37 | .from(table) 38 | .ttl(30) 39 | .parameterize() 40 | ).toEqual([[], 'DELETE FROM tbl USING TTL 30;']); 41 | }); 42 | 43 | it('conditional', function () { 44 | expect(new Delete() 45 | .from(table) 46 | .when('a', 'b') 47 | .parameterize() 48 | ).toEqual([['b'], 'DELETE FROM tbl IF a = ?;']); 49 | }); 50 | 51 | it('complex', function () { 52 | expect(new Delete() 53 | .from(table) 54 | .columns('a', 'b') 55 | .when('a', 'b') 56 | .where('q', '=', new Raw('w')) 57 | .ttl(30) 58 | .parameterize() 59 | ).toEqual([['b'], 'DELETE a, b FROM tbl USING TTL 30 WHERE q = w IF a = ?;']); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /spec/cql/InsertSpec.js: -------------------------------------------------------------------------------- 1 | var Insert = require('../../lib/cql/queries/insert'); 2 | var Raw = require('../../lib/cql/stmt/raw'); 3 | var Table = require('../../lib/cql/table'); 4 | var t = require('../../lib/cql/types'); 5 | 6 | describe('insert', function () { 7 | var table; 8 | beforeEach(function () { 9 | table = new Table('tbl'); 10 | }); 11 | 12 | it('generates basic', function () { 13 | expect(new Insert() 14 | .into(table) 15 | .columns('a', 'b') 16 | .values(1, 2) 17 | .parameterize() 18 | ).toEqual([[1, 2], 'INSERT INTO tbl (a, b) VALUES (?, ?);']); 19 | }); 20 | it('accepts raw', function () { 21 | expect(new Insert() 22 | .into(table) 23 | .columns(t.Text('a'), 'b') 24 | .values(1, new Raw(2)) 25 | .parameterize() 26 | ).toEqual([[1], 'INSERT INTO tbl (a, b) VALUES (?, 2);']); 27 | }); 28 | it('works with map', function () { 29 | expect(new Insert() 30 | .into(table) 31 | .data({ 'a': 1, 'b': 2 }) 32 | .parameterize() 33 | ).toEqual([[1, 2], 'INSERT INTO tbl (a, b) VALUES (?, ?);']); 34 | }); 35 | it('adds if not exists', function () { 36 | expect(new Insert() 37 | .into(table) 38 | .ifNotExists() 39 | .columns('a', 'b') 40 | .values(1, 2) 41 | .parameterize() 42 | ).toEqual([[1, 2], 'INSERT INTO tbl (a, b) VALUES (?, ?) IF NOT EXISTS;']); 43 | }); 44 | it('uses one option', function () { 45 | expect(new Insert() 46 | .into(table) 47 | .columns('a', 'b') 48 | .values(1, 2) 49 | .ttl(30) 50 | .parameterize() 51 | ).toEqual([[1, 2], 'INSERT INTO tbl (a, b) VALUES (?, ?) USING TTL 30;']); 52 | }); 53 | it('uses both options', function () { 54 | expect(new Insert() 55 | .into(table) 56 | .columns('a', 'b') 57 | .values(1, 2) 58 | .ttl(30) 59 | .timestamp(100) 60 | .parameterize() 61 | ).toEqual([[1, 2], 'INSERT INTO tbl (a, b) VALUES (?, ?) USING TTL 30 AND TIMESTAMP 100;']); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /spec/cql/SelectSpec.js: -------------------------------------------------------------------------------- 1 | var Select = require('../../lib/cql/queries/select'); 2 | var Raw = require('../../lib/cql/stmt/raw'); 3 | var Table = require('../../lib/cql/table'); 4 | var t = require('../../lib/cql/types'); 5 | 6 | describe('select', function () { 7 | var table; 8 | beforeEach(function () { 9 | table = new Table('tbl'); 10 | }); 11 | 12 | it('select specific columns as strings', function () { 13 | expect(new Select() 14 | .columns('a', 'b') 15 | .from('tbl') 16 | .parameterize() 17 | ).toEqual([[], 'SELECT a, b FROM tbl;']); 18 | }); 19 | 20 | describe('select', function () { 21 | it('builds a basic select', function () { 22 | expect(new Select() 23 | .from(table) 24 | .parameterize() 25 | ).toEqual([[], 'SELECT * FROM tbl;']); 26 | }); 27 | 28 | it('also accepts a string table name', function () { 29 | expect(new Select() 30 | .from('tbl') 31 | .parameterize() 32 | ).toEqual([[], 'SELECT * FROM tbl;']); 33 | }); 34 | 35 | it('select specific columns', function () { 36 | expect(new Select() 37 | .columns(t.Text('a').as('q'), t.Text('b')) 38 | .from('tbl') 39 | .parameterize() 40 | ).toEqual([[], 'SELECT a as q, b FROM tbl;']); 41 | }); 42 | 43 | it('select specific columns as strings', function () { 44 | expect(new Select() 45 | .columns('a', 'b') 46 | .from('tbl') 47 | .parameterize() 48 | ).toEqual([[], 'SELECT a, b FROM tbl;']); 49 | }); 50 | }); 51 | 52 | it('hooks into where', function () { 53 | expect(new Select() 54 | .columns('a', 'b') 55 | .from('tbl') 56 | .where(t.Text('a'), '<', 3) 57 | .orWhere(function (w) { 58 | w.where('r', '>', 1).orWhere('z', '>', new Raw('x')); 59 | }) 60 | .andWhere('c', '>', 2) 61 | .parameterize() 62 | ).toEqual([[3, 1, 2], 'SELECT a, b FROM tbl WHERE a < ? OR (r > ? OR z > x) AND c > ?;']); 63 | }); 64 | 65 | it('adds limit', function () { 66 | expect(new Select() 67 | .from(table) 68 | .limit(5) 69 | .parameterize() 70 | ).toEqual([[], 'SELECT * FROM tbl LIMIT 5;']); 71 | }); 72 | 73 | it('orders', function () { 74 | expect(new Select() 75 | .from(table) 76 | .orderBy('a', 'asc') 77 | .orderBy(t.Text('b').desc()) 78 | .parameterize() 79 | ).toEqual([[], 'SELECT * FROM tbl ORDER BY a ASC, b DESC;']); 80 | }); 81 | 82 | it('allows filter', function () { 83 | expect(new Select() 84 | .from(table) 85 | .filter() 86 | .parameterize() 87 | ).toEqual([[], 'SELECT * FROM tbl ALLOW FILTERING;']); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /spec/cql/StmtSpec.js: -------------------------------------------------------------------------------- 1 | var Columns = require('../../lib/cql/stmt/columns'); 2 | var Where = require('../../lib/cql/stmt/where'); 3 | var Order = require('../../lib/cql/stmt/order'); 4 | var Assignment = require('../../lib/cql/stmt/assignment'); 5 | var Conditionals = require('../../lib/cql/stmt/conditionals'); 6 | var Tuple = require('../../lib/cql/stmt/termTuple'); 7 | var Raw = require('../../lib/cql/stmt/raw'); 8 | var t = require('../../lib/cql/types'); 9 | 10 | describe('term tuple', function () { 11 | it('handles undefined with null', function () { 12 | expect(new Tuple(1, false, undefined).parameterize()).toEqual([ 13 | [1, false], 14 | '(?, ?, NULL)' 15 | ]); 16 | }); 17 | }); 18 | 19 | describe('where', function () { 20 | it('takes basic raw', function () { 21 | expect(new Where() 22 | .where(new Raw('foo')) 23 | .where(new Raw('bar')) 24 | .parameterize() 25 | ).toEqual([[], 'foo AND bar']); 26 | }); 27 | it('parameterizes constants', function () { 28 | expect(new Where() 29 | .where('a', '<', 3) 30 | .parameterize() 31 | ).toEqual([[3], 'a < ?']); 32 | }); 33 | it('does not parameterize raw', function () { 34 | expect(new Where() 35 | .where('a', '<', new Raw(3)) 36 | .parameterize() 37 | ).toEqual([[], 'a < 3']); 38 | }); 39 | it('chains correct', function () { 40 | expect(new Where() 41 | .where('a', '<', 3) 42 | .orWhere('b', '=', 2) 43 | .andWhere('c', '>', 1) 44 | .parameterize() 45 | ).toEqual([[3, 2, 1], 'a < ? OR b = ? AND c > ?']); 46 | }); 47 | it('nests correctly', function () { 48 | expect(new Where() 49 | .where('a', '<', 3) 50 | .orWhere(function (w) { 51 | w.where('r', '>', 1).orWhere('z', '>', new Raw('x')); 52 | }) 53 | .andWhere('c', '>', 2) 54 | .parameterize() 55 | ).toEqual([[3, 1, 2], 'a < ? OR (r > ? OR z > x) AND c > ?']); 56 | }); 57 | it('handles column tuples', function () { 58 | expect(new Where() 59 | .where(['a', 'b'], '<', 3) 60 | .parameterize() 61 | ).toEqual([[3], '(a, b) < ?']); 62 | }); 63 | it('handles term tuples', function () { 64 | expect(new Where() 65 | .where('a', '<', new Tuple(1, 2, new Tuple(3, new Raw(4)))) 66 | .parameterize() 67 | ).toEqual([[1, 2, 3], 'a < (?, ?, (?, 4))']); 68 | }); 69 | it('does not mutilate complex types', function () { 70 | expect(new Where() 71 | .where('a', '<', [1, 2, 3]) 72 | .andWhere('b', '>', { q: 1, w: 2 }) 73 | .parameterize() 74 | ).toEqual([[[1, 2, 3], { q: 1, w: 2 }], 'a < ? AND b > ?']); 75 | }); 76 | }); 77 | 78 | describe('order', function () { 79 | it('orders with raw string', function () { 80 | expect(new Order() 81 | .orderBy(new Raw('a DESC')) 82 | .toString() 83 | ).toBe('a DESC'); 84 | }); 85 | it('orders by column modifier', function () { 86 | expect(new Order() 87 | .orderBy(t.Text('a').desc()) 88 | .toString() 89 | ).toBe('a DESC'); 90 | }); 91 | it('orders by column name string', function () { 92 | expect(new Order() 93 | .orderBy('a', 'DESC') 94 | .toString() 95 | ).toBe('a DESC'); 96 | }); 97 | it('orders by multiple', function () { 98 | expect(new Order() 99 | .orderBy('a', 'DESC') 100 | .orderBy('b', 'ASC') 101 | .toString() 102 | ).toBe('a DESC, b ASC'); 103 | }); 104 | }); 105 | 106 | describe('conditionals', function () { 107 | it('works with none', function () { 108 | expect(new Conditionals() 109 | .parameterize() 110 | ).toEqual([[], '']); 111 | }); 112 | it('works with one', function () { 113 | expect(new Conditionals() 114 | .when('a', 1) 115 | .parameterize() 116 | ).toEqual([[1], 'a = ?']); 117 | }); 118 | it('works with exists', function () { 119 | expect(new Conditionals() 120 | .when('a') 121 | .parameterize() 122 | ).toEqual([[], 'EXISTS a']); 123 | }); 124 | it('works with many', function () { 125 | expect(new Conditionals() 126 | .when('a', 1) 127 | .when('b', 2) 128 | .when('c', 3) 129 | .parameterize() 130 | ).toEqual([[1, 2, 3], 'a = ? AND b = ? AND c = ?']); 131 | }); 132 | }); 133 | 134 | describe('assignment', function () { 135 | it('takes raw string', function () { 136 | expect(new Assignment() 137 | .set(new Raw('key = value')) 138 | .parameterize() 139 | ).toEqual([[], 'key = value']); 140 | }); 141 | 142 | it('works as column_name = value', function () { 143 | expect(new Assignment() 144 | .set('key', 'value') 145 | .parameterize() 146 | ).toEqual([['value'], 'key = ?']); 147 | 148 | expect(new Assignment() 149 | .set('key', new Raw('value')) 150 | .parameterize() 151 | ).toEqual([[], 'key = value']); 152 | }); 153 | 154 | it('works as set_or_list_item = set_or_list_item + ...', function () { 155 | expect(new Assignment() 156 | .add('set', 'value') 157 | .parameterize() 158 | ).toEqual([['value'], 'set = set + ?']); 159 | 160 | expect(new Assignment() 161 | .add('set', new Raw('value')) 162 | .parameterize() 163 | ).toEqual([[], 'set = set + value']); 164 | }); 165 | 166 | it('works as column_name [ term ] = value', function () { 167 | expect(new Assignment() 168 | .set('set', 2, 'value') 169 | .parameterize() 170 | ).toEqual([[2, 'value'], 'set [?] = ?']); 171 | 172 | expect(new Assignment() 173 | .set('set', new Raw(2), new Raw('value')) 174 | .parameterize() 175 | ).toEqual([[], 'set [2] = value']); 176 | }); 177 | }); 178 | 179 | describe('columns', function () { 180 | it('works with variadic', function () { 181 | expect(new Columns() 182 | .columns('a', 'b') 183 | .toString() 184 | ).toBe('a, b'); 185 | }); 186 | it('works with array argument', function () { 187 | expect(new Columns() 188 | .columns(['a', 'b']) 189 | .toString() 190 | ).toBe('a, b'); 191 | }); 192 | it('takes a default', function () { 193 | expect(new Columns(['a']).toString()).toBe('a'); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /spec/cql/TableSpec.js: -------------------------------------------------------------------------------- 1 | var Table = require('../../lib/cql/table'); 2 | var t = require('../../lib/cql/types'); 3 | 4 | describe('table generation', function () { 5 | it('generates a basic table', function () { 6 | expect(new Table('users') 7 | .addColumn(t.Text('userid')) 8 | .addColumn(t.Set('emails', [t.Text()])) 9 | .addColumn(t.Text('name')) 10 | .toString() 11 | ).toBe('CREATE TABLE users (\r\n' + 12 | ' userid text,\r\n' + 13 | ' emails set,\r\n' + 14 | ' name text\r\n' + 15 | ');'); 16 | }); 17 | 18 | it('sets the name', function () { 19 | expect(new Table('users') 20 | .setName('foo') 21 | .addColumn(t.Text('userid')) 22 | .toString() 23 | ).toBe('CREATE TABLE foo (\r\n' + 24 | ' userid text\r\n' + 25 | ');'); 26 | }); 27 | 28 | it('adds single partition key', function () { 29 | expect(new Table('users') 30 | .addColumn(t.Text('userid')) 31 | .addColumn(t.Set('emails', [t.Text()])) 32 | .addColumn(t.Text('name')) 33 | .addPartitionKey('name') 34 | .toString() 35 | ).toBe('CREATE TABLE users (\r\n' + 36 | ' userid text,\r\n' + 37 | ' emails set,\r\n' + 38 | ' name text,\r\n' + 39 | ' PRIMARY KEY (name)\r\n' + 40 | ');'); 41 | }); 42 | 43 | it('adds multiple partition keys', function () { 44 | expect(new Table('users') 45 | .addColumn(t.Text('userid')) 46 | .addColumn(t.Set('emails', [t.Text()])) 47 | .addColumn(t.Text('name')) 48 | .addPartitionKey('name') 49 | .addPartitionKey('userid') 50 | .toString() 51 | ).toBe('CREATE TABLE users (\r\n' + 52 | ' userid text,\r\n' + 53 | ' emails set,\r\n' + 54 | ' name text,\r\n' + 55 | ' PRIMARY KEY ((name, userid))\r\n' + 56 | ');'); 57 | }); 58 | 59 | it('adds compound key', function () { 60 | expect(new Table('users') 61 | .addColumn(t.Text('userid')) 62 | .addColumn(t.Set('emails', [t.Text()])) 63 | .addColumn(t.Text('name')) 64 | .addPartitionKey('name') 65 | .addCompoundKey('userid') 66 | .toString() 67 | ).toBe('CREATE TABLE users (\r\n' + 68 | ' userid text,\r\n' + 69 | ' emails set,\r\n' + 70 | ' name text,\r\n' + 71 | ' PRIMARY KEY (name, userid)\r\n' + 72 | ');'); 73 | }); 74 | 75 | it('adds column indexing', function () { 76 | expect(new Table('users') 77 | .addColumn(t.Text('userid').index().column) 78 | .addColumn(t.Set('emails', [t.Text()]).index('asdf').column) 79 | .addColumn(t.Text('name')) 80 | .toString() 81 | ).toBe('CREATE TABLE users (\r\n' + 82 | ' userid text,\r\n' + 83 | ' emails set,\r\n' + 84 | ' name text\r\n' + 85 | ');\r\n' + 86 | 'CREATE INDEX ON users (userid);\r\n' + 87 | 'CREATE INDEX asdf ON users (emails);'); 88 | }); 89 | 90 | it('adds table properties', function () { 91 | expect(new Table('users') 92 | .addColumn(t.Text('userid')) 93 | .addProperty('COMPACT STORAGE') 94 | .addProperty('compression', { sstable_compression: 'LZ4Compressor' }) 95 | .addProperty('caching', { keys: 'ALL', rows_per_partition: 'NONE' }) 96 | .addProperty('comment', 'Hello World') 97 | .addProperty('gc_grace_seconds', 864000) 98 | .toString() 99 | ).toBe('CREATE TABLE users (\r\n'+ 100 | ' userid text\r\n'+ 101 | ') WITH COMPACT STORAGE AND\r\n'+ 102 | ' compression={ \'sstable_compression\': \'LZ4Compressor\' } AND\r\n'+ 103 | ' caching=\'{"keys":"ALL","rows_per_partition":"NONE"}\' AND\r\n'+ 104 | ' comment=\'Hello World\' AND\r\n'+ 105 | ' gc_grace_seconds=864000;'); 106 | }); 107 | }); 108 | 109 | // describe('table parser', function () { 110 | // var parsed; 111 | // beforeEach(function () { 112 | // parsed = tableParser( 113 | // "CREATE TABLE users (" + 114 | // " userid text," + 115 | // " emails set," + 116 | // " first_name text," + 117 | // " last_name text," + 118 | // " todo map," + 119 | // " top_scores list," + 120 | // " PRIMARY KEY (userid)" + 121 | // ") WITH" + 122 | // " bloom_filter_fp_chance=0.010000 AND" + 123 | // " caching='{\"keys\":\"ALL\", \"rows_per_partition\":\"NONE\"}' AND" + 124 | // " comment='' AND" + 125 | // " dclocal_read_repair_chance=0.100000 AND" + 126 | // " gc_grace_seconds=864000 AND" + 127 | // " read_repair_chance=0.000000 AND" + 128 | // " default_time_to_live=0 AND" + 129 | // " speculative_retry='99.0PERCENTILE' AND" + 130 | // " memtable_flush_period_in_ms=0 AND" + 131 | // " compaction={'class': 'SizeTieredCompactionStrategy'} AND" + 132 | // " compression={'sstable_compression': 'LZ4Compressor'};" 133 | // ); 134 | // }); 135 | 136 | // it('parses columns correctly', function () { 137 | 138 | // }); 139 | // }); 140 | -------------------------------------------------------------------------------- /spec/cql/TypecastSpec.js: -------------------------------------------------------------------------------- 1 | var errors = require('../../lib/errors'); 2 | var typecast = require('../../lib/cql/typecast/cast'); 3 | var columns = require('../../lib/cql/types'); 4 | 5 | describe('single typecasts', function () { 6 | it('works for integers', function () { 7 | var c = columns.Int('col'); 8 | expect(typecast(c, 2)).toBe(2); 9 | expect(typecast(c, '2')).toBe(2); 10 | expect(typecast(c, '2.5')).toBe(2); 11 | 12 | expect(function () { 13 | typecast(c, 'asdf'); 14 | }).toThrow(new errors.InvalidType()); 15 | }); 16 | it('works for floats', function () { 17 | var c = columns.Float('col'); 18 | expect(typecast(c, 2.5)).toBe(2.5); 19 | expect(typecast(c, '2')).toBe(2); 20 | expect(typecast(c, '2.5')).toBe(2.5); 21 | 22 | expect(function () { 23 | typecast(c, 'asdf'); 24 | }).toThrow(new errors.InvalidType()); 25 | }); 26 | it('works for strings', function () { 27 | var c = columns.Text('col'); 28 | expect(typecast(c, 2)).toBe('2'); 29 | expect(typecast(c, 'asdf')).toBe('asdf'); 30 | }); 31 | it('works for booleans', function () { 32 | var c = columns.Boolean('col'); 33 | expect(typecast(c, true)).toBe(true); 34 | expect(typecast(c, false)).toBe(false); 35 | expect(typecast(c, 1)).toBe(true); 36 | expect(typecast(c, 0)).toBe(false); 37 | }); 38 | it('works for timestamps', function () { 39 | var c = columns.Timestamp('col'); 40 | expect(typecast(c, 42)).toEqual(new Date(42)); 41 | expect(typecast(c, new Date(42))).toEqual(new Date(42)); 42 | expect(typecast(c, { toDate: function () { return new Date(42); }})).toEqual(new Date(42)); 43 | 44 | expect(function () { 45 | typecast(c, 'asdf'); 46 | }).toThrow(new errors.InvalidType()); 47 | }); 48 | it('works for buffer', function () { 49 | var c = columns.BLOB('col'); 50 | expect(Buffer.isBuffer(typecast(c, 'foo'))).toBe(true); 51 | expect(typecast(c, 'foo').toString('utf8')).toBe('foo'); 52 | }); 53 | }); 54 | 55 | describe('collection typecasts', function () { 56 | it('works on sets and lists', function () { 57 | var c = columns.Set('col', ['int']); 58 | expect(typecast(c, [1, '2.5', 3.6])).toEqual([1, 2, 3]); 59 | expect(function () { 60 | typecast(c, [1, 'foo', 3.6]); 61 | }).toThrow(new errors.InvalidType()); 62 | }); 63 | it('works for maps', function () { 64 | var c = columns.Map('col', ['int', 'int']); 65 | expect(typecast(c, { 66 | 1: '2', 67 | '2': 3, 68 | '4': '5.6' 69 | })).toEqual({ 1: 2, 2: 3, 4: 5}); 70 | 71 | 72 | expect(function () { 73 | typecast(c, { 'foo': 2 }); 74 | }).toThrow(new errors.InvalidType()); 75 | expect(function () { 76 | typecast(c, { 1: 'foo' }); 77 | }).toThrow(new errors.InvalidType()); 78 | }); 79 | 80 | it('works for tuples', function () { 81 | var c = columns.Tuple('col', ['int', 'text']); 82 | expect(typecast(c, ['1', '2'])).toEqual([1, '2']); 83 | expect(typecast(c, [1, 2])).toEqual([1, '2']); 84 | 85 | expect(function () { 86 | typecast(c, ['a', 2]); 87 | }).toThrow(new errors.InvalidType()); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /spec/cql/UpdateSpec.js: -------------------------------------------------------------------------------- 1 | var Update = require('../../lib/cql/queries/update'); 2 | var Raw = require('../../lib/cql/stmt/raw'); 3 | var Table = require('../../lib/cql/table'); 4 | var t = require('../../lib/cql/types'); 5 | 6 | describe('insert', function () { 7 | it('generates basic', function () { 8 | expect(new Update() 9 | .table(new Table('tbl')) 10 | .set('a', 'b') 11 | .parameterize() 12 | ).toEqual([['b'], 'UPDATE tbl SET a = ?;']); 13 | }); 14 | it('generates multiple', function () { 15 | expect(new Update() 16 | .table(new Table('tbl')) 17 | .set('a', 'b') 18 | .set('c', 2, 4) 19 | .parameterize() 20 | ).toEqual([['b', 2, 4], 'UPDATE tbl SET a = ?, c [?] = ?;']); 21 | }); 22 | it('using', function () { 23 | expect(new Update() 24 | .table(new Table('tbl')) 25 | .set('a', 'b') 26 | .timestamp(30) 27 | .ttl(60) 28 | .parameterize() 29 | ).toEqual([['b'], 'UPDATE tbl USING TIMESTAMP 30 AND TTL 60 SET a = ?;']); 30 | }); 31 | it('where', function () { 32 | expect(new Update() 33 | .table(new Table('tbl')) 34 | .set('a', 'b') 35 | .where('c', '=', 3) 36 | .parameterize() 37 | ).toEqual([['b', 3], 'UPDATE tbl SET a = ? WHERE c = ?;']); 38 | }); 39 | it('conditional', function () { 40 | expect(new Update() 41 | .table(new Table('tbl')) 42 | .set('a', 'b') 43 | .when('c', 3) 44 | .parameterize() 45 | ).toEqual([['b', 3], 'UPDATE tbl SET a = ? IF c = ?;']); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /spec/fake-connection.js: -------------------------------------------------------------------------------- 1 | var Connection = require('../lib/connection'); 2 | var Bluebird = require('bluebird'); 3 | 4 | module.exports = function () { 5 | var connection = new Connection(); 6 | 7 | connection.resolvedQuery = true; 8 | connection.queryLog = []; 9 | connection.execute = function () { 10 | connection.queryLog.push([].slice.call(arguments)); 11 | return Bluebird.resolve(connection.resolvedQuery); 12 | }; 13 | 14 | return connection; 15 | }; 16 | -------------------------------------------------------------------------------- /spec/helper.js: -------------------------------------------------------------------------------- 1 | var Connection = require('../lib/connection'); 2 | var connection = new Connection({ contactPoints: ['127.0.0.1'], keyspace: 'test' }); 3 | 4 | module.exports = function () { 5 | return connection.execute('DROP TABLE IF EXISTS users') 6 | .then(function () { 7 | return connection.execute( 8 | 'CREATE TABLE users (' + 9 | ' first_name text PRIMARY KEY,' + 10 | ' last_name text,' + 11 | ');'); 12 | }) 13 | .then(function () { 14 | return connection.execute( 15 | 'INSERT INTO users (first_name, last_name) VALUES (?, ?);', 16 | ["Connor", "Peet"] 17 | ); 18 | }) 19 | .bind(connection); 20 | }; 21 | -------------------------------------------------------------------------------- /spec/model/CollectionSpec.js: -------------------------------------------------------------------------------- 1 | var t = require('../../lib/cql/types'); 2 | var Connection = require('../fake-connection'); 3 | var Collection = require('../../lib/model/collection'); 4 | 5 | describe('collection', function () { 6 | var collection, connection; 7 | 8 | beforeEach(function () { 9 | connection = Connection(); 10 | collection = connection.Collection('UsersStuff'); 11 | }); 12 | 13 | it('generates the table', function () { 14 | expect(collection 15 | .columns([ 16 | t.Text('userid').index(), 17 | t.Set('emails', [t.Text()]), 18 | t.Text('name').partitionKey() 19 | ]) 20 | .table 21 | .toString() 22 | ).toBe('CREATE TABLE users_stuff (\r\n' + 23 | ' userid text,\r\n' + 24 | ' emails set,\r\n' + 25 | ' name text,\r\n' + 26 | ' PRIMARY KEY (name)\r\n' + 27 | ');\r\n' + 28 | 'CREATE INDEX ON users_stuff (userid);'); 29 | }); 30 | 31 | it('publishes columns', function () { 32 | var id = t.Text('uuid'); 33 | var name = t.Text('first_name'); 34 | collection.columns([id, name]); 35 | expect(collection.Uuid).toBe(id); 36 | expect(collection.FirstName).toBe(name); 37 | }); 38 | 39 | it('populates models on select', function (done) { 40 | connection.resolvedQuery = { rows: [{ a: 1 }, { b: 1 }], meta: 'data'}; 41 | collection.select().then(function (results) { 42 | expect(results.length).toBe(2); 43 | expect(results[0].isSynced()).toBe(true); 44 | expect(results.meta).toBe('data'); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('selects', function (done) { 50 | collection.select().then(function () { 51 | expect(connection.queryLog).toEqual([ 52 | ['SELECT * FROM users_stuff;', [], {}] 53 | ]); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('inserts', function (done) { 59 | collection.insert().data({ a: 'b' }).then(function () { 60 | expect(connection.queryLog).toEqual([ 61 | ['INSERT INTO users_stuff (a) VALUES (?);', ['b'], {}] 62 | ]); 63 | done(); 64 | }); 65 | }); 66 | 67 | it('updates', function (done) { 68 | collection.update().set('a', 'b').then(function () { 69 | expect(connection.queryLog).toEqual([ 70 | ['UPDATE users_stuff SET a = ?;', ['b'], {}] 71 | ]); 72 | done(); 73 | }); 74 | }); 75 | 76 | it('deletes', function (done) { 77 | collection.delete().where('a', '=', 'b').then(function () { 78 | expect(connection.queryLog).toEqual([ 79 | ['DELETE FROM users_stuff WHERE a = ?;', ['b'], {}] 80 | ]); 81 | done(); 82 | }); 83 | }); 84 | 85 | it('truncates', function (done) { 86 | collection.truncate().table('tbl').then(function () { 87 | expect(connection.queryLog).toEqual([ 88 | ['TRUNCATE tbl;', [], {}] 89 | ]); 90 | done(); 91 | }); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /spec/model/DiffSpec.js: -------------------------------------------------------------------------------- 1 | var diff = require('../../lib/model/diff'); 2 | var t = require('../../lib/cql/types'); 3 | 4 | describe('diff', function () { 5 | function record (method) { 6 | calls.push([].slice.call(arguments)); 7 | } 8 | var calls; 9 | var query = { 10 | setSimple: record.bind(null, 'setSimple'), 11 | add: record.bind(null, 'add'), 12 | subtract: record.bind(null, 'subtract') 13 | }; 14 | 15 | beforeEach(function () { 16 | calls = []; 17 | }); 18 | 19 | it('doesnt modify not changed', function () { 20 | var column = t.Int('a'); 21 | diff(query, column, 1, 1); 22 | expect(calls).toEqual([]); 23 | }); 24 | 25 | it('detects changed basic elements', function () { 26 | var column = t.Int('a'); 27 | diff(query, column, 1, 2); 28 | expect(calls).toEqual([['setSimple', column, 2]]); 29 | }); 30 | 31 | describe('sets', function () { 32 | var column = t.Set('a'); 33 | it('detects new values in array', function () { 34 | diff(query, column, [1, 2, 3], [1, 2, 3, 4]); 35 | expect(calls).toEqual([['add', column, [4]]]); 36 | }); 37 | it('detects removed values from array', function () { 38 | diff(query, column, [1, 2, 3], [2, 1]); 39 | expect(calls).toEqual([['subtract', column, [3]]]); 40 | }); 41 | it('detects reset', function () { 42 | diff(query, column, [1, 2, 3], []); 43 | expect(calls).toEqual([['setSimple', column, []]]); 44 | }); 45 | it('doesnt update when not necessary', function () { 46 | diff(query, column, [1, 2, 3], [1, 2, 3]); 47 | expect(calls).toEqual([]); 48 | }); 49 | it('works with large rewrites', function () { 50 | diff(query, column, [1, 2, 3], [4, 5, 6]); 51 | expect(calls).toEqual([['setSimple', column, [4, 5, 6]]]); 52 | }); 53 | it('doesnt break on empties', function () { 54 | diff(query, column, [], [1]); 55 | expect(calls).toEqual([['setSimple', column, [1]]]); 56 | calls = []; 57 | diff(query, column, [1], []); 58 | expect(calls).toEqual([['setSimple', column, []]]); 59 | calls = []; 60 | diff(query, column, [], []); 61 | expect(calls).toEqual([]); 62 | }); 63 | it('doesnt break on nulls', function () { 64 | diff(query, column, null, [1]); 65 | expect(calls).toEqual([['setSimple', column, [1]]]); 66 | calls = []; 67 | diff(query, column, [1], null); 68 | expect(calls).toEqual([['setSimple', column, null]]); 69 | }); 70 | }); 71 | 72 | describe('lists', function () { 73 | var column = t.List('a'); 74 | it('detects appended values', function () { 75 | diff(query, column, [1, 2, 3], [1, 2, 3, 4, 5]); 76 | expect(calls).toEqual([['add', column, [4, 5]]]); 77 | }); 78 | it('detects prepended values', function () { 79 | diff(query, column, [1, 2, 3], [-1, 0, 1, 2, 3]); 80 | expect(calls).toEqual([['add', [0, -1], column]]); 81 | }); 82 | it('detects removed values', function () { 83 | diff(query, column, [1, 2, 1, 2, 1], [1, 1, 1]); 84 | expect(calls).toEqual([['subtract', column, [2]]]); 85 | }); 86 | it('rewrites if not all removed (forward)', function () { 87 | diff(query, column, [1, 2, 1, 2, 1], [1, 2, 1, 1]); 88 | expect(calls).toEqual([['setSimple', column, [1, 2, 1, 1]]]); 89 | }); 90 | it('rewrites if not all removed (backward)', function () { 91 | diff(query, column, [1, 2, 1, 2, 1], [1, 1, 2, 1]); 92 | expect(calls).toEqual([['setSimple', column, [1, 1, 2, 1]]]); 93 | }); 94 | it('rewrites if changes in middle of list', function () { 95 | diff(query, column, [1, 2, 3, 4, 5], [1, 2, 4, 4, 5]); 96 | expect(calls).toEqual([['setSimple', column, [1, 2, 4, 4, 5]]]); 97 | }); 98 | it('doesnt update when not necessary', function () { 99 | diff(query, column, [1, 2, 3], [1, 2, 3]); 100 | expect(calls).toEqual([]); 101 | }); 102 | it('works with large rewrites', function () { 103 | diff(query, column, [1, 2, 3], [4, 5, 6]); 104 | expect(calls).toEqual([['setSimple', column, [4, 5, 6]]]); 105 | }); 106 | it('doesnt break on empties', function () { 107 | diff(query, column, [], [1]); 108 | expect(calls).toEqual([['setSimple', column, [1]]]); 109 | calls = []; 110 | diff(query, column, [1], []); 111 | expect(calls).toEqual([['setSimple', column, []]]); 112 | calls = []; 113 | diff(query, column, [], []); 114 | expect(calls).toEqual([]); 115 | }); 116 | it('doesnt break on nulls', function () { 117 | diff(query, column, null, [1]); 118 | expect(calls).toEqual([['setSimple', column, [1]]]); 119 | calls = []; 120 | diff(query, column, [1], null); 121 | expect(calls).toEqual([['setSimple', column, null]]); 122 | }); 123 | }); 124 | 125 | describe('sets', function () { 126 | var column = t.Map('a'); 127 | it('detects added values', function () { 128 | diff(query, column, { a: 1, b: 2 }, { a: 1, b: 2, c: 3 }); 129 | expect(calls).toEqual([['add', column, { c: 3 }]]); 130 | }); 131 | it('detects removed values', function () { 132 | diff(query, column, { a: 1, b: 2 }, { a: 1 }); 133 | expect(calls).toEqual([['subtract', column, ['b']]]); 134 | }); 135 | it('detects edited values', function () { 136 | diff(query, column, { a: 2, b: 2 }, { a: 1, b: 2 }); 137 | expect(calls).toEqual([['add', column, { a: 1 }]]); 138 | }); 139 | it('doesnt update when not necessary', function () { 140 | diff(query, column, { a: 1, b: 2 }, { a: 1, b: 2 }); 141 | expect(calls).toEqual([]); 142 | }); 143 | it('doesnt break on nulls', function () { 144 | diff(query, column, null, { a: 1 }); 145 | expect(calls).toEqual([['setSimple', column, { a: 1}]]); 146 | calls = []; 147 | diff(query, column, { a: 1 }, null); 148 | expect(calls).toEqual([['setSimple', column, null]]); 149 | }); 150 | }); 151 | describe('date', function () { 152 | it('doesnt change same', function () { 153 | var column = t.Timestamp('a'); 154 | diff(query, column, new Date(0), new Date(0)); 155 | expect(calls).toEqual([]); 156 | }); 157 | it('changes when different', function () { 158 | var column = t.Timestamp('a'); 159 | diff(query, column, new Date(0), new Date(1)); 160 | expect(calls).toEqual([['setSimple', column, new Date(1)]]); 161 | }); 162 | it('doesnt break on nulls', function () { 163 | var column = t.Timestamp('a'); 164 | diff(query, column, new Date(0), null); 165 | expect(calls).toEqual([['setSimple', column, null]]); 166 | calls = []; 167 | diff(query, column, null, new Date(0)); 168 | expect(calls).toEqual([['setSimple', column, new Date(0)]]); 169 | }); 170 | }); 171 | 172 | // describe('removal from sets and lists', function () { 173 | // it('detects removes from array end', function () { 174 | // diff(query, { a: [1, 2], b: [2] }, { a: [1], b: [2]}); 175 | // expect(calls).toEqual([['subtract', 'a', 2]]); 176 | // }); 177 | 178 | // it('detects removes from array middle', function () { 179 | // diff(query, { a: [1, 2, 3], b: [2] }, { a: [1, 3], b: [2]}); 180 | // expect(calls).toEqual([['subtract', 'a', 2]]); 181 | // }); 182 | 183 | // it('detects removes from array start', function () { 184 | // diff(query, { a: [1, 2, 3], b: [2] }, { a: [2, 3], b: [2]}); 185 | // expect(calls).toEqual([['subtract', 'a', 1]]); 186 | // }); 187 | // }); 188 | }); 189 | -------------------------------------------------------------------------------- /spec/model/ModelSpec.js: -------------------------------------------------------------------------------- 1 | var t = require('../../lib/cql/types'); 2 | var Connection = require('../fake-connection'); 3 | 4 | describe('Model', function () { 5 | var model, collection, connection; 6 | beforeEach(function () { 7 | connection = Connection(); 8 | collection = connection.Collection('foo'); 9 | collection.columns([ 10 | t.Int('a').partitionKey(), 11 | t.List('b', ['int']) 12 | ]); 13 | 14 | model = collection.new(); 15 | model.sync({ a: 1, b: [2, 3] }); 16 | }); 17 | 18 | it('guards hidden properties', function () { 19 | expect(Object.keys(model)).toEqual(['a', 'b']); 20 | }); 21 | 22 | it('extends works', function () { 23 | expect(model.toObject()).toEqual({ a: 1, b: [2, 3] }); 24 | model.extend({ a: 2, b : [3]}); 25 | expect(model.toObject()).toEqual({ a: 2, b: [3] }); 26 | model.extend({ a: 5 }); 27 | expect(model.toObject()).toEqual({ a: 5, b: [3] }); 28 | }); 29 | 30 | it('typecasts', function () { 31 | model.a = '2'; 32 | expect(model.toObject()).toEqual({ a: 2, b: [2, 3] }); 33 | }); 34 | 35 | it('clones old properties', function () { 36 | model.a = 2; 37 | model.b.push(4); 38 | expect(model._.old).toEqual({ a: 1, b: [2, 3] }); 39 | }); 40 | 41 | it('works with isDirty', function () { 42 | expect(model.isDirty('b')).toBe(false); 43 | model.b.push(4); 44 | expect(model.isDirty('b')).toBe(true); 45 | }); 46 | 47 | it('works with isSynced', function () { 48 | expect(model.isSynced()).toBe(true); 49 | model.b.push(4); 50 | expect(model.isSynced()).toBe(false); 51 | }); 52 | 53 | it('sync fixes casing', function () { 54 | model.sync({ A: 3 }); 55 | expect(model.a).toBe(3); 56 | }); 57 | 58 | it('gets raw object', function () { 59 | expect(model.toObject()).toEqual({ a: 1, b: [2, 3] }); 60 | }); 61 | 62 | it('json stringifies', function () { 63 | expect(model.toJson()).toBe('{"a":1,"b":[2,3]}'); 64 | }); 65 | 66 | describe('saving', function () { 67 | it('saves as new', function (done) { 68 | model.reset(); 69 | model.a = 1; 70 | model.b = [2, 3]; 71 | 72 | expect(model.isSynced()).toBe(false); 73 | model.save().then(function () { 74 | expect(connection.queryLog).toEqual([ 75 | ['INSERT INTO foo (a, b) VALUES (?, ?);', [1, [2, 3]], {}] 76 | ]); 77 | expect(model.isSynced()).toBe(true); 78 | done(); 79 | }); 80 | }); 81 | 82 | it('doesnt save when synced', function (done) { 83 | model.save().then(function () { 84 | expect(connection.queryLog).toEqual([]); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('applies options', function (done) { 90 | model.reset(); 91 | model.a = 1; 92 | model.b = [2, 3]; 93 | 94 | model.save({ ttl: 30 }).then(function () { 95 | expect(connection.queryLog).toEqual([ 96 | ['INSERT INTO foo (a, b) VALUES (?, ?) USING TTL 30;', [1, [2, 3]], {}] 97 | ]); 98 | done(); 99 | }); 100 | }); 101 | 102 | it('updates existing', function (done) { 103 | model.a = 2; 104 | model.b = [2, 3, 4]; 105 | 106 | expect(model.isSynced()).toBe(false); 107 | model.save().then(function () { 108 | expect(connection.queryLog).toEqual([ 109 | ['UPDATE foo SET a = ?, b = b + ? WHERE a = ?;', [2, [4], 1], {}] 110 | ]); 111 | expect(model.isSynced()).toBe(true); 112 | done(); 113 | }); 114 | }); 115 | 116 | it('deletes', function (done) { 117 | model.delete().then(function () { 118 | expect(connection.queryLog).toEqual([ 119 | ['DELETE FROM foo WHERE a = ?;', [1], {}] 120 | ]); 121 | done(); 122 | }); 123 | }); 124 | }); 125 | 126 | describe('middleware', function () { 127 | it('adds, runs single', function (done) { 128 | var spy = jasmine.createSpy('beforeUpdate'); 129 | collection.use('beforeUpdate', function (next) { 130 | spy(); 131 | next(); 132 | }); 133 | 134 | model.save(true).then(function () { 135 | expect(spy).toHaveBeenCalled(); 136 | done(); 137 | }); 138 | }); 139 | it('catches error in cb', function (done) { 140 | collection.use('beforeUpdate', function (next) { 141 | next('err'); 142 | }); 143 | 144 | model.save(true).catch(function (e) { 145 | expect(e).toBe('err'); 146 | done(); 147 | }); 148 | }); 149 | it('catches error when thrown', function (done) { 150 | collection.use('beforeUpdate', function (next) { 151 | throw 'err'; 152 | }); 153 | 154 | model.save(true).catch(function (e) { 155 | expect(e).toBe('err'); 156 | done(); 157 | }); 158 | }); 159 | it('handles multiple middleware', function (done) { 160 | var calls = []; 161 | collection.use('beforeUpdate', function (next) { 162 | calls.push('a'); 163 | next(); 164 | }); 165 | collection.use('beforeUpdate', function (next) { 166 | calls.push('b'); 167 | next(); 168 | }); 169 | collection.use('afterUpdate', function (next) { 170 | calls.push('c'); 171 | next(); 172 | }); 173 | 174 | model.save(true).then(function () { 175 | expect(calls).toEqual(['a', 'b', 'c']); 176 | done(); 177 | }); 178 | }); 179 | it('can define multiple at once', function (done) { 180 | var spy = jasmine.createSpy('up'); 181 | collection.use(['beforeUpdate', 'afterUpdate'], function (next) { 182 | spy(); 183 | next(); 184 | }); 185 | 186 | model.save(true).then(function () { 187 | expect(spy).toHaveBeenCalled(); 188 | expect(spy.calls.count()).toBe(2); 189 | done(); 190 | }); 191 | }); 192 | }); 193 | 194 | describe('getters and setters', function () { 195 | it('works with new models', function () { 196 | model = collection.new().extend({ a: 9 }); 197 | expect(model.isSynced()).toBe(false); 198 | }); 199 | it('updates value without setter', function () { 200 | model.a = 9; 201 | expect(model.a).toBe(9); 202 | }); 203 | it('works with setter alone', function () { 204 | model._.setters.a = function (value) { 205 | return value + 'c'; 206 | }; 207 | model.bindAccessors(); 208 | model.a = 'b'; 209 | expect(model.a).toBe('bc'); 210 | }); 211 | it('works with getter alone', function () { 212 | model._.getters.a = function (value) { 213 | return value + 'd'; 214 | }; 215 | model.bindAccessors(); 216 | model.a = 'b'; 217 | expect(model.a).toBe('bd'); 218 | }); 219 | it('works with both getter and setter', function () { 220 | model._.setters.a = function (value) { 221 | return value + 'c'; 222 | }; 223 | model._.getters.a = function (value) { 224 | return value + 'd'; 225 | }; 226 | model.bindAccessors(); 227 | model.a = 'b'; 228 | expect(model.a).toBe('bcd'); 229 | }); 230 | it('toObject functions correctly', function () { 231 | expect(model.toObject()).toEqual({ a: 1, b: [2, 3] }); 232 | model._.getters.a = function (value) { 233 | return value + 2; 234 | }; 235 | model.bindAccessors(); 236 | expect(model.toObject()).toEqual({ a: 3, b: [2, 3] }); 237 | expect(model.toObject(false)).toEqual({ a: 1, b: [2, 3] }); 238 | }); 239 | }); 240 | }); 241 | --------------------------------------------------------------------------------