Repository: dynamoosejs/dynamoose
Pull request #777 (2020-04-05 22:31:30)

General improvements


Patch:
diff --git a/README.md b/README.md
index eeed2667..2ab4053e 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Dynamoose [![Slack Chat](https://img.shields.io/badge/chat-on%20slack-informational.svg)](https://join.slack.com/t/dynamoose/shared_invite/enQtODM4OTI0MTc1NDc3LWI3MmNhMThmNmJmZDk5MmUxOTZmMGEwNGQzNTRkMjhjZGJlNGM5M2JmZjMzMzlkODRhMGY3MTQ5YjQ2Nzg3YTY) [![Twitter Follow](https://img.shields.io/twitter/follow/dynamoosejs?style=social)](https://twitter.com/DynamooseJS) [![CI](https://github.com/dynamoosejs/dynamoose/workflows/CI/badge.svg)](https://github.com/dynamoosejs/dynamoose/actions) [![Coverage Status](https://coveralls.io/repos/github/dynamoosejs/dynamoose/badge.svg?branch=master)](https://coveralls.io/github/dynamoosejs/dynamoose?branch=master)
+# Dynamoose [![npm](https://img.shields.io/npm/v/dynamoose)](https://www.npmjs.com/package/dynamoose) [![npm (beta)](https://img.shields.io/npm/v/dynamoose/beta)](https://www.npmjs.com/package/dynamoose/v/beta) [![License](https://img.shields.io/github/license/dynamoosejs/dynamoose)](https://github.com/dynamoosejs/dynamoose/blob/master/LICENSE) [![npm Downloads](https://img.shields.io/npm/dw/dynamoose)](https://www.npmjs.com/package/dynamoose) [![CI](https://github.com/dynamoosejs/dynamoose/workflows/CI/badge.svg)](https://github.com/dynamoosejs/dynamoose/actions) [![Coverage Status](https://coveralls.io/repos/github/dynamoosejs/dynamoose/badge.svg?branch=master)](https://coveralls.io/github/dynamoosejs/dynamoose?branch=master) [![Slack Chat](https://img.shields.io/badge/chat-on%20slack-informational.svg)](https://join.slack.com/t/dynamoose/shared_invite/enQtODM4OTI0MTc1NDc3LWI3MmNhMThmNmJmZDk5MmUxOTZmMGEwNGQzNTRkMjhjZGJlNGM5M2JmZjMzMzlkODRhMGY3MTQ5YjQ2Nzg3YTY) [![Contact](https://img.shields.io/badge/contact-me-blue)](https://charlie.fish/contact) [![Twitter](https://img.shields.io/twitter/follow/dynamoosejs?style=social)](https://twitter.com/DynamooseJS)
 
 # Dynamoose Version 2.0 is still In Development. Please [contact me](https://charlie.fish/contact) with any feedback or questions.
 
@@ -12,3 +12,8 @@ Dynamoose is a modeling tool for Amazon's DynamoDB (inspired by [Mongoose](http:
 ```sh
 $ npm i dynamoose@beta
 ```
+
+## Documentation
+
+- [v2](https://github.com/dynamoosejs/dynamoose/tree/master/docs)
+- [v1](https://dynamoosejs.com)
diff --git a/docs/api/Condition.md b/docs/api/Condition.md
index 0392bd0f..e37fbc76 100644
--- a/docs/api/Condition.md
+++ b/docs/api/Condition.md
@@ -35,18 +35,22 @@ new dynamoose.Condition().where("id").eq(1).or().where("name").eq("Bob"); // id
 
 This function sets the condition to use the opposite comparison type for the given condition. You can find the list opposite comparison types below.
 
-- equals (EQ) - not equals (NE)
-- less than or equals (LE) - greater than (GT)
-- less than (LT) - greater than or equals (GE)
-- null (NULL) - not null (NOT_NULL)
-- contains (CONTAINS) - not contains (NOT_CONTAINS)
-- exists (EXISTS) - not exists (NOT_EXISTS)
+| Original | Opposite |
+|---|---|
+| equals (EQ) | not equals (NE) |
+| less than or equals (LE) | greater than (GT) |
+| less than (LT) | greater than or equals (GE) |
+| null (NULL) | not null (NOT_NULL) |
+| contains (CONTAINS) | not contains (NOT_CONTAINS) |
+| exists (EXISTS) | not exists (NOT_EXISTS) |
 
 The following comparisons do not have an opposite comparison type, and will throw an error if you try to use condition.not() with them.
 
-- in (IN)
-- between (BETWEEN)
-- begins with (BEGINS_WITH)
+| Original |
+|---|
+| in (IN) |
+| between (BETWEEN) |
+| begins with (BEGINS_WITH) |
 
 ```js
 new dynamoose.Condition().where("id").not().eq(1); // Retrieve all objects where id does NOT equal 1
diff --git a/docs/api/Document.md b/docs/api/Document.md
index 031f923a..fef701f6 100644
--- a/docs/api/Document.md
+++ b/docs/api/Document.md
@@ -24,8 +24,10 @@ This method returns a promise that will resolve when the operation is complete,
 
 You can also pass a settings object in as the first parameter. The following options are available for settings are:
 
-- `overwrite` (default: true) - If an existing document with the same hash key should be overwritten in the database. You can set this to false to not overwrite an existing document with the same hash key.
-- `return` (default: `document`) - If the function should return the `document` 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.
+| Name | Type | Default | Notes |
+|---|---|---|---|
+| overwrite | boolean | true | If an existing document with the same hash key should be overwritten in the database. You can set this to false to not overwrite an existing document with the same hash key. |
+| return | string | `document` | If the function should return the `document` 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 one argument and having that argument be the `callback`. You are not required to pass in `settings` if you just want to pass in a `callback`.
 
diff --git a/docs/api/Model.md b/docs/api/Model.md
index 6592d6f5..b96cb2df 100644
--- a/docs/api/Model.md
+++ b/docs/api/Model.md
@@ -357,7 +357,9 @@ This method returns a promise that will resolve when the operation is complete,
 
 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.
+| Name | Description | Type | Default |
+|------|-------------|------|---------|
+| return | 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. | String | `response` |
 
 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`.
 
diff --git a/docs/api/Schema.md b/docs/api/Schema.md
index 09717c48..44fca2a5 100644
--- a/docs/api/Schema.md
+++ b/docs/api/Schema.md
@@ -6,8 +6,10 @@ You can use this method to create a schema. The `schema` parameter is an object
 
 The `options` parameter is an optional object with the following options:
 
-- `saveUnknown` array | boolean (default: false) - This setting lets you specify if the schema should allow properties not defined in the schema. If you pass `true` in for this option all unknown properties will be allowed. If you pass in an array of strings, only properties that are included in that array will be allowed. If you pass in an array of strings, you can use `*` to indicate a wildcard nested property one level deep, or `**` to indicate a wildcard nested property infinite levels deep. If you retrieve items from DynamoDB with `saveUnknown` enabled, all custom Dynamoose types will be returned as the underlying DynamoDB type (ex. Dates will be returned as a Number representing number of milliseconds since Jan 1 1970).
-- `timestamps` boolean | object (default: false) - This setting lets you indicate to Dynamoose that you would like it to handle storing timestamps in your documents for both creation and most recent update times. If you pass in an object for this setting you must specify two keys `createdAt` & `updatedAt`, each with a value of a string being the name of the attribute for each timestamp. If you pass in `null` for either of those keys that specific timestamp won't be added to the schema. If you set this option to `true` it will use the default attribute names of `createdAt` & `updatedAt`.
+| Name | Type | Default | Information
+|---|---|---|---|
+| `saveUnknown` | array \| boolean | false | This setting lets you specify if the schema should allow properties not defined in the schema. If you pass `true` in for this option all unknown properties will be allowed. If you pass in an array of strings, only properties that are included in that array will be allowed. If you pass in an array of strings, you can use `*` to indicate a wildcard nested property one level deep, or `**` to indicate a wildcard nested property infinite levels deep. If you retrieve items from DynamoDB with `saveUnknown` enabled, all custom Dynamoose types will be returned as the underlying DynamoDB type (ex. Dates will be returned as a Number representing number of milliseconds since Jan 1 1970).
+| `timestamps` | boolean \| object | false | This setting lets you indicate to Dynamoose that you would like it to handle storing timestamps in your documents for both creation and most recent update times. If you pass in an object for this setting you must specify two keys `createdAt` & `updatedAt`, each with a value of a string being the name of the attribute for each timestamp. If you pass in `null` for either of those keys that specific timestamp won't be added to the schema. If you set this option to `true` it will use the default attribute names of `createdAt` & `updatedAt`.
 
 ```js
 const schema = new dynamoose.Schema({
@@ -317,11 +319,14 @@ You can define indexes on properties to be created or updated upon model initial
 
 Your index object can contain the following properties:
 
-- name: string - Name of index (default: `${attribute}${global ? "GlobalIndex" : "LocalIndex"}`)
-- global: boolean - If the index should be a global secondary index or not. Attribute will be the hash key for the index. (default: `false`)
-- rangeKey: string - The range key attribute name for a global secondary index. (default: undefined)
-- project: boolean | [string] - Sets the attributes to be projected for the index. `true` projects all attributes, `false` projects only the key attributes, and an array of strings projects the attributes listed. (default: `true`)
-- throughput: number | {read: number, write: number} - Sets the throughput for the global secondary index. (default: undefined)
+| Name | Type | Default | Notes |
+|---|---|---|---|
+| name | string | `${attribute}${global ? "GlobalIndex" : "LocalIndex"}` | Name of index |
+| global | boolean | false | If the index should be a global secondary index or not. Attribute will be the hash key for the index. |
+| rangeKey | string | undefined | The range key attribute name for a global secondary index. |
+| project | boolean \| [string] | true | Sets the attributes to be projected for the index. `true` projects all attributes, `false` projects only the key attributes, and an array of strings projects the attributes listed. |
+| throughput | number \| {read: number, write: number} | undefined | Sets the throughput for the global secondary index. |
+
 
 If you set `index` to `true`, it will create an index with all of the default settings.
 
@@ -336,3 +341,43 @@ If you set `index` to `true`, it will create an index with all of the default se
 	}
 }
 ```
+
+```js
+{
+	"email": {
+		"type": String,
+		"index": {
+			"name": "emailIndex",
+			"throughput": {"read": 5, "write": 10}
+		} // creates a global index with the name `emailIndex`
+	}
+}
+```
+
+### hashKey: boolean
+
+You can set this to true to overwrite what the `hashKey` for the Model will be. By default the `hashKey` will be the first key in the Schema object.
+
+```js
+{
+	"id": String,
+	"key": {
+		"type": String,
+		"hashKey": true
+	}
+}
+```
+
+### rangeKey: boolean
+
+You can set this to true to overwrite what the `rangeKey` for the Model will be. By default the `rangeKey` won't exist.
+
+```js
+{
+	"id": String,
+	"email": {
+		"type": String,
+		"rangeKey": true
+	}
+}
+```
diff --git a/lib/Condition.js b/lib/Condition.js
index dcaaaadc..bf3be827 100644
--- a/lib/Condition.js
+++ b/lib/Condition.js
@@ -30,36 +30,16 @@ class Condition {
 }
 
 
-const notComparisonTypes = (() => {
-	const obj = {
-		"EQ": "NE",
-		"IN": null,
-		"LE": "GT",
-		"LT": "GE",
-		"BETWEEN": null,
-		"CONTAINS": "NOT_CONTAINS",
-		"EXISTS": "NOT_EXISTS",
-		"BEGINS_WITH": null
-	};
-	Object.keys(obj).forEach((key) => {
-		const val = obj[key];
-		if (val) {
-			obj[val] = key;
-		} else {
-			obj[val] = null;
-		}
-	});
-	return obj;
-})();
-
 function finalizePending(instance) {
 	const pending = instance.settings.pending;
 
 	if (pending.not === true) {
-		if (notComparisonTypes[pending.type] === null) {
-			throw new Error.InvalidFilterComparison(`${pending.type} can not follow not()`);
+		if (!pending.type.not) {
+			throw new Error.InvalidFilterComparison(`${pending.type.typeName} can not follow not()`);
 		}
-		pending.type = notComparisonTypes[pending.type];
+		pending.type = pending.type.not;
+	} else {
+		pending.type = pending.type.typeName;
 	}
 
 	instance.settings.conditions.push([pending.key, {
@@ -89,21 +69,21 @@ Condition.prototype.where = Condition.prototype.filter = Condition.prototype.att
 	return this;
 };
 const types = [
-	{"name": "eq", "typeName": "EQ"},
-	{"name": "lt", "typeName": "LT"},
-	{"name": "le", "typeName": "LE"},
-	{"name": "gt", "typeName": "GT"},
-	{"name": "ge", "typeName": "GE"},
+	{"name": "eq", "typeName": "EQ", "not": "NE"},
+	{"name": "lt", "typeName": "LT", "not": "GE"},
+	{"name": "le", "typeName": "LE", "not": "GT"},
+	{"name": "gt", "typeName": "GT", "not": "LE"},
+	{"name": "ge", "typeName": "GE", "not": "LT"},
 	{"name": "beginsWith", "typeName": "BEGINS_WITH"},
-	{"name": "contains", "typeName": "CONTAINS"},
-	{"name": "exists", "typeName": "EXISTS"},
+	{"name": "contains", "typeName": "CONTAINS", "not": "NOT_CONTAINS"},
+	{"name": "exists", "typeName": "EXISTS", "not": "NOT_EXISTS"},
 	{"name": "in", "typeName": "IN"},
 	{"name": "between", "typeName": "BETWEEN", "multipleArguments": true}
 ];
 types.forEach((type) => {
 	Condition.prototype[type.name] = function(value) {
 		this.settings.pending.value = type.value || (type.multipleArguments ? [...arguments] : value);
-		this.settings.pending.type = type.typeName;
+		this.settings.pending.type = type;
 		finalizePending(this);
 		return this;
 	};
diff --git a/lib/Document.js b/lib/Document.js
index e4e13e7a..8559ec45 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -1,7 +1,7 @@
 const aws = require("./aws");
 const utils = require("./utils");
 const Error = require("./Error");
-const {internalProperties} = require("./Internal");
+const {internalProperties} = require("./Internal").General;
 
 const staticMethods = {
 	"toDynamo": (object, settings = {"type": "object"}) => (settings.type === "value" ? aws.converter().input : aws.converter().marshall)(object),
@@ -44,7 +44,9 @@ function DocumentCarrier(model) {
 			const keys = Object.keys(value);
 			const key = keys[0];
 			const nestedResult = (typeof value[key] === "object" && !(value[key] instanceof Buffer) ? (Array.isArray(value[key]) ? value[key].every((value) => Document.isDynamoObject(value, true)) : Document.isDynamoObject(value[key])) : true);
-			return typeof value === "object" && keys.length === 1 && utils.dynamodb.attribute_types.includes(key) && (nestedResult || Object.keys(value[key]).length === 0 || utils.attribute_types.find((a) => a.dynamodbType === key).isSet);
+			const Schema = require("./Schema");
+			const attributeType = Schema.attributeTypes.findDynamoDBType(key);
+			return typeof value === "object" && keys.length === 1 && attributeType && (nestedResult || Object.keys(value[key]).length === 0 || attributeType.isSet);
 		}
 
 		const keys = Object.keys(object);
@@ -134,6 +136,11 @@ function DocumentCarrier(model) {
 		// Type check
 		const validParents = []; // This array is used to allow for set contents to not be type checked
 		const keysToDelete = [];
+		const getValueTypeCheckResult = (value, key, options = {}) => {
+			const typeDetails = model.schema.getAttributeTypeDetails(key, options);
+			const isValidType = [((typeDetails.customType || {}).functions || {}).isOfType, typeDetails.isOfType].filter((a) => Boolean(a)).some((func) => func(value, settings.type));
+			return {typeDetails, isValidType};
+		};
 		const checkTypeFunction = (item) => {
 			const [key, value] = item;
 			if (validParents.find((parent) => key.startsWith(parent.key) && (parent.infinite || key.split(".").length === parent.key.split(".").length + 1))) {
@@ -142,14 +149,10 @@ function DocumentCarrier(model) {
 			const genericKey = key.replace(/\d+/gu, "0"); // This is a key replacing all list numbers with 0 to standardize things like checking if it exists in the schema
 			const existsInSchema = schemaAttributes.includes(genericKey);
 			if (existsInSchema) {
-				const typeDetails = model.schema.getAttributeTypeDetails(genericKey, {"standardKey": true});
-				const {internalAttributeType} = typeDetails;
-				const expectedJSType = internalAttributeType.javascriptType;
-				const type = typeof value;
-				const isValidType = (expectedJSType && (() => type === expectedJSType)()) || (((typeDetails.customType || {}).functions || {}).isOfType || internalAttributeType.isOfType || (() => false))(value, settings.type);
+				const {isValidType, typeDetails} = getValueTypeCheckResult(value, genericKey, {"standardKey": true});
 				if (!isValidType) {
-					throw new Error.TypeMismatch(`Expected ${key} to be of type ${expectedJSType}, instead found type ${type}.`);
-				} else if (internalAttributeType.isSet) {
+					throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetails.name.toLowerCase()}, instead found type ${typeof value}.`);
+				} else if (typeDetails.isSet) {
 					validParents.push({key, "infinite": true});
 				} else if (/*typeDetails.dynamodbType === "M" || */typeDetails.dynamodbType === "L") {
 					// The code below is an optimization for large array types to speed up the process of not having to check the type for every element but only the ones that are different
@@ -179,13 +182,9 @@ function DocumentCarrier(model) {
 					const defaultValue = await model.schema.defaultCheck(key, value, settings);
 					const isDefaultValueUndefined = typeof defaultValue === "undefined" || defaultValue === null;
 					if (!isDefaultValueUndefined) {
-						const typeDetails = model.schema.getAttributeTypeDetails(key);
-						const {internalAttributeType} = typeDetails;
-						const expectedJSType = internalAttributeType.javascriptType;
-						const type = typeof defaultValue;
-						const isValidType = (expectedJSType && (() => type === expectedJSType)()) || (((typeDetails.customType || {}).functions || {}).isOfType || internalAttributeType.isOfType || (() => false))(defaultValue, settings.type);
+						const {isValidType, typeDetails} = getValueTypeCheckResult(defaultValue, key);
 						if (!isValidType) {
-							throw new Error.TypeMismatch(`Expected ${key} to be of type ${expectedJSType}, instead found type ${type}.`);
+							throw new Error.TypeMismatch(`Expected ${key} to be of type ${typeDetails.name.toLowerCase()}, instead found type ${typeof defaultValue}.`);
 						} else {
 							utils.object.set(returnObject, key, defaultValue);
 						}
@@ -199,12 +198,13 @@ function DocumentCarrier(model) {
 				const value = utils.object.get(returnObject, key);
 				const isValueUndefined = typeof value === "undefined" || value === null;
 				if (!isValueUndefined) {
-					const {customType} = model.schema.getAttributeTypeDetails(key);
-					if (customType) {
+					const typeDetails = model.schema.getAttributeTypeDetails(key);
+					const {customType} = typeDetails;
+					const {type: typeInfo} = typeDetails.isOfType(value);
+					const isCorrectTypeAlready = typeInfo === (settings.type === "toDynamo" ? "underlying" : "main");
+					if (customType && !isCorrectTypeAlready) {
 						const customValue = customType.functions[settings.type](value);
-						if (customValue !== value) {
-							utils.object.set(returnObject, key, customValue);
-						}
+						utils.object.set(returnObject, key, customValue);
 					}
 				}
 			});
@@ -214,9 +214,10 @@ function DocumentCarrier(model) {
 			const [key, value] = item;
 			let typeDetails;
 			try {
-				typeDetails = model.schema.getAttributeTypeDetails(key).internalAttributeType;
+				typeDetails = model.schema.getAttributeTypeDetails(key);
 			} catch (e) {
-				typeDetails = utils.attribute_types.find((type) => type.isOfType && type.isOfType(value, settings.type, settings));
+				const Schema = require("./Schema");
+				typeDetails = Schema.attributeTypes.findTypeForValue(value, settings.type, settings);
 			}
 
 			if (typeDetails && typeDetails[settings.type]) {
@@ -245,7 +246,11 @@ function DocumentCarrier(model) {
 			}));
 		}
 		if (settings.required) {
-			await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
+			let attributesToCheck = Document.attributesWithSchema(returnObject);
+			if (settings.required === "nested") {
+				attributesToCheck = attributesToCheck.filter((attribute) => utils.object.keys(returnObject).find((key) => attribute.startsWith(key)));
+			}
+			await Promise.all(attributesToCheck.map(async (key) => {
 				async function check() {
 					const value = utils.object.get(returnObject, key);
 					await model.schema.requiredCheck(key, value);
diff --git a/lib/Internal.js b/lib/Internal.js
index 6e6002ce..dece75cc 100644
--- a/lib/Internal.js
+++ b/lib/Internal.js
@@ -2,5 +2,7 @@ module.exports = {
 	"Schema": {
 		"internalCache": Symbol("internalCache")
 	},
-	"internalProperties": Symbol("internalProperties")
+	"General": {
+		"internalProperties": Symbol("internalProperties")
+	}
 };
diff --git a/lib/Model.js b/lib/Model.js
index b51c1470..6fec9233 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -424,7 +424,7 @@ Model.prototype.update = function (keyObj, updateObj, settings = {}, callback) {
 
 	const getUpdateExpressionObject = async () => {
 		const updateTypes = [
-			{"name": "$SET", "operator": " = ", "objectFromSchemaSettings": {"validate": true, "enum": true, "forceDefault": true}},
+			{"name": "$SET", "operator": " = ", "objectFromSchemaSettings": {"validate": true, "enum": true, "forceDefault": true, "required": "nested"}},
 			{"name": "$ADD", "objectFromSchemaSettings": {"forceDefault": true}},
 			{"name": "$REMOVE", "attributeOnly": true, "objectFromSchemaSettings": {"required": true, "defaults": true}}
 		].reverse();
@@ -471,7 +471,7 @@ Model.prototype.update = function (keyObj, updateObj, settings = {}, callback) {
 					}
 				}
 
-				if (updateType.objectFromSchemaSettings.required) {
+				if (updateType.objectFromSchemaSettings.required === true) {
 					await this.schema.requiredCheck(subKey, undefined);
 				}
 
@@ -537,7 +537,7 @@ Model.prototype.update = function (keyObj, updateObj, settings = {}, callback) {
 			const value = Object.values(returnObject.ExpressionAttributeValues)[index];
 			const valueKey = Object.keys(returnObject.ExpressionAttributeValues)[index];
 			const dynamoType = this.schema.getAttributeType(attribute, value, {"unknownAttributeAllowed": true});
-			const attributeType = utils.attribute_types.find((a) => a.dynamodbType === dynamoType);
+			const attributeType = Schema.attributeTypes.findDynamoDBType(dynamoType);
 
 			if (attributeType.toDynamo && !attributeType.isOfType(value, "fromDynamo")) {
 				returnObject.ExpressionAttributeValues[valueKey] = attributeType.toDynamo(value);
@@ -662,53 +662,23 @@ Model.prototype.scan = {"carrier": require("./DocumentRetriever")("scan")};
 Model.prototype.query = {"carrier": require("./DocumentRetriever")("query")};
 
 // Methods
-Model.prototype.methods = {
-	"set": function (name, fn) {
-		if (!this.Document[name] || (this.Document[name][Internal.internalProperties] && this.Document[name][Internal.internalProperties].type === "customMethod")) {
-			const self = this;
-			this.Document[name] = function (...args) {
-				const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : undefined;
-				if (cb) {
-					const result = fn.bind(self.Document)(...args);
-					if (result instanceof Promise) {
-						result.then((result) => cb(null, result)).catch((err) => cb(err));
-					}
-				} else {
-					return new Promise((resolve, reject) => {
-						const result = fn.bind(self.Document)(...args, (err, result) => {
-							if (err) {
-								reject(err);
-							} else {
-								resolve(result);
-							}
-						});
-						if (result instanceof Promise) {
-							result.then(resolve).catch(reject);
-						}
-					});
-				}
-			};
-			this.Document[name][Internal.internalProperties] = {"type": "customMethod"};
-		}
-	},
-	"delete": function (name) {
-		if (this.Document[name] && this.Document[name][Internal.internalProperties] && this.Document[name][Internal.internalProperties].type === "customMethod") {
-			this.Document[name] = undefined;
-		}
-	},
-	"document": {
+const customMethodFunctions = (type) => {
+	const entryPoint = (self) => type === "document" ? self.Document.prototype : self.Document;
+	return {
 		"set": function (name, fn) {
-			if (!this.Document.prototype[name] || (this.Document.prototype[name][Internal.internalProperties] && this.Document.prototype[name][Internal.internalProperties].type === "customMethod")) {
-				this.Document.prototype[name] = function (...args) {
+			const self = this;
+			if (!entryPoint(this)[name] || (entryPoint(this)[name][Internal.General.internalProperties] && entryPoint(this)[name][Internal.General.internalProperties].type === "customMethod")) {
+				entryPoint(this)[name] = function (...args) {
+					const bindObject = type === "document" ? this : self.Document;
 					const cb = typeof args[args.length - 1] === "function" ? args[args.length - 1] : undefined;
 					if (cb) {
-						const result = fn.bind(this)(...args);
+						const result = fn.bind(bindObject)(...args);
 						if (result instanceof Promise) {
 							result.then((result) => cb(null, result)).catch((err) => cb(err));
 						}
 					} else {
 						return new Promise((resolve, reject) => {
-							const result = fn.bind(this)(...args, (err, result) => {
+							const result = fn.bind(bindObject)(...args, (err, result) => {
 								if (err) {
 									reject(err);
 								} else {
@@ -721,15 +691,19 @@ Model.prototype.methods = {
 						});
 					}
 				};
-				this.Document.prototype[name][Internal.internalProperties] = {"type": "customMethod"};
+				entryPoint(this)[name][Internal.General.internalProperties] = {"type": "customMethod"};
 			}
 		},
 		"delete": function (name) {
-			if (this.Document.prototype[name] && this.Document.prototype[name][Internal.internalProperties] && this.Document.prototype[name][Internal.internalProperties].type === "customMethod") {
-				this.Document.prototype[name] = undefined;
+			if (entryPoint(this)[name] && entryPoint(this)[name][Internal.General.internalProperties] && entryPoint(this)[name][Internal.General.internalProperties].type === "customMethod") {
+				entryPoint(this)[name] = undefined;
 			}
 		}
-	}
+	};
+};
+Model.prototype.methods = {
+	...customMethodFunctions("model"),
+	"document": customMethodFunctions("document")
 };
 
 // Defaults
diff --git a/lib/Schema.js b/lib/Schema.js
index 642d3630..1d267540 100644
--- a/lib/Schema.js
+++ b/lib/Schema.js
@@ -171,6 +171,110 @@ Schema.prototype.getAttributeValue = function(key, settings = {}) {
 	return (settings.standardKey ? key : key.replace(/\d+/gu, "0")).split(".").reduce((result, part) => utils.object.get(result.schema, part), {"schema": this.schemaObject});
 };
 
+class DynamoDBType {
+	constructor(obj) {
+		Object.keys(obj).forEach((key) => {
+			this[key] = obj[key];
+		});
+	}
+
+	result(typeSettings) {
+		const isSubType = this.dynamodbType instanceof DynamoDBType; // Represents underlying DynamoDB type for custom types
+		const type = isSubType ? this.dynamodbType : this;
+		const result = {
+			"name": this.name,
+			"dynamodbType": isSubType ? this.dynamodbType.dynamodbType : this.dynamodbType,
+			"nestedType": this.nestedType
+		};
+		result.isOfType = this.jsType.func ? this.jsType.func : ((val) => {
+			return [{"value": this.jsType, "type": "main"}, {"value": (isSubType ? type.jsType : null), "type": "underlying"}].filter((a) => Boolean(a.value)).find((jsType) => typeof jsType.value === "string" ? typeof val === jsType.value : val instanceof jsType.value);
+		});
+		if (type.set) {
+			const typeName = type.customDynamoName || type.name;
+			result.set = {
+				"name": `${this.name} Set`,
+				"isSet": true,
+				"dynamodbType": `${type.dynamodbType}S`,
+				"isOfType": (val, type, settings = {}) => {
+					if (type === "toDynamo") {
+						return (!settings.saveUnknown && Array.isArray(val) && val.every((subValue) => result.isOfType(subValue))) || (val instanceof Set && [...val].every((subValue) => result.isOfType(subValue)));
+					} else {
+						return val.wrapperName === "Set" && val.type === typeName && Array.isArray(val.values);
+					}
+				},
+				"toDynamo": (val) => ({"wrapperName": "Set", "type": typeName, "values": [...val]}),
+				"fromDynamo": (val) => new Set(val.values)
+			};
+			if (this.customType) {
+				const functions = this.customType.functions(typeSettings);
+				result.customType = {
+					...this.customType,
+					functions
+				};
+				result.set.customType = {
+					"functions": {
+						"toDynamo": (val) => val.map(functions.toDynamo),
+						"fromDynamo": (val) => ({"values": val.values.map(functions.fromDynamo)}),
+						"isOfType": (val, type) => {
+							if (type === "toDynamo") {
+								return Array.isArray(val) && val.every(functions.isOfType);
+							} else {
+								return val.wrapperName === "Set" && val.type === typeName && Array.isArray(val.values);
+							}
+						}
+					}
+				};
+			}
+		}
+
+		return result;
+	}
+}
+
+const attributeTypesMain = (() => {
+	const numberType = new DynamoDBType({"name": "Number", "dynamodbType": "N", "set": true, "jsType": "number"});
+	return [
+		new DynamoDBType({"name": "Buffer", "dynamodbType": "B", "set": true, "jsType": Buffer, "customDynamoName": "Binary"}),
+		new DynamoDBType({"name": "Boolean", "dynamodbType": "BOOL", "jsType": "boolean"}),
+		new DynamoDBType({"name": "Array", "dynamodbType": "L", "jsType": {"func": Array.isArray}, "nestedType": true}),
+		new DynamoDBType({"name": "Object", "dynamodbType": "M", "jsType": {"func": (val) => Boolean(val) && val.constructor === Object && (val.wrapperName !== "Set" || Object.keys(val).length !== 3 || !val.type || !val.values)}, "nestedType": true}),
+		numberType,
+		new DynamoDBType({"name": "String", "dynamodbType": "S", "set": true, "jsType": "string"}),
+		new DynamoDBType({"name": "Date", "dynamodbType": numberType, "customType": {
+			"functions": (typeSettings) => ({
+				"toDynamo": (val) => {
+					if (typeSettings.storage === "seconds") {
+						return Math.round(val.getTime() / 1000);
+					} else {
+						return val.getTime();
+					}
+				},
+				"fromDynamo": (val) => {
+					if (typeSettings.storage === "seconds") {
+						return new Date(val * 1000);
+					} else {
+						return new Date(val);
+					}
+				},
+				"isOfType": (val, type) => {
+					return type === "toDynamo" ? val instanceof Date : typeof val === "number";
+				}
+			})
+		}, "jsType": Date})
+	];
+})();
+const attributeTypes = utils.array_flatten(attributeTypesMain.filter((checkType) => !checkType.customType).map((checkType) => checkType.result()).map((a) => [a, a.set])).filter((a) => Boolean(a));
+function retrieveTypeInfo(type, isSet, key, typeSettings) {
+	const foundType = attributeTypesMain.find((checkType) => checkType.name.toLowerCase() === type.toLowerCase());
+	if (!foundType) {
+		throw new Error.InvalidType(`${key} contains an invalid type: ${type}`);
+	}
+	const parentType = foundType.result(typeSettings);
+	if (!parentType.set && isSet) {
+		throw new Error.InvalidType(`${key} with type: ${type} is not allowed to be a set`);
+	}
+	return isSet ? parentType.set : parentType;
+}
 Schema.prototype.getAttributeTypeDetails = function(key, settings = {}) {
 	const standardKey = (settings.standardKey ? key : key.replace(/\d+/gu, "0"));
 	if (this[internalCache].getAttributeTypeDetails[standardKey]) {
@@ -199,91 +303,7 @@ Schema.prototype.getAttributeTypeDetails = function(key, settings = {}) {
 		type = typeVal;
 	}
 
-	let returnValue;
-	// `customType` represents a custom Dynamoose type (ex. Date) that is converted to and from a different DynamoDB type (ex. Number)
-	let setAllowed = false, customType = false;
-	switch (type.toLowerCase()) {
-	case "string":
-		returnValue = "S";
-		setAllowed = true;
-		break;
-	case "number":
-		returnValue = "N";
-		setAllowed = true;
-		break;
-	case "boolean":
-		returnValue = "BOOL";
-		break;
-	case "date":
-		returnValue = "N";
-		setAllowed = true;
-		customType = {
-			"functions": {
-				"toDynamo": (val) => {
-					if (isSet ? val.every((a) => typeof a === "number") : typeof val === "number") {
-						return val;
-					}
-
-					const action = (val) => {
-						if (typeSettings.storage === "seconds") {
-							return Math.round(val.getTime() / 1000);
-						} else {
-							return val.getTime();
-						}
-					};
-
-					return isSet ? val.map(action) : action(val);
-				},
-				"fromDynamo": (val) => {
-					const action = (val) => {
-						if (typeSettings.storage === "seconds") {
-							return new Date(val * 1000);
-						} else {
-							return new Date(val);
-						}
-					};
-
-					return isSet ? {"values": val.values.map(action)} : action(val);
-				},
-				"isOfType": (val, type) => {
-					if (isSet) {
-						if (type === "toDynamo") {
-							return Array.isArray(val) && val.every((a) => a instanceof Date);
-						} else {
-							return val.wrapperName === "Set" && val.type === "Number" && Array.isArray(val.values);
-						}
-					} else {
-						return type === "toDynamo" ? val instanceof Date : typeof val === "number";
-					}
-				}
-			}
-		};
-		break;
-	case "object":
-		returnValue = "M";
-		break;
-	case "array":
-		returnValue = "L";
-		break;
-	case "buffer":
-		returnValue = "B";
-		setAllowed = true;
-		break;
-	default:
-		throw new Error.InvalidType(`${key} contains an invalid type: ${type}`);
-	}
-
-	if (isSet && !setAllowed) {
-		throw new Error.InvalidType(`${key} with type: ${type} is not allowed to be a set`);
-	}
-
-	const dynamodbType = `${returnValue}${isSet ? "S" : ""}`;
-	const returnObject = {
-		isSet,
-		customType,
-		dynamodbType,
-		"internalAttributeType": utils.attribute_types.find((type) => type.dynamodbType === dynamodbType)
-	};
+	const returnObject = retrieveTypeInfo(type, isSet, key, typeSettings);
 	this[internalCache].getAttributeTypeDetails[standardKey] = returnObject;
 	return returnObject;
 };
@@ -349,3 +369,7 @@ Schema.prototype.getCreateTableAttributeParams = async function(model) {
 
 
 module.exports = Schema;
+module.exports.attributeTypes = {
+	"findDynamoDBType": (type) => attributeTypes.find((checkType) => checkType.dynamodbType === type),
+	"findTypeForValue": (...args) => attributeTypes.find((checkType) => checkType.isOfType(...args))
+};
diff --git a/lib/utils/attribute_types.js b/lib/utils/attribute_types.js
deleted file mode 100644
index a56af072..00000000
--- a/lib/utils/attribute_types.js
+++ /dev/null
@@ -1,39 +0,0 @@
-module.exports = [
-	{"dynamodbType": "B", "isOfType": (val) => val instanceof Buffer}, // Binary
-	{"dynamodbType": "BOOL", "javascriptType": "boolean"}, // Boolean
-	{"dynamodbType": "BS", "isSet": true, "isOfType": (val, type, settings = {}) => {
-		if (type === "toDynamo") {
-			if (settings.saveUnknown) {
-				return val instanceof Set && [...val].every((subValue) => subValue instanceof Buffer);
-			} else {
-				return Array.isArray(val) && val.every((subValue) => subValue instanceof Buffer);
-			}
-		} else {
-			return val.wrapperName === "Set" && val.type === "Binary" && Array.isArray(val.values);
-		}
-	}, "toDynamo": (val) => ({"wrapperName": "Set", "type": "Binary", "values": [...val]}), "fromDynamo": (val) => new Set(val.values)}, // Binary Set
-	{"dynamodbType": "L", "isOfType": (val) => Array.isArray(val), "nestedType": true}, // List
-	{"dynamodbType": "M", "isOfType": (val) => Boolean(val) && val.constructor === Object && (val.wrapperName !== "Set" || Object.keys(val).length !== 3 || !val.type || !val.values), "javascriptType": "object", "nestedType": true}, // Map
-	{"dynamodbType": "N", "javascriptType": "number"}, // Number
-	{"dynamodbType": "NS", "isSet": true, "isOfType": (val, type, settings = {}) => {
-		if (type === "toDynamo") {
-			if (settings.saveUnknown) {
-				return val instanceof Set && [...val].every((a) => typeof a === "number");
-			} else {
-				return Array.isArray(val) && val.every((a) => typeof a === "number");
-			}
-		} else {
-			return val.wrapperName === "Set" && val.type === "Number" && Array.isArray(val.values);
-		}
-	}, "toDynamo": (val) => ({"wrapperName": "Set", "type": "Number", "values": [...val]}), "fromDynamo": (val) => new Set(val.values)}, // Number Set
-	// Commenting out `NULL` type for now, might add in later
-	// {"dynamodbType": "NULL"}, // Null
-	{"dynamodbType": "S", "javascriptType": "string"}, // String
-	{"dynamodbType": "SS", "isSet": true, "isOfType": (val, type, settings = {}) => {
-		if (type === "toDynamo") {
-			return (!settings.saveUnknown && Array.isArray(val) && val.every((a) => typeof a === "string")) || (val instanceof Set && [...val].every((a) => typeof a === "string"));
-		} else {
-			return val.wrapperName === "Set" && val.type === "String" && Array.isArray(val.values);
-		}
-	}, "toDynamo": (val) => ({"wrapperName": "Set", "type": "String", "values": [...val]}), "fromDynamo": (val) => new Set(val.values)} // String Set
-];
diff --git a/lib/utils/dynamodb/attribute_types.js b/lib/utils/dynamodb/attribute_types.js
deleted file mode 100644
index 7d983f52..00000000
--- a/lib/utils/dynamodb/attribute_types.js
+++ /dev/null
@@ -1,2 +0,0 @@
-const attribute_types = require("../attribute_types");
-module.exports = attribute_types.map((type) => type.dynamodbType);
diff --git a/lib/utils/dynamodb/index.js b/lib/utils/dynamodb/index.js
deleted file mode 100644
index c5fe67f6..00000000
--- a/lib/utils/dynamodb/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-module.exports = {
-	"attribute_types": require("./attribute_types")
-};
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 5dd7add7..530778f9 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -2,12 +2,10 @@ module.exports = {
 	"combine_objects": require("./combine_objects"),
 	"merge_objects": require("./merge_objects"),
 	"timeout": require("./timeout"),
-	"attribute_types": require("./attribute_types"),
 	"capitalize_first_letter": require("./capitalize_first_letter"),
 	"set_immediate_promise": require("./set_immediate_promise"),
 	"unique_array_elements": require("./unique_array_elements"),
 	"array_flatten": require("./array_flatten"),
-	"dynamodb": require("./dynamodb"),
 	"object": require("./object"),
 	"dynamoose": require("./dynamoose")
 };


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 1e90cb5a..a0b90ee6 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -18,8 +18,8 @@ describe("Document", () => {
 	it("Should not have internalProperties if use spread operator on object", () => {
 		const User = new Model("User", {"id": Number, "name": String}, {"create": false, "waitForActive": false});
 		const user = new User({"id": 1, "name": "Bob"});
-		expect(user[Internal.internalProperties]).to.exist;
-		expect({...user}[Internal.internalProperties]).to.not.exist;
+		expect(user[Internal.General.internalProperties]).to.exist;
+		expect({...user}[Internal.General.internalProperties]).to.not.exist;
 	});
 
 	describe("DynamoDB Conversation Methods", () => {
@@ -717,7 +717,7 @@ describe("Document", () => {
 					User = new Model("User", {"id": Number, "name": String, "birthday": Date}, {"create": false, "waitForActive": false});
 					user = new User({"id": 1, "name": "Charlie", "birthday": "test"});
 
-					return expect(callType.func(user).bind(user)()).to.be.rejectedWith("Expected birthday to be of type number, instead found type string.");
+					return expect(callType.func(user).bind(user)()).to.be.rejectedWith("Expected birthday to be of type date, instead found type string.");
 				});
 
 				it("Should save with correct object with more properties than in schema", async () => {
@@ -1546,6 +1546,21 @@ describe("Document", () => {
 				"error": new Error.ValidationError("data is a required property but has no value when trying to save document"),
 				"schema": {"id": {"type": String}, "data": {"type": Object, "schema": {"name": {"type": String, "required": true}}, "required": true}}
 			},
+			{
+				"input": [{"id": "test"}, {"required": true}],
+				"output": {"id": "test"},
+				"schema": {"id": {"type": String}, "data": {"type": Object, "schema": {"email": String, "name": {"type": String, "required": true}}}}
+			},
+			{
+				"input": [{"id": "test"}, {"required": true}],
+				"error": new Error.ValidationError("hash is a required property but has no value when trying to save document"),
+				"schema": {"id": {"type": String}, "hash": {"type": String, "required": true}, "data": {"type": Object, "schema": {"email": String, "name": {"type": String, "required": true}}}}
+			},
+			{
+				"input": [{"id": "test"}, {"required": "nested"}],
+				"output": {"id": "test"},
+				"schema": {"id": {"type": String}, "hash": {"type": String, "required": true}, "data": {"type": Object, "schema": {"email": String, "name": {"type": String, "required": true}}}}
+			},
 			{
 				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true, "customTypesDynamo": true}],
 				"model": ["User", {"id": Number}, {"create": false, "waitForActive": false, "expires": 1000}],
@@ -1662,12 +1677,12 @@ describe("Document", () => {
 			{
 				"input": [{"id": 1, "items": [1, 10000, "test"]}, {"type": "fromDynamo", "customTypesDynamo": true}],
 				"schema": new Schema({"id": Number, "items": {"type": Array, "schema": [Date]}}),
-				"error": new Error.ValidationError("Expected items.2 to be of type number, instead found type string.")
+				"error": new Error.ValidationError("Expected items.2 to be of type date, instead found type string.")
 			},
 			{
 				"input": [{"id": 1, "items": [{"birthday": 1}, {"birthday": 10000}, {"birthday": "test"}]}, {"type": "fromDynamo", "customTypesDynamo": true}],
 				"schema": new Schema({"id": Number, "items": {"type": Array, "schema": [{"type": Object, "schema": {"birthday": Date}}]}}),
-				"error": new Error.ValidationError("Expected items.2.birthday to be of type number, instead found type string.")
+				"error": new Error.ValidationError("Expected items.2.birthday to be of type date, instead found type string.")
 			}
 		];
 
diff --git a/test/Model.js b/test/Model.js
index 8c26c7cf..2b18d722 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -83,7 +83,7 @@ describe("Model", () => {
 			};
 			process.on("unhandledRejection", errorHandler);
 			new dynamoose.Model(tableName, {"id": String});
-			await utils.timeout(10);
+			await utils.timeout(100);
 			expect(failed).to.be.false;
 			process.removeListener("unhandledRejection", errorHandler);
 		});
@@ -878,7 +878,7 @@ describe("Model", () => {
 					expect(user.data).to.eql(new Set([Buffer.from("testdata"), Buffer.from("testdata2")]));
 				});
 
-				it("Should return object with correct values for buffer set", async () => {
+				it("Should return object with correct values for buffer set with saveUnknown", async () => {
 					User = new dynamoose.Model("User", new dynamoose.Schema({"id": Number}, {"saveUnknown": true}));
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "data": {"BS": [Buffer.from("testdata"), Buffer.from("testdata2")]}}});
 					const user = await callType.func(User).bind(User)(1);
@@ -914,7 +914,7 @@ describe("Model", () => {
 					User = new dynamoose.Model("User", {"id": Number, "name": String, "birthday": Date});
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}, "birthday": {"S": "Hello World"}}});
 
-					return expect(callType.func(User).bind(User)(1)).to.be.rejectedWith("Expected birthday to be of type number, instead found type string.");
+					return expect(callType.func(User).bind(User)(1)).to.be.rejectedWith("Expected birthday to be of type date, instead found type string.");
 				});
 
 				it("Should return object with correct values with object property", async () => {
@@ -2405,6 +2405,20 @@ describe("Model", () => {
 					return expect(callType.func(User).bind(User)({"id": 1}, {"friends": ["Bob"]})).to.not.be.rejected;
 				});
 
+				it("Should throw error if trying to replace object without nested required property", () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", {"id": Number, "data": {"type": Object, "schema": {"name": String, "age": {"type": Number, "required": true}}}});
+
+					return expect(callType.func(User).bind(User)({"id": 1}, {"data": {"name": "Charlie"}})).to.be.rejectedWith("data.age is a required property but has no value when trying to save document");
+				});
+
+				it("Should throw error if trying to replace object with $SET without nested required property", () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", {"id": Number, "data": {"type": Object, "schema": {"name": String, "age": {"type": Number, "required": true}}}});
+
+					return expect(callType.func(User).bind(User)({"id": 1}, {"$SET": {"data": {"name": "Charlie"}}})).to.be.rejectedWith("data.age is a required property but has no value when trying to save document");
+				});
+
 				it("Should use default value if deleting property", async () => {
 					updateItemFunction = () => Promise.resolve({});
 					User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "default": "Bob"}});
@@ -3101,178 +3115,37 @@ describe("Model", () => {
 			expect(User.methods).to.be.an("object");
 		});
 
-		describe("Model.methods.set", () => {
-			it("Should be a function", () => {
-				expect(User.methods.set).to.be.a("function");
-			});
-
-			it("Should not throw an error when being called", () => {
-				expect(() => User.methods.set("random", () => {})).to.not.throw();
-			});
-
-			it("Should set correct method on model", () => {
-				User.methods.set("random", () => {});
-				expect(User.random).to.be.a("function");
-			});
-
-			it("Should not overwrite internal methods", () => {
-				const originalMethod = User.get;
-				const newMethod = () => {};
-				User.methods.set("get", newMethod);
-				expect(User.get).to.eql(originalMethod);
-				expect(User.get).to.not.eql(newMethod);
-			});
-
-			it("Should overwrite methods if Internal.internalProperties exists but type doesn't", () => {
-				const originalMethod = User.get;
-				const newMethod = () => {};
-				User.random = originalMethod;
-				User.random[Internal.internalProperties] = {};
-				User.methods.set("random", newMethod);
-				expect(User.random).to.eql(originalMethod);
-				expect(User.random).to.not.eql(newMethod);
-			});
-
-			const methodTypes = [
-				{"name": "Callback", "func": (func) => {
-					return function (...args) {
-						const cb = args[args.length - 1];
-						func.bind(this)(...args).then((result) => cb(null, result)).catch((err) => cb(err));
-					};
-				}},
-				{"name": "Promise", "func": (func) => func}
-			];
-			methodTypes.forEach((methodType) => {
-				describe(`Method Type - ${methodType.name}`, () => {
-					const callerTypes = [
-						{"name": "Callback", "func": util.promisify},
-						{"name": "Promise", "func": (func) => func}
-					];
-					callerTypes.forEach((callerType) => {
-						describe(`Caller Type - ${callerType.name}`, () => {
-							let methodTypeCallDetails = [], methodTypeCallResult = {};
-							beforeEach(() => {
-								User.methods.set("action", methodType.func(async function (...args) {
-									methodTypeCallDetails.push({"arguments": [...args], "this": this});
-									if (methodTypeCallResult.error) {
-										throw methodTypeCallResult.error;
-									} else if (methodTypeCallResult.result) {
-										return methodTypeCallResult.result;
-									}
-								}));
-							});
-							afterEach(() => {
-								methodTypeCallDetails = [];
-								methodTypeCallResult = {};
-							});
-
-							it("Should call custom method with correct arguments", async () => {
-								methodTypeCallResult.result = "Success";
-								await callerType.func(User.action)();
-								expect(methodTypeCallDetails.length).to.eql(1);
-								expect(methodTypeCallDetails[0].arguments.length).to.eql(1);
-								expect(methodTypeCallDetails[0].arguments[0]).to.be.a("function");
-							});
-
-							it("Should call custom method with correct arguments if passing in one argument", async () => {
-								await callerType.func(User.action)("Hello World");
-								expect(methodTypeCallDetails.length).to.eql(1);
-								expect(methodTypeCallDetails[0].arguments.length).to.eql(2);
-								expect(methodTypeCallDetails[0].arguments[0]).to.eql("Hello World");
-								expect(methodTypeCallDetails[0].arguments[1]).to.be.a("function");
-							});
-
-							it("Should call custom method with correct arguments if passing in two arguments", async () => {
-								await callerType.func(User.action)("Hello", "World");
-								expect(methodTypeCallDetails.length).to.eql(1);
-								expect(methodTypeCallDetails[0].arguments.length).to.eql(3);
-								expect(methodTypeCallDetails[0].arguments[0]).to.eql("Hello");
-								expect(methodTypeCallDetails[0].arguments[1]).to.eql("World");
-								expect(methodTypeCallDetails[0].arguments[2]).to.be.a("function");
-							});
-
-							it("Should call custom method with correct `this`", async () => {
-								methodTypeCallResult.result = "Success";
-								await callerType.func(User.action)();
-								expect(methodTypeCallDetails[0].this).to.eql(User);
-							});
-
-							it("Should return correct response that custom method returns", async () => {
-								methodTypeCallResult.result = "Success";
-								const result = await callerType.func(User.action)();
-								expect(result).to.eql("Success");
-							});
-
-							it("Should throw error if custom method throws error", async () => {
-								methodTypeCallResult.error = "ERROR";
-								let result, error;
-								try {
-									result = await callerType.func(User.action)();
-								} catch (e) {
-									error = e;
-								}
-								expect(result).to.not.exist;
-								expect(error).to.eql("ERROR");
-							});
-						});
-					});
-				});
-			});
-		});
-
-		describe("Model.methods.delete", () => {
-			it("Should be a function", () => {
-				expect(User.methods.delete).to.be.a("function");
-			});
-
-			it("Should not delete internal methods", () => {
-				User.methods.delete("get");
-				expect(User.get).to.be.a("function");
-			});
-
-			it("Should not throw error for deleting unknown method", () => {
-				expect(() => User.methods.delete("randomHere123")).to.not.throw();
-			});
-
-			it("Should delete custom method", () => {
-				User.methods.set("myMethod", () => {});
-				expect(User.myMethod).to.be.a("function");
-				User.methods.delete("myMethod");
-				expect(User.myMethod).to.eql(undefined);
-			});
-		});
-
-		describe("Model.methods.document", () => {
-			describe("Model.methods.document.set", () => {
+		function customMethodTests(settings) {
+			describe(`${settings.prefixName}.set`, () => {
 				it("Should be a function", () => {
-					expect(User.methods.document.set).to.be.a("function");
+					expect(settings.methodEntryPoint().set).to.be.a("function");
 				});
 
 				it("Should not throw an error when being called", () => {
-					expect(() => User.methods.document.set("random", () => {})).to.not.throw();
+					expect(() => settings.methodEntryPoint().set("random", () => {})).to.not.throw();
 				});
 
 				it("Should set correct method on model", () => {
-					User.methods.document.set("random", () => {});
-					expect(user.random).to.be.a("function");
+					settings.methodEntryPoint().set("random", () => {});
+					expect(settings.testObject().random).to.be.a("function");
 				});
 
 				it("Should not overwrite internal methods", () => {
-					const originalMethod = user.save;
+					const originalMethod = settings.testObject()[settings.existingMethod];
 					const newMethod = () => {};
-					User.methods.document.set("save", newMethod);
-					expect(user.save).to.eql(originalMethod);
-					expect(user.save).to.not.eql(newMethod);
+					settings.methodEntryPoint().set(settings.existingMethod, newMethod);
+					expect(settings.testObject()[settings.existingMethod]).to.eql(originalMethod);
+					expect(settings.testObject()[settings.existingMethod]).to.not.eql(newMethod);
 				});
 
-				it("Should overwrite methods if Internal.internalProperties exists but type doesn't", () => {
-					const originalMethod = user.save;
+				it("Should overwrite methods if Internal.General.internalProperties exists but type doesn't", () => {
+					const originalMethod = settings.testObject()[settings.existingMethod];
 					const newMethod = () => {};
-					User.prototype.random = originalMethod;
-					User.prototype.random[Internal.internalProperties] = {};
-					User.methods.document.set("random", newMethod);
-					expect(user.random).to.eql(originalMethod);
-					expect(user.random).to.not.eql(newMethod);
+					settings.testObject().random = originalMethod;
+					settings.testObject().random[Internal.General.internalProperties] = {};
+					settings.methodEntryPoint().set(settings.existingMethod, newMethod);
+					expect(settings.testObject().random).to.eql(originalMethod);
+					expect(settings.testObject().random).to.not.eql(newMethod);
 				});
 
 				const methodTypes = [
@@ -3294,7 +3167,7 @@ describe("Model", () => {
 							describe(`Caller Type - ${callerType.name}`, () => {
 								let methodTypeCallDetails = [], methodTypeCallResult = {};
 								beforeEach(() => {
-									User.methods.document.set("action", methodType.func(async function (...args) {
+									settings.methodEntryPoint().set("action", methodType.func(async function (...args) {
 										methodTypeCallDetails.push({"arguments": [...args], "this": this});
 										if (methodTypeCallResult.error) {
 											throw methodTypeCallResult.error;
@@ -3310,14 +3183,14 @@ describe("Model", () => {
 
 								it("Should call custom method with correct arguments", async () => {
 									methodTypeCallResult.result = "Success";
-									await callerType.func(user.action)();
+									await callerType.func(settings.testObject().action)();
 									expect(methodTypeCallDetails.length).to.eql(1);
 									expect(methodTypeCallDetails[0].arguments.length).to.eql(1);
 									expect(methodTypeCallDetails[0].arguments[0]).to.be.a("function");
 								});
 
 								it("Should call custom method with correct arguments if passing in one argument", async () => {
-									await callerType.func(user.action)("Hello World");
+									await callerType.func(settings.testObject().action)("Hello World");
 									expect(methodTypeCallDetails.length).to.eql(1);
 									expect(methodTypeCallDetails[0].arguments.length).to.eql(2);
 									expect(methodTypeCallDetails[0].arguments[0]).to.eql("Hello World");
@@ -3325,7 +3198,7 @@ describe("Model", () => {
 								});
 
 								it("Should call custom method with correct arguments if passing in two arguments", async () => {
-									await callerType.func(user.action)("Hello", "World");
+									await callerType.func(settings.testObject().action)("Hello", "World");
 									expect(methodTypeCallDetails.length).to.eql(1);
 									expect(methodTypeCallDetails[0].arguments.length).to.eql(3);
 									expect(methodTypeCallDetails[0].arguments[0]).to.eql("Hello");
@@ -3335,16 +3208,16 @@ describe("Model", () => {
 
 								it("Should call custom method with correct `this`", async () => {
 									methodTypeCallResult.result = "Success";
-									// Not sure why we have to bind `user` here, when we don't have to bind anything for Model.methods.set
+									// Not sure why we have to bind here
 									// Through manual testing tho, you do not need to bind anything and in production use this works as described in the documentation
 									// Would be nice to figure out why this is only necessary for our test suite and fix it
-									await callerType.func(user.action).bind(user)();
-									expect(methodTypeCallDetails[0].this).to.eql(user);
+									await callerType.func(settings.testObject().action).bind(settings.testObject())();
+									expect(methodTypeCallDetails[0].this).to.eql(settings.testObject());
 								});
 
 								it("Should return correct response that custom method returns", async () => {
 									methodTypeCallResult.result = "Success";
-									const result = await callerType.func(user.action)();
+									const result = await callerType.func(settings.testObject().action)();
 									expect(result).to.eql("Success");
 								});
 
@@ -3352,7 +3225,7 @@ describe("Model", () => {
 									methodTypeCallResult.error = "ERROR";
 									let result, error;
 									try {
-										result = await callerType.func(user.action)();
+										result = await callerType.func(settings.testObject().action)();
 									} catch (e) {
 										error = e;
 									}
@@ -3364,28 +3237,32 @@ describe("Model", () => {
 					});
 				});
 			});
-
-			describe("Model.methods.document.delete", () => {
+			describe(`${settings.prefixName}.delete`, () => {
 				it("Should be a function", () => {
-					expect(User.methods.document.delete).to.be.a("function");
+					expect(settings.methodEntryPoint().delete).to.be.a("function");
 				});
 
 				it("Should not delete internal methods", () => {
-					User.methods.document.delete("save");
-					expect(user.save).to.be.a("function");
+					settings.methodEntryPoint().delete(settings.existingMethod);
+					expect(settings.testObject()[settings.existingMethod]).to.be.a("function");
 				});
 
 				it("Should not throw error for deleting unknown method", () => {
-					expect(() => User.methods.document.delete("randomHere123")).to.not.throw();
+					expect(() => settings.methodEntryPoint().delete("randomHere123")).to.not.throw();
 				});
 
 				it("Should delete custom method", () => {
-					User.methods.document.set("myMethod", () => {});
-					expect(user.myMethod).to.be.a("function");
-					User.methods.document.delete("myMethod");
-					expect(user.myMethod).to.eql(undefined);
+					settings.methodEntryPoint().set("myMethod", () => {});
+					expect(settings.testObject().myMethod).to.be.a("function");
+					settings.methodEntryPoint().delete("myMethod");
+					expect(settings.testObject().myMethod).to.eql(undefined);
 				});
 			});
+		}
+
+		customMethodTests({"prefixName": "Model.methods", "methodEntryPoint": () => User.methods, "testObject": () => User, "existingMethod": "get"});
+		describe("Model.methods.document", () => {
+			customMethodTests({"prefixName": "Model.methods.document", "methodEntryPoint": () => User.methods.document, "testObject": () => user, "existingMethod": "save"});
 		});
 	});
 });
diff --git a/test/Schema.js b/test/Schema.js
index ce897d2d..cb42ccfb 100644
--- a/test/Schema.js
+++ b/test/Schema.js
@@ -105,7 +105,7 @@ describe("Schema", () => {
 					"friends": "ARRay",
 					"picture": "buffer"
 				}
-			},
+			}
 		];
 		const tests = [
 			{"input": undefined, "output": undefined},
diff --git a/test/utils/dynamodb/attribute_types.js b/test/utils/dynamodb/attribute_types.js
deleted file mode 100644
index 2bd980be..00000000
--- a/test/utils/dynamodb/attribute_types.js
+++ /dev/null
@@ -1,26 +0,0 @@
-const {expect} = require("chai");
-const utils = require("../../../lib/utils");
-
-describe("utils.dynamodb.attribute_types", () => {
-	it("Should be an array", () => {
-		expect(utils.dynamodb.attribute_types).to.be.an("array");
-	});
-
-	it("Should consist of all strings", () => {
-		utils.dynamodb.attribute_types.forEach((val) => {
-			expect(val).to.be.a("string");
-		});
-	});
-
-	it("Should consist of all uppercase letters", () => {
-		utils.dynamodb.attribute_types.forEach((val) => {
-			expect(val).to.be.a("string");
-		});
-	});
-
-	it("Should consist of all uppercase letters", () => {
-		utils.dynamodb.attribute_types.forEach((val) => {
-			expect(val).to.match(/^[^a-z]*$/);
-		});
-	});
-});