Repository: dynamoosejs/dynamoose
Pull request #776 (2020-04-04 22:21:16)

Batch methods


Patch:
diff --git a/.nycrc b/.nycrc
index af116a66..15331f23 100644
--- a/.nycrc
+++ b/.nycrc
@@ -2,6 +2,7 @@
 	"exclude": [
 		"test",
 		"coverage",
-		".eslintrc.js"
+		".eslintrc.js",
+		".mocharc.js"
 	]
 }
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 1b2bed65..86ae0027 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -8,11 +8,14 @@ The following is a chart of IAM permissions you need in order to run Dynamoose f
 |------------------|----------------|-------|
 | new Model() | `createTable`, `describeTable`, `updateTable`, `updateTimeToLive`, `describeTimeToLive` | `createTable` is only used if `create` is set to true. `describeTable` is only used if `waitForActive` OR `create` is set to true. `updateTable` is only used if `update` is set to true. `updateTimeToLive` & `describeTimeToLive` is only used if `create` or `update` is set to true and there is an `expires` setting on the model. |
 | Model.get | `getItem` |  |
+| Model.batchGet | `batchGetItem` |  |
 | Model.scan | `scan` | This permission is only required on `scan.exec` |
 | Model.query | `query` | This permission is only required on `query.exec` |
 | Model.create | `putItem` |  |
+| Model.batchPut | `batchWriteItem` |  |
 | Model.update | `updateItem` |  |
 | Model.delete | `deleteItem` |  |
+| Model.batchDelete | `batchWriteItem` |  |
 | document.save | `putItem` |  |
 | document.delete | `deleteItem` |  |
 | dynamoose.transaction | `transactGetItems`, `transactWriteItems` |  |
