Skip to content

Commit

Permalink
Add support for QueryFilter. closes dynamoose#1
Browse files Browse the repository at this point in the history
  • Loading branch information
brandongoode committed May 17, 2014
1 parent 6379d09 commit d39910e
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 63 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ..
- cd ..
22 changes: 13 additions & 9 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand All @@ -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.

Expand Down Expand Up @@ -556,4 +560,4 @@ Start scan at key. Use LastEvaluatedKey returned in scan.exec() callback.

#### scan.attributes(attributes)

Set the attributes to return.
Set the attributes to return.
192 changes: 149 additions & 43 deletions lib/Query.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down Expand Up @@ -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];
Expand All @@ -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) {
Expand All @@ -149,7 +174,7 @@ Query.prototype.exec = function (next) {
queryReq.Limit = 1;
}

if(options.descending || options.ascending === false) {
if(options.descending) {
queryReq.ScanIndexForward = false;
}

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions lib/Scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
"dependencies": {
"debug": "*",
"aws-sdk": "~2.0.0-rc13",
"aws-sdk": "2.0.0-rc.17",
"q": "~1.0.1"
}
}
Loading

0 comments on commit d39910e

Please sign in to comment.