UNPKG

@amazon-dax-sdk/client-dax

Version:

Amazon DAX Client for JavaScript

578 lines (510 loc) 18.2 kB
/* * Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). You may not * use this file except in compliance with the License. A copy of the License * is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ 'use strict'; const antlr4 = require('antlr4'); const DynamoDbGrammarListener = require('./DynamoDbGrammarListener').DynamoDbGrammarListener; const AttributeValueEncoder = require('./AttributeValueEncoder'); const StreamBuffer = require('./ByteStreamBuffer'); const CborEncoder = require('./CborEncoder'); const DaxCborTypes = require('./DaxCborTypes'); const DynamoDbExpressionParser = require('./DynamoDbExpressionParser'); const ExpressionErrorListener = require('./ExpressionErrorListener'); const DaxClientError = require('./DaxClientError'); const DaxErrorCode = require('./DaxErrorCode'); const ENCODING_FORMAT = 1; const ATTRIBUTE_VALUE_PREFIX = ':'; const ATTRIBUTE_NAME_PREFIX = '#'; class CborSExprGenerator extends DynamoDbGrammarListener { constructor(expressionAttributeNames, expressionAttributeValues) { super(); this._mStack = []; this._mExpressionAttributeNames = expressionAttributeNames; this._mExpressionAttributeValues = expressionAttributeValues; if(expressionAttributeNames) { this._mUnusedExpressionAttributeNames = new Set(Object.keys(expressionAttributeNames)); } else { this._mUnusedExpressionAttributeNames = new Set(); } if(expressionAttributeValues) { this._mUnusedExpressionAttributeValues = new Set(Object.keys(expressionAttributeValues)); this._mVariableNameById = {}; this._mVariableValues = []; } else { this._mUnusedExpressionAttributeValues = new Set(); this._mVariableNameById = null; this._mVariableValues = null; } } _reset(type) { this._mType = type; this._mNestingLevel = 0; this._mVariableNameById = {}; this._mVariableValues = []; } static encodeConditionExpression(expression, eAttrStrs, eAttrVals) { return CborSExprGenerator.encodeExpressions(expression, null, null, null, null, eAttrStrs, eAttrVals).Condition; } static encodeKeyConditionExpression(expression, eAttrStrs, eAttrVals) { return CborSExprGenerator.encodeExpressions(null, expression, null, null, null, eAttrStrs, eAttrVals).KeyCondition; } static encodeFilterExpression(expression, eAttrStrs, eAttrVals) { return CborSExprGenerator.encodeExpressions(null, null, expression, null, null, eAttrStrs, eAttrVals).Filter; } static encodeUpdateExpression(expression, eAttrStrs, eAttrVals) { return CborSExprGenerator.encodeExpressions(null, null, null, expression, null, eAttrStrs, eAttrVals).Update; } static encodeProjectionExpression(expression, eAttrStrs) { return CborSExprGenerator.encodeExpressions(null, null, null, null, expression, eAttrStrs, null).Projection; } static encodeExpressions(condExpr, keyCondExpr, filterExpr, updExpr, projExpr, eAttrNames, eAttrVals) { let exprs = {}; exprs.Condition = condExpr; exprs.KeyCondition = keyCondExpr; exprs.Filter = filterExpr; exprs.Update = updExpr; exprs.Projection = projExpr; let output = {}; let generator = new CborSExprGenerator(eAttrNames, eAttrVals); let buffer = new StreamBuffer(); let encoder = new CborEncoder(); Object.keys(exprs).forEach((type) => { let expr = exprs[type]; if(!expr) { output[type] = null; return; } let typeStr = type + 'Expression'; let tree = null; let exprArrayLength = 3; try { switch(type) { case 'Condition': case 'Filter': case 'KeyCondition': exprArrayLength = 3; tree = DynamoDbExpressionParser .parseCondition(expr, new ExpressionErrorListener(expr, typeStr)); break; case 'Projection': exprArrayLength = 2; tree = DynamoDbExpressionParser .parseProjection(expr, new ExpressionErrorListener(expr, typeStr)); break; case 'Update': exprArrayLength = 3; tree = DynamoDbExpressionParser .parseUpdate(expr, new ExpressionErrorListener(expr, typeStr)); break; } } catch(err) { if(err instanceof DaxClientError) { throw err; } else { throw new DaxClientError(err + '\nInvalid ' + typeStr + ': The expression has redundant parentheses', DaxErrorCode.Validation, false); } } generator._reset(type); antlr4.tree.ParseTreeWalker.DEFAULT.walk(generator, tree); generator._validateIntermediateState(); let spec = generator._mStack.pop(); buffer.write(encoder.encodeArrayHeader(exprArrayLength)); buffer.write(encoder.encodeInt(ENCODING_FORMAT)); buffer.write(spec); if('Projection' !== type) { buffer.write(encoder.encodeArrayHeader(generator._mVariableValues.length)); for(let varVal of generator._mVariableValues) { buffer.write(varVal); } } output[type] = buffer.read(); }); generator._validateFinalState(); return output; } _validateIntermediateState() { if(this._mStack.length !== 1) { throw new DaxClientError('Invalid ' + this._mType + 'Expression, Stack size = ' + this._mStack.length, DaxErrorCode.Validation, false); } if(this._mNestingLevel !== 0) { throw new DaxClientError('Invalid ' + this._mType + 'Expression, Nesting level = ' + this._mNestingLevel, DaxErrorCode.Validation, false); } } _validateFinalState() { if(this._mUnusedExpressionAttributeNames.size !== 0) { let names = this._joinMissingNames(this._mUnusedExpressionAttributeNames); throw new DaxClientError('Value provided in ExpressionAttributeNames unused in expressions: keys: {' + names + '}', DaxErrorCode.Validation, false); } if(this._mUnusedExpressionAttributeValues.size !== 0) { let names = this._joinMissingNames(this._mUnusedExpressionAttributeValues); throw new DaxClientError('Value provided in ExpressionAttributeValues unused in expressions: keys: {' + names + '}', DaxErrorCode.Validation, false); } } _validateNotEquals(expType, actual, notExpected) { for(let n of notExpected) { if(actual.toLowerCase() === n.toLowerCase()) { let expTypeStr = (!expType ? '' : expType); throw new DaxClientError('Invalid ' + expTypeStr + 'Expression: The function is not allowed in a ' + expTypeStr.toLowerCase() + ' expression', DaxErrorCode.Validation, false); } } } _joinMissingNames(names) { let result = null; for(let name of names) { result = (result === null ? name : result + ', ' + name); } return result; } enterComparator(ctx) { this._mNestingLevel++; } exitComparator(ctx) { let arg2 = this._mStack.pop(); let func = this._mStack.pop(); let arg1 = this._mStack.pop(); this._mStack.push(this._encodeArray([func, arg1, arg2])); this._mNestingLevel--; } exitComparator_symbol(ctx) { let func = null; switch(ctx.getText()) { case '=': func = Func.Equal; break; case '<>': func = Func.NotEqual; break; case '<': func = Func.LessThan; break; case '<=': func = Func.LessEqual; break; case '>': func = Func.GreaterThan; break; case '>=': func = Func.GreaterEqual; break; default: throw new DaxClientError('invalid function ' + ctx.getText(), DaxErrorCode.Validation, false); } this._mStack.push(this._encodeFunctionCode(func)); } exitPath(ctx) { let components = []; for(let i=ctx.getChildCount()-1; i>=0; i--) { components[i] = this._mStack.pop(); } this._mStack.push(this._encodeFunction(Func.DocumentPath, components)); } exitListAccess(ctx) { let value = ctx.getText(); try { let ordinal = parseInt(value.substr(1, value.length - 2)); // get rid of [] this._mStack.push(this._encodeListAccess(ordinal)); } catch(err) { throw new DaxClientError('Invalid ' + this._mType + 'Expression: List index is not within the allowable range', DaxErrorCode.Validation, false); } } exitId(ctx) { let id = ctx.getText(); if(id[0] === ATTRIBUTE_NAME_PREFIX) { let sub = this._mExpressionAttributeNames[id]; if(!sub) { throw new DaxClientError('Invalid ' + this._mType + 'Expression. Substitution value not provided for ' + id, DaxErrorCode.Validation, false); } this._mUnusedExpressionAttributeNames.delete(id); this._mStack.push(this._encodeAttributeValue({'S': sub})); } else { this._mStack.push(this._encodeDocumentPathElement(id)); // FIXME Should this be a function? } } exitLiteralSub(ctx) { let literal = ctx.getText(); this._mStack.push(this._encodeVariable(literal.substr(1))); } exitAnd(ctx) { let arg2 = this._mStack.pop(); let arg1 = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.And, [arg1, arg2])); } exitOr(ctx) { let arg2 = this._mStack.pop(); let arg1 = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.Or, [arg1, arg2])); } exitNegation(ctx) { let arg = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.Not, [arg])); } enterIn(ctx) { this._mNestingLevel++; } exitIn(ctx) { let numArgs = 1 + (ctx.getChildCount() - 3) / 2; // arg + IN + ( + args*2-1 + ) let args = []; while(numArgs-- > 1) { args[numArgs-1] = this._mStack.pop(); } let arg1 = this._mStack.pop(); // a in (b,c,d) => (In a (b c d)) this._mStack.push(this._encodeFunction(Func.In, [arg1, this._encodeArray(args)])); this._mNestingLevel--; } enterBetween(ctx) { this._mNestingLevel++; } exitBetween(ctx) { let arg3 = this._mStack.pop(); let arg2 = this._mStack.pop(); let arg1 = this._mStack.pop(); // a between b and c => (Between a b c) this._mStack.push(this._encodeFunction(Func.Between, [arg1, arg2, arg3])); this._mNestingLevel--; } enterFunctionCall(ctx) { let funcName = ctx.ID().getText(); if(this._mType) { switch(this._mType) { case 'Update': this._validateNotEquals(this._mType, funcName, ['attribute_exists', 'attribute_not_exists', 'attribute_type', 'begins_with', 'contains', 'size']); if(this._mNestingLevel > 0 && !(funcName.toLowerCase() === 'if_not_exists')) { throw new DaxClientError('Only if_not_exists() function can be nested', DaxErrorCode.Validation, false); } break; case 'Filter': case 'Condition': // If nesting level is 0, function should return type boolean (which is all functions other than size) // If nesting level > 0, only size function is allowed this._validateNotEquals(this._mType, funcName, ['if_not_exists', 'list_append']); if(this._mNestingLevel === 0 && (funcName.toLowerCase() === 'size')) { let expTypeStr = (!this._mType) ? '' : this._mType; throw new DaxClientError('Invalid ' + expTypeStr + 'Expression: The function is not allowed to be used this way in an expression', DaxErrorCode.Validation, false); } else if(this._mNestingLevel > 0 && !(funcName.toLowerCase() === 'size')) { throw new DaxClientError('Only size() function is allowed to be nested', DaxErrorCode.Validation, false); } break; default: break; } } this._mNestingLevel++; } exitFunctionCall(ctx) { let funcName = ctx.ID().getText(); let func = null; switch(funcName.toLowerCase()) { case 'attribute_exists': func = Func.AttributeExists; break; case 'attribute_not_exists': func = Func.AttributeNotExists; break; case 'attribute_type': func = Func.AttributeType; break; case 'begins_with': // FIXME validate argument type is string for BeginsWith func = Func.BeginsWith; break; case 'contains': func = Func.Contains; break; case 'size': func = Func.Size; break; case 'if_not_exists': func = Func.IfNotExists; break; case 'list_append': func = Func.ListAppend; break; default: throw new DaxClientError('Invalid ' + this._mType + 'Expression: Invalid function name: function: ' + funcName.toLowerCase(), DaxErrorCode.Validation, false); } let numArgs = (ctx.getChildCount() - 2) / 2; // children = fname + ( + numOperands*2-1 +) let args = []; while(numArgs-- > 0) { args[numArgs] = this._mStack.pop(); } // func(a,b,c,..) => (func a b c ..) this._mStack.push(this._encodeFunction(func, args)); this._mNestingLevel--; } exitProjection(ctx) { let numPaths = (ctx.getChildCount() + 1) / 2; // path, path, ... path let paths = []; while(numPaths-->0) { paths[numPaths] = this._mStack.pop(); } this._mStack.push(this._encodeArray(paths)); } exitUpdate(ctx) { let updates = []; let remaining = this._mStack.length; while(remaining > 0) { updates[--remaining] = this._mStack.pop(); } this._mStack.push(this._encodeArray(updates)); } exitSet_action(ctx) { let operand = this._mStack.pop(); let path = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.SetAction, [path, operand])); } exitRemove_action(ctx) { let path = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.RemoveAction, [path])); } exitAdd_action(ctx) { let value = this._mStack.pop(); let path = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.AddAction, [path, value])); } exitDelete_action(ctx) { let value = this._mStack.pop(); let path = this._mStack.pop(); this._mStack.push(this._encodeFunction(Func.DeleteAction, [path, value])); } enterPlusMinus(ctx) { this._mNestingLevel++; } exitPlusMinus(ctx) { let op2 = this._mStack.pop(); let op1 = this._mStack.pop(); let func = null; let operator = ctx.getChild(1).getText(); switch(operator) { case '+': func = Func.Plus; break; case '-': func = Func.Minus; break; default: throw new DaxClientError('Must be +/-', DaxErrorCode.Validation, false); } this._mStack.push(this._encodeFunction(func, [op1, op2])); this._mNestingLevel--; } _encodeDocumentPathElement(str) { return new CborEncoder().encodeString(str); } _encodeAttributeValue(val) { return AttributeValueEncoder.encodeAttributeValue(val); } _encodeArray(arr) { let encoder = new CborEncoder(); let buffer = new StreamBuffer(); buffer.write(encoder.encodeArrayHeader(arr.length)); for(let obj of arr) { buffer.write(obj); } return buffer.read(); } _encodeFunctionCode(func) { return new CborEncoder().encodeInt(func); } _encodeFunction(func, args) { let encoder = new CborEncoder(); let buffer = new StreamBuffer(); buffer.write(encoder.encodeArrayHeader(args.length+1)); buffer.write(encoder.encodeInt(func)); for(let i = 0; i < args.length; ++i) { buffer.write(args[i]); } return buffer.read(); } _encodeListAccess(ordinal) { let encoder = new CborEncoder(); let buffer = new StreamBuffer(); buffer.write(encoder.encodeTag(DaxCborTypes.TAG_DDB_DOCUMENT_PATH_ORDINAL)); buffer.write(encoder.encodeInt(ordinal)); return buffer.read(); } _encodeVariable(varName) { let fullName = ATTRIBUTE_VALUE_PREFIX + varName; let val = this._mExpressionAttributeValues[fullName]; if(!val) { throw new DaxClientError('Invalid ' + this._mType + 'Expression: An expression attribute value used in expression is not defined: attribute value: ' + fullName, DaxErrorCode.Validation, false); } this._mUnusedExpressionAttributeValues.delete(fullName); let varId = this._mVariableNameById[varName]; if(!varId) { varId = this._mVariableValues.length; this._mVariableNameById[varName] = varId; this._mVariableValues.push(this._encodeAttributeValue(val)); } let buffer = new StreamBuffer(); let encoder = new CborEncoder(); buffer.write(encoder.encodeArrayHeader(2)); buffer.write(encoder.encodeInt(Func.Variable)); buffer.write(encoder.encodeInt(varId)); return buffer.read(); } } const Func = { // NOTE: Ordinal is used as identifiers in CBor encoded format /* Comparison operators */ Equal: 0, NotEqual: 1, LessThan: 2, GreaterEqual: 3, GreaterThan: 4, LessEqual: 5, /* Logical operators */ And: 6, Or: 7, Not: 8, /* Range operators */ Between: 9, /* Enumeration operators */ In: 10, /* Functions */ AttributeExists: 11, AttributeNotExists: 12, AttributeType: 13, BeginsWith: 14, Contains: 15, Size: 16, /* Document path elements */ Variable: 17, // takes 1 argument which is a placeholder for a value. function substitutes argument with corresponding value DocumentPath: 18, // maps a CBOR object to a document path /* Update Actions */ SetAction: 19, AddAction: 20, DeleteAction: 21, RemoveAction: 22, /* Update operations */ IfNotExists: 23, ListAppend: 24, Plus: 25, Minus: 26, }; module.exports = { CborSExprGenerator: CborSExprGenerator, ATTRIBUTE_VALUE_PREFIX: ATTRIBUTE_VALUE_PREFIX, ATTRIBUTE_NAME_PREFIX: ATTRIBUTE_NAME_PREFIX, };