diff --git a/docs/api/Model.md b/docs/api/Model.md
index aeab8e3f..b8a291df 100644
--- a/docs/api/Model.md
+++ b/docs/api/Model.md
@@ -217,6 +217,111 @@ User.get({"id": 1}, (error, myUser) => {
 });
 ```
 
+## Model.batchGet(keys[, settings][, callback])
+
+You can use Model.batchGet to retrieve multiple documents from DynamoDB. This method uses the `batchGetItem` DynamoDB API call to retrieve the object.
+
+This method returns a promise that will resolve when the operation is complete, this promise will reject upon failure. You can also pass in a function into the `callback` parameter to have it be used in a callback format as opposed to a promise format. An array of Document instances will be the result of the promise or callback response. In the event no items can be found in DynamoDB this method will return an empty array.
+
+The array you receive back is a standard JavaScript array of objects. However, the array has some special properties with extra information about your scan operation that you can access. This does not prevent the ability do running loops or accessing the objects within the array.
+
+The extra properties attached to the array are:
+
+- `unprocessedKeys` - In the event there are more items to get in DynamoDB this property will be equal to an array of unprocessed keys. You can take this property and call `batchGet` again to retrieve those items. Normally DynamoDB returns this property as a DynamoDB object, but Dynamoose returns it and handles it as a standard JS object without the DynamoDB types.
+
+You can also pass in an object for the optional `settings` parameter that is an object. The table below represents the options for the `settings` object.
+
+| Name | Description | Type | Default |
+|------|-------------|------|---------|
+| return | What the function should return. Can be `documents`, or `request`. In the event this is set to `request` the request Dynamoose will make to DynamoDB will be returned, and no request to DynamoDB will be made. If this is `request`, the function will not be async anymore. | String | `documents` |
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+try {
+	const myUsers = await User.batchGet([1, 2]);
+	console.log(myUsers);
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchGet([1, 2], (error, myUsers) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(myUsers);
+	}
+});
+```
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+const retrieveUsersRequest = User.batchGet([1, 2], {"return": "request"});
+// {
+// 	"RequestItems": {
+// 		"User": {
+// 			"Keys": [
+// 				{"id": {"N": "1"}},
+// 				{"id": {"N": "2"}}
+// 			]
+// 		}
+// 	}
+// }
+
+// OR
+
+User.batchGet([1, 2], {"return": "request"}, (error, request) => {
+	console.log(request);
+});
+```
+
+In the event you have a rangeKey for your model, you can pass in an object for the `hashKey` parameter.
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "rangeKey": true}});
+
+try {
+	const myUsers = await User.batchGet([{"id": 1, "name": "Tim"}, {"id": 2, "name": "Charlie"}]);
+	console.log(myUsers);
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchGet({"id": 1, "name": "Tim"}, (error, myUsers) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(myUsers);
+	}
+});
+```
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+try {
+	const myUsers = await User.batchGet([{"id": 1}, {"id": 2}]);
+	console.log(myUsers);
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchGet([{"id": 1}, {"id": 2}], (error, myUsers) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(myUsers);
+	}
+});
+```
+
 ## Model.create(document, [settings], [callback])
 
 This function lets you create a new document for a given model. This function is almost identical to creating a new document and calling `document.save`, with one key difference, this function will default to setting `overwrite` to false.
@@ -244,6 +349,54 @@ User.create({"id": 1, "name": "Tim"}, (error, user) => {  // If a user with `id=
 });
 ```
 
+## Model.batchPut(documents, [settings], [callback])
+
+This saves documents to DynamoDB. This method uses the `batchWriteItem` DynamoDB API call to store your objects in the given table associated with the model. This method is overwriting, and will overwrite the data you currently have in place for the existing key for your table.
+
+This method returns a promise that will resolve when the operation is complete, this promise will reject upon failure. You can also pass in a function into the `callback` parameter to have it be used in a callback format as opposed to a promise format. Nothing will be passed into the result for the promise or callback.
+
+You can also pass a settings object in as the second parameter. The following options are available for settings are:
+
+- `return` (default: `response`) - If the function should return the `response` or `request`. If you set this to `request` the request that would be made to DynamoDB will be returned, but no requests will be made to DynamoDB.
+
+Both `settings` and `callback` parameters are optional. You can pass in a `callback` without `settings`, just by passing in your array of objects as the first parameter, and the second argument as the `callback` function. You are not required to pass in `settings` if you just want to pass in a `callback`.
+
+```js
+//...
+
+try {
+	const result = await User.batchPut([
+		{"id": 1, "name": "Charlie"},
+		{"id": 2, "name": "Bob"}
+	]);
+	console.log(result);
+	// {
+	// 	"unprocessedItems": []
+	// }
+
+	// OR
+
+	// {
+	// 	"unprocessedItems": [{"id": 1, "name": "Charlie"}]
+	// }
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+await User.batchPut([
+	{"id": 1, "name": "Charlie"},
+	{"id": 2, "name": "Bob"}
+], (error) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(result);
+	}
+});
+```
+
 ## Model.update(keyObj[, updateObj[, settings]],[ callback])
 
 This function lets you update an existing document in the database. You can either pass in one object combining both the hashKey you wish to update along with the update object, or keep them separate by passing in two objects.
@@ -308,7 +461,7 @@ The `validate` Schema attribute property will only be run on `$SET` values. This
 
 ## Model.delete(hashKey[, settings][, callback])
 
-You can use Model.delete to delete a document from DynamoDB. This method uses the `deleteItem` DynamoDB API call to retrieve the object.
+You can use Model.delete to delete a document from DynamoDB. This method uses the `deleteItem` DynamoDB API call to delete the object.
 
 This method returns a promise that will resolve when the operation is complete, this promise will reject upon failure. You can also pass in a function into the `callback` parameter to have it be used in a callback format as opposed to a promise format. In the event the operation was successful, noting will be returned to you. Otherwise an error will be thrown.
 
@@ -361,7 +514,7 @@ In the event you have a rangeKey for your model, you can pass in an object for t
 const User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "rangeKey": true}});
 
 try {
-	await User.get({"id": 1, "name": "Tim"});
+	await User.delete({"id": 1, "name": "Tim"});
 	console.log("Successfully deleted item");
 } catch (error) {
 	console.error(error);
@@ -369,7 +522,7 @@ try {
 
 // OR
 
-User.get({"id": 1, "name": "Tim"}, (error) => {
+User.delete({"id": 1, "name": "Tim"}, (error) => {
 	if (error) {
 		console.error(error);
 	} else {
@@ -382,7 +535,7 @@ User.get({"id": 1, "name": "Tim"}, (error) => {
 const User = new dynamoose.Model("User", {"id": Number, "name": String});
 
 try {
-	await User.get({"id": 1});
+	await User.delete({"id": 1});
 	console.log("Successfully deleted item");
 } catch (error) {
 	console.error(error);
@@ -390,7 +543,7 @@ try {
 
 // OR
 
-User.get({"id": 1}, (error) => {
+User.delete({"id": 1}, (error) => {
 	if (error) {
 		console.error(error);
 	} else {
@@ -399,6 +552,120 @@ User.get({"id": 1}, (error) => {
 });
 ```
 
