diff --git a/.travis.yml b/.travis.yml index b1519f731..e845f63bf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ node_js: - "0.10" before_script: - npm install -g grunt-cli - - wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_2014-01-08.tar.gz + - wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_2014-04-24.tar.gz - tar xfz dynamodb_local_2014-01-08.tar.gz - cd dynamodb_local_2014-01-08 - java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -inMemory & - - cd .. \ No newline at end of file + - cd .. diff --git a/Readme.md b/Readme.md index befa8e7d7..2e60b15a6 100644 --- a/Readme.md +++ b/Readme.md @@ -10,7 +10,7 @@ Dynamoose uses the official [AWS SDK](https://github.com/aws/aws-sdk-js). ## Installation $ npm install dynamoose - + ## Stability **Unstable** This module is currently under development and functionally may change. @@ -189,7 +189,7 @@ Applies a default to the attribute's value when saving, if the values is null or If default is a function, the function is called, and the response is assigned to the attribute's value. -If it is a value, the value is simply assigned. +If it is a value, the value is simply assigned. **validate**: function | RegExp | value @@ -280,7 +280,7 @@ Overwrite existing item. Defaults to true. #### Model.create(object, options, callback) -Creates a new instance of the model and save the item in the table. +Creates a new instance of the model and save the item in the table. ```js Dog.create({ @@ -297,7 +297,7 @@ Dog.create({ #### Model.get(key, options, callback) -Gets an item from the table. +Gets an item from the table. ```js Dog.get('{ownerId: 4, name: 'Odie'}, function(err, odie) { @@ -308,7 +308,7 @@ Dog.get('{ownerId: 4, name: 'Odie'}, function(err, odie) { #### Model.delete(key, options, callback) -Deletes an item from the table. +Deletes an item from the table. ```js Dog.delete({ownerId: 4, name: 'Odie'}, function(err) { @@ -404,7 +404,11 @@ Executes the query against the table or index. #### query.where(rangeKey) -Set the range key of the table or index to query. +Set the range key of the table or index to query. + +#### query.filter(filter) + +Set the atribulte on which to filter. #### query.eq(value) @@ -477,7 +481,7 @@ Dog.scan({breed: {contains: 'Terrier'} }, function (err, dogs) { }); ``` -To get all the items in a table, do not provide a filter. +To get all the items in a table, do not provide a filter. ```js Dog.scan().exec(function (err, dogs, lastKey) { @@ -498,7 +502,7 @@ Executes a scan against a table For readability only. Scans us AND logic for multiple attributes. `and()` does not provide any functionality and can be omitted. -#### scan.where(filter) +#### scan.where(filter) | scan.filter(filter) Add additional attribute to the filter list. @@ -556,4 +560,4 @@ Start scan at key. Use LastEvaluatedKey returned in scan.exec() callback. #### scan.attributes(attributes) -Set the attributes to return. \ No newline at end of file +Set the attributes to return. diff --git a/lib/Query.js b/lib/Query.js index 19e01ca05..d2b51e372 100644 --- a/lib/Query.js +++ b/lib/Query.js @@ -23,7 +23,8 @@ function Query (Model, query, options) { // } this.query = {hashKey: {}}; - this.buildState = ''; + this.filters = {}; + this.buildState = false; var hashKeyName, hashKeyVal; if(typeof query === 'string') { @@ -95,6 +96,8 @@ Query.prototype.exec = function (next) { ComparisonOperator: 'EQ' }; + var i, val; + if(this.query.rangeKey) { var rangeKey = this.query.rangeKey; var rangeAttr = schema.attributes[rangeKey.name]; @@ -111,26 +114,48 @@ Query.prototype.exec = function (next) { } } - if(rangeKey.value === null || rangeKey.value === undefined) { + if(!rangeKey || rangeKey.values === undefined) { debug('No range key value (i.e. get all)'); - } else if(rangeKey.value2 === null || rangeKey.value2 === undefined) { - debug('Single range key value'); - queryReq.KeyConditions[rangeKey.name] = { - AttributeValueList: [rangeAttr.toDynamo(rangeKey.value)], - ComparisonOperator: rangeKey.comparison.toUpperCase() - }; } else { - debug('Two range key values'); - queryReq.KeyConditions[rangeKey.name] = { - AttributeValueList: [ - rangeAttr.toDynamo(rangeKey.value), - rangeAttr.toDynamo(rangeKey.value2) - ], + debug('Range key: %s', rangeKey.name); + var keyConditions = queryReq.KeyConditions[rangeKey.name] = { + AttributeValueList: [], ComparisonOperator: rangeKey.comparison.toUpperCase() }; + for (i = 0; i < rangeKey.values.length; i++) { + val = rangeKey.values [i]; + keyConditions.AttributeValueList.push( + rangeAttr.toDynamo(val, true) + ); + } + } + } + + + if(this.filters && Object.keys(this.filters).length > 0) { + queryReq.QueryFilter = {}; + for(var name in this.filters) { + debug('Filter on: %s', name); + var filter = this.filters[name]; + var filterAttr = schema.attributes[name]; + queryReq.QueryFilter[name] = { + AttributeValueList: [], + ComparisonOperator: filter.comparison.toUpperCase() + }; + if(filter.values) { + for (i = 0; i < filter.values.length; i++) { + val = filter.values[i]; + queryReq.QueryFilter[name].AttributeValueList.push( + filterAttr.toDynamo(val, true) + ); + } + } } + } + if(options.or) { + queryReq.ConditionalOperator = 'OR'; // defualts to AND } if(options.attributes) { @@ -149,7 +174,7 @@ Query.prototype.exec = function (next) { queryReq.Limit = 1; } - if(options.descending || options.ascending === false) { + if(options.descending) { queryReq.ScanIndexForward = false; } @@ -221,65 +246,146 @@ Query.prototype.where = function (rangeKey) { return this; }; -Query.prototype.eq = function (val) { - if(this.buildState !== 'hashKey' && this.buildState !== 'rangeKey') { - throw errors.QueryError('Invalid query state; eq must follow query(\'string\') or where(\'string\')'); +Query.prototype.filter = function (filter) { + if(this.buildState) { + throw errors.QueryError('Invalid query state; filter() must follow comparison'); + } + if(typeof filter === 'string') { + this.buildState = 'filter'; + this.currentFilter = filter; + if(this.filters[filter]) { + throw errors.QueryError('Invalid query state; %s filter can only be used once', filter); + } + this.filters[filter] = {name: filter}; } + + return this; +}; + +var VALID_RANGE_KEYS = ['EQ', 'LE', 'LT', 'GE', 'GT', 'BEGINS_WITH', 'BETWEEN']; +Query.prototype.compVal = function (vals, comp) { if(this.buildState === 'hashKey') { - this.query.hashKey.value = val; + if(comp !== 'EQ') { + throw errors.QueryError('Invalid query state; eq must follow query()'); + } + this.query.hashKey.value = vals[0]; + } else if (this.buildState === 'rangeKey'){ + if(VALID_RANGE_KEYS.indexOf(comp) < 0) { + throw errors.QueryError('Invalid query state; %s must follow filter()', comp); + } + this.query.rangeKey.values = vals; + this.query.rangeKey.comparison = comp; + } else if (this.buildState === 'filter') { + this.filters[this.currentFilter].values = vals; + this.filters[this.currentFilter].comparison = comp; } else { - this.query.rangeKey.value = val; - this.query.rangeKey.comparison = 'EQ'; - + throw errors.QueryError('Invalid query state; %s must follow query(), where() or filter()', comp); } - this.buildState = ''; + + this.buildState = false; + this.notState = false; return this; }; -Query.prototype.rangeVal = function (val, val2, comp) { - if(this.buildState !== 'rangeKey') { - throw errors.QueryError('Invalid query state; %s must follow where(\'string\')', comp); - } - if(!comp) { - comp = val2; - val2 = null; - } - this.query.rangeKey.value = val; - if(val2 !== null && val2 !== undefined) { - this.query.rangeKey.value2 = val2; - } - this.query.rangeKey.comparison = comp; +Query.prototype.and = function() { + this.options.or = false; + + return this; +}; - this.buildState = ''; +Query.prototype.or = function() { + this.options.or = true; return this; }; +Query.prototype.not = function() { + this.notState = true; + return this; +}; + +Query.prototype.null = function() { + if(this.notState) { + return this.compVal(null, 'NOT_NULL'); + } else { + return this.compVal(null, 'NULL'); + } +}; + + +Query.prototype.eq = function (val) { + if(this.notState) { + return this.compVal([val], 'NE'); + } else { + return this.compVal([val], 'EQ'); + } +}; + + Query.prototype.lt = function (val) { - return this.rangeVal(val, 'LT'); + if(this.notState) { + return this.compVal([val], 'GE'); + } else { + return this.compVal([val], 'LT'); + } }; Query.prototype.le = function (val) { - return this.rangeVal(val, 'LE'); + if(this.notState) { + return this.compVal([val], 'GT'); + } else { + return this.compVal([val], 'LE'); + } }; Query.prototype.ge = function (val) { - return this.rangeVal(val, 'GE'); + if(this.notState) { + return this.compVal([val], 'LT'); + } else { + return this.compVal([val], 'GE'); + } }; Query.prototype.gt = function (val) { - return this.rangeVal(val, 'GT'); + if(this.notState) { + return this.compVal([val], 'LE'); + } else { + return this.compVal([val], 'GT'); + } +}; + +Query.prototype.contains = function (val) { + if(this.notState) { + return this.compVal([val], 'NOT_CONTAINS'); + } else { + return this.compVal([val], 'CONTAINS'); + } }; Query.prototype.beginsWith = function (val) { - return this.rangeVal(val, 'BEGINS_WITH'); + if(this.notState) { + throw new errors.QueryError('Invalid Query state: beginsWith() cannot follow not()'); + } + return this.compVal([val], 'BEGINS_WITH'); +}; + +Query.prototype.in = function (vals) { + if(this.notState) { + throw new errors.QueryError('Invalid Query state: in() cannot follow not()'); + } + + return this.compVal(vals, 'IN'); }; Query.prototype.between = function (a, b) { - return this.rangeVal(a, b, 'BETWEEN'); + if(this.notState) { + throw new errors.QueryError('Invalid Query state: between() cannot follow not()'); + } + return this.compVal([a, b], 'BETWEEN'); }; + Query.prototype.limit = function (limit) { this.options.limit = limit; return this; diff --git a/lib/Scan.js b/lib/Scan.js index d5ef0a463..bc5da4507 100644 --- a/lib/Scan.js +++ b/lib/Scan.js @@ -111,7 +111,7 @@ Scan.prototype.and = function() { Scan.prototype.where = function (filter) { if(this.buildState) { - throw errors.ScanError('Invalid scan state; where() must follow eq()'); + throw errors.ScanError('Invalid scan state; where() must follow comparison'); } if(typeof filter === 'string') { this.buildState = filter; @@ -123,10 +123,11 @@ Scan.prototype.where = function (filter) { return this; }; +Scan.prototype.filter = Scan.prototype.where; Scan.prototype.compVal = function (vals, comp) { if(!this.buildState) { - throw errors.ScanError('Invalid scan state; %s must follow scan(\'string\') or where(\'string\')', comp); + throw errors.ScanError('Invalid scan state; %s must follow scan(), where(), or filter()', comp); } this.filters[this.buildState].values = vals; diff --git a/package.json b/package.json index f5e67aea7..7dd69e1a2 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ }, "dependencies": { "debug": "*", - "aws-sdk": "~2.0.0-rc13", + "aws-sdk": "2.0.0-rc.17", "q": "~1.0.1" } } diff --git a/test/Query.js b/test/Query.js index f3ab50d2d..ff3a3bd44 100644 --- a/test/Query.js +++ b/test/Query.js @@ -51,7 +51,7 @@ describe('Query', function (){ } }); - var Dog = dynamoose.model('Dog', dogSchema); + var Dog = dynamoose.model('Dog', dogSchema, {create: false}); function addDogs (dogs) { if(dogs.length <= 0) { @@ -66,7 +66,10 @@ describe('Query', function (){ }); } - setTimeout(function() { + Dog.$__.table.create(function (err) { + if(err) { + done(err); + } addDogs([ {ownerId:1, name: 'Foxy Lady', breed: 'Jack Russell Terrier', color: 'White, Brown and Black'}, {ownerId:2, name: 'Quincy', breed: 'Jack Russell Terrier', color: 'White and Brown'}, @@ -87,10 +90,9 @@ describe('Query', function (){ {ownerId:16, name: 'Marley', breed: 'Labrador Retriever', color: 'Yellow'}, {ownerId:17, name: 'Beethoven', breed: 'St. Bernard'}, {ownerId:18, name: 'Lassie', breed: 'Collie', color: 'tan and white'}, - {ownerId:19, name: 'Snoopy', breed: 'beagle', color: 'black and white'}]); - }, 1000); - - + {ownerId:19, name: 'Snoopy', breed: 'beagle', color: 'black and white'} + ]); + }); }); after(function (done) { @@ -232,4 +234,55 @@ describe('Query', function (){ done(); }); }); + + it('Basic Query on SGI with filter contains', function (done) { + var Dog = dynamoose.model('Dog'); + + Dog.query('breed').eq('Jack Russell Terrier') + .where('ownerId').eq(1) + .filter('color').contains('Black').exec() + .then(function (dogs) { + dogs.length.should.eql(1); + dogs[0].ownerId.should.eql(1); + done(); + }); + }); + + it('Basic Query on SGI with filter null', function (done) { + var Dog = dynamoose.model('Dog'); + + Dog.query('breed').eq('unknown') + .filter('color').not().null().exec() + .then(function (dogs) { + dogs.length.should.eql(5); + done(); + }); + }); + + it('Basic Query on SGI with filter not eq and not lt', function (done) { + var Dog = dynamoose.model('Dog'); + + Dog.query('breed').eq('unknown') + .filter('color').not().eq('Brown') + .and() + .filter('ownerId').not().lt(10).exec() + .then(function (dogs) { + dogs.length.should.eql(1); + dogs[0].ownerId.should.eql(11); + done(); + }); + }); + + it('Basic Query on SGI with filter not contains or beginsWith', function (done) { + var Dog = dynamoose.model('Dog'); + + Dog.query('breed').eq('Jack Russell Terrier') + .filter('color').not().contains('Brown') + .or() + .filter('name').beginsWith('Q').exec() + .then(function (dogs) { + dogs.length.should.eql(2); + done(); + }); + }); });