UNPKG

@amazon-dax-sdk/client-dax

Version:

Amazon DAX Client for JavaScript

621 lines (516 loc) 18.7 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 BigDecimal = require('./BigDecimal'); const BigNumber = require('bignumber.js'); const CborDecoder = require('./CborDecoder'); const CborSkipper = require('./CborSkipper'); const CborTypes = require('./CborTypes'); const Constants = require('./Constants'); const DaxCborDecoder = require('./DaxCborDecoder'); const DaxCborTypes = require('./DaxCborTypes'); const DaxClientError = require('./DaxClientError'); const DaxErrorCode = require('./DaxErrorCode'); const DaxResponseParam = require('./Constants').DaxResponseParam; const DaxServiceError = require('./DaxServiceError'); const ItemBuilder = require('./ItemBuilder'); const LexDecimal = require('./LexDecimal'); const StreamBuffer = require('./ByteStreamBuffer'); const SUCCESS = CborTypes.TYPE_ARRAY + 0; class Assembler { constructor(request, buffer) { this._request = request; this._keySchema = request ? this._request._keySchema : null; this._buffer = buffer || new StreamBuffer(); this._buffer.reset(); } feed(data) { this._buffer.write(data); const valueCount = CborSkipper.skipCbor(this._buffer.buf, this._buffer._pos, this._buffer._end); if(valueCount === null || valueCount < 1) { // Need at least 1 value to determine whether response is error or success return; } if(this._buffer.buf[this._buffer._pos] === SUCCESS) { // If first value indicates success, then we'll need additional value(s) for the response // payload if(valueCount >= 1 + this._expectedResponseValues()) { this.dec = new DaxCborDecoder(this._buffer.readSlice()); this.dec._consume(1); // advance past the SUCCESS we saw return this._assembleResult(); } } else { // DaxServer always sends 3 items in case of exception // [responseCodes, errorMessage and DDB error details] if(valueCount >= 3) { this.dec = new DaxCborDecoder(this._buffer.readSlice()); throw this._assembleError(); } } } /** * Number of response values expected for a successful response, not counting the first value that * marks the response as a success. May be overridden to implement non-standard logic. */ _expectedResponseValues() { return 1; } /** * This can be overridden in a CustomAssembler to implement non-standard logic. */ _assembleResult() { return this._decodeNormalOperation(); } _assembleError() { let codeSeq = this.dec.decodeObject(); let errMsg = this.dec.decodeString(); let requestId; let errorCode; let statusCode = -1; let cancellationReasons; if(!this.dec.tryDecodeNull()) { let length = this.dec.decodeArrayLength(); requestId = this.dec.decodeObject(); errorCode = this.dec.decodeObject(); statusCode = this.dec.decodeObject(); if(length === 4) { cancellationReasons = []; let cancellationReasonsLength = this.dec.decodeArrayLength() / 3; for(let i = 0; i < cancellationReasonsLength; ++i) { let cancellationReasonCode = this.dec.decodeObject(); let cancellationReasonMsg = this.dec.decodeObject(); let cancellationReasonItem = null; if(!this.dec.tryDecodeNull()) { cancellationReasonItem = Assembler._decodeStreamItem(this.dec.decodeCbor()); if(cancellationReasonItem._attrListId) { cancellationReasonItem = Object.assign(cancellationReasonItem, this._request._keysPerRequest[i]); } } cancellationReasons.push({ Item: cancellationReasonItem, Code: cancellationReasonCode, Message: cancellationReasonMsg, }); } } } return new DaxServiceError(errMsg, errorCode, undefined, requestId, statusCode, codeSeq, cancellationReasons); } _decodeNormalOperation() { let result = {}; if(this.dec.tryDecodeNull()) { return result; } this.dec.processMap(() => { let param = this.dec.decodeInt(); this._decodeResponseItem(param, result); }); /** * Ideally, this would go into `scan_N1875390620_1` in `generated-src/Operations.js` * Dax doesn't return ConsumedCapacity if the request has been a cache hit, but does return it if it was a cache miss. * If a user has requested for ConsumedCapacity, and our result doesn't provide one, then we default to 0. * It follows similar logic as: https://code.amazon.com/packages/DaxJavaClient/blobs/919d32616b8971c4bf2930aa322aa39a2b38ac13/--/src/com/amazon/dax/client/dynamodbv2/DaxClient.java#L772 */ if(this._request.ReturnConsumedCapacity != null && this._request.ReturnConsumedCapacity !== 'NONE' && result.ConsumedCapacity == null) { result.ConsumedCapacity = { TableName: this._request.TableName, CapacityUnits: 0, }; } return result; } _decodeResponseItem(param, result) { switch(param) { case DaxResponseParam.Item: this._decodeItem(result); break; case DaxResponseParam.ConsumedCapacity: this._decodeConsumedCapacity(result); break; case DaxResponseParam.Attributes: this._decodeAttributes(result); break; case DaxResponseParam.ItemCollectionMetrics: this._decodeItemCollectionMetrics(result); break; case DaxResponseParam.Items: this._decodeItems(result); break; case DaxResponseParam.Count: this._decodeCount(result); break; case DaxResponseParam.LastEvaluatedKey: this._decodeLastEvaluatedKey(result); break; case DaxResponseParam.ScannedCount: this._decodeScannedCount(result); break; default: throw new DaxClientError('Unknown response field ' + param, DaxErrorCode.MalformedResult); } } _decodeItem(result) { let projOrdinals = this._request._projectionOrdinals; result.Item = this._decodeItemInternalHelper(projOrdinals); } _decodeConsumedCapacity(result) { if(this.dec.tryDecodeNull()) { return; } let consumedCapacity = Assembler._decodeConsumedCapacityData(this.dec.decodeCbor()); if(this._request.ReturnConsumedCapacity && this._request.ReturnConsumedCapacity !== 'NONE') { result.ConsumedCapacity = consumedCapacity; } } static _decodeConsumedCapacityData(dec) { let consumedCapacity = {}; consumedCapacity.TableName = dec.decodeString(); consumedCapacity.CapacityUnits = dec.decodeNumber(); if(!dec.tryDecodeNull()) { consumedCapacity.Table = { CapacityUnits: dec.decodeNumber(), }; } if(!dec.tryDecodeNull()) { consumedCapacity.GlobalSecondaryIndexes = Assembler._decodeIndexConsumedCapacity(dec); } if(!dec.tryDecodeNull()) { consumedCapacity.LocalSecondaryIndexes = Assembler._decodeIndexConsumedCapacity(dec); } return consumedCapacity; } static _decodeIndexConsumedCapacity(dec) { let indexConsumedCapacity = dec.buildMap(() => { let indexName = dec.decodeString(); let units = dec.decodeNumber(); return [indexName, {CapacityUnits: units}]; }); return indexConsumedCapacity; } _decodeAttributes(result) { let returnValues = this._request.ReturnValues; let isProjection = returnValues && (returnValues === 'UPDATED_NEW' || returnValues === 'UPDATED_OLD'); let item; if(isProjection) { item = Assembler._decodeStreamItemProjection(this.dec.decodeCbor()); } else { item = Assembler._decodeStreamItem(this.dec.decodeCbor()); this._reinsertKey(item); } result.Attributes = item; } _decodeItemCollectionMetrics(result) { if(this.dec.tryDecodeNull()) { return; } result.ItemCollectionMetrics = Assembler._decodeItemCollectionMetricsData(this.dec.decodeCbor(), this._keySchema); } static _decodeItemCollectionMetricsData(dec, keySchema) { let keyAV = Assembler._decodeAttributeValue(dec); let sizeLower = dec.decodeFloat(); let sizeUpper = dec.decodeFloat(); let itemCollectionMetrics = { ItemCollectionKey: {[keySchema[0].AttributeName]: keyAV}, SizeEstimateRangeGB: [sizeLower, sizeUpper], }; return itemCollectionMetrics; } _decodeItems(result) { let projOrdinals = this._request._projectionOrdinals; result.Items = this.dec.buildArray(() => this._decodeItemInternalHelper(projOrdinals)); } _decodeCount(result) { result.Count = this.dec.decodeInt(); } _decodeScannedCount(result) { result.ScannedCount = this.dec.decodeInt(); } _decodeLastEvaluatedKey(result) { let lastEvalKey; if(this._request.hasOwnProperty('IndexName')) { lastEvalKey = Assembler._decodeCompoundKey(this.dec.decodeCbor()); } else { lastEvalKey = Assembler._decodeKeyBytes(this.dec, this._keySchema); } result.LastEvaluatedKey = lastEvalKey; } _decodeItemInternalHelper(projOrdinals) { let item = Assembler._decodeItemInternal(this.dec, this._keySchema, projOrdinals) || {}; return this._reinsertKey(item); } _reinsertKey(item) { // Handle GetItem, UpdateItem, DeleteItem if(this._request.Key && !this._request._projectionOrdinals) { Object.assign(item, this._request.Key); // The key attributes are only added if it's NOT a projection return item; } // Handle PutItem if(this._request.Item) { for(let keyAttr of this._keySchema) { if(!(keyAttr.AttributeName in this._request.Item)) { throw new DaxClientError(`Request Item is missing key attribute "${keyAttr.AttributeName}".`, DaxErrorCode.MalformedResult); } item[keyAttr.AttributeName] = this._request.Item[keyAttr.AttributeName]; } } return item; } static _decodeItemInternal(dec, keySchema, projOrdinals) { if(dec.tryDecodeNull()) { return null; } let t = dec.peek(); let item; switch(CborTypes.majorType(t)) { case CborTypes.TYPE_MAP: item = Assembler._decodeProjection(dec, projOrdinals); break; case CborTypes.TYPE_BYTES: item = Assembler._decodeStreamItem(dec.decodeCbor()); break; case CborTypes.TYPE_ARRAY: item = Assembler._decodeScanResult(dec, keySchema); break; default: throw new DaxClientError('Unknown Item type: ' + t, DaxErrorCode.MalformedResult); } return item; } static _decodeProjection(dec, projOrdinals) { let builder = new ItemBuilder(); dec.processMap(() => { let ordinal = dec.decodeInt(); let path = projOrdinals[ordinal]; let av = Assembler._decodeAttributeValue(dec); builder.with(path, av); }); return builder.toItem(); } static _decodeStreamItem(dec) { let attrListId = dec.decodeInt(); let anonAttrValues = Assembler._decodeAnonymousStreamedValues(dec); return { _attrListId: attrListId, _anonymousAttributeValues: anonAttrValues, }; } static _decodeStreamItemProjection(dec) { // only a partial item is present that will be reconstructed during // de-anonymization, when the attrList is available // so for now, store the attributes in a map indexed by the ordinal let attrListId = dec.decodeInt(); let anonAttrValues = []; dec.processMap(() => { let ordinal = dec.decodeInt(); anonAttrValues[ordinal] = Assembler._decodeAttributeValue(dec); }); // If there are no values (which happens when UPDATED_OLD/NEW have no changes) // Pretend that there is no attrListId // This will result in an empty Attributes list, which is not what DDB proper does // It returns the changed attributes, even if the attributes didn't actually change if(anonAttrValues.length > 0) { return { _attrListId: attrListId, _anonymousAttributeValues: anonAttrValues, }; } else { return {}; } } static _decodeScanResult(dec, keySchema) { let size = dec.decodeArrayLength(); if(size != 2) { throw new DaxClientError('Invalid scan item length {} (expected 2)'.format(size), DaxErrorCode.MalformedResult); } let item = {}; // Array item 1 -> key let key = Assembler._decodeKeyBytes(dec, keySchema); item = Object.assign(item, key); // Array item 2 -> value let value = Assembler._decodeScanValue(dec); item = Object.assign(item, value); return item; } static _decodeScanValue(dec) { return Assembler._decodeStreamItem(dec.decodeCbor()); } static _decodeAnonymousStreamedValues(dec) { // There is no delimiter on the item attributes; the AVs are concatenated // and must be read until there is no more data. let values = []; while(true) { let av; try { av = Assembler._decodeAttributeValue(dec); } catch(e) { if(e instanceof CborDecoder.NeedMoreData) { break; } else { throw e; } } values.push(av); } return values; } static _decodeAttributeValue(dec) { let t = dec.peek(); let mt = CborTypes.majorType(t); switch(mt) { case CborTypes.TYPE_ARRAY: return {L: dec.buildArray(() => Assembler._decodeAttributeValue(dec))}; case CborTypes.TYPE_MAP: return {M: dec.buildMap(() => [dec.decodeString(), Assembler._decodeAttributeValue(dec)])}; default: { let v = dec.decodeObject(); if(v === null) { return {NULL: true}; } if(v === true || v === false) { return {BOOL: v}; } let tv = typeof(v); switch(tv) { case 'number': return {N: v.toString()}; case 'string': return {S: v}; default: if(v instanceof String) { return {S: v}; } else if(v instanceof Buffer) { return {B: v}; } else if(v instanceof Number || v instanceof BigNumber || v instanceof BigDecimal) { return {N: v.toString()}; } else if(v instanceof Array) { return {L: v}; } else if(v instanceof DaxCborTypes._DdbSet) { return v.toAV(); } else { throw new DaxClientError('Unknown type: ' + (tv === 'object' ? v.constructor.name : tv), DaxErrorCode.MalformedResult); } } } } } static _decodeCompoundKey(dec) { // Compund keys ignore the key schema and simply encode what is given // Used for indexed Scan/Query let key = {}; dec.processMap(() => { let name = dec.decodeString(); let value = Assembler._decodeAttributeValue(dec); key[name] = value; }); return key; } static _decodeKeyBytes(dec, keySchema) { let key = {}; let hashAttr = keySchema[0]; let hashAttrType = hashAttr['AttributeType']; let hashAttrName = hashAttr['AttributeName']; if(keySchema.length == 1) { let value; switch(hashAttrType) { case 'S': value = dec.decodeBytes().toString('utf8'); break; case 'N': value = dec.decodeCbor().decodeNumber().toString(); break; case 'B': value = dec.decodeBytes(); break; default: throw new DaxClientError('Hash key must be S, B or N, got ' + hashAttrType, DaxErrorCode.MalformedResult); } key[hashAttrName] = {[hashAttrType]: value}; } else if(keySchema.length == 2) { let keyDec = dec.decodeCbor(); let hashValue; switch(hashAttrType) { case 'S': hashValue = keyDec.decodeString(); break; case 'N': hashValue = keyDec.decodeNumber().toString(); break; case 'B': hashValue = keyDec.decodeBytes(); break; default: throw new DaxClientError('Hash key must be S, B or N, got ' + hashAttrType, DaxErrorCode.MalformedResult); } key[hashAttrName] = {[hashAttrType]: hashValue}; let rangeAttr = keySchema[1]; let rangeAttrType = rangeAttr['AttributeType']; let rangeAttrName = rangeAttr['AttributeName']; let rangeValue; switch(rangeAttrType) { case 'S': rangeValue = keyDec.drainAsString('utf8'); break; case 'N': let ref = []; let used = LexDecimal.decode(keyDec.buffer, keyDec.start, ref); keyDec._consume(used); rangeValue = ref[0].toString(); break; case 'B': rangeValue = keyDec.drain(); break; default: throw new DaxClientError('Range key must be S, B or N, got ' + rangeAttrType, DaxErrorCode.MalformedResult); } key[rangeAttrName] = {[rangeAttrType]: rangeValue}; } else { throw new DaxClientError( `Key schema must be of length 1 or 2; got ${keySchema.length} (${keySchema})`, DaxErrorCode.MalformedResult); } return key; } // Reads consumed capacity, recursively converting keys from enum values // to names. static _decodeConsumedCapacityExtended(dec) { if(dec.tryDecodeNull()) { return null; } function _decodeConsumedCapacityEntry() { let k; // If the key is a string, it must be an index name. Every other key // is an unsigned int. if(CborTypes.majorType(dec.peek()) === CborTypes.TYPE_UTF) { k = dec.decodeString(); } else { let enumVal = dec.decodeInt(); k = Constants.ConsumedCapacityValues[enumVal]; if(k === undefined) { throw new DaxClientError('Invalid consumed capacity key: ' + k, DaxErrorCode.Decoder); } } let v; if(CborTypes.majorType(dec.peek()) === CborTypes.TYPE_MAP) { v = dec.buildMap(_decodeConsumedCapacityEntry); } else { v = dec.decodeObject(); } return [k, v]; } return dec.buildMap(_decodeConsumedCapacityEntry); } } module.exports = Assembler;