UNPKG

dynalite

Version:

An implementation of Amazon's DynamoDB built on LevelDB

974 lines (891 loc) 33 kB
var crypto = require('crypto'), events = require('events'), async = require('async'), Lazy = require('lazy'), levelup = require('levelup'), memdown = require('memdown'), sub = require('subleveldown'), lock = require('lock'), Big = require('big.js'), once = require('once') exports.MAX_SIZE = 409600 // TODO: get rid of this? or leave for backwards compat? exports.create = create exports.lazy = lazyStream exports.validateKey = validateKey exports.validateItem = validateItem exports.validateUpdates = validateUpdates exports.validateKeyPiece = validateKeyPiece exports.validateKeyPaths = validateKeyPaths exports.createKey = createKey exports.createIndexKey = createIndexKey exports.traverseKey = traverseKey exports.traverseIndexes = traverseIndexes exports.toRangeStr = toRangeStr exports.toLexiStr = toLexiStr exports.hashPrefix = hashPrefix exports.validationError = validationError exports.limitError = limitError exports.checkConditional = checkConditional exports.itemSize = itemSize exports.capacityUnits = capacityUnits exports.addConsumedCapacity = addConsumedCapacity exports.matchesFilter = matchesFilter exports.matchesExprFilter = matchesExprFilter exports.compare = compare exports.mapPaths = mapPaths exports.mapPath = mapPath exports.queryTable = queryTable exports.updateIndexes = updateIndexes exports.getIndexActions = getIndexActions function create(options) { options = options || {} if (options.createTableMs == null) options.createTableMs = 500 if (options.deleteTableMs == null) options.deleteTableMs = 500 if (options.updateTableMs == null) options.updateTableMs = 500 if (options.maxItemSizeKb == null) options.maxItemSizeKb = exports.MAX_SIZE / 1024 options.maxItemSize = options.maxItemSizeKb * 1024 var db = levelup(options.path ? require('leveldown')(options.path) : memdown()), subDbs = Object.create(null), tableDb = getSubDb('table') // XXX: Is there a better way to get this? tableDb.awsAccountId = (process.env.AWS_ACCOUNT_ID || '0000-0000-0000').replace(/[^\d]/g, '') tableDb.awsRegion = process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || 'us-east-1' function getItemDb(name) { return getSubDb('item-' + name) } function deleteItemDb(name, cb) { deleteSubDb('item-' + name, cb) } function getIndexDb(indexType, tableName, indexName) { return getSubDb('index-' + indexType.toLowerCase() + '~' + tableName + '~' + indexName) } function deleteIndexDb(indexType, tableName, indexName, cb) { deleteSubDb('index-' + indexType.toLowerCase() + '~' + tableName + '~' + indexName, cb) } function getTagDb(name) { return getSubDb('tag-' + name) } function deleteTagDb(name, cb) { deleteSubDb('tag-' + name, cb) } function getSubDb(name) { if (!subDbs[name]) { subDbs[name] = sub(db, name, {valueEncoding: 'json'}) subDbs[name].lock = lock.Lock() } return subDbs[name] } function deleteSubDb(name, cb) { cb = once(cb) var subDb = getSubDb(name) delete subDbs[name] lazyStream(subDb.createKeyStream(), cb).join(function(keys) { subDb.batch(keys.map(function(key) { return {type: 'del', key: key} }), cb) }) } function getTable(name, checkStatus, cb) { if (typeof checkStatus == 'function') cb = checkStatus tableDb.get(name, function(err, table) { if (!err && checkStatus && (table.TableStatus == 'CREATING' || table.TableStatus == 'DELETING')) { err = new Error('NotFoundError') err.name = 'NotFoundError' } if (err) { if (err.name == 'NotFoundError') { err.statusCode = 400 err.body = { __type: 'com.amazonaws.dynamodb.v20120810#ResourceNotFoundException', message: 'Requested resource not found', } if (!checkStatus) err.body.message += ': Table: ' + name + ' not found' } return cb(err) } cb(null, table) }) } function recreate() { var self = this, newStore = create(options) Object.keys(newStore).forEach(function(key) { self[key] = newStore[key] }) } return { options: options, db: db, tableDb: tableDb, getItemDb: getItemDb, deleteItemDb: deleteItemDb, getIndexDb: getIndexDb, deleteIndexDb: deleteIndexDb, getTagDb: getTagDb, deleteTagDb: deleteTagDb, getTable: getTable, recreate: recreate, } } function lazyStream(stream, errHandler) { if (errHandler) stream.on('error', errHandler) var streamAsLazy = new Lazy(stream) stream.removeAllListeners('readable') stream.on('data', streamAsLazy.emit.bind(streamAsLazy, 'data')) if (stream.destroy) streamAsLazy.on('pipe', stream.destroy.bind(stream)) return streamAsLazy } function validateKey(dataKey, table, keySchema) { if (keySchema == null) keySchema = table.KeySchema if (keySchema.length != Object.keys(dataKey).length) { return validationError('The provided key element does not match the schema') } return traverseKey(table, keySchema, function(attr, type, isHash) { return validateKeyPiece(dataKey, attr, type, isHash) }) } function validateItem(dataItem, table) { return traverseKey(table, function(attr, type, isHash) { if (dataItem[attr] == null) { return validationError('One or more parameter values were invalid: ' + 'Missing the key ' + attr + ' in the item') } if (dataItem[attr][type] == null) { return validationError('One or more parameter values were invalid: ' + 'Type mismatch for key ' + attr + ' expected: ' + type + ' actual: ' + Object.keys(dataItem[attr])[0]) } if (dataItem[attr][type] === '' && (type === 'S' || type === 'B')) { return validationError('One or more parameter values are not valid. ' + 'The AttributeValue for a key attribute cannot contain an empty ' + (type === 'S' ? 'string' : 'binary') + ' value. Key: ' + attr) } return checkKeySize(dataItem[attr][type], type, isHash) }) || traverseIndexes(table, function(attr, type, index) { if (dataItem[attr] != null && dataItem[attr][type] == null) { return validationError('One or more parameter values were invalid: ' + 'Type mismatch for Index Key ' + attr + ' Expected: ' + type + ' Actual: ' + Object.keys(dataItem[attr])[0] + ' IndexName: ' + index.IndexName) } }) } function validateUpdates(attributeUpdates, expressionUpdates, table) { if (attributeUpdates == null && expressionUpdates == null) return return traverseKey(table, function(attr) { var hasKey = false if (expressionUpdates) { var sections = expressionUpdates.sections for (var j = 0; j < sections.length; j++) { if (sections[j].path[0] == attr) { hasKey = true break } } } else { hasKey = attributeUpdates[attr] != null } if (hasKey) { return validationError('One or more parameter values were invalid: ' + 'Cannot update attribute ' + attr + '. This attribute is part of the key') } }) || traverseIndexes(table, function(attr, type, index) { var actualType if (expressionUpdates) { var sections = expressionUpdates.sections for (var i = 0; i < sections.length; i++) { var section = sections[i] if (section.path.length == 1 && section.path[0] == attr) { actualType = section.attrType break } } } else { actualType = attributeUpdates[attr] && attributeUpdates[attr].Value ? Object.keys(attributeUpdates[attr].Value)[0] : null } if (actualType != null && actualType != type) { return validationError('One or more parameter values were invalid: ' + 'Type mismatch for Index Key ' + attr + ' Expected: ' + type + ' Actual: ' + actualType + ' IndexName: ' + index.IndexName) } }) || validateKeyPaths((expressionUpdates || {}).nestedPaths, table) } function validateKeyPiece(key, attr, type, isHash) { if (key[attr] == null || key[attr][type] == null) { return validationError('The provided key element does not match the schema') } if (key[attr][type] === '' && (type === 'S' || type === 'B')) { return validationError('One or more parameter values were invalid: ' + 'The AttributeValue for a key attribute cannot contain an empty ' + (type === 'S' ? 'string' : 'binary') + ' value. Key: ' + attr) } return checkKeySize(key[attr][type], type, isHash) } function validateKeyPaths(nestedPaths, table) { if (!nestedPaths) return return traverseKey(table, function(attr) { if (nestedPaths[attr]) { return validationError('Key attributes must be scalars; ' + 'list random access \'[]\' and map lookup \'.\' are not allowed: Key: ' + attr) } }) || traverseIndexes(table, function(attr) { if (nestedPaths[attr]) { return validationError('Key attributes must be scalars; ' + 'list random access \'[]\' and map lookup \'.\' are not allowed: IndexKey: ' + attr) } }) } function createKey(item, table, keySchema) { if (keySchema == null) keySchema = table.KeySchema var keyStr traverseKey(table, keySchema, function(attr, type, isHash) { if (isHash) keyStr = hashPrefix(item[attr][type], type) + '/' keyStr += toRangeStr(item[attr][type], type) + '/' }) return keyStr } function createIndexKey(item, table, keySchema) { var tableKeyPieces = [] traverseKey(table, function(attr, type) { tableKeyPieces.push(item[attr][type], type) }) return createKey(item, table, keySchema) + hashPrefix.apply(this, tableKeyPieces) } function traverseKey(table, keySchema, visitKey) { if (typeof keySchema == 'function') { visitKey = keySchema; keySchema = table.KeySchema } var i, j, attr, type, found for (i = 0; i < keySchema.length; i++) { attr = keySchema[i].AttributeName for (j = 0; j < table.AttributeDefinitions.length; j++) { if (table.AttributeDefinitions[j].AttributeName != attr) continue type = table.AttributeDefinitions[j].AttributeType break } found = visitKey(attr, type, !i) if (found) return found } } function traverseIndexes(table, visitIndex) { var i, j, k, attr, type, found if (table.GlobalSecondaryIndexes) { for (i = 0; i < table.GlobalSecondaryIndexes.length; i++) { for (k = 0; k < table.GlobalSecondaryIndexes[i].KeySchema.length; k++) { attr = table.GlobalSecondaryIndexes[i].KeySchema[k].AttributeName for (j = 0; j < table.AttributeDefinitions.length; j++) { if (table.AttributeDefinitions[j].AttributeName != attr) continue type = table.AttributeDefinitions[j].AttributeType break } found = visitIndex(attr, type, table.GlobalSecondaryIndexes[i], true) if (found) return found } } } if (table.LocalSecondaryIndexes) { for (i = 0; i < table.LocalSecondaryIndexes.length; i++) { attr = table.LocalSecondaryIndexes[i].KeySchema[1].AttributeName for (j = 0; j < table.AttributeDefinitions.length; j++) { if (table.AttributeDefinitions[j].AttributeName != attr) continue type = table.AttributeDefinitions[j].AttributeType break } found = visitIndex(attr, type, table.LocalSecondaryIndexes[i], false) if (found) return found } } } function checkKeySize(keyPiece, type, isHash) { // Numbers are always fine if (type == 'N') return null if (type == 'B') keyPiece = Buffer.from(keyPiece, 'base64') if (isHash && keyPiece.length > 2048) return validationError('One or more parameter values were invalid: ' + 'Size of hashkey has exceeded the maximum size limit of2048 bytes') else if (!isHash && keyPiece.length > 1024) return validationError('One or more parameter values were invalid: ' + 'Aggregated size of all range keys has exceeded the size limit of 1024 bytes') } function toRangeStr(keyPiece, type) { if (type == null) { type = Object.keys(keyPiece)[0] keyPiece = keyPiece[type] } if (type == 'S') return Buffer.from(keyPiece, 'utf8').toString('hex') return toLexiStr(keyPiece, type) } // Creates lexigraphically sortable number strings // 0 7c 009 = '07c009' = -99.1 // |-| |--| |-----| // sign exp digits // // Sign is 0 for negative, 1 for positive // Exp is hex for the exponent modified by adding 130 if sign is positive or subtracting from 125 if negative // Digits are unchanged if sign is positive, or added to 10 if negative // Hence, in '07c009', the sign is negative, exponent is 125 - 124 = 1, digits are 10 + -0.09 = 9.91 => -9.91e1 // function toLexiStr(keyPiece, type) { if (keyPiece == null) return '' if (type == null) { type = Object.keys(keyPiece)[0] keyPiece = keyPiece[type] } if (type == 'B') return Buffer.from(keyPiece, 'base64').toString('hex') if (type != 'N') return keyPiece var bigNum = new Big(keyPiece), digits, exp = !bigNum.c[0] ? 0 : bigNum.s == -1 ? 125 - bigNum.e : 130 + bigNum.e if (bigNum.s == -1) { bigNum.e = 0 digits = new Big(10).plus(bigNum).toFixed().replace(/\./, '') } else { digits = bigNum.c.join('') } return (bigNum.s == -1 ? '0' : '1') + ('0' + exp.toString(16)).slice(-2) + digits } function hashPrefix(hashKey, hashType, rangeKey, rangeType) { if (hashType == 'S') { hashKey = Buffer.from(hashKey, 'utf8') } else if (hashType == 'N') { hashKey = numToBuffer(hashKey) } else if (hashType == 'B') { hashKey = Buffer.from(hashKey, 'base64') } if (rangeKey) { if (rangeType == 'S') { rangeKey = Buffer.from(rangeKey, 'utf8') } else if (rangeType == 'N') { rangeKey = numToBuffer(rangeKey) } else if (rangeType == 'B') { rangeKey = Buffer.from(rangeKey, 'base64') } } else { rangeKey = Buffer.from([]) } // TODO: Can use the whole hash if we deem it important - for now just first six chars return crypto.createHash('md5').update('Outliers').update(hashKey).update(rangeKey).digest('hex').slice(0, 6) } function numToBuffer(num) { if (+num === 0) return Buffer.from([-128]) num = new Big(num) var scale = num.s, mantissa = num.c, exponent = num.e + 1, appendZero = exponent % 2 ? 1 : 0, byteArrayLengthWithoutExponent = Math.floor((mantissa.length + appendZero + 1) / 2), byteArray, appendedZero = false, mantissaIndex, byteArrayIndex if (byteArrayLengthWithoutExponent < 20 && scale == -1) { byteArray = new Array(byteArrayLengthWithoutExponent + 2) byteArray[byteArrayLengthWithoutExponent + 1] = 102 } else { byteArray = new Array(byteArrayLengthWithoutExponent + 1) } byteArray[0] = Math.floor((exponent + appendZero) / 2) - 64 if (scale == -1) byteArray[0] ^= 0xffffffff for (mantissaIndex = 0; mantissaIndex < mantissa.length; mantissaIndex++) { byteArrayIndex = Math.floor((mantissaIndex + appendZero) / 2) + 1 if (appendZero && !mantissaIndex && !appendedZero) { byteArray[byteArrayIndex] = 0 appendedZero = true mantissaIndex-- } else if ((mantissaIndex + appendZero) % 2 === 0) { byteArray[byteArrayIndex] = mantissa[mantissaIndex] * 10 } else { byteArray[byteArrayIndex] += mantissa[mantissaIndex] } if (((mantissaIndex + appendZero) % 2) || (mantissaIndex == mantissa.length - 1)) { if (scale == -1) byteArray[byteArrayIndex] = 101 - byteArray[byteArrayIndex] else byteArray[byteArrayIndex]++ } } return Buffer.from(byteArray) } function checkConditional(data, existingItem) { existingItem = existingItem || {} if (data._condition) { if (!matchesExprFilter(existingItem, data._condition.expression)) { return conditionalError() } return null } else if (!data.Expected) { return null } if (!matchesFilter(existingItem, data.Expected, data.ConditionalOperator)) { return conditionalError() } } function validationError(msg) { var err = new Error(msg) err.statusCode = 400 err.body = { __type: 'com.amazon.coral.validate#ValidationException', message: msg, } return err } function conditionalError(msg) { if (msg == null) msg = 'The conditional request failed' var err = new Error(msg) err.statusCode = 400 err.body = { __type: 'com.amazonaws.dynamodb.v20120810#ConditionalCheckFailedException', message: msg, } return err } function limitError(msg) { var err = new Error(msg) err.statusCode = 400 err.body = { __type: 'com.amazonaws.dynamodb.v20120810#LimitExceededException', message: msg, } return err } function itemSize(item, compress, addMetaSize, rangeKey) { // Size of compressed item (for checking query/scan limit) seems complicated, // probably due to some internal serialization format. var rangeKeySize = 0 var size = Object.keys(item).reduce(function(sum, attr) { var size = valSizeWithStorage(item[attr], compress && attr != rangeKey) if (compress && attr == rangeKey) { rangeKeySize = size return sum } return sum + size + (compress ? 1 : attr.length) }, 0) return !addMetaSize ? size : 2 + size + ((1 + Math.floor((1 + size) / 3072)) * (18 + rangeKeySize)) } function valSizeWithStorage(itemAttr, compress) { var type = Object.keys(itemAttr)[0] var val = itemAttr[type] var size = valSize(val, type, compress) if (!compress) return size switch (type) { case 'S': return size + (size < 128 ? 1 : size < 16384 ? 2 : 3) case 'B': return size + 1 case 'N': return size + 1 case 'SS': return size + val.length + 1 case 'BS': return size + val.length + 1 case 'NS': return size + val.length + 1 case 'NULL': return 0 case 'BOOL': return 1 case 'L': return size case 'M': return size } } function valSize(val, type, compress) { switch (type) { case 'S': return val.length case 'B': return Buffer.from(val, 'base64').length case 'N': val = new Big(val) var numDigits = val.c.length if (numDigits == 1 && val.c[0] === 0) return 1 return 1 + Math.ceil(numDigits / 2) + (numDigits % 2 || val.e % 2 ? 0 : 1) + (val.s == -1 ? 1 : 0) case 'SS': return val.reduce(function(sum, x) { return sum + valSize(x, 'S') }, 0) // eslint-disable-line no-loop-func case 'BS': return val.reduce(function(sum, x) { return sum + valSize(x, 'B') }, 0) // eslint-disable-line no-loop-func case 'NS': return val.reduce(function(sum, x) { return sum + valSize(x, 'N') }, 0) // eslint-disable-line no-loop-func case 'NULL': return 1 case 'BOOL': return 1 case 'L': return 3 + val.reduce(function(sum, val) { return sum + 1 + valSizeWithStorage(val, compress) }, 0) case 'M': return 3 + Object.keys(val).length + itemSize(val, compress) } } function capacityUnits(item, isRead, isConsistent) { var size = item ? Math.ceil(itemSize(item) / 1024 / (isRead ? 4 : 1)) : 1 return size / (!isRead || isConsistent ? 1 : 2) } function addConsumedCapacity(data, isRead, newItem, oldItem) { if (~['TOTAL', 'INDEXES'].indexOf(data.ReturnConsumedCapacity)) { var capacity = capacityUnits(newItem, isRead, data.ConsistentRead) if (oldItem != null) { capacity = Math.max(capacity, capacityUnits(oldItem, isRead, data.ConsistentRead)) } return { CapacityUnits: capacity, TableName: data.TableName, Table: data.ReturnConsumedCapacity == 'INDEXES' ? {CapacityUnits: capacity} : undefined, } } } function valsEqual(val1, val2) { if (Array.isArray(val1) && Array.isArray(val2)) { if (val1.length != val2.length) return false return val1.every(function(val) { return ~val2.indexOf(val) }) } else { return val1 == val2 } } function matchesFilter(val, filter, conditionalOperator) { for (var attr in filter) { var comp = filter[attr].Exists != null ? (filter[attr].Exists ? 'NOT_NULL' : 'NULL') : filter[attr].ComparisonOperator || 'EQ' var result = compare(comp, val[attr], filter[attr].AttributeValueList || filter[attr].Value) if (!result) { return false } else if (conditionalOperator == 'OR') { return true } } return true } function matchesExprFilter(item, expr) { if (expr.type == 'and') { return matchesExprFilter(item, expr.args[0]) && matchesExprFilter(item, expr.args[1]) } else if (expr.type == 'or') { return matchesExprFilter(item, expr.args[0]) || matchesExprFilter(item, expr.args[1]) } else if (expr.type == 'not') { return !matchesExprFilter(item, expr.args[0]) } var args = expr.args.map(function(arg) { return resolveArg(arg, item) }) return compare(expr.type == 'function' ? expr.name : expr.type, args[0], args.slice(1)) } function resolveArg(arg, item) { if (Array.isArray(arg)) { return mapPath(arg, item) } else if (arg.type == 'function' && arg.name == 'size') { var args = arg.args.map(function(arg) { return resolveArg(arg, item) }) var val = args[0], length if (!val) { return null } else if (val.S) { length = val.S.length } else if (val.B) { length = Buffer.from(val.B, 'base64').length } else if (val.SS || val.BS || val.NS || val.L) { length = (val.SS || val.BS || val.NS || val.L).length } else if (val.M) { length = Object.keys(val.M).length } return length != null ? {N: length.toString()} : null } else { return arg } } function compare(comp, val, compVals) { if (!Array.isArray(compVals)) compVals = [compVals] var attrType = val ? Object.keys(val)[0] : null var attrVal = attrType ? val[attrType] : null var compType = compVals && compVals[0] ? Object.keys(compVals[0])[0] : null var compVal = compType ? compVals[0][compType] : null switch (comp) { case 'EQ': case '=': if (compType != attrType || !valsEqual(attrVal, compVal)) return false break case 'NE': case '<>': if (compType == attrType && valsEqual(attrVal, compVal)) return false break case 'LE': case '<=': if (compType != attrType || (attrType == 'N' && !new Big(attrVal).lte(compVal)) || (attrType != 'N' && toLexiStr(attrVal, attrType) > toLexiStr(compVal, attrType))) return false break case 'LT': case '<': if (compType != attrType || (attrType == 'N' && !new Big(attrVal).lt(compVal)) || (attrType != 'N' && toLexiStr(attrVal, attrType) >= toLexiStr(compVal, attrType))) return false break case 'GE': case '>=': if (compType != attrType || (attrType == 'N' && !new Big(attrVal).gte(compVal)) || (attrType != 'N' && toLexiStr(attrVal, attrType) < toLexiStr(compVal, attrType))) return false break case 'GT': case '>': if (compType != attrType || (attrType == 'N' && !new Big(attrVal).gt(compVal)) || (attrType != 'N' && toLexiStr(attrVal, attrType) <= toLexiStr(compVal, attrType))) return false break case 'NOT_NULL': case 'attribute_exists': if (attrVal == null) return false break case 'NULL': case 'attribute_not_exists': if (attrVal != null) return false break case 'CONTAINS': case 'contains': return contains(compType, compVal, attrType, attrVal) case 'NOT_CONTAINS': return !contains(compType, compVal, attrType, attrVal) case 'BEGINS_WITH': case 'begins_with': if (compType != attrType) return false if (compType == 'B') { attrVal = Buffer.from(attrVal, 'base64').toString() compVal = Buffer.from(compVal, 'base64').toString() } if (attrVal.indexOf(compVal) !== 0) return false break case 'IN': case 'in': if (!attrVal) return false if (!compVals.some(function(compVal) { compType = Object.keys(compVal)[0] compVal = compVal[compType] return compType == attrType && valsEqual(attrVal, compVal) })) return false break case 'BETWEEN': case 'between': if (!attrVal || compType != attrType || (attrType == 'N' && (!new Big(attrVal).gte(compVal) || !new Big(attrVal).lte(compVals[1].N))) || (attrType != 'N' && (toLexiStr(attrVal, attrType) < toLexiStr(compVal, attrType) || toLexiStr(attrVal, attrType) > toLexiStr(compVals[1][compType], attrType)))) return false break case 'attribute_type': if (!attrVal || !valsEqual(attrType, compVal)) return false } return true } function contains(compType, compVal, attrType, attrVal) { if (compType === 'S') { if (attrType === 'S') return !!~attrVal.indexOf(compVal) if (attrType === 'SS') return attrVal.some(function(val) { return val === compVal }) if (attrType === 'L') return attrVal.some(function(val) { return val && val.S && val.S === compVal }) return false } if (compType === 'N') { if (attrType === 'NS') return attrVal.some(function(val) { return val === compVal }) if (attrType === 'L') return attrVal.some(function(val) { return val && val.N && val.N === compVal }) return false } if (compType === 'B') { if (attrType !== 'B' && attrType !== 'BS' && attrType !== 'L') return false var compValString = Buffer.from(compVal, 'base64').toString() if (attrType === 'B') { var attrValString = Buffer.from(attrVal, 'base64').toString() return !!~attrValString.indexOf(compValString) } return attrVal.some(function(val) { if (attrType !== 'L') return compValString === Buffer.from(val, 'base64').toString() if (attrType === 'L' && val.B) return compValString === Buffer.from(val.B, 'base64').toString() return false }) } } function mapPaths(paths, item) { var returnItem = Object.create(null), toSquash = [] for (var i = 0; i < paths.length; i++) { var path = paths[i] if (!Array.isArray(path)) path = [path] var resolved = mapPath(path, item) if (resolved == null) { continue } if (path.length == 1) { returnItem[path[0]] = resolved continue } var curItem = {M: returnItem} for (var j = 0; j < path.length; j++) { var piece = path[j] if (typeof piece == 'number') { curItem.L = curItem.L || [] if (piece > curItem.L.length && !~toSquash.indexOf(curItem)) { toSquash.push(curItem) } if (j < paths[i].length - 1) { curItem.L[piece] = curItem.L[piece] || {} curItem = curItem.L[piece] } else { curItem.L[piece] = resolved } } else { curItem.M = curItem.M || {} if (j < paths[i].length - 1) { curItem.M[piece] = curItem.M[piece] || {} curItem = curItem.M[piece] } else { curItem.M[piece] = resolved } } } } toSquash.forEach(function(obj) { obj.L = obj.L.filter(Boolean) }) return returnItem } function mapPath(path, item) { if (path.length == 1) { return item[path[0]] } var resolved = {M: item} for (var i = 0; i < path.length; i++) { var piece = path[i] if (typeof piece == 'number' && resolved.L) { resolved = resolved.L[piece] } else if (resolved.M) { resolved = resolved.M[piece] } else { resolved = null } if (resolved == null) { break } } return resolved } function queryTable(store, table, data, opts, isLocal, fetchFromItemDb, startKeyNames, cb) { cb = once(cb) var itemDb = store.getItemDb(data.TableName), vals if (data.IndexName) { var indexDb = store.getIndexDb(isLocal ? 'local' : 'global', data.TableName, data.IndexName) vals = lazyStream(indexDb.createValueStream(opts), cb) } else { vals = lazyStream(itemDb.createValueStream(opts), cb) } var tableCapacity = 0, indexCapacity = 0, calculateCapacity = ~['TOTAL', 'INDEXES'].indexOf(data.ReturnConsumedCapacity) if (fetchFromItemDb) { var em = new events.EventEmitter var queue = async.queue(function(key, cb) { if (!key) { em.emit('end') return cb() } itemDb.get(key, function(err, item) { if (err) { em.emit('error', err) return cb(err) } if (calculateCapacity) tableCapacity += itemSize(item) em.emit('data', item) cb() }) }) var oldVals = vals vals = new Lazy(em) oldVals.map(function(item) { if (calculateCapacity) indexCapacity += itemSize(item) queue.push(createKey(item, table)) }).once('pipe', queue.push.bind(queue, '')) } var size = 0, count = 0, rangeKey = table.KeySchema[1] && table.KeySchema[1].AttributeName vals = vals.takeWhile(function(val) { if (count >= data.Limit || size >= 1024 * 1024) { return false } if (calculateCapacity && !fetchFromItemDb) { var capacitySize = itemSize(val) if (data.IndexName) { indexCapacity += capacitySize } else { tableCapacity += capacitySize } } count++ size += itemSize(val, true, true, rangeKey) return true }) vals.join(function(items) { var lastItem = items[items.length - 1] var queryFilter = data.QueryFilter || data.ScanFilter if (data._filter) { items = items.filter(function(val) { return matchesExprFilter(val, data._filter.expression) }) } else if (queryFilter) { items = items.filter(function(val) { return matchesFilter(val, queryFilter, data.ConditionalOperator) }) } var result = {ScannedCount: count} if (count >= data.Limit || size >= 1024 * 1024) { if (data.Limit) items.splice(data.Limit) if (lastItem) { result.LastEvaluatedKey = startKeyNames.reduce(function(key, attr) { key[attr] = lastItem[attr] return key }, {}) } } var paths = data._projection ? data._projection.paths : data.AttributesToGet if (paths) { items = items.map(mapPaths.bind(this, paths)) } result.Count = items.length if (data.Select != 'COUNT') result.Items = items if (calculateCapacity) { var tableUnits = Math.ceil(tableCapacity / 1024 / 4) * (data.ConsistentRead ? 1 : 0.5) var indexUnits = Math.ceil(indexCapacity / 1024 / 4) * (data.ConsistentRead ? 1 : 0.5) result.ConsumedCapacity = { CapacityUnits: tableUnits + indexUnits, TableName: data.TableName, } if (data.ReturnConsumedCapacity == 'INDEXES') { result.ConsumedCapacity.Table = {CapacityUnits: tableUnits} if (data.IndexName) { var indexAttr = isLocal ? 'LocalSecondaryIndexes' : 'GlobalSecondaryIndexes' result.ConsumedCapacity[indexAttr] = {} result.ConsumedCapacity[indexAttr][data.IndexName] = {CapacityUnits: indexUnits} } } } cb(null, result) }) } function updateIndexes(store, table, existingItem, item, cb) { if (!existingItem && !item) return cb() var puts = [], deletes = [] ;['Local', 'Global'].forEach(function(indexType) { var indexes = table[indexType + 'SecondaryIndexes'] || [] var actions = getIndexActions(indexes, existingItem, item, table) puts = puts.concat(actions.puts.map(function(action) { var indexDb = store.getIndexDb(indexType, table.TableName, action.index) return indexDb.put.bind(indexDb, action.key, action.item) })) deletes = deletes.concat(actions.deletes.map(function(action) { var indexDb = store.getIndexDb(indexType, table.TableName, action.index) return indexDb.del.bind(indexDb, action.key) })) }) async.parallel(deletes, function(err) { if (err) return cb(err) async.parallel(puts, cb) }) } function getIndexActions(indexes, existingItem, item, table) { var puts = [], deletes = [], tableKeys = table.KeySchema.map(function(key) { return key.AttributeName }) indexes.forEach(function(index) { var indexKeys = index.KeySchema.map(function(key) { return key.AttributeName }), key = null, itemPieces = item if (item && indexKeys.every(function(key) { return item[key] != null })) { if (index.Projection.ProjectionType != 'ALL') { var indexAttrs = indexKeys.concat(tableKeys, index.Projection.NonKeyAttributes || []) itemPieces = indexAttrs.reduce(function(obj, attr) { obj[attr] = item[attr] return obj }, Object.create(null)) } key = createIndexKey(itemPieces, table, index.KeySchema) puts.push({index: index.IndexName, key: key, item: itemPieces}) } if (existingItem && indexKeys.every(function(key) { return existingItem[key] != null })) { var existingKey = createIndexKey(existingItem, table, index.KeySchema) if (existingKey != key) { deletes.push({index: index.IndexName, key: existingKey}) } } }) return {puts: puts, deletes: deletes} }