UNPKG

dynalite

Version:

An implementation of Amazon's DynamoDB built on LevelDB

1,387 lines (1,318 loc) 39.4 kB
var Big = require('big.js'), db = require('../db'), conditionParser = require('../db/conditionParser'), projectionParser = require('../db/projectionParser'), updateParser = require('../db/updateParser') exports.checkTypes = checkTypes exports.checkValidations = checkValidations exports.toLowerFirst = toLowerFirst exports.findDuplicate = findDuplicate exports.validateAttributeValue = validateAttributeValue exports.validateConditions = validateConditions exports.validateAttributeConditions = validateAttributeConditions exports.validateExpressionParams = validateExpressionParams exports.validateExpressions = validateExpressions exports.convertKeyCondition = convertKeyCondition function checkTypes (data, types) { var key for (key in data) { // TODO: deal with nulls if (!types[key] || data[key] == null) delete data[key] } return Object.keys(types).reduce(function (newData, key) { var val = checkType(data[key], types[key]) if (val != null) newData[key] = val return newData }, {}) function typeError (msg) { var err = new Error(msg) err.statusCode = 400 err.body = { __type: 'com.amazon.coral.service#SerializationException', Message: msg, } return err } function checkType (val, type) { if (val == null) return null var children = type.children if (typeof children == 'string') children = { type: children } if (type.type) type = type.type var subtypeMatch = type.match(/(.+?)<(.+)>$/), subtype if (subtypeMatch != null) { type = subtypeMatch[1] subtype = subtypeMatch[2] } if (type == 'AttrStruct') { return checkType(val, { type: subtype + '<AttributeValue>', children: { S: 'String', B: 'Blob', N: 'String', BOOL: 'Boolean', NULL: 'Boolean', BS: { type: 'List', children: 'Blob', }, NS: { type: 'List', children: 'String', }, SS: { type: 'List', children: 'String', }, L: { type: 'List', children: 'AttrStruct<ValueStruct>', }, M: { type: 'Map<AttributeValue>', children: 'AttrStruct<ValueStruct>', }, }, }) } switch (type) { case 'Boolean': switch (typeof val) { case 'number': throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type) case 'string': // 'true'/'false'/'1'/'0'/'no'/'yes' seem to convert fine val = val.toUpperCase() if (~[ 'TRUE', '1', 'YES' ].indexOf(val)) { val = true } else if (~[ 'FALSE', '0', 'NO' ].indexOf(val)) { val = false } else { throw typeError('Unexpected token received from parser') } break case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type) throw typeError('Start of structure or map found where not expected') } return val case 'Short': case 'Integer': case 'Long': case 'Double': switch (typeof val) { case 'boolean': throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type) case 'number': if (type != 'Double') val = Math.floor(val) break case 'string': throw typeError('STRING_VALUE cannot be converted to ' + type) case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type) throw typeError('Start of structure or map found where not expected') } return val case 'String': switch (typeof val) { case 'boolean': throw typeError((val ? 'TRUE_VALUE' : 'FALSE_VALUE') + ' cannot be converted to ' + type) case 'number': throw typeError((val % 1 === 0 ? 'NUMBER_VALUE' : 'DECIMAL_VALUE') + ' cannot be converted to ' + type) case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.lang.' + type) throw typeError('Start of structure or map found where not expected') } return val case 'Blob': switch (typeof val) { case 'boolean': case 'number': throw typeError('only base-64-encoded strings are convertible to bytes') case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class java.nio.ByteBuffer') throw typeError('Start of structure or map found where not expected') } if (val.length % 4) throw typeError('Base64 encoded length is expected a multiple of 4 bytes but found: ' + val.length) // TODO: need a better check than this... if (Buffer.from(val, 'base64').toString('base64') != val) throw typeError('Invalid last non-pad Base64 character dectected') return val case 'List': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError('Unexpected field type') case 'object': if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected') } return val.map(function (child) { return checkType(child, children) }) case 'ParameterizedList': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError("class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to class java.lang.Class (sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl and java.lang.Class are in module java.base of loader 'bootstrap')") case 'object': if (!Array.isArray(val)) throw typeError('Start of structure or map found where not expected') } return val.map(function (child) { return checkType(child, children) }) case 'ParameterizedMap': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError("class sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl cannot be cast to class java.lang.Class (sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl and java.lang.Class are in module java.base of loader 'bootstrap')") case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type java.util.Map<java.lang.String, com.amazonaws.dynamodb.v20120810.AttributeValue>') } return Object.keys(val).reduce(function (newVal, key) { newVal[key] = checkType(val[key], children) return newVal }, {}) case 'Map': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError('Unexpected field type') case 'object': if (Array.isArray(val)) { throw typeError('Unrecognized collection type java.util.Map<java.lang.String, ' + (~subtype.indexOf('.') ? subtype : 'com.amazonaws.dynamodb.v20120810.' + subtype) + '>') } } return Object.keys(val).reduce(function (newVal, key) { newVal[key] = checkType(val[key], children) return newVal }, {}) case 'ValueStruct': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError('Unexpected value type in payload') case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype) } return checkTypes(val, children) case 'FieldStruct': switch (typeof val) { case 'boolean': case 'number': case 'string': throw typeError('Unexpected field type') case 'object': if (Array.isArray(val)) throw typeError('Unrecognized collection type class com.amazonaws.dynamodb.v20120810.' + subtype) } return checkTypes(val, children) default: throw new Error('Unknown type: ' + type) } } } var validateFns = {} function checkValidations (data, validations, custom, store) { var attr, msg, errors = [] for (attr in validations) { if (validations[attr].required && data[attr] == null) { throw db.validationError('The parameter \'' + attr + '\' is required but was not present in the request') } if (validations[attr].tableName) { msg = validateTableName(attr, data[attr]) if (msg) throw db.validationError(msg) } } function checkNonRequireds (data, types, parent) { for (attr in types) { checkNonRequired(attr, data[attr], types[attr], parent) } } checkNonRequireds(data, validations) function checkNonRequired (attr, data, validations, parent) { if (validations == null || typeof validations != 'object') return for (var validation in validations) { if (errors.length >= 10) return if (~[ 'type', 'required', 'tableName' ].indexOf(validation)) continue if (validation != 'notNull' && data == null) continue if (validation == 'children') { if (/List$/.test(validations.type)) { for (var i = 0; i < data.length; i++) { checkNonRequired('member', data[i], validations.children, (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + (i + 1)) } continue } else if (/Map/.test(validations.type)) { Object.keys(data).forEach(function (key) { checkNonRequired('member', data[key], validations.children, (parent ? parent + '.' : '') + toLowerFirst(attr) + '.' + key) }) continue } checkNonRequireds(data, validations.children, (parent ? parent + '.' : '') + toLowerFirst(attr)) continue } validateFns[validation](parent, attr, validations[validation], data, errors) } } if (errors.length) throw db.validationError(errors.length + ' validation error' + (errors.length > 1 ? 's' : '') + ' detected: ' + errors.join('; ')) if (custom) { msg = custom(data, store) if (msg) throw db.validationError(msg) } } validateFns.notNull = function (parent, key, val, data, errors) { validate(data != null, 'Member must not be null', data, parent, key, errors) } validateFns.greaterThanOrEqual = function (parent, key, val, data, errors) { validate(data >= val, 'Member must have value greater than or equal to ' + val, data, parent, key, errors) } validateFns.lessThanOrEqual = function (parent, key, val, data, errors) { validate(data <= val, 'Member must have value less than or equal to ' + val, data, parent, key, errors) } validateFns.regex = function (parent, key, pattern, data, errors) { validate(RegExp('^' + pattern + '$').test(data), 'Member must satisfy regular expression pattern: ' + pattern, data, parent, key, errors) } validateFns.lengthGreaterThanOrEqual = function (parent, key, val, data, errors) { var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length validate(length >= val, 'Member must have length greater than or equal to ' + val, data, parent, key, errors) } validateFns.lengthLessThanOrEqual = function (parent, key, val, data, errors) { var length = (typeof data == 'object' && !Array.isArray(data)) ? Object.keys(data).length : data.length validate(length <= val, 'Member must have length less than or equal to ' + val, data, parent, key, errors) } validateFns.enum = function (parent, key, val, data, errors) { validate(~val.indexOf(data), 'Member must satisfy enum value set: [' + val.join(', ') + ']', data, parent, key, errors) } validateFns.keys = function (parent, key, val, data, errors) { Object.keys(data).forEach(function (mapKey) { try { Object.keys(val).forEach(function (validation) { validateFns[validation]('', '', val[validation], mapKey, []) }) } catch { var msgs = Object.keys(val).map(function (validation) { if (validation == 'lengthGreaterThanOrEqual') return 'Member must have length greater than or equal to ' + val[validation] if (validation == 'lengthLessThanOrEqual') return 'Member must have length less than or equal to ' + val[validation] if (validation == 'regex') return 'Member must satisfy regular expression pattern: ' + val[validation] }) validate(false, 'Map keys must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors) } }) } validateFns.values = function (parent, key, val, data, errors) { Object.keys(data).forEach(function (mapKey) { try { Object.keys(val).forEach(function (validation) { validateFns[validation]('', '', val[validation], data[mapKey], []) }) } catch { var msgs = Object.keys(val).map(function (validation) { if (validation == 'lengthGreaterThanOrEqual') return 'Member must have length greater than or equal to ' + val[validation] if (validation == 'lengthLessThanOrEqual') return 'Member must have length less than or equal to ' + val[validation] }) validate(false, 'Map value must satisfy constraint: [' + msgs.join(', ') + ']', data, parent, key, errors) } }) } function validate (predicate, msg, data, parent, key, errors) { if (predicate) return var value = valueStr(data) if (value != 'null') value = '\'' + value + '\'' parent = parent ? parent + '.' : '' errors.push('Value ' + value + ' at \'' + parent + toLowerFirst(key) + '\' failed to satisfy constraint: ' + msg) } function validateTableName (key, val) { if (val == null) return null if (val.length < 3 || val.length > 255) return key + ' must be at least 3 characters long and at most 255 characters long' } function toLowerFirst (str) { return str[0].toLowerCase() + str.slice(1) } function validateAttributeValue (value) { var types = Object.keys(value), msg, i, attr if (!types.length) return 'Supplied AttributeValue is empty, must contain exactly one of the supported datatypes' for (var type in value) { if (type == 'N') { msg = checkNum(type, value) if (msg) return msg } if (type == 'NULL' && !value[type]) return 'One or more parameter values were invalid: Null attribute value types must have the value of true' if (type == 'SS' && !value[type].length) return 'One or more parameter values were invalid: An string set may not be empty' if (type == 'NS' && !value[type].length) return 'One or more parameter values were invalid: An number set may not be empty' if (type == 'BS' && !value[type].length) return 'One or more parameter values were invalid: Binary sets should not be empty' if (type == 'NS') { for (i = 0; i < value[type].length; i++) { msg = checkNum(i, value[type]) if (msg) return msg } } if (type == 'SS' && findDuplicate(value[type])) return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + ' contains duplicates.' if (type == 'NS' && findDuplicate(value[type])) return 'Input collection contains duplicates' if (type == 'BS' && findDuplicate(value[type])) return 'One or more parameter values were invalid: Input collection ' + valueStr(value[type]) + 'of type BS contains duplicates.' if (type == 'M') { for (attr in value[type]) { msg = validateAttributeValue(value[type][attr]) if (msg) return msg } } if (type == 'L') { for (i = 0; i < value[type].length; i++) { msg = validateAttributeValue(value[type][i]) if (msg) return msg } } } if (types.length > 1) return 'Supplied AttributeValue has more than one datatypes set, must contain exactly one of the supported datatypes' } function checkNum (attr, obj) { if (!obj[attr]) return 'The parameter cannot be converted to a numeric value' var bigNum try { bigNum = new Big(obj[attr]) } catch { return 'The parameter cannot be converted to a numeric value: ' + obj[attr] } if (bigNum.e > 125) return 'Number overflow. Attempting to store a number with magnitude larger than supported range' else if (bigNum.e < -130) return 'Number underflow. Attempting to store a number with magnitude smaller than supported range' else if (bigNum.c.length > 38) return 'Attempting to store more than 38 significant digits in a Number' obj[attr] = bigNum.toFixed() } function valueStr (data) { return data == null ? 'null' : Array.isArray(data) ? '[' + data.map(valueStr).join(', ') + ']' : typeof data == 'object' ? JSON.stringify(data) : data } function findDuplicate (arr) { if (!arr) return null var vals = Object.create(null) for (var i = 0; i < arr.length; i++) { if (vals[arr[i]]) return arr[i] vals[arr[i]] = true } } function validateAttributeConditions (data) { for (var key in data.Expected) { var condition = data.Expected[key] if ('AttributeValueList' in condition && 'Value' in condition) return 'One or more parameter values were invalid: ' + 'Value and AttributeValueList cannot be used together for Attribute: ' + key if ('ComparisonOperator' in condition) { if ('Exists' in condition) return 'One or more parameter values were invalid: ' + 'Exists and ComparisonOperator cannot be used together for Attribute: ' + key if (condition.ComparisonOperator != 'NULL' && condition.ComparisonOperator != 'NOT_NULL' && !('AttributeValueList' in condition) && !('Value' in condition)) return 'One or more parameter values were invalid: ' + 'Value or AttributeValueList must be used with ComparisonOperator: ' + condition.ComparisonOperator + ' for Attribute: ' + key var values = condition.AttributeValueList ? condition.AttributeValueList.length : condition.Value ? 1 : 0 var validAttrCount = false switch (condition.ComparisonOperator) { case 'EQ': case 'NE': case 'LE': case 'LT': case 'GE': case 'GT': case 'CONTAINS': case 'NOT_CONTAINS': case 'BEGINS_WITH': if (values === 1) validAttrCount = true break case 'NOT_NULL': case 'NULL': if (values === 0) validAttrCount = true break case 'IN': if (values > 0) validAttrCount = true break case 'BETWEEN': if (values === 2) validAttrCount = true break } if (!validAttrCount) return 'One or more parameter values were invalid: ' + 'Invalid number of argument(s) for the ' + condition.ComparisonOperator + ' ComparisonOperator' if (condition.AttributeValueList && condition.AttributeValueList.length) { var type = Object.keys(condition.AttributeValueList[0])[0] if (condition.AttributeValueList.some(function (attr) { return Object.keys(attr)[0] != type })) { return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type' } if (condition.ComparisonOperator == 'BETWEEN' && db.compare('GT', condition.AttributeValueList[0], condition.AttributeValueList[1])) { return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound' } } } else if ('AttributeValueList' in condition) { return 'One or more parameter values were invalid: ' + 'AttributeValueList can only be used with a ComparisonOperator for Attribute: ' + key } else { var exists = condition.Exists == null || condition.Exists if (exists && condition.Value == null) return 'One or more parameter values were invalid: ' + 'Value must be provided when Exists is ' + (condition.Exists == null ? 'null' : condition.Exists) + ' for Attribute: ' + key else if (!exists && condition.Value != null) return 'One or more parameter values were invalid: ' + 'Value cannot be used when Exists is false for Attribute: ' + key if (condition.Value != null) { var msg = validateAttributeValue(condition.Value) if (msg) return msg } } } } function validateConditions (conditions) { var lengths = { NULL: 0, NOT_NULL: 0, EQ: 1, NE: 1, LE: 1, LT: 1, GE: 1, GT: 1, CONTAINS: 1, NOT_CONTAINS: 1, BEGINS_WITH: 1, IN: [ 1 ], BETWEEN: 2, } var types = { EQ: [ 'S', 'N', 'B', 'SS', 'NS', 'BS' ], NE: [ 'S', 'N', 'B', 'SS', 'NS', 'BS' ], LE: [ 'S', 'N', 'B' ], LT: [ 'S', 'N', 'B' ], GE: [ 'S', 'N', 'B' ], GT: [ 'S', 'N', 'B' ], CONTAINS: [ 'S', 'N', 'B' ], NOT_CONTAINS: [ 'S', 'N', 'B' ], BEGINS_WITH: [ 'S', 'B' ], IN: [ 'S', 'N', 'B' ], BETWEEN: [ 'S', 'N', 'B' ], } for (var key in conditions) { var comparisonOperator = conditions[key].ComparisonOperator var attrValList = conditions[key].AttributeValueList || [] for (var i = 0; i < attrValList.length; i++) { var msg = validateAttributeValue(attrValList[i]) if (msg) return msg } if ((typeof lengths[comparisonOperator] == 'number' && attrValList.length != lengths[comparisonOperator]) || (attrValList.length < lengths[comparisonOperator][0] || attrValList.length > lengths[comparisonOperator][1])) return 'One or more parameter values were invalid: Invalid number of argument(s) for the ' + comparisonOperator + ' ComparisonOperator' if (attrValList.length) { var type = Object.keys(attrValList[0])[0] if (attrValList.some(function (attr) { return Object.keys(attr)[0] != type })) { return 'One or more parameter values were invalid: AttributeValues inside AttributeValueList must be of same type' } } if (types[comparisonOperator]) { for (i = 0; i < attrValList.length; i++) { if (!~types[comparisonOperator].indexOf(Object.keys(attrValList[i])[0])) return 'One or more parameter values were invalid: ComparisonOperator ' + comparisonOperator + ' is not valid for ' + Object.keys(attrValList[i])[0] + ' AttributeValue type' } } if (comparisonOperator == 'BETWEEN' && db.compare('GT', attrValList[0], attrValList[1])) { return 'The BETWEEN condition was provided a range where the lower bound is greater than the upper bound' } } } function validateExpressionParams (data, expressions, nonExpressions) { var exprParams = expressions.filter(function (expr) { return data[expr] != null }) if (exprParams.length) { // Special case for KeyConditions and KeyConditionExpression if (data.KeyConditions != null && data.KeyConditionExpression == null) { nonExpressions.splice(nonExpressions.indexOf('KeyConditions'), 1) } var nonExprParams = nonExpressions.filter(function (expr) { return data[expr] != null }) if (nonExprParams.length) { return 'Can not use both expression and non-expression parameters in the same request: ' + 'Non-expression parameters: {' + nonExprParams.join(', ') + '} ' + 'Expression parameters: {' + exprParams.join(', ') + '}' } } if (data.ExpressionAttributeNames != null && !exprParams.length) { return 'ExpressionAttributeNames can only be specified when using expressions' } var valExprs = expressions.filter(function (expr) { return expr != 'ProjectionExpression' }) if (valExprs.length && data.ExpressionAttributeValues != null && valExprs.every(function (expr) { return data[expr] == null })) { return 'ExpressionAttributeValues can only be specified when using expressions: ' + valExprs.join(' and ') + ' ' + (valExprs.length > 1 ? 'are' : 'is') + ' null' } } function validateExpressions (data) { var key, msg, result, context = { attrNames: data.ExpressionAttributeNames, attrVals: data.ExpressionAttributeValues, unusedAttrNames: {}, unusedAttrVals: {}, } if (data.ExpressionAttributeNames != null) { if (!Object.keys(data.ExpressionAttributeNames).length) return 'ExpressionAttributeNames must not be empty' for (key in data.ExpressionAttributeNames) { if (!/^#[0-9a-zA-Z_]+$/.test(key)) { return 'ExpressionAttributeNames contains invalid key: Syntax error; key: "' + key + '"' } context.unusedAttrNames[key] = true } } if (data.ExpressionAttributeValues != null) { if (!Object.keys(data.ExpressionAttributeValues).length) return 'ExpressionAttributeValues must not be empty' for (key in data.ExpressionAttributeValues) { if (!/^:[0-9a-zA-Z_]+$/.test(key)) { return 'ExpressionAttributeValues contains invalid key: Syntax error; key: "' + key + '"' } context.unusedAttrVals[key] = true } for (key in data.ExpressionAttributeValues) { msg = validateAttributeValue(data.ExpressionAttributeValues[key]) if (msg) { msg = 'ExpressionAttributeValues contains invalid value: ' + msg + ' for key ' + key return msg } } } if (data.UpdateExpression != null) { result = parse(data.UpdateExpression, updateParser, context) if (typeof result == 'string') { return 'Invalid UpdateExpression: ' + result } data._updates = result } if (data.ConditionExpression != null) { result = parse(data.ConditionExpression, conditionParser, context) if (typeof result == 'string') { return 'Invalid ConditionExpression: ' + result } data._condition = result } if (data.KeyConditionExpression != null) { context.isKeyCondition = true result = parse(data.KeyConditionExpression, conditionParser, context) if (typeof result == 'string') { return 'Invalid KeyConditionExpression: ' + result } data._keyCondition = result } if (data.FilterExpression != null) { result = parse(data.FilterExpression, conditionParser, context) if (typeof result == 'string') { return 'Invalid FilterExpression: ' + result } data._filter = result } if (data.ProjectionExpression != null) { result = parse(data.ProjectionExpression, projectionParser, context) if (typeof result == 'string') { return 'Invalid ProjectionExpression: ' + result } data._projection = result } if (Object.keys(context.unusedAttrNames).length) { return 'Value provided in ExpressionAttributeNames unused in expressions: ' + 'keys: {' + Object.keys(context.unusedAttrNames).join(', ') + '}' } if (Object.keys(context.unusedAttrVals).length) { return 'Value provided in ExpressionAttributeValues unused in expressions: ' + 'keys: {' + Object.keys(context.unusedAttrVals).join(', ') + '}' } } function parse (str, parser, context) { if (str == '') return 'The expression can not be empty;' context.isReserved = isReserved context.compare = db.compare try { return parser.parse(str, { context: context }) } catch (e) { return e.name == 'SyntaxError' ? 'Syntax error; ' + e.message : e.message } } function convertKeyCondition (expression) { var keyConds = Object.create(null) return checkExpr(expression, keyConds) || keyConds } function checkExpr (expr, keyConds) { if (!expr || !expr.type) return if (~[ 'or', 'not', 'in', '<>' ].indexOf(expr.type)) { return 'Invalid operator used in KeyConditionExpression: ' + expr.type.toUpperCase() } if (expr.type == 'function' && ~[ 'attribute_exists', 'attribute_not_exists', 'attribute_type', 'contains' ].indexOf(expr.name)) { return 'Invalid operator used in KeyConditionExpression: ' + expr.name } if (expr.type == 'function' && expr.name == 'size') { return 'KeyConditionExpressions cannot contain nested operations' } if (expr.type == 'between' && !Array.isArray(expr.args[0])) { return 'Invalid condition in KeyConditionExpression: ' + expr.type.toUpperCase() + ' operator must have the key attribute as its first operand' } if (expr.type == 'function' && expr.name == 'begins_with' && !Array.isArray(expr.args[0])) { return 'Invalid condition in KeyConditionExpression: ' + expr.name + ' operator must have the key attribute as its first operand' } if (expr.args) { var attrName = '', attrIx = 0 for (var i = 0; i < expr.args.length; i++) { if (Array.isArray(expr.args[i])) { if (attrName) { return 'Invalid condition in KeyConditionExpression: Multiple attribute names used in one condition' } if (expr.args[i].length > 1) { return 'KeyConditionExpressions cannot have conditions on nested attributes' } attrName = expr.args[i][0] attrIx = i } else if (expr.args[i].type) { var result = checkExpr(expr.args[i], keyConds) if (result) return result } } if (expr.type != 'and') { if (!attrName) { return 'Invalid condition in KeyConditionExpression: No key attribute specified' } if (keyConds[attrName]) { return 'KeyConditionExpressions must only contain one condition per key' } if (attrIx != 0) { expr.type = { '=': '=', '<': '>', '<=': '>=', '>': '<', '>=': '<=', }[expr.type] expr.args[1] = expr.args[0] } keyConds[attrName] = { ComparisonOperator: { '=': 'EQ', '<': 'LT', '<=': 'LE', '>': 'GT', '>=': 'GE', 'between': 'BETWEEN', 'function': 'BEGINS_WITH', }[expr.type], AttributeValueList: expr.args.slice(1), } } } } // http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html var RESERVED_WORDS = { ABORT: true, ABSOLUTE: true, ACTION: true, ADD: true, AFTER: true, AGENT: true, AGGREGATE: true, ALL: true, ALLOCATE: true, ALTER: true, ANALYZE: true, AND: true, ANY: true, ARCHIVE: true, ARE: true, ARRAY: true, AS: true, ASC: true, ASCII: true, ASENSITIVE: true, ASSERTION: true, ASYMMETRIC: true, AT: true, ATOMIC: true, ATTACH: true, ATTRIBUTE: true, AUTH: true, AUTHORIZATION: true, AUTHORIZE: true, AUTO: true, AVG: true, BACK: true, BACKUP: true, BASE: true, BATCH: true, BEFORE: true, BEGIN: true, BETWEEN: true, BIGINT: true, BINARY: true, BIT: true, BLOB: true, BLOCK: true, BOOLEAN: true, BOTH: true, BREADTH: true, BUCKET: true, BULK: true, BY: true, BYTE: true, CALL: true, CALLED: true, CALLING: true, CAPACITY: true, CASCADE: true, CASCADED: true, CASE: true, CAST: true, CATALOG: true, CHAR: true, CHARACTER: true, CHECK: true, CLASS: true, CLOB: true, CLOSE: true, CLUSTER: true, CLUSTERED: true, CLUSTERING: true, CLUSTERS: true, COALESCE: true, COLLATE: true, COLLATION: true, COLLECTION: true, COLUMN: true, COLUMNS: true, COMBINE: true, COMMENT: true, COMMIT: true, COMPACT: true, COMPILE: true, COMPRESS: true, CONDITION: true, CONFLICT: true, CONNECT: true, CONNECTION: true, CONSISTENCY: true, CONSISTENT: true, CONSTRAINT: true, CONSTRAINTS: true, CONSTRUCTOR: true, CONSUMED: true, CONTINUE: true, CONVERT: true, COPY: true, CORRESPONDING: true, COUNT: true, COUNTER: true, CREATE: true, CROSS: true, CUBE: true, CURRENT: true, CURSOR: true, CYCLE: true, DATA: true, DATABASE: true, DATE: true, DATETIME: true, DAY: true, DEALLOCATE: true, DEC: true, DECIMAL: true, DECLARE: true, DEFAULT: true, DEFERRABLE: true, DEFERRED: true, DEFINE: true, DEFINED: true, DEFINITION: true, DELETE: true, DELIMITED: true, DEPTH: true, DEREF: true, DESC: true, DESCRIBE: true, DESCRIPTOR: true, DETACH: true, DETERMINISTIC: true, DIAGNOSTICS: true, DIRECTORIES: true, DISABLE: true, DISCONNECT: true, DISTINCT: true, DISTRIBUTE: true, DO: true, DOMAIN: true, DOUBLE: true, DROP: true, DUMP: true, DURATION: true, DYNAMIC: true, EACH: true, ELEMENT: true, ELSE: true, ELSEIF: true, EMPTY: true, ENABLE: true, END: true, EQUAL: true, EQUALS: true, ERROR: true, ESCAPE: true, ESCAPED: true, EVAL: true, EVALUATE: true, EXCEEDED: true, EXCEPT: true, EXCEPTION: true, EXCEPTIONS: true, EXCLUSIVE: true, EXEC: true, EXECUTE: true, EXISTS: true, EXIT: true, EXPLAIN: true, EXPLODE: true, EXPORT: true, EXPRESSION: true, EXTENDED: true, EXTERNAL: true, EXTRACT: true, FAIL: true, FALSE: true, FAMILY: true, FETCH: true, FIELDS: true, FILE: true, FILTER: true, FILTERING: true, FINAL: true, FINISH: true, FIRST: true, FIXED: true, FLATTERN: true, FLOAT: true, FOR: true, FORCE: true, FOREIGN: true, FORMAT: true, FORWARD: true, FOUND: true, FREE: true, FROM: true, FULL: true, FUNCTION: true, FUNCTIONS: true, GENERAL: true, GENERATE: true, GET: true, GLOB: true, GLOBAL: true, GO: true, GOTO: true, GRANT: true, GREATER: true, GROUP: true, GROUPING: true, HANDLER: true, HASH: true, HAVE: true, HAVING: true, HEAP: true, HIDDEN: true, HOLD: true, HOUR: true, IDENTIFIED: true, IDENTITY: true, IF: true, IGNORE: true, IMMEDIATE: true, IMPORT: true, IN: true, INCLUDING: true, INCLUSIVE: true, INCREMENT: true, INCREMENTAL: true, INDEX: true, INDEXED: true, INDEXES: true, INDICATOR: true, INFINITE: true, INITIALLY: true, INLINE: true, INNER: true, INNTER: true, INOUT: true, INPUT: true, INSENSITIVE: true, INSERT: true, INSTEAD: true, INT: true, INTEGER: true, INTERSECT: true, INTERVAL: true, INTO: true, INVALIDATE: true, IS: true, ISOLATION: true, ITEM: true, ITEMS: true, ITERATE: true, JOIN: true, KEY: true, KEYS: true, LAG: true, LANGUAGE: true, LARGE: true, LAST: true, LATERAL: true, LEAD: true, LEADING: true, LEAVE: true, LEFT: true, LENGTH: true, LESS: true, LEVEL: true, LIKE: true, LIMIT: true, LIMITED: true, LINES: true, LIST: true, LOAD: true, LOCAL: true, LOCALTIME: true, LOCALTIMESTAMP: true, LOCATION: true, LOCATOR: true, LOCK: true, LOCKS: true, LOG: true, LOGED: true, LONG: true, LOOP: true, LOWER: true, MAP: true, MATCH: true, MATERIALIZED: true, MAX: true, MAXLEN: true, MEMBER: true, MERGE: true, METHOD: true, METRICS: true, MIN: true, MINUS: true, MINUTE: true, MISSING: true, MOD: true, MODE: true, MODIFIES: true, MODIFY: true, MODULE: true, MONTH: true, MULTI: true, MULTISET: true, NAME: true, NAMES: true, NATIONAL: true, NATURAL: true, NCHAR: true, NCLOB: true, NEW: true, NEXT: true, NO: true, NONE: true, NOT: true, NULL: true, NULLIF: true, NUMBER: true, NUMERIC: true, OBJECT: true, OF: true, OFFLINE: true, OFFSET: true, OLD: true, ON: true, ONLINE: true, ONLY: true, OPAQUE: true, OPEN: true, OPERATOR: true, OPTION: true, OR: true, ORDER: true, ORDINALITY: true, OTHER: true, OTHERS: true, OUT: true, OUTER: true, OUTPUT: true, OVER: true, OVERLAPS: true, OVERRIDE: true, OWNER: true, PAD: true, PARALLEL: true, PARAMETER: true, PARAMETERS: true, PARTIAL: true, PARTITION: true, PARTITIONED: true, PARTITIONS: true, PATH: true, PERCENT: true, PERCENTILE: true, PERMISSION: true, PERMISSIONS: true, PIPE: true, PIPELINED: true, PLAN: true, POOL: true, POSITION: true, PRECISION: true, PREPARE: true, PRESERVE: true, PRIMARY: true, PRIOR: true, PRIVATE: true, PRIVILEGES: true, PROCEDURE: true, PROCESSED: true, PROJECT: true, PROJECTION: true, PROPERTY: true, PROVISIONING: true, PUBLIC: true, PUT: true, QUERY: true, QUIT: true, QUORUM: true, RAISE: true, RANDOM: true, RANGE: true, RANK: true, RAW: true, READ: true, READS: true, REAL: true, REBUILD: true, RECORD: true, RECURSIVE: true, REDUCE: true, REF: true, REFERENCE: true, REFERENCES: true, REFERENCING: true, REGEXP: true, REGION: true, REINDEX: true, RELATIVE: true, RELEASE: true, REMAINDER: true, RENAME: true, REPEAT: true, REPLACE: true, REQUEST: true, RESET: true, RESIGNAL: true, RESOURCE: true, RESPONSE: true, RESTORE: true, RESTRICT: true, RESULT: true, RETURN: true, RETURNING: true, RETURNS: true, REVERSE: true, REVOKE: true, RIGHT: true, ROLE: true, ROLES: true, ROLLBACK: true, ROLLUP: true, ROUTINE: true, ROW: true, ROWS: true, RULE: true, RULES: true, SAMPLE: true, SATISFIES: true, SAVE: true, SAVEPOINT: true, SCAN: true, SCHEMA: true, SCOPE: true, SCROLL: true, SEARCH: true, SECOND: true, SECTION: true, SEGMENT: true, SEGMENTS: true, SELECT: true, SELF: true, SEMI: true, SENSITIVE: true, SEPARATE: true, SEQUENCE: true, SERIALIZABLE: true, SESSION: true, SET: true, SETS: true, SHARD: true, SHARE: true, SHARED: true, SHORT: true, SHOW: true, SIGNAL: true, SIMILAR: true, SIZE: true, SKEWED: true, SMALLINT: true, SNAPSHOT: true, SOME: true, SOURCE: true, SPACE: true, SPACES: true, SPARSE: true, SPECIFIC: true, SPECIFICTYPE: true, SPLIT: true, SQL: true, SQLCODE: true, SQLERROR: true, SQLEXCEPTION: true, SQLSTATE: true, SQLWARNING: true, START: true, STATE: true, STATIC: true, STATUS: true, STORAGE: true, STORE: true, STORED: true, STREAM: true, STRING: true, STRUCT: true, STYLE: true, SUB: true, SUBMULTISET: true, SUBPARTITION: true, SUBSTRING: true, SUBTYPE: true, SUM: true, SUPER: true, SYMMETRIC: true, SYNONYM: true, SYSTEM: true, TABLE: true, TABLESAMPLE: true, TEMP: true, TEMPORARY: true, TERMINATED: true, TEXT: true, THAN: true, THEN: true, THROUGHPUT: true, TIME: true, TIMESTAMP: true, TIMEZONE: true, TINYINT: true, TO: true, TOKEN: true, TOTAL: true, TOUCH: true, TRAILING: true, TRANSACTION: true, TRANSFORM: true, TRANSLATE: true, TRANSLATION: true, TREAT: true, TRIGGER: true, TRIM: true, TRUE: true, TRUNCATE: true, TTL: true, TUPLE: true, TYPE: true, UNDER: true, UNDO: true, UNION: true, UNIQUE: true, UNIT: true, UNKNOWN: true, UNLOGGED: true, UNNEST: true, UNPROCESSED: true, UNSIGNED: true, UNTIL: true, UPDATE: true, UPPER: true, URL: true, USAGE: true, USE: true, USER: true, USERS: true, USING: true, UUID: true, VACUUM: true, VALUE: true, VALUED: true, VALUES: true, VARCHAR: true, VARIABLE: true, VARIANCE: true, VARINT: true, VARYING: true, VIEW: true, VIEWS: true, VIRTUAL: true, VOID: true, WAIT: true, WHEN: true, WHENEVER: true, WHERE: true, WHILE: true, WINDOW: true, WITH: true, WITHIN: true, WITHOUT: true, WORK: true, WRAPPED: true, WRITE: true, YEAR: true, ZONE: true, } function isReserved (name) { return RESERVED_WORDS[name.toUpperCase()] != null }