"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 logCaching = true;

/**
 * Blackout Data Layer
 * 
 * Provides a data layer above DynamoDB and Redis to manage data with caching support.
 * All data requests should go through this library.
 * 
 * Apologies for the lack of documentation at this point...
 *
 * When params.group is required, it should be:
 *
 * params:
	{
		table: 'TableName',
		group: { field: 'FieldName', value: 'FieldValue' index: 'IndexNameWithout:-index' },
		caching: enableCache,
		pk: primaryKey,
		id: id,
		returnErrors: returnErrors,
	}
 * 
 */

module.exports = {

	init() {
		// Reset variables
		this.locale = { locale: 'en', baseLocale: 'en' }; // Default locale
	},

	setLocale(locale) {
		this.locale = locale;
	},

	setParamDefaults(params, needId) {

		params.pk = params.pk || 'Id';
		params.caching = params.caching === undefined ? true : params.caching;

		if (needId) {
			params.id = params.id || params.item[params.pk];
		}

		return params;
	},

	/**
  * Use this to create a brand new item
  * If you only want to update an item, use updateItem
  * 
 	{
 		table: 'table', // Required
 		item: {}, // Required
 		pk: 'Id',
 	}
  * 
  */
	putItem(params, callback) {
		var _this = this;

		return _asyncToGenerator(function* () {

			// Defaults
			params = _this.setParamDefaults(params);

			try {

				// Create item in dynamo (dynamo will generate an id for us)
				let item = yield _this.dynamo.putItemAsync(params);

				// Update group cache?
				if (params.caching && params.groups && !blackout.isEmpty(params.groups)) {

					for (let key of params.groups) {

						let group = { field: key, value: item[key], index: params.groups[key] };
						yield _this.addIdsToGroupAsync(params, [item[params.pk]], group);
					}
				}

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

	/**
  * Gets an item
 	{
 		table: '', // Required
 		id: '', // Required
 		cacheOnly: false // Set to try cache only
 	}
  */
	getItem(params, callback) {
		var _this2 = this;

		return _asyncToGenerator(function* () {

			try {

				// Defaults
				params = _this2.setParamDefaults(params);

				const redis = _this2.redis;
				let cacheKey = _this2.makeCacheKey(params);
				params.cacheKey = cacheKey;

				if (params.caching && !params.indexField) {

					// Check redis
					let item = yield redis.getmapAsync(cacheKey);

					if (!item) {
						if (params.cacheOnly) {
							callback(null, null);
						} else {
							let item = yield _this2.getItemFromDBAsync(params);
							callback(null, _this2.processItem(item));
						}
					} else {
						if (logCaching) {
							console.log('CACHE -> ', cacheKey);
						}
						callback(null, _this2.processItem(item));
					}
				} else {
					let item = yield _this2.getItemFromDBAsync(params);
					callback(null, _this2.processItem(item));
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Get all rows from a table, or group
  * 
 	{
 		table: 'table', // Required
 		attrs: [],      // Optional. List of fields to get
 		caching: true,  // Optional
 		group: {        // Optional
 			field: 'FieldName',
 			value: 'FieldValue',
 			index: 'IndexNameWithout:-index'
 		},
 	}
  * 
  * Returns {data,meta}
  */
	getAll(params, callback) {
		var _this3 = this;

		return _asyncToGenerator(function* () {

			// Defaults
			params = _this3.setParamDefaults(params);

			try {

				if (params.indexField) {
					// Rather than just respond with all items, throw an error
					// I almost selected all clubs, and deleted them all with this mistake! - Jeremy
					throw new Error(`db.getAll() was called with indexField (${params.indexField}) but this field is not supported. Did you mean to add a group?`);
				}

				if (params.caching) {

					if (params.group) {

						let { ids, meta } = yield _this3.getIdsAsync(params);

						/**
       * If we're sorting, and caching is turned on, if certain conditions are met in getIds()
       * the full rows may have already been retrieved from the cache, and sorted
       * (and truncated if paging). This data may have been returned here as ids
       */
						if (meta.fullData) {

							callback(null, { data: ids, meta: meta });
						} else {

							let data = yield _this3.getFromIdsAsync(params, ids);
							callback(null, { data: data, meta: meta });
						}
					} else {

						// Get ids only from dynamo, then we can check cache for each id
						let { ids, meta } = yield _this3.getIdsFromDBAsync(params);
						let data = yield _this3.getFromIdsAsync(params, ids);
						callback(null, { data: data, meta: meta });
					}
				} else {
					_this3.getAllFromDB(params, callback);
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	getFromIds(params, ids, callback) {
		var _this4 = this;

		return _asyncToGenerator(function* () {

			try {

				let sortData = function (data) {

					if (params.sorts && !blackout.isEmpty(params.sorts)) {

						// Prepare to sort
						let ss = [];
						for (let key in params.sorts) {

							let sort = params.sorts[key];
							let sortField = sort.field;
							if (sort.direction === 'desc') {
								sortField = '-' + sortField;
							}
							ss.push(sortField);
						}

						// Sort!
						data = blackout.sortBy(data, ss);
					}

					return data;
				};

				if (params.caching) {

					const redis = _this4.redis;

					if (ids.length) {

						// Attempt to get all items from cache
						let cacheKeys = _this4.makeCacheKeys(params, ids);
						let cacheData = yield redis.mgetmapAsync(cacheKeys);

						// Scan data in reverse order so we can remove keys from the ids list correctly
						for (let i = cacheData.length - 1; i >= 0; i--) {

							if (cacheData[i]) {
								// Item was found. Remove id from ids list
								ids.splice(i, 1);
								if (logCaching) {
									console.log('CACHE -> ', cacheKeys[i]);
								}
							} else {
								// Remove empty element from data list
								cacheData.splice(i, 1);
							}
						}

						// If there are still ids needed
						if (ids.length) {

							// For sorting, we may be trying to get all ids from the cache
							if (params.onlyUseDBIfLessThan && ids.length > params.onlyUseDBIfLessThan) {

								callback(null, false);
							} else {

								// Go to DB
								params.ids = ids;

								let getFromDB = (() => {
									var _ref = _asyncToGenerator(function* (lock) {

										try {

											let data = yield _this4.getMultipleFromDBAsync(params);

											if (data) {
												data = cacheData.concat(data);
											} else {
												data = cacheData;
											}

											if (params.group && lock) {
												// Clean up ids from cache
												yield _this4.removeIdsNotFoundAsync(params, ids, data);
												yield _this4.unlockGroupAsync(lock);
											}

											callback(null, sortData(data));
										} catch (err) {
											blackout.errorInternal(err);
										}
									});

									return function getFromDB(_x) {
										return _ref.apply(this, arguments);
									};
								})();

								if (params.group) {

									let groupCacheKey = _this4.makeGroupCacheKey(params, params.group);

									// Get a lock for this list (group)
									let lock = yield _this4.lockGroupAsync(groupCacheKey);

									if (lock) {
										getFromDB(lock);
									} else {
										redis.del(groupCacheKey);
										getFromDB();
									}
								} else {
									getFromDB();
								}
							}
						} else {

							// Yay! All items were in the cache
							callback(null, sortData(_this4.processItems(cacheData)));
						}
					} else {
						callback(null, []);
					}
				} else {

					params.ids = ids;
					_this4.getMultipleFromDB(params, callback);
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Update an item
  * 
  * Params
 		{
 			table: 'table', 	// Required
 			patch: {}, 				// Required // a patch containing the primary key, and any changed fields
 			groups: {},				// Semi-Required if resource can be grouped // a list of group fields usually defined on the schema, { field: index, ... }
 			pk: 'field',			// Optional // primary key field, default is 'Id'
 		}
  */
	updateItem(params, callback) {
		var _this5 = this;

		return _asyncToGenerator(function* () {

			try {

				// Setup
				params = _this5.setParamDefaults(params);

				const redis = _this5.redis,
				      dynamo = _this5.dynamo;

				// ------------------------ Get original item if needed

				let originalItem;

				if (params.groups && params.caching) {

					// We need to know at this stage (before updating any data) if any groups are changing
					for (let key in params.patch) {

						// Is this key a group?
						if (params.groups[key]) {

							// Get full item
							if (params.item) {
								originalItem = blackout.cloneObject(params.item, new Error());
							} else {

								let getParams = {
									table: params.table,
									id: params.patch[params.pk],
									pk: params.pk
								};

								originalItem = yield _this5.getItemAsync(getParams);
							}

							break;
						}
					}
				}

				// ------------------------ Update

				// The rest of the promises here can be run asynchronously, so let's await right at the end
				let awaits = [];

				// Update dynamo
				awaits.push(dynamo.updateItemAsync(params));

				// Update cache
				if (params.caching) {

					let cacheKey = _this5.makeCacheKey(params);

					awaits.push(redis.patchmapAsync(cacheKey, params.patch));

					// Check if group has changed
					// If original item is available, it means caching groups may need to be updated
					if (params.groups && originalItem) {

						for (let key in params.patch) {

							// Is this key a group?
							if (params.groups[key]) {

								let from = originalItem[key];
								let to = params.patch[key];

								if (to !== from) {
									// Group has changed

									// Remove this item from old group
									let removeParams = {
										table: params.table,
										item: originalItem,
										pk: params.pk
									};

									awaits.push(_this5.removeIdsFromGroupAsync(removeParams, [params.patch[params.pk]], { field: key, value: from }));

									// Add this item to new group
									let addParams = {
										table: params.table,
										pk: params.pk
									};

									awaits.push(_this5.addIdsToGroupAsync(addParams, [params.patch[params.pk]], { field: key, value: to }));
								}
							}
						}
					}
				}

				// Wait for all promises to resolve
				yield Promise.all(awaits);

				// Now we're done
				callback();
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Updates an item in the cache only
  * FULL item must be provided
 	{
 		table: table
 		item: {} // Full item
 	}
  */
	updateItemInCacheOnly(params, callback) {
		var _this6 = this;

		return _asyncToGenerator(function* () {

			try {

				// Setup
				params = _this6.setParamDefaults(params);
				let cacheKey = _this6.makeCacheKey(params);

				// Update
				yield _this6.redis.setmapAsync(cacheKey, params.item);
				callback();
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	getKeyvals(params, callback) {
		var _this7 = this;

		return _asyncToGenerator(function* () {

			// Setup
			params = _this7.setParamDefaults(params);

			const dynamo = _this7.dynamo,
			      redis = _this7.redis;
			let cacheKey = _this7.makeKeyvalCacheKey(params);

			let processDataFromDB = function (err, { data }) {

				if (logCaching) {
					console.log('DB KEYVALS -> ', cacheKey);
				}

				// Get the data we want, in a single object
				let single = {};
				for (let item of data) {

					// Ensure value exists
					if (!item.Value) {
						blackout.errorInternal(new Error('Value field must exist for keyval table `' + params.table + '`'));
						return;
					}
					single[item[params.pk]] = item.Value;
				}

				// If caching, store in cache
				if (Object.keys(single).length) {
					redis.setmap(cacheKey, single);
				}

				callback(_this7.processItem(single));
			};

			let getKeyvalsFromDB = function () {

				// If grouped
				if (params.group) {
					params.getAllAttrs = true;
					dynamo.getFromIndex(params, processDataFromDB);
				} else {
					dynamo.getAll(params, processDataFromDB);
				}
			};

			if (params.caching) {

				try {

					// Check cache
					let single = yield redis.getmapAsync(cacheKey);
					if (single) {
						callback(_this7.processItem(single));
					} else {
						getKeyvalsFromDB();
					}
				} catch (err) {
					getKeyvalsFromDB();
				}
			} else {

				getKeyvalsFromDB();
			}
		})();
	},

	/**
  * Full item should be provided in params.item
 	{
 		table: 'table', 	// Required
 		item: {}, 				// Required // the full item, which helps in the case of grouped items
 		groups: [],				// Semi-Required if resource can be grouped // a list of group fields usually defined on the schema
 		pk: 'field',			// Optional // primary key field, default is 'Id'
 	}
  */
	deleteItem(params, callback) {
		var _this8 = this;

		return _asyncToGenerator(function* () {

			const redis = _this8.redis,
			      dynamo = _this8.dynamo;
			params = _this8.setParamDefaults(params, true);

			// Ensure full item exists if needed
			if (!params.item && params.groups && params.groups.length) {
				let err = new Error("Item was not provided in params when deleting a grouped item");
				blackout.errorInternal(err);
				return;
			}

			// Ensure id exists
			if (!('id' in params)) {
				blackout.errorInternal(new Error('Primary key was not found in params, or params.item while using db.deleteItem'));
				return;
			}

			try {

				// Wait for all promises
				let awaits = [];

				// Delete from dynamo
				awaits.push(dynamo.deleteItemAsync(params));

				// Delete from cache
				/**
     * We always delete from cache even if caching isn't set. This is for security in case we forget to set caching to true when removing auth related items
     */
				redis.del(_this8.makeCacheKey(params));

				// Update group cache?
				if (params.caching && params.groups && !blackout.isEmpty(params.groups)) {

					for (let key in params.groups) {
						let group = { field: key, value: params.item[key], index: params.groups[key] };
						awaits.push(_this8.removeIdsFromGroupAsync(params, [params.item[params.pk]], group));
					}
				}

				// Wait for promises to resolve
				yield Promise.all(awaits);

				// We're done
				callback();
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Full items should be provided in params.items
  */
	putItems(params, callback) {
		var _this9 = this;

		return _asyncToGenerator(function* () {

			// Setup
			params = _this9.setParamDefaults(params);

			//const redis = this.redis,dynamo = this.dynamo;
			const dynamo = _this9.dynamo;

			try {

				/**
     * Make sure there are ids here.
     * Otherwise throw an error so we don't end up with situations where
     * we think things are getting deleted when they're not
     */
				if (!params.items.length) {
					blackout.errorInternal(new Error('No items were given to db.putItems()'));
					return;
				}

				let items = yield dynamo.putItemsAsync(params);

				// Update group cache?
				if (params.caching && params.groups && !blackout.isEmpty(params.groups) && items.length) {

					// Wait for group insert to run asynchronously.
					let awaits = [];

					// Add item to any groups
					for (let item of items) {

						for (let key in params.groups) {

							let group = { field: key, value: item[key], index: params.groups[key] };
							awaits.push(_this9.addIdsToGroupAsync(params, [item[params.pk]], group));
						}
					}

					yield Promise.all(awaits);
				}

				callback(null, items);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * Full items should be provided in params.items
 	{
 		table: 'table',
 		items: '',
 	}
  */
	deleteItems(params, callback) {
		var _this10 = this;

		return _asyncToGenerator(function* () {

			// Setup
			params = _this10.setParamDefaults(params);

			//const redis = this.redis,dynamo = this.dynamo;
			const redis = _this10.redis,
			      dynamo = _this10.dynamo;

			// Create ids list if needed
			if (!params.ids) {

				params.ids = [];
				for (let item of params.items) {
					params.ids.push(item[params.pk]);
				}
			}

			try {

				/**
     * Make sure there are ids here.
     * Otherwise throw an error so we don't end up with situations where
     * we think things are getting deleted when they're not
     */
				if (!params.ids.length) {
					blackout.errorInternal(new Error('No ids were given to db.deleteItems()'));
					return;
				}

				yield dynamo.deleteItemsAsync(params);

				// Delete from cache
				/**
     * We always delete from cache even if caching isn't set. This is for security in case we forget to set caching to true when removing auth related items
     */
				let cacheKeys = _this10.makeCacheKeys(params, params.ids);
				redis.del(cacheKeys);

				// Update group cache?
				if (params.caching && params.groups && !blackout.isEmpty(params.groups) && params.items.length) {

					let awaits = [];

					// Remove each item from any groups
					for (let key in params.groups) {

						// Make groups of ids
						let ids = {};
						for (let item of params.items) {

							let groupVal = item[key];
							if (!(groupVal in ids)) {
								ids[groupVal] = [];
							}
							ids[groupVal].push(item[params.pk]);
						}

						// Go through unique groups, and remove ids (faster)
						for (let groupVal in ids) {

							let group = { field: key, value: groupVal, index: params.groups[key] };
							awaits.push(_this10.removeIdsFromGroupAsync(params, ids[groupVal], group));
						}
					}

					yield Promise.all(awaits);
				}

				callback();
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * -------------------------------------------------------------------------------------
  * ------------------------------------------------------------------------------------- Private functions
  * -------------------------------------------------------------------------------------
  */

	getItemFromDB(params, callback) {
		var _this11 = this;

		return _asyncToGenerator(function* () {

			const redis = _this11.redis,
			      dynamo = _this11.dynamo;

			if (!params.indexField) {

				let data = yield dynamo.getItemAsync(params);

				try {

					if (data && params.cacheKey && params.caching) {
						yield redis.setmapAsync(params.cacheKey, data);
					}
					if (logCaching) {
						console.log('DB -> ', params.cacheKey);
					}
					callback(null, data);
				} catch (err) {
					blackout.errorInternal(err);
				}
			} else {

				// Check cache for linker
				let linkerCacheKey = _this11.makeCacheKey(params);

				if (params.caching) {

					let data;

					try {

						// Check redis
						data = yield redis.getAsync(linkerCacheKey);
					} catch (err) {
						//
					}

					if (!data) {

						let id = yield _this11.getIdFromIndexInDBAsync(params);
						if (logCaching) {
							console.log('DB -> INDEX ', linkerCacheKey);
						}

						delete params.indexField;
						params.id = id;

						if (id) {

							try {

								// Set linker in cache
								yield redis.setAsync(linkerCacheKey, params.id);

								// Get full row
								_this11.getItem(params, callback);
							} catch (err) {
								blackout.errorInternal(err);
							}
						} else {
							callback(null, null);
						}
					} else {

						delete params.indexField;
						params.id = data;
						if (logCaching) {
							console.log('CACHE -> INDEX ', linkerCacheKey);
						}
						_this11.getItem(params, callback);
					}
				} else {

					let id = yield _this11.getIdFromIndexInDBAsync(params);

					if (logCaching) {
						console.log('DB -> INDEX ', linkerCacheKey);
					}

					if (id) {

						delete params.indexField;
						params.id = id;

						// Get full row
						_this11.getItem(params, callback);
					} else {
						callback(null, null);
					}
				}
			}
		})();
	},

	/**
  * Will try cache first, DB second
  * 
  * Note: caching must be allowed before calling this function
  * Note: params.group must exist. We're assuming only id lists of groups are kept in the cache.
  */
	getIds(params, callback) {
		var _this12 = this;

		return _asyncToGenerator(function* () {

			const redis = _this12.redis;
			let newParams = blackout.cloneObject(params, new Error());

			if (!newParams.group || !newParams.group.index) {
				blackout.errorInternal(new Error('Group is not set or group information is insufficient in getIds()'));
				return false;
			}

			// If paging in a group, we need to specify start and stop points in the list
			let cacheStartIndex = params.cacheStartIndex || 0;
			let cacheStopIndex = newParams.cacheStartIndex + params.limit; // Includes +1 for detecting next page if paging

			let getPotentiallyPagedIdsFromDB = (() => {
				var _ref2 = _asyncToGenerator(function* () {

					let savedLimit;
					if (params.paging && params.group && params.limit) {
						/**
       * We're about to get a bunch of ids from the database for a group
       * We're also paging, which means a limit has been set, but we must not
       * populate the cache with a partial list of ids, since this list is
       * assumed to be complete when reading groups.
       * 
       * So we must remove the limit temporarily and get all ids into the cache
       */
						savedLimit = params.limit;
						delete params.limit;
					}

					try {

						// We'll need to grab keys from the database
						let { ids } = yield _this12.getIdsFromDBAsync(params);

						if (savedLimit) {
							params.limit = savedLimit;
						}

						if (ids && ids.length && params.group && params.caching) {
							let groupCacheKey = _this12.makeGroupCacheKey(params, params.group);

							// Get a lock for this list (group)
							let lock = yield _this12.lockGroupAsync(groupCacheKey);

							// IMPORTANT: Clear list first to avoid duplicates
							// Also, delete list whether we have lock or not
							// Since if we didn't get a lock, we'd delete anyway
							redis.del(groupCacheKey);

							if (lock) {
								yield redis.rpushAsync(groupCacheKey, ids);
								yield _this12.unlockGroupAsync(lock);
							}
						}

						if (ids && params.paging) {
							// We only need to return a subset of these ids, so let's get our subset
							ids = ids.slice(cacheStartIndex, cacheStopIndex + 1);
						}

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

				return function getPotentiallyPagedIdsFromDB() {
					return _ref2.apply(this, arguments);
				};
			})();

			let makeMeta = function (ids) {

				let meta = {};

				meta.cacheLastIndex = ids.length - 1;

				if (params.paging) {

					meta.cacheLastIndex = cacheStartIndex + ids.length - 1;

					/**
      * We need to see if we got `limit` + 1 items, and if so, we know there's
      * another page, so we can set `cacheLastIndex` in the meta response
      */
					meta.isLastPage = true;

					if (params.limit && ids.length > 1 && ids.length === params.limit + 1) {
						meta.isLastPage = false;
						meta.cacheLastIndex--;

						ids.splice(-1, 1);
					}
				}

				return meta;
			};

			// If we're not sorting we should set boundaries now, otherwise we always get all and truncate later
			if (params.paging && !params.sorts) {
				newParams.cacheStartIndex = cacheStartIndex;
				newParams.cacheStopIndex = cacheStopIndex; // Includes +1 for detecting next page if paging
			}

			// Cache start and stop indexes are sometimes already set before this function is called,
			// so we must manually delete if sorting to ensure we get ALL rows
			if (params.sorts) {
				delete newParams.cacheStartIndex;
				delete newParams.cacheStopIndex;
			}

			let ids = yield _this12.getIdsFromIndexInCacheAsync(newParams);

			if (ids && ids.length) {

				if (params.sorts) {

					/**
      * If we're going to be sorting at the cache level, we need
      * to get all the items from the cache and do our own sort
      * We can do this as long as there aren't too many items in
      * the group, otherwise we'll go direct to the db for the ids
      */

					if (ids.length <= 500) {

						// We must make sure we do this based on newParams to get start/stop indexes or lack thereof
						let newNewParams = blackout.cloneObject(newParams, new Error());

						// Attempt to get all items from the cache
						newNewParams.onlyUseDBIfLessThan = 100;

						let data = yield _this12.getFromIdsAsync(newNewParams, ids);

						if (data !== false) {

							// We have all possible rows, now let's sort them

							// Prepare to sort
							let ss = [];
							for (let key in params.sorts) {
								let sort = params.sorts[key];
								let sortField = sort.field;
								if (sort.direction === 'desc') {
									sortField = '-' + sortField;
								}
								ss.push(sortField);
							}

							// Sort!
							data = blackout.sortBy(data, ss);

							// Now truncate?
							if (params.paging && params.limit) {
								data = data.splice(cacheStartIndex, params.limit + 1);
							}

							let meta = makeMeta(data);
							meta.fullData = true; // Let caller know we actually have all data and not just ids
							callback(null, { ids: data, meta: meta });
						} else {

							// There were too many items not found in the cache

							// Get sorted ids direct from the database
							getPotentiallyPagedIdsFromDB();
						}
					} else {

						// Get sorted ids direct from the database
						getPotentiallyPagedIdsFromDB();
					}
				} else {

					let meta = makeMeta(ids);
					callback(null, { ids: ids, meta: meta });
				}
			} else {

				getPotentiallyPagedIdsFromDB();
			}
		})();
	},

	/**
  * params.group must exist
  */
	getIdsFromIndexInCache(params, callback) {
		var _this13 = this;

		return _asyncToGenerator(function* () {

			const redis = _this13.redis;

			try {

				// First, let's see if the ids for this group are in the cache
				let groupCacheKey = _this13.makeGroupCacheKey(params, params.group);

				let start = params.cacheStartIndex || 0;
				let stop = params.cacheStopIndex || -1;

				let ids = yield redis.lrangeAsync(groupCacheKey, start, stop);
				if (ids && ids.length && logCaching) {
					console.log('CACHE -> GROUP ' + (stop === -1 ? '' : '(LIMITED) '), groupCacheKey);
				}
				callback(null, ids);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * params.group must exist
  */
	getIdFromIndexInDB(params, callback) {
		var _this14 = this;

		return _asyncToGenerator(function* () {

			const dynamo = _this14.dynamo;
			let newParams = blackout.cloneObject(params, new Error());

			if (!newParams.indexField) {
				newParams.indexField = newParams.group.field;
				newParams.id = newParams.group.value;
			}
			if (!newParams.group) {
				newParams.group = { field: newParams.indexField, value: newParams.id, index: newParams.indexField };
			}

			let { data } = yield dynamo.getFromIndexAsync(newParams);

			if (!blackout.isEmpty(data)) {

				if (newParams.isIndexTable) {
					// Index tables return one item
					callback(null, data[newParams.pk]);
				} else {
					// This will return an array, so get the first item
					callback(null, data[0][newParams.pk]);
				}
			} else {
				callback(null, null);
			}
		})();
	},

	getIdsFromIndexInDB(params, callback) {
		var _this15 = this;

		return _asyncToGenerator(function* () {

			const dynamo = _this15.dynamo;
			try {

				let newParams = blackout.cloneObject(params, new Error());

				if (!newParams.group || !newParams.group.index) {
					blackout.errorInternal(new Error('Group is not set or group information is insufficient in getIdsFromIndexInDB()'));
					return false;
				}
				newParams.indexField = newParams.group.field;
				newParams.id = newParams.group.value;

				let { data } = yield dynamo.getFromIndexAsync(newParams);
				if (logCaching) {
					console.log('DB -> GROUP getIdsFromIndexInDB ', _this15.makeGroupCacheKey(newParams, newParams.group));
				}

				let ids = [];
				for (let id of data) {
					ids.push(id[newParams.pk]);
				}
				callback(null, ids);
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	getAllFromDB(params, callback) {
		var _this16 = this;

		return _asyncToGenerator(function* () {

			// Defaults
			params = _this16.setParamDefaults(params);

			const dynamo = _this16.dynamo;

			if (params.group) {

				let { data, meta } = yield dynamo.getAllViaIndexAsync(params);
				callback(null, { data: _this16.processItems(data), meta: meta });
			} else {

				let { data, meta } = yield dynamo.getAllAsync(params);
				callback(null, { data: _this16.processItems(data), meta: meta });
			}
		})();
	},

	getIdsFromDB(params, callback) {
		var _this17 = this;

		return _asyncToGenerator(function* () {

			/**
    * This will hit a dynamodb limit at 1MB worth of IDs (approx. 40,000 22-char ids), however, we don't see this as a limit we will ever hit, as this API should never be requesting so many records at once, but rather should use paging or groups for large resources.
    */

			// Defaults
			params = _this17.setParamDefaults(params);

			const dynamo = _this17.dynamo;
			let newParams = blackout.cloneObject(params, new Error());

			if (params.group) {

				newParams.indexField = params.group.field;
				newParams.id = params.group.value;

				let ids = yield _this17.getIdsFromIndexInDBAsync(newParams);
				callback(null, { ids: ids });
			} else {

				if (!params.paging) {
					delete newParams.limit;
				}
				newParams.attrs = [params.pk];

				let { data, meta } = yield dynamo.getAllAsync(newParams);
				let ids = [];
				for (let id of data) {
					ids.push(id[params.pk]);
				}
				callback(null, { ids: ids, meta: meta });
			}
		})();
	},

	getMultipleFromDB(params, callback) {
		var _this18 = this;

		return _asyncToGenerator(function* () {

			/**
    * This will hit a dynamodb limit of 16MB or 100 items. This is why the paging item limit is set at 100
    */

			const redis = _this18.redis,
			      dynamo = _this18.dynamo;

			let data = yield dynamo.batchGetItemsAsync(params);

			if (data && params.caching) {

				try {

					let newParams = blackout.cloneObject(params, new Error());
					let awaits = [];

					// Store in cache
					for (let item of data) {

						if (item) {

							newParams.id = item[params.pk];
							let cacheKey = _this18.makeCacheKey(newParams);
							if (logCaching) {
								console.log('DB -> ', cacheKey);
							}
							awaits.push(redis.setmapAsync(cacheKey, item));
						}
					}

					yield Promise.all(awaits);
				} catch (err) {
					blackout.errorInternal(err);
				}
			}

			callback(null, _this18.processItems(data));
		})();
	},

	/**
  * A group lock should be obtained before calling this function
  * and before reading items from the database, and then the lock
  * should be released after this function has been called
  */
	removeIdsNotFound(params, ids, data, callback) {
		var _this19 = this;

		return _asyncToGenerator(function* () {

			const redis = _this19.redis;

			try {

				let groupCacheKey = _this19.makeGroupCacheKey(params, params.group);

				// Get returned ids
				let foundIds = [];
				for (let item of data) {
					foundIds.push(item[params.pk]);
				}

				let awaits = [];

				for (let id of ids) {
					if (!foundIds.includes(id)) {
						// Remove id from cache list
						awaits.push(redis.lremAsync(groupCacheKey, 0, id));
					}
				}

				yield Promise.all(awaits);

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

	/**
  * Get's the last item in a sorted index
  * 
  * WARNING: This function uses dynamodb's query function
  * which makes it difficult to cache at this level.
  * You should either be calling this function infrequently,
  * or using caching above this level.
  * 
 	{
 		table: table,
 		id: Partition key or GSI key
 	}
  */
	getLastItem(params, callback) {
		var _this20 = this;

		return _asyncToGenerator(function* () {

			params = _this20.setParamDefaults(params);
			delete params.caching;

			// Assert id exists
			if (!params.id || blackout.isEmpty(params.id)) {
				blackout.errorInternal(new Error('Id was not provided to db.getLastItem'));
				return;
			}

			// Get last item
			params.sort = 'desc';
			params.limit = 1;
			params.conditions = {};
			params.conditions[params.pk] = params.id;

			let data = yield _this20.dynamo.queryAsync(params);

			if (data && data.length) {
				callback(null, data[0]);
			} else {
				callback(null, null);
			}
		})();
	},

	/**
  * --------------------------------------------------------------- Higher level actions
  */

	/**
  * Get a random item from a table, group, or filtered group
  * Has full caching support.
  * 
 	{
 		table: 'table', // Required
 		group: { field: 'field', value: 'value', index: 'index' }, // Optional
 		caching: true, // Optional. Caching is only used if using a group, or filtered group
 	}
  * 
  */
	getRandomItem(params, callback) {
		var _this21 = this;

		return _asyncToGenerator(function* () {

			params = _this21.setParamDefaults(params);

			// Called if the random item does not exist on direct request after picking a random id/index
			let itemDidNotExist = function () {

				// It's quite possible the data could change between when we get the list of ids, and then retrieving the item, so let's try again
				if (!params.attempt) {
					params.attempts = 1;
				} else {
					params.attempts++;
				}

				// Only attempt a few times
				if (params.attempts < 5) {
					_this21.getRandomItem(params, callback);
				} else {
					throw new Error('Could not get random item after a few attempts.');
				}
			};

			try {

				// --------------------------------------------- Filtered Group

				if (params.caching && params.group && params.filter) {

					// Get a lock, but allow all local locks to continue after first
					let lockKey = 'Locks:getRandomItem:Table-' + params.table + '-Group-' + params.group.field + '->' + params.group.value + '-Filter-' + JSON.stringify(params.filter);
					let lock = yield blackout.waitForLockAsync(lockKey, 3000, true);

					let item;

					// We don't want the whole group at this point, so no use calling getAll
					// Get group cache key
					let filterGroupCacheKey = _this21.makeFilterGroupCacheKey(params);

					// Is group in cache?
					let groupLength = yield _this21.redis.llenAsync(filterGroupCacheKey);
					if (groupLength > 0) {

						// If so, get random index
						let randomIndex = blackout.rand(0, groupLength);

						// Get item from cache list
						let id = yield _this21.redis.lindexAsync(filterGroupCacheKey, randomIndex);
						if (!id) {
							itemDidNotExist();
							return;
						} else {

							item = yield _this21.getItemAsync({
								table: params.table,
								id: id,
								pk: params.pk
							});

							if (!id) {
								itemDidNotExist();
								return;
							}
						}

						// If not
					} else {

						// Get filtered group (TODO, getAll should support filtered groups, and caching, locking etc.)
						let filterParams = blackout.cloneObject(params, new Error());

						// Only get ids
						if (!params.cacheAllItems) {
							filterParams.attrs = [params.pk];
						}

						// Turn off caching since getAll doesn't support filters yet
						filterParams.caching = false;
						let { data } = yield _this21.getAllAsync(filterParams);

						if (data.length) {

							// Store all ids in redis filter group
							let ids = [];
							for (let item of data) {
								ids.push(item[params.pk]);
							}
							yield _this21.redis.rpushAsync(filterGroupCacheKey, ids);

							// Store full items in redis
							if (params.cacheAllItems) {
								yield _this21.storeItemsInCacheAsync(data, params);
							}

							// Get random id
							let id = ids.getRandom();

							// Get item
							item = yield _this21.getItemAsync({
								table: params.table,
								id: id,
								pk: params.pk
							});
							if (!id) {
								itemDidNotExist();
								return;
							}
						} else {
							yield blackout.unlockAsync(lock);
							console.log('No items exist for getRandomItem: ', params);
							callback(null, null);
							return;
						}
					}

					// Return
					yield blackout.unlockAsync(lock);
					callback(null, item);

					// --------------------------------------------- Group
				} else if (params.caching && params.group) {

					let item;

					// We don't want the whole group at this point, so no use calling getAll
					// Get group cache key
					let groupCacheKey = _this21.makeGroupCacheKey(params);

					// Is group in cache?
					let groupLength = yield _this21.redis.llenAsync(groupCacheKey);
					if (groupLength > 0) {

						// If so, get random index
						let randomIndex = blackout.rand(0, groupLength);

						// Get item from cache list
						let id = yield _this21.redis.lindexAsync(groupCacheKey, randomIndex);
						if (!id) {
							itemDidNotExist();
							return;
						} else {
							item = yield _this21.getItemAsync({
								table: params.table,
								id: id,
								pk: params.pk
							});
							if (!id) {
								itemDidNotExist();
								return;
							}
						}

						// If not
					} else {

						// Get group (will be cached as well)
						let { data } = yield _this21.getAllAsync(params);

						if (data.length) {

							// Get random item
							item = data.getRandom();
						} else {
							console.log('No items exist for getRandomItem: ', params);
							callback(null, null);
							return;
						}
					}

					// Return
					callback(null, item);

					// --------------------------------------------- Whole table

				} else {

					let idParams = {
						table: params.table,
						attrs: [params.pk],
						caching: false
					};

					if (params.group) {
						idParams.group = params.group;
					}

					// Get ids from table
					let { data } = yield _this21.getAllAsync(idParams);
					if (data.length) {

						let itemId = data.getRandom();

						let itemParams = {
							table: params.table,
							id: itemId[params.pk]
						};

						let item = yield _this21.getItemAsync(itemParams);

						// If item no loner exists
						if (blackout.isEmpty(item)) {
							itemDidNotExist();
							return;
						}

						callback(null, data);
					} else {
						console.log('No items exist for getRandomItem: ', params);
						callback(null, null);
						return;
					}
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	/**
  * -------------------------------------------------------------------------------------
  * ------------------------------------------------------------------------------------- Facilitative functions
  * -------------------------------------------------------------------------------------
  */

	storeItemsInCache(items, params, callback) {
		var _this22 = this;

		return _asyncToGenerator(function* () {
			let arr = [];
			for (let item of items) {
				let cacheKey = _this22.makeCacheKey({
					table: params.table,
					id: item[params.pk],
					pk: params.pk
				});
				arr.push(cacheKey, item);
			}
			yield blackout.redis.msetmapAsync(arr);
			callback();
		})();
	},

	processItems(data) {

		if (!data) {
			return data;
		}

		if (blackout.isArray(data)) {
			for (let i in data) {
				data[i] = this.processItem(data[i]);
			}
			return data;
		} else {
			return this.processItem(data);
		}
	},

	processItem(item) {

		if (!item) {
			return item;
		}
		const locale = this.locale.locale;
		const baseLocale = this.locale.baseLocale;

		for (let key in item) {

			let val = item[key];
			if (typeof val === 'object' && val.en) {

				// We have a translated item, check for locale
				if (val[locale]) {
					item[key] = val[locale];
				} else if (val[baseLocale]) {
					item[key] = val[baseLocale];
				} else {
					item[key] = val.en;
				}
			}
		}

		return item;
	},

	lockGroup(groupCacheKey, callback) {
		var _this23 = this;

		return _asyncToGenerator(function* () {

			const redis = _this23.redis;
			let lock;

			try {

				// Get a lock for this list (group)
				lock = yield blackout.waitForLockAsync(groupCacheKey);
			} catch (err) {
				//
			}

			if (!lock) {
				// Log a failed lock but allow to continue
				console.log(new Error('Failed to get lock during addIdsToGroup for key ' + groupCacheKey));

				/**
     * If we couldn't get a lock, there's most likely something
     * awry with the whole system, so let's just delete the list
     * from the cache to force a rebuild next time it's read
     */
				yield redis.delAsync(groupCacheKey);

				callback(null, false);
			} else {
				callback(null, lock);
			}
		})();
	},

	unlockGroup(lock, callback) {
		blackout.unlock(lock, callback);
	},

	addIdsToGroup(params, ids, group, callback) {
		var _this24 = this;

		return _asyncToGenerator(function* () {

			const redis = _this24.redis;
			if (!ids || !ids.length) {
				callback();
				return;
			}

			try {

				let groupCacheKey = _this24.makeGroupCacheKey(params, group);

				// Get a lock for this list (group)
				let lock = yield _this24.lockGroupAsync(groupCacheKey);
				if (lock) {

					// Make sure group exists in cache first!
					// Otherwise we might end up creating an incomplete list
					let exists = yield redis.existsAsync(groupCacheKey);

					if (exists) {

						try {

							yield redis.rpushAsync(groupCacheKey, ids);
						} catch (err) {
							// If there's an error, delete the whole list
							yield redis.delAsync(groupCacheKey);
						}
					}

					_this24.unlockGroup(lock, callback);
				} else {
					blackout.errorInternal(new Error('Failed to get lock during addIdsToGroup for key ' + groupCacheKey));
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	removeIdsFromGroup(params, ids, group, callback) {
		var _this25 = this;

		return _asyncToGenerator(function* () {

			const redis = _this25.redis;

			try {

				let groupCacheKey = _this25.makeGroupCacheKey(params, group);

				// Get a lock for this list (group)
				let lock = yield blackout.waitForLockAsync(groupCacheKey, 3000);

				if (!lock) {
					// Log a failed lock but allow to continue
					console.log('Failed to get lock during removeIdsFromGroup for key ' + groupCacheKey);

					/**
      * If we couldn't get a lock, there's most likely something
      * awry with the whole system, so let's just delete the list
      * from the cache to force a rebuild next time it's read
      */
					yield redis.delAsync(groupCacheKey);

					callback();
				} else {

					// Make sure group exists in cache first
					let exists = redis.existsAsync(groupCacheKey);

					if (exists) {

						try {

							let awaits = [];

							if (!blackout.isArray(ids)) {
								ids = [ids];
							}

							for (let id of ids) {
								awaits.push(redis.lremAsync(groupCacheKey, 0, id));
							}

							yield Promise.all(awaits);
						} catch (err) {
							// If there's an error, delete the whole list
							yield redis.delAsync(groupCacheKey);
						}
					}

					yield blackout.unlockAsync(lock);
					callback();
				}
			} catch (err) {
				blackout.errorInternal(err);
			}
		})();
	},

	normaliseField(field) {

		return field.upperCaseFirst();
	},

	makeCacheKey(params) {

		let id = params.id ? params.id : params.patch ? params.patch[params.pk] : params.item ? params.item[params.pk] : null;
		if (!id) {
			console.log('error params', params);
			blackout.errorInternal(new Error("Could not determine id for makeCacheKey"));
		}
		return 'Rugby:Tables:' + params.table + ':Items:' + (params.indexField ? params.indexField : params.pk) + '-' + id;
	},

	makeCacheKeys(params, idsOrItems) {

		let keys = [];
		let newParams = blackout.cloneObject(params, new Error());
		for (let idOrItem of idsOrItems) {
			if (blackout.isArray(idOrItem)) {
				newParams.id = idOrItem[params.pk];
			} else {
				newParams.id = idOrItem;
			}
			keys.push(this.makeCacheKey(newParams));
		}

		return keys;
	},

	makeGroupCacheKey(params, group) {

		if (!group && 'group' in params) group = params.group;
		if (group.value === undefined) {
			throw new Error(`Group value was undefined for group on table (${params.table}) with field (${group.field})`);
		}
		return 'Rugby:Tables:' + params.table + ':Groups:' + group.field + '-' + group.value;
	},

	makeFilterGroupCacheKey(params) {

		// Get group key
		let groupCacheKey = this.makeGroupCacheKey(params);

		// Make filters key
		let filterKeys = [];
		for (let key in params.filter) {
			let val = params.filter[key];
			filterKeys.push(key + '->' + val);
		}
		let filterCacheKey = filterKeys.join('+');

		return groupCacheKey + ':Filters:' + filterCacheKey;
	},

	makeKeyvalCacheKey(params) {

		return 'Rugby:Tables:' + params.table + ':Keyvals:' + (params.group ? 'Group:' + params.group.field + '-' + params.group.value : 'Keyvals');
	},

	makeCountryNameTypeCacheKey(params) {

		return 'Names-Filter:Country-' + params.country + '-NameType-' + params.nameType;
	}

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