+## Model.batchDelete(keys[, settings][, callback])
+
+You can use Model.batchDelete to delete documents from DynamoDB. This method uses the `batchWriteItem` DynamoDB API call to delete the objects.
+
+This method returns a promise that will resolve when the operation is complete, this promise will reject upon failure. You can also pass in a function into the `callback` parameter to have it be used in a callback format as opposed to a promise format. In the event the operation was successful, an object with the `unprocessedItems` will be returned to you. Otherwise an error will be thrown.
+
+You can also pass in an object for the optional `settings` parameter that is an object. The table below represents the options for the `settings` object.
+
+| Name | Description | Type | Default |
+|------|-------------|------|---------|
+| return | What the function should return. Can be `response`, or `request`. In the event this is set to `request` the request Dynamoose will make to DynamoDB will be returned, and no request to DynamoDB will be made. If this is `request`, the function will not be async anymore. | String | `response` |
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+try {
+	const response = await User.batchDelete([1, 2]);
+	console.log(response);
+	// {
+	// 	"unprocessedItems": []
+	// }
+
+	// OR
+
+	// {
+	// 	"unprocessedItems": [{"id": 1}]
+	// }
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchDelete([1, 2], (error, response) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(`Successfully deleted items. ${response.unprocessedItems.count} of unprocessed items.`);
+	}
+});
+```
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+const deleteUserRequest = User.batchDelete([1, 2], {"return": "request"});
+// {
+// 	"RequestItems": {
+// 		"User": [
+// 			{
+// 				"DeleteRequest": {
+// 					"Key": {"id": {"N": "1"}}
+// 				}
+// 			},
+// 			{
+// 				"DeleteRequest": {
+// 					"Key": {"id": {"N": "2"}}
+// 				}
+// 			}
+// 		]
+// 	}
+// }
+
+// OR
+
+User.batchDelete([1, 2], {"return": "request"}, (error, request) => {
+	console.log(request);
+});
+```
+
+In the event you have a rangeKey for your model, you can pass in an object for the `hashKey` parameter.
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "rangeKey": true}});
+
+try {
+	const response = await User.batchDelete([{"id": 1, "name": "Tim"}, {"id": 2, "name": "Charlie"}]);
+	console.log(`Successfully deleted item. ${response.unprocessedItems.count} of unprocessed items.`);
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchDelete([{"id": 1, "name": "Tim"}, {"id": 2, "name": "Charlie"}], (error, response) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(`Successfully deleted item. ${response.unprocessedItems.count} of unprocessed items.`);
+	}
+});
+```
+
+```js
+const User = new dynamoose.Model("User", {"id": Number, "name": String});
+
+try {
+	const response = await User.batchDelete([{"id": 1}, {"id": 2}]);
+	console.log(`Successfully deleted item. ${response.unprocessedItems.count} of unprocessed items.`);
+} catch (error) {
+	console.error(error);
+}
+
+// OR
+
+User.batchDelete([{"id": 1}, {"id": 2}], (error, response) => {
+	if (error) {
+		console.error(error);
+	} else {
+		console.log(`Successfully deleted item. ${response.unprocessedItems.count} of unprocessed items.`);
+	}
+});
+```
+
 ## Model.transaction
 
 This object has the following methods that you can call.
diff --git a/lib/Model.js b/lib/Model.js
index b99cb44b..b8b2e4d8 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -96,7 +96,7 @@ class Model {
 			}},
 			{"key": "condition", "settingsIndex": -1, "dynamoKey": "ConditionCheck", "function": (key, options) => ({
 				...options,
-				"Key": convertObjectToKey.bind(this)(key),
+				"Key": this.Document.toDynamo(convertObjectToKey.bind(this)(key)),
 				"TableName": this.name
 			})}
 		].reduce((accumulator, currentValue) => {
@@ -257,7 +257,7 @@ function convertObjectToKey(key) {
 		};
 	}
 
-	return this.Document.toDynamo(keyObject);
+	return keyObject;
 }
 
 
@@ -270,7 +270,7 @@ Model.prototype.get = function (key, settings = {}, callback) {
 	const documentify = (document) => (new this.Document(document, {"fromDynamo": true})).conformToSchema({"customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "modifiers": ["get"], "type": "fromDynamo"});
 
 	const getItemParams = {
-		"Key": convertObjectToKey.bind(this)(key),
+		"Key": this.Document.toDynamo(convertObjectToKey.bind(this)(key)),
 		"TableName": this.name
 	};
 	if (settings.return === "request") {
@@ -292,6 +292,61 @@ Model.prototype.get = function (key, settings = {}, callback) {
 		})();
 	}
 };
+Model.prototype.batchGet = function (keys, settings = {}, callback) {
+	if (typeof settings === "function") {
+		callback = settings;
+		settings = {};
+	}
+
+	const documentify = (document) => (new this.Document(document, {"fromDynamo": true})).conformToSchema({"customTypesDynamo": true, "checkExpiredItem": true, "saveUnknown": true, "modifiers": ["get"], "type": "fromDynamo"});
+	const prepareResponse = async (response) => {
+		const tmpResult = await Promise.all(response.Responses[this.name].map((item) => documentify(item)));
+		const unprocessedArray = response.UnprocessedKeys[this.name] ? response.UnprocessedKeys[this.name].Keys : [];
+		const tmpResultUnprocessed = await Promise.all(unprocessedArray.map((item) => this.Document.fromDynamo(item)));
+		const startArray = [];
+		startArray.unprocessedKeys = [];
+		return keyObjects.reduce((result, key) => {
+			const keyProperties = Object.keys(key);
+			let item = tmpResult.find((item) => keyProperties.every((keyProperty) => item[keyProperty] === key[keyProperty]));
+			if (item) {
+				result.push(item);
+			} else {
+				item = tmpResultUnprocessed.find((item) => keyProperties.every((keyProperty) => item[keyProperty] === key[keyProperty]));
+				if (item) {
+					result.unprocessedKeys.push(item);
+				}
+			}
+			return result;
+		}, startArray);
+	};
+
+	const keyObjects = keys.map((key) => convertObjectToKey.bind(this)(key));
+	const params = {
+		"RequestItems": {
+			[this.name]: {
+				"Keys": keyObjects.map((key) => this.Document.toDynamo(key))
+			}
+		}
+	};
+	if (settings.return === "request") {
+		if (callback) {
+			callback(null, params);
+			return;
+		} else {
+			return params;
+		}
+	}
+	const promise = this.pendingTaskPromise().then(() => aws.ddb().batchGetItem(params).promise());
+
+	if (callback) {
+		promise.then((response) => prepareResponse(response)).then((response) => callback(null, response)).catch((error) => callback(error));
+	} else {
+		return (async () => {
+			const response = await promise;
+			return prepareResponse(response);
+		})();
+	}
+};
 
 Model.prototype.create = function (document, settings = {}, callback) {
 	if (typeof settings === "function" && !callback) {
@@ -301,6 +356,52 @@ Model.prototype.create = function (document, settings = {}, callback) {
 
 	return (new this.Document(document)).save({"overwrite": false, ...settings}, callback);
 };
+Model.prototype.batchPut = function (items, settings = {}, callback) {
+	if (typeof settings === "function") {
+		callback = settings;
+		settings = {};
+	}
+
+	const prepareResponse = async (response) => {
+		const unprocessedArray = response.UnprocessedItems && response.UnprocessedItems[this.name] ? response.UnprocessedItems[this.name] : [];
+		const tmpResultUnprocessed = await Promise.all(unprocessedArray.map((item) => this.Document.fromDynamo(item.PutRequest.Item)));
+		return items.reduce((result, document) => {
+			const item = tmpResultUnprocessed.find((item) => Object.keys(document).every((keyProperty) => item[keyProperty] === document[keyProperty]));
+			if (item) {
+				result.unprocessedItems.push(item);
+			}
+			return result;
+		}, {"unprocessedItems": []});
+	};
+
+	const paramsPromise = (async () => ({
+		"RequestItems": {
+			[this.name]: await Promise.all(items.map(async (item) => ({
+				"PutRequest": {
+					"Item": await (new this.Document(item)).toDynamo({"defaults": true, "validate": true, "required": true, "enum": true, "forceDefault": true, "saveUnknown": true, "customTypesDynamo": true, "updateTimestamps": true, "modifiers": ["set"]})
+				}
+			})))
+		}
+	}))();
+	if (settings.return === "request") {
+		if (callback) {
+			paramsPromise.then((result) => callback(null, result));
+			return;
+		} else {
+			return paramsPromise;
+		}
+	}
+	const promise = this.pendingTaskPromise().then(() => paramsPromise).then((params) => aws.ddb().batchWriteItem(params).promise());
+
+	if (callback) {
+		promise.then((response) => prepareResponse(response)).then((response) => callback(null, response)).catch((error) => callback(error));
+	} else {
+		return (async () => {
+			const response = await promise;
+			return prepareResponse(response);
+		})();
+	}
+};
 
 Model.prototype.update = function (keyObj, updateObj, settings = {}, callback) {
 	if (typeof updateObj === "function") {
@@ -490,7 +591,7 @@ Model.prototype.delete = function (key, settings = {}, callback) {
 	}
 
 	const deleteItemParams = {
-		"Key": convertObjectToKey.bind(this)(key),
+		"Key": this.Document.toDynamo(convertObjectToKey.bind(this)(key)),
 		"TableName": this.name
 	};
 	if (settings.return === "request") {
@@ -509,6 +610,53 @@ Model.prototype.delete = function (key, settings = {}, callback) {
 		return promise;
 	}
 };
+Model.prototype.batchDelete = function (keys, settings = {}, callback) {
+	if (typeof settings === "function") {
+		callback = settings;
+		settings = {};
+	}
+
+	const prepareResponse = async (response) => {
+		const unprocessedArray = response.UnprocessedItems && response.UnprocessedItems[this.name] ? response.UnprocessedItems[this.name] : [];
+		const tmpResultUnprocessed = await Promise.all(unprocessedArray.map((item) => this.Document.fromDynamo(item.DeleteRequest.Key)));
+		return keyObjects.reduce((result, key) => {
+			const item = tmpResultUnprocessed.find((item) => Object.keys(key).every((keyProperty) => item[keyProperty] === key[keyProperty]));
+			if (item) {
+				result.unprocessedItems.push(item);
+			}
+			return result;
+		}, {"unprocessedItems": []});
+	};
+
+	const keyObjects = keys.map((key) => convertObjectToKey.bind(this)(key));
+	const params = {
+		"RequestItems": {
+			[this.name]: keyObjects.map((key) => ({
+				"DeleteRequest": {
+					"Key": this.Document.toDynamo(key)
+				}
+			}))
+		}
+	};
+	if (settings.return === "request") {
+		if (callback) {
+			callback(null, params);
+			return;
+		} else {
+			return params;
+		}
+	}
+	const promise = this.pendingTaskPromise().then(() => aws.ddb().batchWriteItem(params).promise());
+
+	if (callback) {
+		promise.then((response) => prepareResponse(response)).then((response) => callback(null, response)).catch((error) => callback(error));
+	} else {
+		return (async () => {
+			const response = await promise;
+			return prepareResponse(response);
+		})();
+	}
+};
 
 Model.prototype.scan = {"carrier": require("./DocumentRetriever")("scan")};
 Model.prototype.query = {"carrier": require("./DocumentRetriever")("query")};


Test patch:
diff --git a/test/Model.js b/test/Model.js
index f8f50061..3ea66232 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -1085,6 +1085,220 @@ describe("Model", () => {
 		});
 	});
 
+	describe("Model.batchGet", () => {
+		let User, params, promiseFunction;
+		beforeEach(() => {
+			User = new dynamoose.Model("User", {"id": Number, "name": String});
+			params = null;
+			promiseFunction = null;
+			dynamoose.aws.ddb.set({
+				"batchGetItem": (paramsB) => {
+					params = paramsB;
+					return {"promise": promiseFunction};
+				}
+			});
+		});
+		afterEach(() => {
+			User = null;
+			params = null;
+			promiseFunction = null;
+			dynamoose.aws.ddb.revert();
+		});
+
+		it("Should be a function", () => {
+			expect(User.batchGet).to.be.a("function");
+		});
+
+		const functionCallTypes = [
+			{"name": "Promise", "func": (Model) => Model.batchGet},
+			{"name": "Callback", "func": (Model) => util.promisify(Model.batchGet)}
+		];
+
+		functionCallTypes.forEach((callType) => {
+			describe(callType.name, () => {
+				it("Should send correct params to batchGetItem", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});
+					await callType.func(User).bind(User)([1]);
+					expect(params).to.be.an("object");
+					expect(params).to.eql({
+						"RequestItems": {
+							"User": {
+								"Keys": [
+									{"id": {"N": "1"}}
+								]
+							}
+						}
+					});
+				});
+
+				it("Should return correct request if setting option return to request", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});
+					const paramsB = await callType.func(User).bind(User)([1], {"return": "request"});
+					expect(params).to.not.exist;
+					expect(paramsB).to.be.an("object");
+					expect(paramsB).to.eql({
+						"RequestItems": {
+							"User": {
+								"Keys": [
+									{"id": {"N": "1"}}
+								]
+							}
+						}
+					});
+				});
+
+				it("Should send correct params to batchGetItem for multiple items", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}, {"id": {"N": "2"}, "name": {"S": "Bob"}}]}, "UnprocessedKeys": {}});
+					await callType.func(User).bind(User)([1, 2]);
+					expect(params).to.be.an("object");
+					expect(params).to.eql({
+						"RequestItems": {
+							"User": {
+								"Keys": [
+									{"id": {"N": "1"}},
+									{"id": {"N": "2"}}
+								]
+							}
+						}
+					});
+				});
+
+				it("Should return correct result from batchGet", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});
+					const result = await callType.func(User).bind(User)([1]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"}
+					]);
+				});
+
+				it("Should return correct result from batchGet for multiple items", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}, {"id": {"N": "2"}, "name": {"S": "Bob"}}]}, "UnprocessedKeys": {}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"},
+						{"id": 2, "name": "Bob"}
+					]);
+				});
+
+				it("Should return correct result from batchGet for multiple items that aren't sorted correctly", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "2"}, "name": {"S": "Bob"}}, {"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"},
+						{"id": 2, "name": "Bob"}
+					]);
+				});
+
+				it("Should return correct result from batchGet with unprocessed keys", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {"User": {"Keys": [{"id": {"N": 2}}]}}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([{"id": 2}]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"}
+					]);
+				});
+
+				it("Should return correct result from batchGet for multiple items with unprocessed keys", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}, {"id": {"N": "3"}, "name": {"S": "Bob"}}]}, "UnprocessedKeys": {"User": {"Keys": [{"id": {"N": 2}}]}}});
+					const result = await callType.func(User).bind(User)([1, 2, 3]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([{"id": 2}]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"},
+						{"id": 3, "name": "Bob"}
+					]);
+				});
+
+				it("Should return correct result from batchGet for multiple items that aren't sorted with unprocessed keys", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "3"}, "name": {"S": "Bob"}}, {"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {"User": {"Keys": [{"id": {"N": 2}}]}}});
+					const result = await callType.func(User).bind(User)([1, 2, 3]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([{"id": 2}]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"},
+						{"id": 3, "name": "Bob"}
+					]);
+				});
+
+				it("Should return correct result from batchGet for multiple unprocessed keys that aren't sorted", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {"User": {"Keys": [{"id": {"N": 3}}, {"id": {"N": 2}}]}}});
+					const result = await callType.func(User).bind(User)([1, 2, 3]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([{"id": 2}, {"id": 3}]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"}
+					]);
+				});
+
+				it("Should handle correctly if item not in Responses or UnprocessedKeys", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {"User": {"Keys": [{"id": {"N": 3}}, {"id": {"N": 2}}]}}});
+					const result = await callType.func(User).bind(User)([1, 2, 3, 4]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([{"id": 2}, {"id": 3}]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"}
+					]);
+				});
+
+				it("Should handle correctly if item not in Responses", async () => {
+					promiseFunction = () => Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.be.an("array");
+					expect(result.unprocessedKeys).to.eql([]);
+					expect(result.map((item) => ({...item}))).to.eql([
+						{"id": 1, "name": "Charlie"}
+					]);
+				});
+
+				it("Should throw error if DynamoDB responds with error", () => {
+					promiseFunction = () => Promise.reject({"error": "Error"});
+
+					return expect(callType.func(User).bind(User)([1, 2, 3])).to.be.rejectedWith({"error": "Error"});
+				});
+
+				it("Should wait for model to be ready prior to running DynamoDB API call", async () => {
+					let calledBatchGetItem = false;
+					promiseFunction = () => {calledBatchGetItem = true; return Promise.resolve({"Responses": {"User": [{"id": {"N": "1"}, "name": {"S": "Charlie"}}]}, "UnprocessedKeys": {}});};
+					let describeTableResponse = {
+						"Table": {"TableStatus": "CREATING"}
+					};
+					dynamoose.aws.ddb.set({
+						"describeTable": () => ({
+							"promise": () => Promise.resolve(describeTableResponse)
+						}),
+						"batchGetItem": () => ({
+							"promise": promiseFunction
+						})
+					});
+					const model = new dynamoose.Model("User", {"id": Number, "name": String}, {"waitForActive": {"enabled": true, "check": {"frequency": 0, "timeout": 100}}});
+					await utils.set_immediate_promise();
+
+					let users;
+					callType.func(model).bind(model)([1]).then((item) => users = item);
+
+					await utils.set_immediate_promise();
+					expect(calledBatchGetItem).to.be.false;
+					expect(users).to.not.exist;
+					expect(model.Model.pendingTasks.length).to.eql(1);
+
+					describeTableResponse = {
+						"Table": {"TableStatus": "ACTIVE"}
+					};
+					await utils.set_immediate_promise();
+					expect(calledBatchGetItem).to.be.true;
+					expect(users.map((user) => ({...user}))).to.eql([{"id": 1, "name": "Charlie"}]);
+				});
+			});
+		});
+	});
+
 	describe("Model.create", () => {
 		let User, createItemParams, createItemFunction;
 		beforeEach(() => {
@@ -1209,6 +1423,110 @@ describe("Model", () => {
 		});
 	});
 
+	describe("Model.batchPut", () => {
+		let User, params, promiseFunction;
+		beforeEach(() => {
+			User = new dynamoose.Model("User", {"id": Number, "name": String});
+			dynamoose.aws.ddb.set({
+				"batchWriteItem": (paramsB) => {
+					params = paramsB;
+					return {"promise": promiseFunction};
+				}
+			});
+		});
+		afterEach(() => {
+			User = null;
+			params = null;
+			promiseFunction = null;
+			dynamoose.aws.ddb.revert();
+		});
+
+		it("Should be a function", () => {
+			expect(User.batchPut).to.be.a("function");
+		});
+
+		const functionCallTypes = [
+			{"name": "Promise", "func": (Model) => Model.batchPut},
+			{"name": "Callback", "func": (Model) => util.promisify(Model.batchPut)}
+		];
+		functionCallTypes.forEach((callType) => {
+			describe(callType.name, () => {
+				it("Should should send correct parameters to batchWriteItem", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {}});
+					await callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}]);
+					expect(params).to.be.an("object");
+					expect(params).to.eql({
+						"RequestItems": {
+							"User": [
+								{
+									"PutRequest": {
+										"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}}
+									}
+								},
+								{
+									"PutRequest": {
+										"Item": {"id": {"N": "2"}, "name": {"S": "Bob"}}
+									}
+								}
+							]
+						}
+					});
+				});
+
+				it("Should return correct result from batchPut with no UnprocessedItems", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {}});
+					const result = await callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}]);
+					expect(result).to.eql({
+						"unprocessedItems": []
+					});
+				});
+
+				it("Should return correct result from batchPut with UnprocessedItems", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {"User": [{"PutRequest": {"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}}}}]}});
+					const result = await callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}]);
+					expect(result).to.eql({
+						"unprocessedItems": [{"id": 1, "name": "Charlie"}]
+					});
+				});
+
+				it("Should return correct result from batchPut with UnprocessedItems in wrong order", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {"User": [{"PutRequest": {"Item": {"id": {"N": "3"}, "name": {"S": "Tim"}}}}, {"PutRequest": {"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}}}}]}});
+					const result = await callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}, {"id": 3, "name": "Tim"}]);
+					expect(result).to.eql({
+						"unprocessedItems": [{"id": 1, "name": "Charlie"}, {"id": 3, "name": "Tim"}]
+					});
+				});
+
+				it("Should return request if return request setting is set", async () => {
+					const result = await callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}], {"return": "request"});
+					expect(params).to.not.exist;
+					expect(result).to.eql({
+						"RequestItems": {
+							"User": [
+								{
+									"PutRequest": {
+										"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}}
+									}
+								},
+								{
+									"PutRequest": {
+										"Item": {"id": {"N": "2"}, "name": {"S": "Bob"}}
+									}
+								}
+							]
+						}
+					});
+				});
+
+				it("Should throw error if error is returned from DynamoDB", () => {
+					promiseFunction = () => Promise.reject({"error": "ERROR"});
+
+					return expect(callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}, {"id": 2, "name": "Bob"}])).to.be.rejectedWith({"error": "ERROR"});
+				});
+			});
+		});
+	});
+
 	describe("Model.update", () => {
 		let User, updateItemParams, updateItemFunction;
 		beforeEach(() => {
@@ -2403,6 +2721,110 @@ describe("Model", () => {
 		});
 	});
 
+	describe("Model.batchDelete", () => {
+		let User, params, promiseFunction;
+		beforeEach(() => {
+			User = new dynamoose.Model("User", {"id": Number, "name": String});
+			dynamoose.aws.ddb.set({
+				"batchWriteItem": (paramsB) => {
+					params = paramsB;
+					return {"promise": promiseFunction};
+				}
+			});
+		});
+		afterEach(() => {
+			User = null;
+			params = null;
+			promiseFunction = null;
+			dynamoose.aws.ddb.revert();
+		});
+
+		it("Should be a function", () => {
+			expect(User.batchDelete).to.be.a("function");
+		});
+
+		const functionCallTypes = [
+			{"name": "Promise", "func": (Model) => Model.batchDelete},
+			{"name": "Callback", "func": (Model) => util.promisify(Model.batchDelete)}
+		];
+		functionCallTypes.forEach((callType) => {
+			describe(callType.name, () => {
+				it("Should should send correct parameters to batchWriteItem", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {}});
+					await callType.func(User).bind(User)([1, 2]);
+					expect(params).to.be.an("object");
+					expect(params).to.eql({
+						"RequestItems": {
+							"User": [
+								{
+									"DeleteRequest": {
+										"Key": {"id": {"N": "1"}}
+									}
+								},
+								{
+									"DeleteRequest": {
+										"Key": {"id": {"N": "2"}}
+									}
+								}
+							]
+						}
+					});
+				});
+
+				it("Should return correct result from batchDelete with no UnprocessedItems", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.eql({
+						"unprocessedItems": []
+					});
+				});
+
+				it("Should return correct result from batchDelete with UnprocessedItems", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {"User": [{"DeleteRequest": {"Key": {"id": {"N": "1"}}}}]}});
+					const result = await callType.func(User).bind(User)([1, 2]);
+					expect(result).to.eql({
+						"unprocessedItems": [{"id": 1}]
+					});
+				});
+
+				it("Should return correct result from batchDelete with UnprocessedItems in wrong order", async () => {
+					promiseFunction = () => Promise.resolve({"UnprocessedItems": {"User": [{"DeleteRequest": {"Key": {"id": {"N": "3"}}}}, {"DeleteRequest": {"Key": {"id": {"N": "1"}}}}]}});
+					const result = await callType.func(User).bind(User)([1, 2, 3]);
+					expect(result).to.eql({
+						"unprocessedItems": [{"id": 1}, {"id": 3}]
+					});
+				});
+
+				it("Should return request if return request setting is set", async () => {
+					const result = await callType.func(User).bind(User)([1, 2], {"return": "request"});
+					expect(params).to.not.exist;
+					expect(result).to.eql({
+						"RequestItems": {
+							"User": [
+								{
+									"DeleteRequest": {
+										"Key": {"id": {"N": "1"}}
+									}
+								},
+								{
+									"DeleteRequest": {
+										"Key": {"id": {"N": "2"}}
+									}
+								}
+							]
+						}
+					});
+				});
+
+				it("Should throw error if error is returned from DynamoDB", () => {
+					promiseFunction = () => Promise.reject({"error": "ERROR"});
+
+					return expect(callType.func(User).bind(User)([{"id": 1, "name": "Charlie"}])).to.be.rejectedWith({"error": "ERROR"});
+				});
+			});
+		});
+	});
+
 	describe("Model.table.create.request", () => {
 		it("Should be a function", () => {
 			expect(new dynamoose.Model("User", {"id": String}).table.create.request).to.be.a("function");