"use strict";

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

const database = "Rugby";
const logParams = false;

/**
 * There are tables which are not specific to a particular game, but are global across Blackout Sports games.
 * @type {Array}
 */
const globalTables = ['Users', 'UsersEmail', 'Countries', 'I18n', 'Names'];

module.exports = {

	init() {
		this.cache = {};
		this.dynamodb = blackout.dynamodb;
	},

	/**
  * WiP
  * Either the KeyConditions or KeyConditionExpression parameter must be specified
  */
	query(options = {}, callback = null) {
		var _this = this;

		return _asyncToGenerator(function* () {

			// Defaults
			options.pk = options.pk || 'Id';

			// Setup
			let table = _this.getTable(options.table);
			let params = {
				TableName: table
			};
			let keys = {},
			    values = {};

			// IndexName
			if (options.index) {
				params.IndexName = options.index + '-index';
			}

			// Sort
			if (options.sort) {
				params.ScanIndexForward = options.sort.toLowerCase() === 'desc' ? false : true;
			}

			// Limit
			if (options.limit) {
				params.Limit = options.limit;
			}

			// Get only some attrs? (Dynamo gets all by default)
			if (options.attrs) {
				params = Object.assign(params, _this.constructAttributeExpression(options));
			}

			// Assert conditions exists
			if (!options.conditions || blackout.isEmpty(options.conditions)) {
				blackout.errorInternal(new Error('Conditions were not provided to dynamo.query'));
				return;
			}

			// Filter results
			if (options.filter) {

				let i = 1,
				    filters = [];
				for (let key in options.filter) {

					let keyTag = '#K' + (Object.keys(keys).length + 1);
					let valTag = ':fv' + i;
					keys[keyTag] = key;
					values[valTag] = _this.makeValue(options.filter[key]);
					filters.push(`${keyTag} = ${valTag}`);
					i++;
				}

				params.FilterExpression = filters.join(' and ');
			}

			try {

				// Key condition expression
				let conditions = [],
				    i = 1;
				for (let key in options.conditions) {

					let keyTag = '#K' + (Object.keys(keys).length + 1);
					let valTag = ':cv' + i;
					keys[keyTag] = key;
					values[valTag] = _this.makeValue(options.conditions[key]);
					conditions.push(`${keyTag} = ${valTag}`);
					i++;
				}
				params.KeyConditionExpression = conditions.join(', ');

				// Add attribute keys and vals
				if (!blackout.isEmpty(keys)) params.ExpressionAttributeNames = keys;
				if (!blackout.isEmpty(values)) params.ExpressionAttributeValues = values;
				_this.validateNamesAndValues(keys, values, params);

				// Log
				if (logParams || options.log) {
					console.log('Dynamo.query params', params);
				}

				let data = yield _this.dynamodb.queryAsync(params);
				data = _this.unpackDynamo(data);
				callback(null, data);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	scan(options = {}, callback = null) {
		var _this2 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';

				// Setup
				let table = _this2.getTable(options.table);
				let params = {
					TableName: table
				};
				let keys = {},
				    values = {};

				// IndexName
				if (options.index) {
					params.IndexName = options.index;
				}

				// Sort
				if (options.sort) {
					params.ScanIndexForward = options.sort.toLowerCase() === 'desc' ? false : true;
				}

				// Limit
				if (options.limit) {
					params.Limit = options.limit;
				}

				// Get only some attrs? (Dynamo gets all by default)
				if (options.attrs) {
					params = Object.assign(params, _this2.constructAttributeExpression(options));
				}

				// Filter results
				if (options.filter) {

					let i = 1,
					    filters = [];
					for (let key in options.filter) {

						let keyTag = '#K' + (Object.keys(keys).length + 1);
						let valTag = ':fv' + i;
						keys[keyTag] = key;
						values[valTag] = _this2.makeValue(options.filter[key]);
						filters.push(`${keyTag} = ${valTag}`);
						i++;
					}

					params.FilterExpression = filters.join(' and ');
				}

				// Add attribute keys and vals
				if (!blackout.isEmpty(keys)) params.ExpressionAttributeNames = keys;
				if (!blackout.isEmpty(values)) params.ExpressionAttributeValues = values;
				_this2.validateNamesAndValues(keys, values, params);

				// Log
				if (logParams || options.log) {
					console.log('Dynamo.scan params', params);
				}

				let data = yield _this2.dynamodb.scanAsync(params);
				data = _this2.unpackDynamo(data);
				callback(null, data);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	getItem(options = {}, callback = null) {
		var _this3 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';

				// Setup
				let key = _this3.makeKey(options.id, options.pk);
				let table = _this3.getTable(options.table);

				let params = {
					TableName: table,
					Key: key
				};
				if (options.attrs) {
					params = Object.assign(params, _this3.constructAttributeExpression(options));
				}
				if (logParams || options.log) {
					console.log('Dynamo.getItem params', params);
				}

				console.time("getItem");
				let data = yield _this3.dynamodb.getItemAsync(params);
				console.timeEnd("getItem");

				data = _this3.unpackDynamo(data);
				callback(null, data);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	scanUntilDone(method, caller, options, params, lastEvaluatedKey, scans, allData, callback) {
		var _this4 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';
				let keys = params.ExpressionAttributeNames || {};
				let values = params.ExpressionAttributeValues || {};

				if (scans === null || scans === undefined) {
					scans = 0;
				}
				if (allData === null || allData === undefined) {
					allData = [];
				}

				// Add last key
				if (lastEvaluatedKey) {
					params.ExclusiveStartKey = lastEvaluatedKey;
				}

				// Filter results
				if (options.filter) {

					let i = 1,
					    filters = [];
					for (let key in options.filter) {

						let keyTag = '#K' + (Object.keys(keys).length + 1);
						let valTag = ':fv' + i;
						keys[keyTag] = key;
						values[valTag] = _this4.makeValue(options.filter[key]);
						filters.push(`${keyTag} = ${valTag}`);
						i++;
					}

					params.FilterExpression = filters.join(' and ');
				}

				// Add attribute keys and vals
				if (!blackout.isEmpty(keys)) params.ExpressionAttributeNames = keys;
				if (!blackout.isEmpty(values)) params.ExpressionAttributeValues = values;
				_this4.validateNamesAndValues(keys, values, params);

				// Get count only?
				if (options.getCount) {
					params.Select = 'COUNT';
					delete params.ProjectionExpression;
				}

				scans++;
				if (logParams || options.log) {
					console.log('Dynamo.scanUntilDone->' + method + ' ' + caller + '() params (Scan ' + scans + ')', params);
				}

				let meta, data;
				data = yield _this4.dynamodb[method](params);
				({ data, meta } = _this4.multiRowResult(options, data));

				allData = allData.concat(data);

				// Do we need to keep scanning?
				if (!options.limit && !options.paging && meta.lastEvaluatedKey) {

					// Set new key
					lastEvaluatedKey = meta.lastEvaluatedKey;

					// Keep going
					_this4.scanUntilDone(method, caller, options, params, lastEvaluatedKey, scans, allData, callback);
				} else {
					callback(null, { data: allData, meta: meta });
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * If params.limit or params.paging are set, this will return a result straight away
  * Otherwise it will continue scanning the table until all results are returned
  */
	getAll(options, callback) {

		let params = this.multiRowParams(options);
		this.scanUntilDone('scanAsync', 'getAll', options, params, null, null, null, callback);
	},

	getFromIndex(options, callback) {
		var _this5 = this;

		return _asyncToGenerator(function* () {

			// Defaults
			options.pk = options.pk || 'Id';

			if (!options.group) {
				let err = new Error("Tried to query dynamodb without providing a group");
				console.log(err, err.stack);
				blackout.errorInternal(err);
				return;
			}

			let params = _this5.multiRowParams(options, true);

			// --------------------------------------- indexTable (Whole other table as index)

			if (options.isIndexTable) {

				try {

					let newParams = {
						TableName: options.table + options.group.field,
						Key: _this5.makeKey(options.id, options.group.field)
					};

					// We need all fields from index since we may need to use 2nd to last item for paging
					newParams.attrs = [options.pk, options.group.field];

					newParams = Object.assign(newParams, _this5.constructAttributeExpression(newParams));
					delete newParams.attrs;

					if (logParams || options.log) {
						console.log('Dynamo.getItem getFromIndex() params', newParams);
					}

					let data = yield _this5.dynamodb.getItemAsync(newParams);

					if (!options.dontUnpack) {
						callback(null, { data: _this5.unpackDynamo(data) });
					} else {
						callback(null, { data: data });
					}
				} catch (err) {
					blackout.errorInternal(err);
				}

				// --------------------------------------- indexField (GSI)
			} else {

				let newParams = Object.assign(params, {
					ExpressionAttributeValues: {
						":v1": _this5.makeValue(options.group.value)
					},
					ExpressionAttributeNames: {
						"#K1": options.group.field
					},
					KeyConditionExpression: "#K1 = :v1",
					IndexName: options.group.index + "-index"
				});

				// Construct attrs if we're not getting all (from index which contains all fields)
				if (!options.getAllAttrs) {
					// We need id, and index-field from index since we may need to use 2nd to last item for paging
					newParams.attrs = [options.pk, options.group.field];
				}

				if (options.sorts && Object.keys(options.sorts).length) {

					// Can only do first sort at this level
					let sort = options.sorts[Object.keys(options.sorts)[0]];
					newParams.IndexName = sort.indexName + '-index';
					newParams.ScanIndexForward = sort.direction !== 'desc';
					if (newParams.attrs) {
						newParams.attrs.push(sort.field);
					}
				}

				if (newParams.attrs) {
					newParams = Object.assign(newParams, _this5.constructAttributeExpression(newParams));
					delete newParams.attrs;
				}

				_this5.scanUntilDone('queryAsync', 'getFromIndex', options, newParams, null, null, null, callback);
			}
		})();
	},

	/**
  * Get all items from a table, filtered using an index
  * 
 	{
 		table: 'table',
 		group: {
 			field: 'field',
 			index: 'name', // Not including '-index'
 			value: ''
 		},
 		attrs: [], // List of fields to get
 	}
  * 
  * Returns {data,meta}
  */
	getAllViaIndex(options, callback) {
		var _this6 = this;

		return _asyncToGenerator(function* () {

			// Defaults
			options.pk = options.pk || 'Id';

			//options.isIndex = true;

			try {

				if (!options.attrs) {
					// Attempt to get all attrs, since if they all exist, we don't need to use batchGetItems
					options.getAllAttrs = true;
				}

				// Get from index
				let { data: ids, meta } = yield _this6.getFromIndexAsync(options);

				// Check to see if we only want keys
				if (options.attrs && (options.attrs.indexOf(options.pk) >= 0 && options.attrs.length === 1 || options.group && options.attrs.indexOf(options.group.field) >= 0 && options.attrs.indexOf(options.pk) >= 0 && options.attrs.length === 2 || options.group && options.attrs.indexOf(options.pk) >= 0 && options.attrs.length === 1)) {

					// We only needed keys anyway, so send'em back
					callback(null, { data: ids, meta: meta });

					// At this point, if the GSI only has keys, our item(s) should have two keys: primary, and the index field.
					// Check to see if other attributes exist
				} else if (ids.length && Object.keys(ids[0]).length > 2) {

					// We can assume we have all fields (All Dynamo GSIs should either have all attributes, or keys only)
					callback(null, { data: ids, meta: meta });
				} else {

					// We have keys only so need to use batchGetItem

					// We only needs ids
					let idsOnly = [];
					for (let item of ids) {

						//let idOnlyItem = {};
						//idOnlyItem[options.pk] = item[options.pk];
						//idsOnly.push(idOnlyItem);
						idsOnly.push({ [options.pk]: item[options.pk] });
					}
					ids = idsOnly;

					// Store ids for later
					let localIds = blackout.cloneObject(ids, new Error());

					// Reset dontUnpack in case it was set earlier
					options.dontUnpack = false;

					// Create new params from options
					let params = _this6.multiRowParams(options, true);

					// Batch get items based on these ids
					params.ids = ids;
					params.table = options.table;
					params.storeById = true;

					if (params.ids.length) {
						let data = yield _this6.batchGetItemsAsync(params);

						// Restore ids
						ids = localIds;

						// Add items in order
						for (let i = ids.length - 1; i >= 0; i--) {

							let id = ids[i][options.pk];

							if (data[id]) {
								ids[i] = data[id];
							} else {
								ids.splice(i, 1);
							}
						}
					} else {
						ids = [];

						if (options.filter) {
							console.log(`NOTICE: No items were found when filtering for table: ${options.table}. This may be perfectly ok, but if you were expecting items, please check that your field names are correct.`);
						}
					}

					callback(null, { data: ids, meta: meta });
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	multiRowParams(options, indexMode) {

		// Defaults
		options.pk = options.pk || 'Id';

		// Setup
		let table = this.getTable(options.table);

		let params = {
			TableName: table
		};

		if (options.paging || options.limit) {
			params.Limit = options.limit ? options.limit + (options.paging ? 1 : 0) : 10;
		}

		if (options.startKey) {
			params.ExclusiveStartKey = options.startKey;
		}

		if (options.attrs && !indexMode) {
			params = Object.assign(params, this.constructAttributeExpression(options));
		}

		return params;
	},

	multiRowResult(options, data) {
		let meta = {};

		meta.lastEvaluatedKey = data.LastEvaluatedKey;
		meta.count = data.Count;

		if (options.paging) {
			meta.isLastPage = true;

			if (options.limit && data.Items.length > 1 && data.Items.length === options.limit + 1) {
				meta.isLastPage = false;

				if (options.isIndex) {
					meta.lastEvaluatedKey = data.Items[data.Items.length - 2];
				} else {
					if (!meta.lastEvaluatedKey) {
						meta.lastEvaluatedKey = {};
					}
					meta.lastEvaluatedKey[options.pk] = data.Items[data.Items.length - 2][options.pk];
				}

				data.Items.splice(-1, 1);
			}
		}

		if (!options.dontUnpack) {

			let unpackOpts = {};
			if (options.pkOnly) {
				unpackOpts.pkOnly = options.pkOnly;
			}

			// Remove anything other than the pk, needed above for lastEvaluatedKey, but no longer needed, and will mess up any further calls to batchGetItem
			data = this.unpackDynamo(data, unpackOpts);
		}

		return { data: data, meta: meta };
	},

	/**
  * Wrapper for dynamo->batchGetItem
  * 
  * Automatically handles when there are more items to get than dynamodb allows in one call
  * 
 	{
 		table: 'table',
 		ids: [],
 	}
  */
	batchGetItems(options, callback) {
		var _this7 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';
				options = blackout.cloneObject(options, new Error());

				// Setup
				let table = _this7.getTable(options.table);

				// Create batch groups
				let keyGroups = [];
				let cursor = 0;
				let limit = 100;
				do {
					if (keyGroups.length < cursor + 1) {
						keyGroups[cursor] = [];
					}
					// Format ids as dynamo keys
					keyGroups[cursor].push(_this7.makeKey(options.ids.shift(), options.pk));
					if (keyGroups[cursor].length >= limit) {
						cursor++;
					}
				} while (options.ids.length);

				// Make asynchronous calls to DynamoDB to get all ids
				let allItems = options.storeById ? {} : [];
				let awaits = [];

				let i = 1;
				for (let keys of keyGroups) {

					var RequestItems = {};
					RequestItems[table] = {
						Keys: keys
					};

					if (options.attrs) {
						RequestItems[table] = Object.assign(RequestItems[table], _this7.constructAttributeExpression(options));
					}

					var params = {
						RequestItems: RequestItems
					};

					if (logParams || options.log) {
						console.log('Dynamo.batchGetItems params ' + i);
						console.logObject(params);
					}

					awaits.push(_this7.dynamodb.batchGetItemAsync(params).then(function (data) {

						data = _this7.unpackDynamo(data.Responses[table], { pkForStoringById: options.storeById ? options.pk : null });
						if (options.storeById) {
							allItems = Object.assign(allItems, data);
						} else {
							allItems = allItems.concat(data);
						}
					}));

					i++;
				}

				yield Promise.all(awaits);

				callback(null, allItems);
			} catch (err) {
				if (options.returnErrors) {
					callback(err);
				} else {
					blackout.errorInternal(err);
				}
			}
		})();
	},

	putItem(options, callback) {
		var _this8 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				let defaults = {
					pk: 'Id',
					dontReplace: true
				};
				options = Object.assign(defaults, options);

				// Setup
				let table = _this8.getTable(options.table);
				let item = options.item;

				if (!item[options.pk]) {
					item[options.pk] = _this8.uuid();
				}

				let packedItem = _this8.packDynamo(item);

				var params = {
					Item: packedItem,
					//ReturnConsumedCapacity: "TOTAL", 
					TableName: table
				};

				// If we don't want to replace an existing item
				if (options.dontReplace) {
					params.ExpressionAttributeNames = {
						'#K1': options.pk
					};
					params.ConditionExpression = 'attribute_not_exists(#K1)';
				}

				if (logParams || options.log) {
					console.log('Dynamo.putItem params');
					console.logObject(params);
				}

				yield _this8.dynamodb.putItemAsync(params);
				callback(null, item);
			} catch (err) {
				if (options.returnErrors) {
					callback(err);
				} else {
					blackout.errorInternal(err);
				}
			}
		})();
	},

	updateItem(options, callback) {
		var _this9 = this;

		return _asyncToGenerator(function* () {

			// Defaults
			options.pk = options.pk || 'Id';

			if (!options.patch) {
				blackout.errorInternal(new Error("Attempted to update an item in dynamodb without providing a patch item"));
				return;
			}

			if (!options.patch[options.pk]) {
				blackout.errorInternal(new Error("Attempted to update an item in dynamodb without providing the primary key (id) in the patch"));
				return;
			}

			try {

				// Setup
				let key = _this9.makeKey(options.patch[options.pk], options.pk);
				let table = _this9.getTable(options.table);

				// Pack for dynamodb
				let Patch = _this9.packDynamo(options.patch);

				// Attributes
				let ExpressionAttributeNames = {},
				    ExpressionAttributeValues = {},
				    Sets = [],
				    i = 1;
				for (let key in Patch) {
					if (key !== options.pk) {
						let keyTag = '#K' + i,
						    valueTag = ':v' + i;
						ExpressionAttributeNames[keyTag] = key;
						ExpressionAttributeValues[valueTag] = Patch[key];
						Sets.push(keyTag + ' = ' + valueTag);
						i++;
					}
				}

				var params = {
					TableName: table,
					Key: key,
					ExpressionAttributeNames: ExpressionAttributeNames,
					ExpressionAttributeValues: ExpressionAttributeValues,
					UpdateExpression: 'SET ' + Sets.join(', '),
					ConditionExpression: `attribute_exists(${options.pk})` };

				if (logParams || options.log) {
					console.log('Dynamo.updateItem params', params);
				}

				yield _this9.dynamodb.updateItemAsync(params);
				callback();
			} catch (err) {
				if (err.code === 'ConditionalCheckFailedException') {
					// No item exists to update
					// Like a relational database, if the item doesn't exist, we don't need to throw an error
					callback();
				} else {
					blackout.errorInternal(err);
				}
			}
		})();
	},

	deleteItem(options, callback) {
		var _this10 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';

				// Setup
				let id = options.id;
				let table = _this10.getTable(options.table);

				// Make key
				id = typeof id === "number" ? { N: id.toString() } : { S: id };
				var key = {};
				key[options.pk] = id;

				var params = {
					TableName: table,
					Key: key
				};

				if (logParams || options.log) {
					console.log('Dynamo.deleteItem params', params);
				}

				yield _this10.dynamodb.deleteItemAsync(params);
				callback();
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Deletes a list of ids from a table
  * 
 	{
 		table: 'table',
 		ids: [],
 	}
  */
	deleteItems(options, callback) {
		var _this11 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';
				options = blackout.cloneObject(options, new Error());

				// Is there anything to do
				if (!options.ids.length) {
					callback();
					return;
				}

				// Backoff
				let wait = 0;
				let backoff = 400;
				let waited = 0;
				let maxWait = 30000;

				// Setup
				let table = _this11.getTable(options.table);

				// Create batch groups
				let keyGroups = [];
				let cursor = 0;
				let limit = 25;
				do {
					if (keyGroups.length < cursor + 1) {
						keyGroups[cursor] = [];
					}
					// Format ids as dynamo keys
					keyGroups[cursor].push(_this11.makeKey(options.ids.shift(), options.pk));
					if (keyGroups[cursor].length >= limit) {
						cursor++;
					}
				} while (options.ids.length);

				// Make asynchronous calls to DynamoDB to run requests
				let i = 1;
				let isProcessing = true,
				    retryRequestItems,
				    keys;
				let isRetry = false;
				do {

					if (keyGroups.length >= i) {

						let RequestItems;

						// Is this a retry?
						if (isRetry) {

							RequestItems = retryRequestItems;
							isRetry = false;
						} else {

							cursor = i - 1;
							keys = keyGroups[cursor];
							RequestItems = {};
							RequestItems[table] = [];

							for (let key of keys) {
								RequestItems[table].push({
									DeleteRequest: {
										Key: key
									}
								});
							}
						}

						var params = {
							RequestItems: RequestItems
						};

						if (logParams || options.log) {
							console.log('Dynamo.batchWriteItems.Delete params ' + i);
							console.logObject(params);
						}

						let data = yield _this11.dynamodb.batchWriteItemAsync(params);

						// If there are unprocessed items
						if (data.UnprocessedItems && !blackout.isEmpty(data.UnprocessedItems)) {

							// Update time waited
							waited += wait;

							if (waited >= maxWait) {

								throw new Error('Had to wait too long for batchWriteItem operations');
							} else {

								if (logParams || options.log) {
									console.log('Dynamo.batchWriteItems UnprocessedItems', data.UnprocessedItems);
								}

								// Wait
								wait += Math.round(Math.random() * backoff);
								yield blackout.sleep(wait);

								// Retry
								retryRequestItems = data.UnprocessedItems;
								isRetry = true;
							}

							delete data.UnprocessedItems;
						} else {
							// Increase cursor
							i++;
						}
					} else {
						// No more keys to delete
						isProcessing = false;
					}
				} while (isProcessing);

				callback();
			} catch (err) {
				if (options.returnErrors) {
					callback(err);
				} else {
					blackout.errorInternal(err);
				}
			}
		})();
	},

	putItems(options, callback) {
		var _this12 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				options.pk = options.pk || 'Id';
				options = blackout.cloneObject(options, new Error());

				// Is there anything to do
				if (!options.items.length) {
					callback();
					return;
				}

				// Backoff
				let wait = 0;
				let backoff = 400;
				let waited = 0;
				let maxWait = 30000;

				// Setup
				let table = _this12.getTable(options.table);

				// Create batch groups
				let itemGroups = [];
				let cursor = 0;
				let limit = 25;
				do {
					if (itemGroups.length < cursor + 1) {
						itemGroups[cursor] = [];
					}

					let item = options.items.shift();

					if (!item[options.pk]) {
						item[options.pk] = _this12.uuid();
					}

					// Format ids as dynamo keys
					itemGroups[cursor].push(_this12.packDynamo(item));

					if (itemGroups[cursor].length >= limit) {
						cursor++;
					}
				} while (options.items.length);

				// Make asynchronous calls to DynamoDB to run requests
				let i = 1;
				let isProcessing = true,
				    retryRequestItems,
				    retryItems,
				    items;
				let allItems = [];
				let isRetry = false;
				do {

					if (itemGroups.length >= i) {

						let RequestItems;

						// Is this a retry?
						if (isRetry) {

							RequestItems = retryRequestItems;
							items = retryItems;
							isRetry = false;
						} else {

							cursor = i - 1;
							items = itemGroups[cursor];
							RequestItems = {};
							RequestItems[table] = [];

							for (let item of items) {
								RequestItems[table].push({
									PutRequest: {
										Item: item
									}
								});
							}
						}

						var params = {
							RequestItems: RequestItems
						};

						if (logParams || options.log) {
							console.log('Dynamo.batchWriteItems Put params ' + i);
							console.logObject(params);
						}

						let data = yield _this12.dynamodb.batchWriteItemAsync(params);

						// If there are unprocessed items
						if (data.UnprocessedItems && !blackout.isEmpty(data.UnprocessedItems)) {

							// Update time waited
							waited += wait;

							if (waited >= maxWait) {

								throw new Error('Had to wait too long for batchWriteItem operations');
							} else {

								if (logParams || options.log) {
									console.log('Dynamo.batchWriteItems UnprocessedItems', data.UnprocessedItems);
								}

								// Wait
								wait += Math.round(Math.random() * backoff);
								yield blackout.sleep(wait);

								let unprocessedItems = data.UnprocessedItems;

								// Only retain unprocessed items
								let reducedItems = [];
								for (let item of unprocessedItems[table]) {
									reducedItems.push(item.PutRequest.Item);
									// Remove from successful items
									let itemIndex = items.indexOf(item);
									if (itemIndex >= 0) {
										items.splice(itemIndex, 1);
									}
								}

								// Track successful items
								allItems = allItems.concat(items);

								// Retry
								retryRequestItems = data.UnprocessedItems;
								retryItems = reducedItems;
								isRetry = true;
							}

							delete data.UnprocessedItems;
						} else {

							// Track successful items
							allItems = allItems.concat(items);

							// Increase cursor
							i++;
						}
					} else {
						// No more keys to delete
						isProcessing = false;
					}
				} while (isProcessing);

				callback(null, allItems);
			} catch (err) {
				if (options.returnErrors) {
					callback(err);
				} else {
					blackout.errorInternal(err);
				}
			}
		})();
	},

	/**
  * Warning, this will override any previously set ExpressionAttributeNames or ProjectionExpression on a params map if Object.assign()'d
  */
	constructAttributeExpression(options) {

		let params = { ExpressionAttributeNames: {} },
		    tags = [];
		if (options.pk) {
			options.attrs.push(options.pk);
		}
		let attrs = blackout.unique(options.attrs);
		for (let [i, attr] of attrs.entries()) {
			let tag = '#K' + i;
			params.ExpressionAttributeNames[tag] = attr;
			tags.push(tag);
		}

		params.ProjectionExpression = tags.join(',');

		return params;
	},

	/**
  * Parses a raw dynamodb result or result set into basic objects
  * @param  {dynamodb-item} item               The item or set of items returned from dynamodb
  * @param  {bool}          raw                Internal flag for recursive calls
  * @param  {string}        pkForStoringById   Set this to the table pk if you want rows returned in an object under the item's id
  * @return {object or array of objects}
  */
	unpackDynamo(item, opts = {}, raw = false) {

		if (blackout.isEmpty(item)) {
			return blackout.isArray(item) ? [] : null;
		}

		if (!raw && (item.Items || blackout.isArray(item))) {

			let items = blackout.isArray(item) ? item : item.Items;

			if (opts.pkForStoringById) {

				let itemsObj = {};
				for (let item of items) {
					item = this.unpackDynamo(item, opts, true);
					itemsObj[item[opts.pkForStoringById]] = item;
				}

				return itemsObj;
			} else {

				for (let [i, item] of items.entries()) {
					items[i] = this.unpackDynamo(item, opts, true);
				}

				return items;
			}
		} else {

			let parsedItem = {};
			item = raw || !item.Item ? item : item.Item;

			let keys = Object.keys(item);
			if (opts.pkOnly) {
				if (typeof opts.pkOnly !== 'string') {
					blackout.errorInternal(new Error('options.pkOnly was provided, but was not a string. It should be set as the pk field name, not as a boolean or anything else.'));
					return;
				}
				keys = [opts.pkOnly];
			}

			for (let key of keys) {

				if (item[key].S) {
					parsedItem[key] = item[key].S;
				} else if (item[key].N) {
					parsedItem[key] = Number(item[key].N);
				} else if (item[key].L) {
					for (let [index, listItem] of item[key].L.entries()) {
						let parent = { child: listItem };
						item[key].L[index] = this.unpackDynamo(parent, opts, true).child;
					}
					parsedItem[key] = item[key].L;
				} else if (item[key].M) {
					parsedItem[key] = this.unpackDynamo(item[key].M, opts, true);
				} else if (item[key].BOOL) {
					parsedItem[key] = item[key].BOOL;
				}
			}

			return parsedItem;
		}
	},

	unpackDynamoId(item, pk) {
		if (item[pk].S) {
			return item[pk].S;
		} else if (item[pk].N) {
			return Number(item[pk].N);
		} else {
			return item[pk][Object.keys(item[pk])[0]];
		}
	},

	/**
  * Converts our basic objects into dynamodb items
  * @param  {object or array of objects} item     The item or set of items to send to dynamodb returned from dynamodb
  * @param  {bool}          raw      Internal flag for recursive calls
  * @return {dynamodb-item}
  */
	packDynamo(item, isChild) {

		if (item) {

			let dynamoItem = {};

			if (isChild) {
				item = { child: item };
			}

			for (let key in item) {

				let val = item[key];

				if (blackout.isNumeric(val)) {
					dynamoItem[key] = { N: val.toString() };
				} else if (Array.isArray(val)) {
					for (let [index, listItem] of val.entries()) {
						val[index] = this.packDynamo(listItem, true);
					}
					dynamoItem[key] = { L: val };
				} else if (val === null || val === "" || val === undefined) {
					dynamoItem[key] = { NULL: true };
				} else if (typeof val === 'object' && typeof val.then === 'function') {
					throw new Error("A promise was given to dynamo for storage in a Map under key: " + key);
				} else if (typeof val === 'function') {
					throw new Error("A function was given to dynamo for storage under key: " + key);
				} else if (typeof val === 'object') {
					dynamoItem[key] = { M: this.packDynamo(val) };
				} else if (typeof val === 'boolean') {
					dynamoItem[key] = { BOOL: val };
				} else {
					dynamoItem[key] = { S: val };
				}
			}

			if (isChild) {
				dynamoItem = dynamoItem.child;
			}

			return dynamoItem;
		} else {
			blackout.errorInternal(new Error("An completely empty item was given to packDynamo()"));
		}
	},

	/**
  * Make sure all provided keys and values are set
  * @param  {object} keys   List of keys
  * @param  {object} values List of values
  * @param  {object} params Request params (to help with finding the problem)
  */
	validateNamesAndValues(names, values, params) {

		// Keys
		for (let key in names) {
			if (blackout.isEmpty(names[key])) {
				console.log('Error params', params);
				throw new Error(`The value for ExpressionAttributeNames ${key} was empty`);
			}
		}

		// Values
		for (let key in values) {
			if (values[key][Object.keys(values[key])[0]] === undefined) {
				console.log('Error params', params);
				throw new Error(`The value for ExpressionAttributeValues ${key} was undefined`);
			}
		}
	},

	/**
  * Get table name based on database
  */
	getTable(table) {

		if (!globalTables.includes(table)) {
			return database + table;
		}
		return table;
	},

	/**
  * Turn a given string or number into a dynamodb typed value, e.g. { S: "xyz123" }
  */
	makeValue(id) {
		return blackout.isNumeric(id) ? { N: id.toString() } : { S: id };
	},

	/**
  * Turn a given string or number into a dynamodb key
  */
	makeKey(id, pk) {
		if (typeof id !== 'object') {
			id = this.makeValue(id);
		}
		let key;
		if (id[pk]) {
			id[pk] = this.makeValue(id[pk]);
			key = id;
		} else {
			key = {};
			key[pk] = id;
		}
		return key;
	},

	/**
  * Generate a base64 compatible string, 22 characters long
  * 
  * Why not use standard UUIDs?
  * base64 is shorter
  * 22 characters of base64 has comparable (actually more) possibilities than standard UUID
  * Double clicking on a standard UUID only selects part of it which is annoying for quick copying and pasting
  */
	uuid() {
		/*let buf = new Buffer(16);
  let uuid = blackout.uuidV4(null,buf);
  return blackout.base62.encode(uuid);*/
		return blackout.uuid();
	}

};
//# sourceMappingURL=dynamo.js.map