Comparing version 0.8.2 to 0.9.0
var assert = require('assert') | ||
var typ = require('typ') | ||
var typeUtil = require('./typeUtil') | ||
var ConditionJunction = require('./ConditionJunction') | ||
var util = require('util') | ||
/** | ||
* @param {Object=} opt_conditions | ||
* @constructor | ||
*/ | ||
function ConditionBuilder(opt_conditions) { | ||
this._conditions = opt_conditions || {} | ||
this._uniqueName = '' | ||
function ConditionBuilder() { | ||
this._exprs = [] | ||
} | ||
/** | ||
* @param {Array.<ConditionBuilder|ConditionJunction>} conditions | ||
* @return {ConditionJunction} | ||
* Creates a new Conditional expression with an operator (AND, EQ, etc) | ||
* @param {Op} op One of the ops defined in the Op enum. | ||
* @param {Array<ConditionExpr>} args The arguments to the op. | ||
* @return {ConditionExpr} | ||
*/ | ||
ConditionBuilder.prototype._op = function (op, args) { | ||
if (!op) throw new Error('Missing op') | ||
return new ConditionExprOp(op, args) | ||
} | ||
/** | ||
* Creates a new Conditional expression that evaluates to an attribute name | ||
* @param {string} name An attribute name | ||
* @return {ConditionExpr} | ||
*/ | ||
ConditionBuilder.prototype._attr = function (name) { | ||
return new ConditionExprAttr(name) | ||
} | ||
/** | ||
* Creates a new Conditional expression that evaluates to a value | ||
* @param {*} val Any literal value representable in a dynamodb expression | ||
* @return {ConditionExpr} | ||
*/ | ||
ConditionBuilder.prototype._val = function (val) { | ||
return new ConditionExprVal(val) | ||
} | ||
/** | ||
* Returns this condition builder as a ConditionExpr, the internal representation | ||
* of a condition. | ||
* @return {ConditionExpr} | ||
*/ | ||
ConditionBuilder.prototype._asExpr = function () { | ||
return this._op(Op.AND, this._exprs) | ||
} | ||
/** | ||
* @param {Array.<ConditionBuilder>} conditions | ||
* @return {ConditionBuilder} | ||
*/ | ||
ConditionBuilder.andConditions = function (conditions) { | ||
ConditionBuilder.validateConditions(conditions) | ||
return new ConditionJunction('AND', conditions) | ||
var builder = new ConditionBuilder() | ||
builder._exprs.push(builder._op(Op.AND, conditions.map(function (c) { | ||
return c._asExpr() | ||
}))) | ||
return builder | ||
} | ||
/** | ||
* @param {Array.<ConditionBuilder|ConditionJunction>} conditions | ||
* @return {ConditionJunction} | ||
* @param {Array.<ConditionBuilder>} conditions | ||
* @return {ConditionBuilder} | ||
*/ | ||
ConditionBuilder.orConditions = function (conditions) { | ||
ConditionBuilder.validateConditions(conditions) | ||
return new ConditionJunction('OR', conditions) | ||
var builder = new ConditionBuilder() | ||
builder._exprs.push(builder._op('OR', conditions.map(function (c) { | ||
return c._asExpr() | ||
}))) | ||
return builder | ||
} | ||
/** | ||
* @param {ConditionBuilder|ConditionJunction} condition | ||
* @return {ConditionJunction} | ||
* @param {ConditionBuilder} condition | ||
* @return {ConditionBuilder} | ||
*/ | ||
@@ -40,3 +84,5 @@ ConditionBuilder.notCondition = function (condition) { | ||
ConditionBuilder.validateConditions(conditions) | ||
return new ConditionJunction('NOT', conditions) | ||
var builder = new ConditionBuilder() | ||
builder._exprs.push(builder._op('NOT', [condition._asExpr()])) | ||
return builder | ||
} | ||
@@ -48,5 +94,4 @@ | ||
var condition = conditions[i] | ||
assert.ok(condition instanceof ConditionBuilder || | ||
condition instanceof ConditionJunction, | ||
'Expected ConditionBuilder or ConditionJunction') | ||
assert.ok(condition instanceof ConditionBuilder, | ||
'Expected ConditionBuilder') | ||
} | ||
@@ -56,19 +101,2 @@ } | ||
/** | ||
* @param {!Object} field | ||
* @param {Array.<ConditionBuilder>} conditions | ||
*/ | ||
ConditionBuilder.populateFieldFromConditionBuilderList = function (field, conditions) { | ||
if (conditions) { | ||
assert.ok(Array.isArray(conditions), 'Expected array') | ||
for (var i = 0; i < conditions.length; i++) { | ||
var condition = conditions[i] | ||
assert.ok(condition instanceof ConditionBuilder, 'Expected ConditionBuilder') | ||
for (var attr in condition._conditions) { | ||
field[attr] = condition._conditions[attr] | ||
} | ||
} | ||
} | ||
} | ||
/** | ||
* @param {Object} data | ||
@@ -78,3 +106,3 @@ * @param {string} fieldName The fieldName on `data` | ||
* @param {{count: number}} nameMutex | ||
* @return {ConditionBuilder|ConditionJunction} | ||
* @return {ConditionBuilder} | ||
*/ | ||
@@ -84,3 +112,3 @@ ConditionBuilder.populateExpressionField = function (data, fieldName, conditions, nameMutex) { | ||
var junction = new ConditionJunction('AND', conditions) | ||
var junction = ConditionBuilder.andConditions(conditions) | ||
junction.assignUniqueNames(nameMutex) | ||
@@ -106,6 +134,3 @@ | ||
ConditionBuilder.prototype.assignUniqueNames = function (nameMutex) { | ||
if (!nameMutex.count) { | ||
nameMutex.count = 1 | ||
} | ||
this._uniqueName = 'C' + nameMutex.count++ | ||
this._asExpr().assignUniqueNames(nameMutex) | ||
} | ||
@@ -115,3 +140,5 @@ | ||
ConditionBuilder.prototype.buildAttributeNames = function () { | ||
return typeUtil.buildAttributeNames(Object.keys(this._conditions)) | ||
var result = [] | ||
this._asExpr().appendAttributeNames(result) | ||
return typeUtil.buildAttributeNames(result) | ||
} | ||
@@ -122,10 +149,3 @@ | ||
var result = {} | ||
Object.keys(this._conditions).map(function (key) { | ||
var list = this._conditions[key].AttributeValueList | ||
if (!list) return | ||
list.map(function (value, index) { | ||
result[this._getValueAlias(key, index)] = value | ||
}, this) | ||
}, this) | ||
this._asExpr().appendAttributeValues(result) | ||
return result | ||
@@ -135,10 +155,2 @@ } | ||
/** | ||
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ExpressionPlaceholders.html#ExpressionAttributeValues | ||
*/ | ||
ConditionBuilder.prototype._getValueAlias = function (key, index) { | ||
if (!this._uniqueName) throw new Error('Names have not been assigned yet') | ||
return ':V' + this._uniqueName + 'X' + key + 'X' + index | ||
} | ||
/** | ||
* @return {string} String suitable for FilterExpression and KeyExpression | ||
@@ -148,56 +160,13 @@ * @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html#ConditionExpressionReference.Syntax | ||
ConditionBuilder.prototype.buildExpression = function () { | ||
var filters = Object.keys(this._conditions).map(function (key) { | ||
var operator = this._conditions[key].ComparisonOperator | ||
var attrAlias = typeUtil.getAttributeAlias(key) | ||
var valueAliases = [] | ||
if (this._conditions[key].AttributeValueList) { | ||
valueAliases = this._conditions[key].AttributeValueList.map(function (val, index) { | ||
return this._getValueAlias(key, index) | ||
}, this) | ||
} | ||
switch (operator) { | ||
case 'BEGINS_WITH': | ||
return 'begins_with(' + attrAlias + ', ' + valueAliases[0] + ')' | ||
case 'EQ': | ||
return '(' + attrAlias + ' = ' + valueAliases[0] + ')' | ||
case 'NE': | ||
return '(' + attrAlias + ' <> ' + valueAliases[0] + ')' | ||
case 'LE': | ||
return '(' + attrAlias + ' <= ' + valueAliases[0] + ')' | ||
case 'LT': | ||
return '(' + attrAlias + ' < ' + valueAliases[0] + ')' | ||
case 'GE': | ||
return '(' + attrAlias + ' >= ' + valueAliases[0] + ')' | ||
case 'GT': | ||
return '(' + attrAlias + ' > ' + valueAliases[0] + ')' | ||
case 'BETWEEN': | ||
return '(' + attrAlias + ' BETWEEN ' + valueAliases[0] + ' AND ' + valueAliases[1] + ')' | ||
case 'IN': | ||
return '(' + attrAlias + ' IN (' + valueAliases.join(', ') + '))' | ||
case 'NOT_CONTAINS': | ||
return '(attribute_exists(' + attrAlias + ') AND NOT contains(' + attrAlias + ', ' + valueAliases[0] + '))' | ||
case 'CONTAINS': | ||
return 'contains(' + attrAlias + ', ' + valueAliases[0] + ')' | ||
case 'NULL': | ||
return 'attribute_not_exists(' + attrAlias + ')' | ||
case 'NOT_NULL': | ||
return 'attribute_exists(' + attrAlias + ')' | ||
default: | ||
throw new Error('Invalid comparison operator \'' + operator + '\'') | ||
} | ||
}, this) | ||
return filters.join(' AND ') | ||
return this._asExpr().buildExpression() | ||
} | ||
/** | ||
* Iterate through all the conditions | ||
* @param {function(Object, string)} callback | ||
* Iterate through all the expressions. Intended for FakeDynamo | ||
* @param {function(Object)} callback | ||
*/ | ||
ConditionBuilder.prototype.forEachCondition = function (callback) { | ||
Object.keys(this._conditions).map(function (key) { | ||
callback(this._conditions[key], key) | ||
}, this) | ||
ConditionBuilder.prototype.visitExpressionsPostOrder = function (callback) { | ||
this._exprs.forEach(function (expr) { | ||
expr.visitPostOrder(callback) | ||
}) | ||
} | ||
@@ -210,44 +179,5 @@ | ||
ConditionBuilder.prototype.buildFilterFn = function () { | ||
var filters = Object.keys(this._conditions).map(function (key) { | ||
var operator = this._conditions[key].ComparisonOperator | ||
var values = [] | ||
if (this._conditions[key].AttributeValueList) { | ||
values = this._conditions[key].AttributeValueList.map(typeUtil.objectToValue) | ||
} | ||
switch (operator) { | ||
case 'BEGINS_WITH': | ||
return function(item) { return item[key].indexOf(values[0]) === 0 } | ||
case 'EQ': | ||
return function(item) { return item[key] == values[0] } | ||
case 'NE': | ||
return function(item) { return item[key] != values[0] } | ||
case 'LE': | ||
return function(item) { return item[key] <= values[0] } | ||
case 'LT': | ||
return function(item) { return item[key] < values[0] } | ||
case 'GE': | ||
return function(item) { return item[key] >= values[0] } | ||
case 'GT': | ||
return function(item) { return item[key] > values[0] } | ||
case 'BETWEEN': | ||
return function(item) { return item[key] >= values[0] && item[key] <= values[1] } | ||
case 'IN': | ||
return function(item) { return values.indexOf(item[key]) != -1 } | ||
case 'NOT_CONTAINS': | ||
return function(item) { return item[key].indexOf(values[0]) == -1 } | ||
case 'CONTAINS': | ||
return function(item) { return item[key].indexOf(values[0]) != -1 } | ||
case 'NULL': | ||
return function(item) { return !item.hasOwnProperty(key) } | ||
case 'NOT_NULL': | ||
return function(item) { return item.hasOwnProperty(key) } | ||
default: | ||
throw new Error('Invalid comparison operator \'' + operator + '\'') | ||
} | ||
}, this) | ||
var builder = this | ||
return function (item) { | ||
return filters.every(function (filter) { return filter(item) }) | ||
return !!builder._asExpr().evaluate(item) | ||
} | ||
@@ -261,64 +191,86 @@ } | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'EQ', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('EQ', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeEqualsAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('EQ', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeNotEquals = function (key, val) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'NE', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('NE', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeNotEqualsAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('NE', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeLessThanEqual = function (key, val) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'LE', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('LE', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeLessThanEqualAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('LE', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeLessThan = function (key, val) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'LT', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('LT', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeLessThanAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('LT', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeGreaterThanEqual = function (key, val) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'GE', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('GE', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeGreaterThanEqualAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('GE', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeGreaterThan = function (key, val) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'GT', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('GT', [this._attr(key), this._val(val)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeGreaterThanAttribute = function (key1, key2) { | ||
if (typ.isNullish(key1)) throw new Error("Key1 must be defined") | ||
if (typ.isNullish(key2)) throw new Error("Key2 must be defined") | ||
this._exprs.push(this._op('GT', [this._attr(key1), this._attr(key2)])) | ||
return this | ||
} | ||
ConditionBuilder.prototype.filterAttributeNotNull = function (key) { | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'NOT_NULL' | ||
} | ||
this._exprs.push(this._op('NOT_NULL', [this._attr(key)])) | ||
return this | ||
@@ -330,5 +282,3 @@ } | ||
if (typ.isNullish(key)) throw new Error("Key must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'NULL' | ||
} | ||
this._exprs.push(this._op('NULL', [this._attr(key)])) | ||
return this | ||
@@ -340,6 +290,3 @@ } | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'CONTAINS', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('CONTAINS', [this._attr(key), this._val(val)])) | ||
return this | ||
@@ -351,6 +298,3 @@ } | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'NOT_CONTAINS', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('NOT_CONTAINS', [this._attr(key), this._val(val)])) | ||
return this | ||
@@ -362,6 +306,3 @@ } | ||
if (typ.isNullish(val)) throw new Error("Val must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'BEGINS_WITH', | ||
AttributeValueList: [typeUtil.valueToObject(val)] | ||
} | ||
this._exprs.push(this._op('BEGINS_WITH', [this._attr(key), this._val(val)])) | ||
return this | ||
@@ -374,6 +315,3 @@ } | ||
if (typ.isNullish(val2)) throw new Error("Val 2 must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'BETWEEN', | ||
AttributeValueList: [typeUtil.valueToObject(val1), typeUtil.valueToObject(val2)] | ||
} | ||
this._exprs.push(this._op('BETWEEN', [this._attr(key), this._val(val1), this._val(val2)])) | ||
return this | ||
@@ -385,9 +323,288 @@ } | ||
if (typ.isNullish(vals)) throw new Error("Vals must be defined") | ||
this._conditions[key] = { | ||
ComparisonOperator: 'IN', | ||
AttributeValueList: vals.map(typeUtil.valueToObject) | ||
} | ||
this._exprs.push(this._op('IN', [this._attr(key)].concat(vals.map(this._val.bind(this))))) | ||
return this | ||
} | ||
/** | ||
* Represents an abstract conditional expression internally. | ||
* | ||
* May contain zero or more arguments, which are also ConditionExprs. | ||
* | ||
* @param {Array<ConditionExpr>} args | ||
* @constructor | ||
*/ | ||
function ConditionExpr(args) { | ||
this.args = args || [] | ||
this.uniqueName = '' | ||
} | ||
/** | ||
* Iterate through all the expressions | ||
* @param {function(Object)} callback | ||
*/ | ||
ConditionExpr.prototype.visitPostOrder = function (callback) { | ||
this.args.forEach(function (arg) { | ||
arg.visitPostOrder(callback) | ||
}) | ||
callback(this) | ||
} | ||
/** | ||
* @param {Object} nameMutex | ||
*/ | ||
ConditionExpr.prototype.assignUniqueNames = function (nameMutex) { | ||
if (!nameMutex.count) { | ||
nameMutex.count = 1 | ||
} | ||
this.visitPostOrder(function (expr) { | ||
expr.uniqueName = 'C' + nameMutex.count++ | ||
}) | ||
} | ||
/** | ||
* Appends the attribute names to the given array. | ||
*/ | ||
ConditionExpr.prototype.appendAttributeNames = function (result) { | ||
this.args.forEach(function (arg) { | ||
arg.appendAttributeNames(result) | ||
}) | ||
} | ||
/** | ||
* Appends the attribute values to the given object. | ||
*/ | ||
ConditionExpr.prototype.appendAttributeValues = function (result) { | ||
this.args.forEach(function (arg) { | ||
arg.appendAttributeValues(result) | ||
}) | ||
} | ||
/** | ||
* Builds an expression for consumption by the AWS expression API | ||
*/ | ||
ConditionExpr.prototype.buildExpression = function () { | ||
throw new Error('Expected buildExpression impl') | ||
} | ||
/** | ||
* Client-side evaluation. | ||
*/ | ||
ConditionExpr.prototype.evaluate = function () { | ||
throw new Error('Expected evaluate impl') | ||
} | ||
/** | ||
* Represents a conditional expression with 1 or more arguments. | ||
* @param {Op} op One of the ops defined in the Op enum. | ||
* @param {Array<ConditionExpr>} args The arguments to the op. | ||
* @constructor | ||
*/ | ||
function ConditionExprOp(op, args) { | ||
ConditionExpr.call(this, args) | ||
this.op = op | ||
} | ||
util.inherits(ConditionExprOp, ConditionExpr) | ||
/** | ||
* Builds an expression for arg i, asserting that the arg exists | ||
*/ | ||
ConditionExprOp.prototype.buildArgExpr = function (i) { | ||
if (i >= this.args.length) throw new Error('Operator ' + this.op + ' expected arg at position ' + i) | ||
return this.args[i].buildExpression() | ||
} | ||
/** @override */ | ||
ConditionExprOp.prototype.buildExpression = function () { | ||
var operator = this.op | ||
switch (operator) { | ||
case Op.NOT: | ||
return '(NOT ' + this.buildArgExpr(0) + ')' | ||
case Op.AND: | ||
if (this.args.length == 0) return '' | ||
if (this.args.length == 1) return this.buildArgExpr(0) | ||
var andExprs = this.args.map(function (arg) { | ||
return arg.buildExpression() | ||
}, this) | ||
return '(' + andExprs.join(' AND ') + ')' | ||
case Op.OR: | ||
if (this.args.length == 0) return '' | ||
if (this.args.length == 1) return this.buildArgExpr(0) | ||
var orExprs = this.args.map(function (arg) { | ||
return arg.buildExpression() | ||
}, this) | ||
return '(' + orExprs.join(' OR ') + ')' | ||
case Op.BEGINS_WITH: | ||
return 'begins_with(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + ')' | ||
case Op.EQ: | ||
return '(' + this.buildArgExpr(0) + ' = ' + this.buildArgExpr(1) + ')' | ||
case Op.NE: | ||
return '(' + this.buildArgExpr(0) + ' <> ' + this.buildArgExpr(1) + ')' | ||
case Op.LE: | ||
return '(' + this.buildArgExpr(0) + ' <= ' + this.buildArgExpr(1) + ')' | ||
case Op.LT: | ||
return '(' + this.buildArgExpr(0) + ' < ' + this.buildArgExpr(1) + ')' | ||
case Op.GE: | ||
return '(' + this.buildArgExpr(0) + ' >= ' + this.buildArgExpr(1) + ')' | ||
case Op.GT: | ||
return '(' + this.buildArgExpr(0) + ' > ' + this.buildArgExpr(1) + ')' | ||
case Op.BETWEEN: | ||
return '(' + this.buildArgExpr(0) + ' BETWEEN ' + this.buildArgExpr(1) + ' AND ' + this.buildArgExpr(2) + ')' | ||
case Op.IN: | ||
var values = this.args.slice(1).map(function (arg) { | ||
return arg.buildExpression() | ||
}, this) | ||
return '(' + this.buildArgExpr(0) + ' IN (' + values + '))' | ||
case Op.NOT_CONTAINS: | ||
return '(attribute_exists(' + this.buildArgExpr(0) + ') AND NOT contains(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + '))' | ||
case Op.CONTAINS: | ||
return 'contains(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + ')' | ||
case Op.NULL: | ||
return 'attribute_not_exists(' + this.buildArgExpr(0) + ')' | ||
case Op.NOT_NULL: | ||
return 'attribute_exists(' + this.buildArgExpr(0) + ')' | ||
default: | ||
throw new Error('Invalid comparison operator \'' + operator + '\'') | ||
} | ||
} | ||
/** | ||
* Evaluate the value for arg i, asserting that the arg exists | ||
*/ | ||
ConditionExprOp.prototype.evalArg = function (i, item) { | ||
if (i >= this.args.length) throw new Error('Operator ' + this.op + ' expected arg at position ' + i) | ||
return this.args[i].evaluate(item) | ||
} | ||
/** | ||
* Evaluate the value for arg i, asserting that the arg exists | ||
*/ | ||
ConditionExprOp.prototype.evalArgs = function (item) { | ||
return this.args.map(function (arg) { | ||
return arg.evaluate(item) | ||
}) | ||
} | ||
/** @override */ | ||
ConditionExprOp.prototype.evaluate = function (item) { | ||
var argExprs = this.args | ||
var operator = this.op | ||
switch (operator) { | ||
case Op.NOT: | ||
return !this.evalArg(0, item) | ||
case Op.AND: | ||
return this.evalArgs(item).every(function (val) { | ||
return val | ||
}) | ||
case Op.OR: | ||
return this.evalArgs(item).some(function (val) { | ||
return val | ||
}) | ||
case Op.BEGINS_WITH: | ||
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) === 0 | ||
case Op.EQ: | ||
return item && this.evalArg(0, item) == this.evalArg(1, item) | ||
case Op.NE: | ||
return item && this.evalArg(0, item) != this.evalArg(1, item) | ||
case Op.LE: | ||
return item && this.evalArg(0, item) <= this.evalArg(1, item) | ||
case Op.LT: | ||
return item && this.evalArg(0, item) < this.evalArg(1, item) | ||
case Op.GE: | ||
return item && this.evalArg(0, item) >= this.evalArg(1, item) | ||
case Op.GT: | ||
return this.evalArg(0, item) > this.evalArg(1, item) | ||
case Op.BETWEEN: | ||
return item && this.evalArg(0, item) >= this.evalArg(1, item) && this.evalArg(0, item) <= this.evalArg(2, item) | ||
case Op.IN: | ||
return item && this.evalArgs(item).slice(1).indexOf(this.evalArg(0, item)) != -1 | ||
case Op.NOT_CONTAINS: | ||
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) == -1 | ||
case Op.CONTAINS: | ||
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) != -1 | ||
case Op.NULL: | ||
return item && !item.hasOwnProperty(argExprs[0].name) | ||
case Op.NOT_NULL: | ||
return item && item.hasOwnProperty(argExprs[0].name) | ||
default: | ||
throw new Error('Invalid comparison operator \'' + operator + '\'') | ||
} | ||
} | ||
/** | ||
* Represents a value in a conditional expression. | ||
* @param {*} val Any literal value representable in a dynamodb expression | ||
* @constructor | ||
*/ | ||
function ConditionExprVal(val) { | ||
ConditionExpr.call(this, []) | ||
this.val = typeUtil.valueToObject(val) | ||
} | ||
util.inherits(ConditionExprVal, ConditionExpr) | ||
/** @override */ | ||
ConditionExprVal.prototype.buildExpression = function () { | ||
return ':V' + this.uniqueName | ||
} | ||
/** @override */ | ||
ConditionExprVal.prototype.appendAttributeValues = function (result) { | ||
result[this.buildExpression()] = this.val | ||
} | ||
/** @override */ | ||
ConditionExprVal.prototype.evaluate = function () { | ||
return typeUtil.objectToValue(this.val) | ||
} | ||
/** | ||
* Represents an item attribute in a conditional expression. | ||
* @param {string} name An attribute name | ||
* @constructor | ||
*/ | ||
function ConditionExprAttr(name) { | ||
ConditionExpr.call(this, []) | ||
this.name = name | ||
} | ||
util.inherits(ConditionExprAttr, ConditionExpr) | ||
/** @override */ | ||
ConditionExprAttr.prototype.buildExpression = function () { | ||
return typeUtil.getAttributeAlias(this.name) | ||
} | ||
/** @override */ | ||
ConditionExprAttr.prototype.evaluate = function (item) { | ||
return item[this.name] | ||
} | ||
/** @override */ | ||
ConditionExprAttr.prototype.appendAttributeNames = function (result) { | ||
result.push(this.name) | ||
} | ||
/** | ||
* An enum of all the possible Ops in our ConditionExpr syntax tree. | ||
*/ | ||
var Op = { | ||
NOT: 'NOT', | ||
AND: 'AND', | ||
OR: 'OR', | ||
BEGINS_WITH: 'BEGINS_WITH', | ||
EQ: 'EQ', | ||
NE: 'NE', | ||
LE: 'LE', | ||
LT: 'LT', | ||
GE: 'GE', | ||
GT: 'GT', | ||
BETWEEN: 'BETWEEN', | ||
IN: 'IN', | ||
NOT_CONTAINS: 'NOT_CONTAINS', | ||
CONTAINS: 'CONTAINS', | ||
NULL: 'NULL', | ||
NOT_NULL: 'NOT_NULL' | ||
} | ||
module.exports = ConditionBuilder |
var ConditionBuilder = require('./ConditionBuilder') | ||
var UpdateExpressionBuilder = require('./UpdateExpressionBuilder') | ||
var typeUtil = require('./typeUtil') | ||
@@ -16,2 +17,4 @@ | ||
this._filterBuilder = null | ||
this._conditionBuilder = null | ||
this._updateExpressionBuilder = null | ||
} | ||
@@ -64,5 +67,5 @@ | ||
DynamoRequest.prototype.setUpdates = function (attributeUpdates) { | ||
// TODO(nick): Switch this to use UpdateExpression | ||
if (attributeUpdates) { | ||
this.data.AttributeUpdates = attributeUpdates | ||
this._updateExpressionBuilder = UpdateExpressionBuilder.populateUpdateExpression( | ||
this.data, attributeUpdates, this._nameMutex) | ||
} | ||
@@ -124,5 +127,6 @@ return this | ||
DynamoRequest.prototype.setExpected = function (conditions) { | ||
// TODO(nick): Switch this to use ConditionExpression | ||
this.data.Expected = {} | ||
ConditionBuilder.populateFieldFromConditionBuilderList(this.data.Expected, conditions) | ||
if (conditions) { | ||
this._conditionBuilder = ConditionBuilder.populateExpressionField( | ||
this.data, 'ConditionExpression', conditions, this._nameMutex) | ||
} | ||
return this | ||
@@ -276,2 +280,10 @@ } | ||
if (!this.data.ConditionExpression) { | ||
delete this.data.ConditionExpression | ||
} | ||
if (!this.data.UpdateExpression) { | ||
delete this.data.UpdateExpression | ||
} | ||
return this.data | ||
@@ -278,0 +290,0 @@ } |
@@ -15,3 +15,7 @@ var Q = require('kew') | ||
function forEachKeyCondition(data, callback) { | ||
return data._requestBuilder._keyConditionBuilder.forEachCondition(callback) | ||
return data._requestBuilder._keyConditionBuilder.visitExpressionsPostOrder(function (expr) { | ||
if (expr.op && expr.args[0] && expr.args[0].name) { | ||
callback(expr, expr.args[0].name) | ||
} | ||
}) | ||
} | ||
@@ -38,2 +42,7 @@ | ||
function getConditionFn(data) { | ||
var builder = data._requestBuilder._conditionBuilder | ||
return builder ? builder.buildFilterFn() : function() { return true } | ||
} | ||
/** | ||
@@ -159,3 +168,3 @@ * @param {string} name | ||
var hash = data.HashKeyValue || | ||
typeUtil.objectToValue(getKeyConditionByName(data, this.primaryKey.hash.name).AttributeValueList[0]) | ||
getKeyConditionByName(data, this.primaryKey.hash.name).args[1].evaluate() | ||
@@ -226,3 +235,3 @@ var indexedKeyValues = data.IndexName ? this._getIndexedKeyValuesForHash(hash, indexedKeyName) : this._getRangeKeyValuesForHash(hash) | ||
forEachKeyCondition(data, function (condition, key) { | ||
if (!sortByKey && condition.ComparisonOperator != 'EQ') { | ||
if (!sortByKey && condition.op != 'EQ') { | ||
sortByKey = key | ||
@@ -333,7 +342,6 @@ } | ||
var key = this._extractKey(data) | ||
if (data.Expected) { | ||
var item = this._getItemByKey(key) | ||
this._checkExpected(data.Expected, item) | ||
} | ||
var item = this._getItemByKey(key) | ||
this._checkExpected(getConditionFn(data), item) | ||
// create the item to store | ||
@@ -367,5 +375,3 @@ var obj = {} | ||
// run the conditional if it exists | ||
if (data.Expected) { | ||
this._checkExpected(data.Expected, item) | ||
} | ||
this._checkExpected(getConditionFn(data), item) | ||
@@ -384,3 +390,3 @@ var itemExists = !!item | ||
var oldItem = typeUtil.packObjectOrArray(item) | ||
var newItem = localUpdater.update(oldItem, data.AttributeUpdates) | ||
var newItem = localUpdater.update(oldItem, data._requestBuilder._updateExpressionBuilder._attributes) | ||
@@ -406,5 +412,3 @@ // store the item | ||
var item = this._getItemByKey(key) | ||
if (data.Expected) { | ||
this._checkExpected(data.Expected, item) | ||
} | ||
this._checkExpected(getConditionFn(data), item) | ||
this._putItemAtKey(key, undefined) | ||
@@ -467,24 +471,8 @@ | ||
* | ||
* @param {Object} expected expected data | ||
* @param {function(Object)} filterFn A filter function that returns false if the item is filtered, true if allowed. | ||
* @param {Object|undefined} currentData current data for the data being changed | ||
*/ | ||
FakeTable.prototype._checkExpected = function(expected, currentData) { | ||
for (var key in expected) { | ||
// TODO(nathan) Combine with ConditionBuilder.buildFilterFn | ||
switch (expected[key].ComparisonOperator) { | ||
case 'EQ': | ||
if (!currentData) { | ||
throw new DynamoError('ConditionalCheckFailedException', 'Attribute (' + key + ') missing when expected.') | ||
} | ||
var value = typeUtil.objectToValue(expected[key].AttributeValueList[0]) | ||
if (currentData[key] !== value) { | ||
throw new DynamoError('ConditionalCheckFailedException', 'Values are different than expected.') | ||
} | ||
break | ||
case 'NULL': | ||
if (currentData && !typ.isNullish(currentData[key])) { | ||
throw new DynamoError('ConditionalCheckFailedException', 'Attribute (' + key + ') found when none expected.') | ||
} | ||
break | ||
} | ||
FakeTable.prototype._checkExpected = function(filterFn, currentData) { | ||
if (!filterFn(currentData)) { | ||
throw new DynamoError('ConditionalCheckFailedException', 'Values are different than expected.') | ||
} | ||
@@ -491,0 +479,0 @@ } |
@@ -26,2 +26,4 @@ var typ = require('typ') | ||
this._returnValues = UpdateBuilder.RETURN_VALUES.ALL_OLD | ||
this._uniqueName = '' | ||
} | ||
@@ -28,0 +30,0 @@ util.inherits(UpdateBuilder, Builder) |
{ | ||
"name": "dynamite", | ||
"description": "promise-based DynamoDB client", | ||
"version": "0.8.2", | ||
"version": "0.9.0", | ||
"homepage": "https://github.com/Medium/dynamite", | ||
@@ -6,0 +6,0 @@ "license": "Apache-2.0", |
@@ -387,4 +387,4 @@ // Copyright 2013 The Obvious Corporation. | ||
test.equal('{"#comment":"comment"}', JSON.stringify(data.ExpressionAttributeNames)) | ||
test.equal('{":VC1XcommentX0":{"S":"HEY"}}', JSON.stringify(data.ExpressionAttributeValues)) | ||
test.equal('begins_with(#comment, :VC1XcommentX0)', data.KeyConditionExpression) | ||
test.equal('{":VC2":{"S":"HEY"}}', JSON.stringify(data.ExpressionAttributeValues)) | ||
test.equal('begins_with(#comment, :VC2)', data.KeyConditionExpression) | ||
test.done() | ||
@@ -391,0 +391,0 @@ }) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
249051
46
6866