Repository: dynamoosejs/dynamoose
Pull request #759 (2020-03-28 21:53:10)

Performance fixes


Patch:
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 9c03c5a7..1b2bed65 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -24,3 +24,7 @@ Both the `create` & `waitForActive` model options add overhead to creating model
 ## Is Dynamoose's goal to be compatible with Mongoose?
 
 No. Although Dynamoose was inspired by Mongoose, there are a lot of differences between the two database engines. We do not have the goal of a fully compatible API with Mongoose, although you will find a lot of similarities. Some areas of Dynamoose we will not attempt to take any inspiration from Mongoose, and design it in our own way.
+
+## Can I use an undocumented property, class, method or function in Dynamoose?
+
+Definitely not. Anything that is undocumented in Dynamoose can change at anytime without a breaking change version, and using anything that is undocumented can lead to unexpected behavior. If you notice something in the internal codebase that you would like to make publicly accessible to use in your own services, please create a PR or issue to add documentation to it, and it will be reviewed to ensure the functionality is able to remain stable.
diff --git a/docs/api/Schema.md b/docs/api/Schema.md
index 18ef52a2..09717c48 100644
--- a/docs/api/Schema.md
+++ b/docs/api/Schema.md
@@ -6,7 +6,7 @@ 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)
+- `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`.
 
 ```js
diff --git a/lib/Document.js b/lib/Document.js
index 29fe1cf4..9cf75661 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -11,7 +11,7 @@ function DocumentCarrier(model) {
 	// Document represents an item in a Model that is either pending (not saved) or saved
 	class Document {
 		constructor(object = {}, settings = {}) {
-			const documentObject = Document.isDynamoObject(object) ? aws.converter().unmarshall({...object}) : object;
+			const documentObject = Document.isDynamoObject(object) ? aws.converter().unmarshall(object) : object;
 			Object.keys(documentObject).forEach((key) => this[key] = documentObject[key]);
 			this[internalProperties] = {};
 			this[internalProperties].originalObject = {...object};
@@ -61,10 +61,15 @@ function DocumentCarrier(model) {
 	};
 	// This function will return a list of attributes combining both the schema attributes with the document attributes. This also takes into account all attributes that could exist (ex. properties in sets that don't exist in document), adding the indexes for each item in the document set.
 	// https://stackoverflow.com/a/59928314/894067
+	const attributesWithSchemaCache = {};
 	Document.attributesWithSchema = function(document) {
+		const attributes = model.schema.attributes();
+		if (attributesWithSchemaCache[document] && attributesWithSchemaCache[document][attributes]) {
+			return attributesWithSchemaCache[document][attributes];
+		}
 		// build a tree out of schema attributes
 		const root = {};
-		model.schema.attributes().forEach((attribute) => {
+		attributes.forEach((attribute) => {
 			let node = root;
 			attribute.split(".").forEach((part) => {
 				node[part] = node[part] || {};
@@ -100,7 +105,9 @@ function DocumentCarrier(model) {
 		}
 		const out = [];
 		traverse(document, root, [], (val) => out.push(val.join(".")));
-		return out.slice(1);
+		const result = out.slice(1);
+		attributesWithSchemaCache[document] = {[attributes]: result};
+		return result;
 	};
 	// This function will return an object that conforms to the schema (removing any properties that don't exist, using default values, etc.) & throws an error if there is a typemismatch.
 	Document.objectFromSchema = async function(object, settings = {"type": "toDynamo"}) {
@@ -108,53 +115,48 @@ function DocumentCarrier(model) {
 			return undefined;
 		}
 
+		const returnObject = {...object};
+		const schemaAttributes = model.schema.attributes();
 		const dynamooseUndefined = require("./index").undefined;
-		function mainCheck(finalObjectPre, key, value) {
-			const finalObject = {...finalObjectPre};
-			if (value === undefined) {
-				utils.object.set(finalObject, key, undefined);
-				return finalObject;
+
+		// Type check
+		const validParents = []; // This array is used to allow for set contents to not be type checked
+		const keysToDelete = [];
+		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))) {
+				return;
 			}
 			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 = model.schema.attributes().includes(genericKey);
-			const typeDetails = existsInSchema && model.schema.getAttributeTypeDetails(key);
-			const isCustomType = existsInSchema && typeDetails.customType;
-			const valueType = typeof value;
-			const customValue = isCustomType ? typeDetails.customType.functions[settings.type](value) : null;
-			const customValueType = isCustomType ? typeof customValue : null;
-			let attributeType = existsInSchema ? utils.attribute_types.find((type) => type.dynamodbType === model.schema.getAttributeType(key)) : null;
-			const expectedType = existsInSchema ? attributeType.javascriptType : null;
-			let typeMatches = existsInSchema ? valueType === expectedType || (attributeType.isOfType && attributeType.isOfType(value, settings.type)) : null;
-			if (existsInSchema && attributeType.isOfType && attributeType.isOfType(isCustomType && settings.type === "toDynamo" ? customValue : value, settings.type)) {
-				typeMatches = true;
-			}
-			const saveUnknownSetting = model.schema.getSettingValue("saveUnknown");
-			const saveUnknownAllowed = utils.dynamoose.saveunknown_attribute_allowed_check(saveUnknownSetting, key);
-			if ((existsInSchema && (typeMatches || (settings.customTypesDynamo && customValueType === expectedType) || (value === dynamooseUndefined))) || (settings.saveUnknown && saveUnknownAllowed)) {
-				if (!existsInSchema && settings.saveUnknown && saveUnknownAllowed) {
-					const foundAttributeType = utils.attribute_types.find((type) => type.isOfType && type.isOfType(customValue || value, settings.type, {"saveUnknown": true}));
-					if (foundAttributeType) {
-						attributeType = foundAttributeType;
-					}
-				}
-
-				const setValue = attributeType && attributeType[settings.type] ? attributeType[settings.type](customValue || value) : (customValue || value);
-
-				if (attributeType && attributeType.nestedType) {
-					utils.object.set(finalObject, key, attributeType.defaultNestedType());
-					Object.keys(setValue).forEach((nestedKey) => {
-						const subKey = `${key}.${nestedKey}`;
-						mainCheck(finalObject, subKey, utils.object.get(object, subKey));
+			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);
+				if (!isValidType) {
+					throw new Error.TypeMismatch(`Expected ${key} to be of type ${expectedJSType}, instead found type ${type}.`);
+				} else if (internalAttributeType.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
+					value.forEach((subValue, index, array) => {
+						if (index === 0 || typeof subValue !== typeof array[0]) {
+							checkTypeFunction([`${key}.${index}`, subValue]);
+						}
 					});
-				} else {
-					utils.object.set(finalObject, key, setValue);
+					validParents.push({key});
+				}
+			} else {
+				// Check saveUnknown
+				if (!settings.saveUnknown || !utils.dynamoose.saveunknown_attribute_allowed_check(model.schema.getSettingValue("saveUnknown"), key)) {
+					keysToDelete.push(key);
 				}
-			} else if (existsInSchema && !typeMatches && !(settings.customTypesDynamo && customValueType === expectedType)) {
-				throw new Error.TypeMismatch(`Expected ${key} to be of type ${attributeType.javascriptType}, instead found type ${valueType}.`);
 			}
-			return finalObject;
-		}
-		const returnObject = Object.keys(object).reduce((finalObject, key) => mainCheck(finalObject, key, utils.object.get(object, key)), {});
+		};
+		utils.object.entries(returnObject).filter((item) => item[1] !== undefined && item[1] !== dynamooseUndefined).map(checkTypeFunction);
+		keysToDelete.reverse().forEach((key) => utils.object.delete(returnObject, key));
 
 		if (settings.defaults || settings.forceDefault) {
 			await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
@@ -165,16 +167,55 @@ function DocumentCarrier(model) {
 					const defaultValue = await model.schema.defaultCheck(key, value, settings);
 					const isDefaultValueUndefined = typeof defaultValue === "undefined" || defaultValue === null;
 					if (!isDefaultValueUndefined) {
-						const newObject = mainCheck({}, key, defaultValue);
-						utils.object.set(returnObject, key, utils.object.get(newObject, key));
+						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);
+						if (!isValidType) {
+							throw new Error.TypeMismatch(`Expected ${key} to be of type ${expectedJSType}, instead found type ${type}.`);
+						} else {
+							utils.object.set(returnObject, key, defaultValue);
+						}
 					}
 				}
 			}));
 		}
+		// Custom Types
+		if (settings.customTypesDynamo) {
+			Document.attributesWithSchema(returnObject).map((key) => {
+				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 customValue = customType.functions[settings.type](value);
+						if (customValue !== value) {
+							utils.object.set(returnObject, key, customValue);
+						}
+					}
+				}
+			});
+		}
+		// DynamoDB Type Handler (ex. converting sets to correct value for toDynamo & fromDynamo)
+		utils.object.entries(returnObject).filter((item) => typeof item[1] === "object").forEach((item) => {
+			const [key, value] = item;
+			let typeDetails;
+			try {
+				typeDetails = model.schema.getAttributeTypeDetails(key).internalAttributeType;
+			} catch (e) {
+				typeDetails = utils.attribute_types.find((type) => type.isOfType && type.isOfType(value, settings.type, settings));
+			}
+
+			if (typeDetails && typeDetails[settings.type]) {
+				utils.object.set(returnObject, key, typeDetails[settings.type](value));
+			}
+		});
 		if (settings.validate) {
 			await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
 				const value = utils.object.get(returnObject, key);
-				if (value) {
+				const isValueUndefined = typeof value === "undefined" || value === null;
+				if (!isValueUndefined) {
 					const validator = await model.schema.getAttributeSettingValue("validate", key, {"returnFunction": true});
 					if (validator) {
 						let result;
@@ -238,7 +279,7 @@ function DocumentCarrier(model) {
 		return returnObject;
 	};
 	Document.prototype.toDynamo = async function(settings = {}) {
-		settings = {...settings, "type": "toDynamo"};
+		settings.type = "toDynamo";
 		Document.prepareForObjectFromSchema(this, settings);
 		const object = await Document.objectFromSchema(this, settings);
 		return Document.toDynamo(object);
diff --git a/lib/DocumentRetriever.js b/lib/DocumentRetriever.js
index a9a2816c..e07ed069 100644
--- a/lib/DocumentRetriever.js
+++ b/lib/DocumentRetriever.js
@@ -153,8 +153,7 @@ function main(documentRetrieverTypeString) {
 				object.IndexName = this.settings.index;
 			} else if (documentRetrieverType.type === "query") {
 				const indexes = await model.schema.getIndexes(model);
-				// TODO change `Array.prototype.concat.apply` to be a custom flatten function
-				const preferredIndexes = Array.prototype.concat.apply([], Object.values(indexes)).filter((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qha"] && key.KeyType === "HASH")));
+				const preferredIndexes = utils.array_flatten(Object.values(indexes)).filter((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qha"] && key.KeyType === "HASH")));
 				const index = !object.ExpressionAttributeNames["#qra"] ? preferredIndexes[0] : preferredIndexes.find((index) => Boolean(index.KeySchema.find((key) => key.AttributeName === object.ExpressionAttributeNames["#qra"] && key.KeyType === "RANGE")));
 				object.IndexName = index.IndexName;
 			}
diff --git a/lib/Internal.js b/lib/Internal.js
new file mode 100644
index 00000000..64e8662b
--- /dev/null
+++ b/lib/Internal.js
@@ -0,0 +1,5 @@
+module.exports = {
+	"Schema": {
+		"internalCache": Symbol("internalCache")
+	}
+};
diff --git a/lib/Model.js b/lib/Model.js
index 61beb4c5..ab175bac 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -4,6 +4,7 @@ const Document = require("./Document");
 const ModelStore = require("./ModelStore");
 const utils = require("./utils");
 const aws = require("./aws");
+const Internal = require("./Internal");
 
 // Model represents one DynamoDB table
 class Model {
@@ -34,6 +35,7 @@ class Model {
 				},
 				"default": () => new Date(Date.now() + options.expires.ttl)
 			};
+			schema[Internal.Schema.internalCache].attributes = undefined;
 		}
 		this.schema = schema;
 
diff --git a/lib/Schema.js b/lib/Schema.js
index 2c4d307c..e1929013 100644
--- a/lib/Schema.js
+++ b/lib/Schema.js
@@ -1,5 +1,7 @@
 const Error = require("./Error");
 const utils = require("./utils");
+const Internal = require("./Internal");
+const internalCache = Internal.Schema.internalCache;
 
 class Schema {
 	constructor(object, settings = {}) {
@@ -25,8 +27,12 @@ class Schema {
 			object[settings.timestamps.updatedAt] = Date;
 		}
 
+		// Anytime `this.schemaObject` is modified, `this[internalCache].attributes` must be set to undefined or null
 		this.schemaObject = object;
 		this.settings = settings;
+		this[internalCache] = {
+			"getAttributeTypeDetails": {}
+		};
 
 		const checkAttributeNameDots = (object/*, existingKey = ""*/) => {
 			return Object.keys(object).forEach((key) => {
@@ -128,7 +134,7 @@ Schema.prototype.getSettingValue = function(setting) {
 	return this.settings[setting];
 };
 
-Schema.prototype.attributes = function() {
+function attributes() {
 	const main = (object, existingKey = "") => {
 		return Object.keys(object).reduce((accumulator, key) => {
 			const keyWithExisting = `${existingKey ? `${existingKey}.` : ""}${key}`;
@@ -148,22 +154,25 @@ Schema.prototype.attributes = function() {
 	};
 
 	return main(this.schemaObject);
+}
+Schema.prototype.attributes = function() {
+	if (!this[internalCache].attributes) {
+		this[internalCache].attributes = attributes.call(this);
+	}
+
+	return this[internalCache].attributes;
 };
 
-Schema.prototype.getAttributeValue = function(key) {
-	const keyParts = key.split(".");
-	let returnItem;
-	keyParts.forEach((part) => {
-		if (parseInt(part) > 0) {
-			part = "0";
-		}
-		returnItem = utils.object.get((returnItem || {}).schema || this.schemaObject, part);
-	});
-	return returnItem;
+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});
 };
 
-Schema.prototype.getAttributeTypeDetails = function(key) {
-	const val = this.getAttributeValue(key);
+Schema.prototype.getAttributeTypeDetails = function(key, settings = {}) {
+	const standardKey = (settings.standardKey ? key : key.replace(/\d+/gu, "0"));
+	if (this[internalCache].getAttributeTypeDetails[standardKey]) {
+		return this[internalCache].getAttributeTypeDetails[standardKey];
+	}
+	const val = this.getAttributeValue(standardKey, {"standardKey": true});
 	if (!val) {
 		throw new Error.UnknownAttribute(`Invalid Attribute: ${key}`);
 	}
@@ -207,6 +216,10 @@ Schema.prototype.getAttributeTypeDetails = function(key) {
 		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 val.getTime() / 1000;
@@ -215,11 +228,7 @@ Schema.prototype.getAttributeTypeDetails = function(key) {
 						}
 					};
 
-					try {
-						return isSet ? val.map(action) : action(val);
-					} catch (e) {
-						return null;
-					}
+					return isSet ? val.map(action) : action(val);
 				},
 				"fromDynamo": (val) => {
 					const action = (val) => {
@@ -231,6 +240,17 @@ Schema.prototype.getAttributeTypeDetails = function(key) {
 					};
 
 					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";
+					}
 				}
 			}
 		};
@@ -253,17 +273,21 @@ Schema.prototype.getAttributeTypeDetails = function(key) {
 		throw new Error.InvalidType(`${key} with type: ${type} is not allowed to be a set`);
 	}
 
-	return {
+	const dynamodbType = `${returnValue}${isSet ? "S" : ""}`;
+	const returnObject = {
 		isSet,
 		customType,
-		"dynamodbType": `${returnValue}${isSet ? "S" : ""}`
+		dynamodbType,
+		"internalAttributeType": utils.attribute_types.find((type) => type.dynamodbType === dynamodbType)
 	};
+	this[internalCache].getAttributeTypeDetails[standardKey] = returnObject;
+	return returnObject;
 };
 Schema.prototype.getAttributeType = function(key, value, settings = {}) {
 	try {
 		return this.getAttributeTypeDetails(key).dynamodbType;
 	} catch (e) {
-		if (e.message === `Invalid Attribute: ${key}` && value && settings.unknownAttributeAllowed) {
+		if (settings.unknownAttributeAllowed && e.message === `Invalid Attribute: ${key}` && value) {
 			return Object.keys(require("./Document")({}).toDynamo(value, {"type": "item"}))[0];
 		} else {
 			throw e;
@@ -300,8 +324,7 @@ Schema.prototype.getCreateTableAttributeParams = async function(model) {
 		});
 	}
 
-	// TODO change `Array.prototype.concat.apply` to be a custom flatten function
-	Array.prototype.concat.apply([], (await Promise.all([this.getIndexAttributes(), this.getIndexRangeKeyAttributes()]))).map((obj) => obj.attribute).forEach((index) => {
+	utils.array_flatten(await Promise.all([this.getIndexAttributes(), this.getIndexRangeKeyAttributes()])).map((obj) => obj.attribute).forEach((index) => {
 		if (AttributeDefinitionsNames.includes(index)) {
 			return;
 		}
diff --git a/lib/utils/array_flatten.js b/lib/utils/array_flatten.js
new file mode 100644
index 00000000..4c5a64c0
--- /dev/null
+++ b/lib/utils/array_flatten.js
@@ -0,0 +1,2 @@
+// This function flattens an array non recursively
+module.exports = (array) => Array.prototype.concat.apply([], array);
diff --git a/lib/utils/attribute_types.js b/lib/utils/attribute_types.js
index bd52e372..a56af072 100644
--- a/lib/utils/attribute_types.js
+++ b/lib/utils/attribute_types.js
@@ -12,8 +12,8 @@ module.exports = [
 			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, "defaultNestedType": () => []}, // List
-	{"dynamodbType": "M", "isOfType": (val) => Boolean(val) && val.constructor === Object, "javascriptType": "object", "nestedType": true, "defaultNestedType": () => ({})}, // Map
+	{"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") {
@@ -31,11 +31,7 @@ module.exports = [
 	{"dynamodbType": "S", "javascriptType": "string"}, // String
 	{"dynamodbType": "SS", "isSet": true, "isOfType": (val, type, settings = {}) => {
 		if (type === "toDynamo") {
-			if (settings.saveUnknown) {
-				return val instanceof Set && [...val].every((a) => typeof a === "string");
-			} else {
-				return Array.isArray(val) && val.every((a) => typeof a === "string");
-			}
+			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);
 		}
diff --git a/lib/utils/combine_objects.js b/lib/utils/combine_objects.js
index 5662e2bb..2df5b07f 100644
--- a/lib/utils/combine_objects.js
+++ b/lib/utils/combine_objects.js
@@ -1,4 +1,4 @@
-// This function is used to merge objects for combining multiple responses.
+// This function is meant to combine objects for providing default options. Values will be overwritten as opposed to merged.
 const main = module.exports = (...args) => {
 	let returnObject;
 
diff --git a/lib/utils/dynamoose/saveunknown_attribute_allowed_check.js b/lib/utils/dynamoose/saveunknown_attribute_allowed_check.js
index 5b7419cf..cb9f0199 100644
--- a/lib/utils/dynamoose/saveunknown_attribute_allowed_check.js
+++ b/lib/utils/dynamoose/saveunknown_attribute_allowed_check.js
@@ -2,17 +2,18 @@ module.exports = (saveUnknown, checkKey) => {
 	if (Array.isArray(saveUnknown)) {
 		return Boolean(saveUnknown.find((key) => {
 			const keyParts = key.split(".");
-			let index = 0;
-			return checkKey.split(".").every((checkKeyPart) => {
-				const keyPart = keyParts[index];
-
-				if (keyPart === "**" || !isNaN(parseInt(checkKeyPart))) {
+			const checkKeyParts = checkKey.split(".");
+			let index = 0, keyPart = keyParts[0];
+			for (let i = 0; i < checkKeyParts.length; i++) {
+				if (keyPart === "**") {
 					return true;
-				} else {
-					index++;
-					return keyPart === "*" || checkKeyPart === keyPart;
 				}
-			});
+				if (keyPart !== "*" && checkKeyParts[i] !== keyPart) {
+					return false;
+				}
+				keyPart = keyParts[++index];
+			}
+			return true;
 		}));
 	} else {
 		return saveUnknown;
diff --git a/lib/utils/index.js b/lib/utils/index.js
index b5b14f7d..5dd7add7 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -6,6 +6,7 @@ module.exports = {
 	"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")
diff --git a/lib/utils/merge_objects.js b/lib/utils/merge_objects.js
index 9fc3e4ae..289fae57 100644
--- a/lib/utils/merge_objects.js
+++ b/lib/utils/merge_objects.js
@@ -1,4 +1,4 @@
-// This function is meant to combine objects for providing default options. Values will be overwritten as opposed to merged.
+// This function is used to merge objects for combining multiple responses.
 module.exports = (...args) => {
 	let returnObject;
 
diff --git a/lib/utils/object/delete.js b/lib/utils/object/delete.js
new file mode 100644
index 00000000..81e4c82d
--- /dev/null
+++ b/lib/utils/object/delete.js
@@ -0,0 +1,23 @@
+module.exports = (object, key) => {
+	const keys = key.split(".");
+
+	if (keys.length === 1) {
+		if (Array.isArray(object)) {
+			object.splice(parseInt(keys[0]), 1);
+		} else {
+			delete object[keys[0]];
+		}
+	} else {
+		const lastKey = keys.pop();
+		const nextLastKey = keys.pop();
+		const nextLastObj = keys.reduce((a, key) => a[key], object);
+
+		if (Array.isArray(nextLastObj[nextLastKey])) {
+			nextLastObj[nextLastKey].splice(parseInt(lastKey), 1);
+		} else if (typeof nextLastObj[nextLastKey] !== "undefined" && nextLastObj[nextLastKey] !== null) {
+			delete nextLastObj[nextLastKey][lastKey];
+		}
+	}
+
+	return object;
+};
diff --git a/lib/utils/object/entries.js b/lib/utils/object/entries.js
new file mode 100644
index 00000000..4312bfa7
--- /dev/null
+++ b/lib/utils/object/entries.js
@@ -0,0 +1,13 @@
+const main = module.exports = (object, existingKey = "") => {
+	return Object.entries(object).reduce((accumulator, entry) => {
+		const [key, value] = entry;
+		const keyWithExisting = `${existingKey ? `${existingKey}.` : ""}${key}`;
+		accumulator.push([keyWithExisting, value]);
+
+		if (typeof value === "object") {
+			accumulator.push(...main(value, keyWithExisting));
+		}
+
+		return accumulator;
+	}, []);
+};
diff --git a/lib/utils/object/get.js b/lib/utils/object/get.js
index 851201fe..3a29fe7f 100644
--- a/lib/utils/object/get.js
+++ b/lib/utils/object/get.js
@@ -1,10 +1,13 @@
 module.exports = (object, key) => {
 	const keyParts = key.split(".");
 	let returnValue = object;
-	keyParts.forEach((part) => {
+	for (let i = 0; i < keyParts.length; i++) {
+		const part = keyParts[i];
 		if (returnValue) {
 			returnValue = returnValue[part];
+		} else {
+			break;
 		}
-	});
+	}
 	return returnValue;
 };
diff --git a/lib/utils/object/index.js b/lib/utils/object/index.js
index 0097eae6..cc7797d9 100644
--- a/lib/utils/object/index.js
+++ b/lib/utils/object/index.js
@@ -1,5 +1,7 @@
 module.exports = {
 	"get": require("./get"),
 	"set": require("./set"),
-	"keys": require("./keys")
+	"delete": require("./delete"),
+	"keys": require("./keys"),
+	"entries": require("./entries")
 };
diff --git a/lib/utils/object/keys.js b/lib/utils/object/keys.js
index e7fbbfa5..5d93a71c 100644
--- a/lib/utils/object/keys.js
+++ b/lib/utils/object/keys.js
@@ -1,14 +1,3 @@
-const get = require("./get");
+const entries = require("./entries");
 
-const main = module.exports = (object, existingKey = "") => {
-	return Object.keys(object).reduce((accumulator, key) => {
-		const keyWithExisting = `${existingKey ? `${existingKey}.` : ""}${key}`;
-		accumulator.push(keyWithExisting);
-
-		if (typeof get(object, key) === "object") {
-			accumulator.push(...main(get(object, key), keyWithExisting));
-		}
-
-		return accumulator;
-	}, []);
-};
+module.exports = (object, existingKey = "") => entries(object, existingKey).map((a) => a[0]);
diff --git a/package-lock.json b/package-lock.json
index 3269bc54..a6603c26 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -437,9 +437,9 @@
       "dev": true
     },
     "aws-sdk": {
-      "version": "2.643.0",
-      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.643.0.tgz",
-      "integrity": "sha512-4r7VGQFqshrhXnOCVQdlatAWiK/8kmmtAtY9gbITPNpY5Is+SfIy6k/1BgrnL5H/2sYd27H+Xp8itXZoCnQeTw==",
+      "version": "2.648.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.648.0.tgz",
+      "integrity": "sha512-b+PdZmCFvZBisqXEH68jO4xB30LrDHQMWrEX6MJoZaOlxPJfpOqRFUH3zsiAXF5Q2jTdjYLtS5bs3vcIwRzi3Q==",
       "requires": {
         "buffer": "4.9.1",
         "events": "1.1.1",
@@ -573,6 +573,7 @@
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
       "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
+      "dev": true,
       "requires": {
         "check-error": "^1.0.2"
       }
@@ -597,7 +598,8 @@
     "check-error": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
-      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII="
+      "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
+      "dev": true
     },
     "chokidar": {
       "version": "3.3.0",
diff --git a/package.json b/package.json
index 898cfc68..9970911b 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
     "coveralls": "^3.0.11",
     "eslint": "^6.8.0",
     "mocha": "^7.1.1",
-    "nyc": "^15.0.0"
+    "nyc": "^15.0.0",
+    "chai-as-promised": "^7.1.1"
   },
   "scripts": {
     "test": "nyc --all mocha --recursive && npm run report-coverage",
@@ -26,13 +27,21 @@
     "type": "git",
     "url": "git+https://github.com/dynamoosejs/dynamoose.git"
   },
-  "author": "Charlie Fish & Brandon Goode",
+  "author": {
+    "name": "Charlie Fish",
+    "email": "fishcharlie.code@gmail.com",
+    "url": "https://charlie.fish"
+  },
+  "contributors": [
+    {
+      "name": "Brandon Goode"
+    }
+  ],
   "bugs": {
     "url": "https://github.com/dynamoosejs/dynamoose/issues"
   },
   "dependencies": {
-    "aws-sdk": "^2.643.0",
-    "chai-as-promised": "^7.1.1"
+    "aws-sdk": "^2.648.0"
   },
   "license": "Unlicense",
   "keywords": [
@@ -55,7 +64,8 @@
     "store",
     "document store",
     "table",
-    "json"
+    "json",
+    "object"
   ],
   "engines": {
     "node": ">=8.0.0"


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 8657c4e7..642ea43f 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -196,6 +196,18 @@ describe("Document", () => {
 					}]);
 				});
 
+				// TODO: reenable this test
+				it.skip("Should save with correct object with string set and saveUnknown with specific values", async () => {
+					putItemFunction = () => Promise.resolve();
+					User = new Model("User", new Schema({"id": Number}, {"saveUnknown": ["friends", "friends.1"]}));
+					user = new User({"id": 1, "friends": new Set(["Charlie", "Tim", "Bob"])});
+					await callType.func(user).bind(user)();
+					expect(putParams).to.eql([{
+						"Item": {"id": {"N": "1"}, "friends": {"SS": ["Tim"]}},
+						"TableName": "User"
+					}]);
+				});
+
 				it("Should save with correct object with number set", async () => {
 					putItemFunction = () => Promise.resolve();
 					User = new Model("User", {"id": Number, "numbers": [Number]});
@@ -301,6 +313,18 @@ describe("Document", () => {
 					}]);
 				});
 
+				it("Should save with correct object with custom type passed in as underlying type mixed with custom type in array", async () => {
+					putItemFunction = () => Promise.resolve();
+					User = new Model("User", {"id": Number, "name": String, "birthday": {"type": Array, "schema": [Date]}}, {"create": false, "waitForActive": false});
+					const birthday = new Date();
+					user = new User({"id": 1, "name": "Charlie", "birthday": [birthday.getTime(), birthday]});
+					await callType.func(user).bind(user)();
+					expect(putParams).to.eql([{
+						"Item": {"id": {"N": "1"}, "name": {"S": "Charlie"}, "birthday": {"L": [{"N": `${birthday.getTime()}`}, {"N": `${birthday.getTime()}`}]}},
+						"TableName": "User"
+					}]);
+				});
+
 				it("Should save with correct object with object type in schema", async () => {
 					putItemFunction = () => Promise.resolve();
 					User = new Model("User", {"id": Number, "address": {"type": Object, "schema": {"street": String, "country": {"type": String, "required": true}}}}, {"create": false, "waitForActive": false});
@@ -430,7 +454,7 @@ describe("Document", () => {
 					user = new User({"id": 1, "addresses": [{"country": "world", "zip": 12345, "metadata": [{"name": "Home"}]}]});
 					await callType.func(user).bind(user)();
 					expect(putParams).to.eql([{
-						"Item": {"id": {"N": "1"}, "addresses": {"L": [{"M": {"country": {"S": "world"}, "zip": {"N": "12345"}, "metadata": {"L": [{"M": {}}]}}}]}},
+						"Item": {"id": {"N": "1"}, "addresses": {"L": [{"M": {}}]}},
 						"TableName": "User"
 					}]);
 				});
@@ -773,6 +797,25 @@ describe("Document", () => {
 					return expect(callType.func(user).bind(user)()).to.be.rejectedWith("age with a value of 4 had a validation error when trying to save the document");
 				});
 
+				// This test is here since if you want to enforce that the property exists, you must use both `required` & `validate`, not just `validate`
+				it("Should not run validation function if property doesn't exist", async () => {
+					putItemFunction = () => Promise.resolve();
+					let didRun = false;
+					User = new Model("User", {"id": Number, "age": {"type": Number, "validate": () => {didRun = true; return true;}}}, {"create": false, "waitForActive": false});
+					user = new User({"id": 1});
+					await callType.func(user).bind(user)();
+					expect(didRun).to.be.false;
+				});
+
+				it("Should run validation function if property is falsy", async () => {
+					putItemFunction = () => Promise.resolve();
+					let didRun = false;
+					User = new Model("User", {"id": Number, "data": {"type": Boolean, "validate": () => {didRun = true; return true;}}}, {"create": false, "waitForActive": false});
+					user = new User({"id": 1, "data": false});
+					await callType.func(user).bind(user)();
+					expect(didRun).to.be.true;
+				});
+
 				it("Should save with correct object with validation function", async () => {
 					putItemFunction = () => Promise.resolve();
 					User = new Model("User", {"id": Number, "age": {"type": Number, "validate": (val) => val > 5}}, {"create": false, "waitForActive": false});
@@ -1488,10 +1531,25 @@ describe("Document", () => {
 				"schema": {"id": {"type": String}, "data": {"type": Object, "schema": {"name": {"type": String, "required": true}}, "required": true}}
 			},
 			{
-				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true}],
+				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true, "customTypesDynamo": true}],
 				"model": ["User", {"id": Number}, {"create": false, "waitForActive": false, "expires": 1000}],
 				"output": {"id": 1, "ttl": new Date(1000)}
 			},
+			{
+				"input": [{"id": 1, "birthday": new Date(0)}, {"type": "toDynamo", "customTypesDynamo": true}],
+				"model": ["User", {"id": Number, "birthday": Date}, {"create": false, "waitForActive": false}],
+				"output": {"id": 1, "birthday": 0}
+			},
+			{
+				"input": [{"id": 1, "birthday": 0}, {"type": "toDynamo", "customTypesDynamo": true}],
+				"model": ["User", {"id": Number, "birthday": Date}, {"create": false, "waitForActive": false}],
+				"output": {"id": 1, "birthday": 0}
+			},
+			{
+				"input": [{"id": 1, "birthday": 0}, {"type": "fromDynamo", "customTypesDynamo": true}],
+				"model": ["User", {"id": Number, "birthday": Date}, {"create": false, "waitForActive": false}],
+				"output": {"id": 1, "birthday": new Date(0)}
+			},
 			{
 				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true}],
 				"model": ["User", {"id": Number}, {"create": false, "waitForActive": false, "expires": {"ttl": 1000, "attribute": "ttl", "items": {"returnExpired": false}}}],
@@ -1501,6 +1559,99 @@ describe("Document", () => {
 				"input": [{"id": 1, "items": ["test"]}, {"type": "toDynamo", "saveUnknown": true}],
 				"schema": new Schema({"id": Number, "items": Array}, {"saveUnknown": true}),
 				"output": {"id": 1, "items": ["test"]}
+			},
+			{
+				"input": [{"id": 1, "items": ["hello", "world"]}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number, "items": Array}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": ["hello", "world"]}
+			},
+			{
+				"input": [{"id": 1, "items": ["hello", "world"]}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": ["hello", "world"]}
+			},
+			{
+				"input": [{"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}, {"type": "fromDynamo"}],
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+				"output": {"id": 1, "items": {"data": new Set(["hello", "world"])}}
+			},
+			{
+				"input": [{"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}, {"type": "fromDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": {"data": new Set(["hello", "world"])}}
+			},
+			{
+				"input": [{"id": 1, "items": {"data": ["hello", "world"]}}, {"type": "toDynamo"}],
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+				"output": {"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}
+			},
+			{
+				"input": [{"id": 1, "items": {"data": new Set(["hello", "world"])}}, {"type": "toDynamo"}],
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+				"output": {"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}
+			},
+			{
+				"input": [{"id": 1, "items": {"data": new Set(["hello", "world"])}}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}
+			},
+			{
+				"input": [{"id": 1, "data": ["hello", "world", "universe", "galaxy"]}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number}, {"saveUnknown": ["data", "data.1", "data.3"]}),
+				"output": {"id": 1, "data": ["world", "galaxy"]}
+			},
+
+			// TODO: uncomment these
+			// {
+			// 	"input": [{"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", 1]}}}, {"type": "fromDynamo"}],
+			// 	"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+			// 	"error": new Error.ValidationError("data.1 should be a string")
+			// },
+			// {
+			// 	"input": [{"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", 1]}}}, {"type": "fromDynamo", "saveUnknown": true}],
+			// 	"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+			// 	"error": new Error.ValidationError("data.1 should be a string")
+			// },
+			// {
+			// 	"input": [{"id": 1, "items": {"data": ["hello", 1]}}, {"type": "toDynamo"}],
+			// 	"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+			// 	"error": new Error.ValidationError("data.1 should be a string")
+			// },
+			// {
+			// 	"input": [{"id": 1, "items": {"data": new Set(["hello", 1])}}, {"type": "toDynamo"}],
+			// 	"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+			// 	"error": new Error.ValidationError("data.1 should be a string")
+			// },
+			// {
+			// 	"input": [{"id": 1, "items": {"data": new Set(["hello", 1])}}, {"type": "toDynamo", "saveUnknown": true}],
+			// 	"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+			// 	"error": new Error.ValidationError("data.1 should be a string")
+			// },
+
+			{
+				"input": [{"id": 1, "items": [{"name": "Charlie"}, {"name": "Bob"}]}, {"type": "toDynamo", "saveUnknown": true}],
+				"schema": new Schema({"id": Number}, {"saveUnknown": true}),
+				"output": {"id": 1, "items": [{"name": "Charlie"}, {"name": "Bob"}]}
+			},
+			{
+				"input": [{"id": 1, "items": [new Date(1), new Date(10000)]}, {"type": "toDynamo", "customTypesDynamo": true}],
+				"schema": new Schema({"id": Number, "items": {"type": Array, "schema": [Date]}}),
+				"output": {"id": 1, "items": [1, 10000]}
+			},
+			{
+				"input": [{"id": 1, "items": [1, 10000]}, {"type": "fromDynamo", "customTypesDynamo": true}],
+				"schema": new Schema({"id": Number, "items": {"type": Array, "schema": [Date]}}),
+				"output": {"id": 1, "items": [new Date(1), new Date(10000)]}
+			},
+			{
+				"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.")
+			},
+			{
+				"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.")
 			}
 		];
 
diff --git a/test/utils/array_flatten.js b/test/utils/array_flatten.js
new file mode 100644
index 00000000..e1eb688e
--- /dev/null
+++ b/test/utils/array_flatten.js
@@ -0,0 +1,45 @@
+const {expect} = require("chai");
+const utils = require("../../lib/utils");
+
+describe("utils.array_flatten", () => {
+	it("Should be a function", () => {
+		expect(utils.array_flatten).to.be.a("function");
+	});
+
+	const tests = [
+		{
+			"input": ["hello"],
+			"output": ["hello"]
+		},
+		{
+			"input": [["hello"]],
+			"output": ["hello"]
+		},
+		{
+			"input": [1, 2, 3, 4],
+			"output": [1, 2, 3, 4]
+		},
+		{
+			"input": [1, [2, 3, 4]],
+			"output": [1, 2, 3, 4]
+		},
+		{
+			"input": [[1, 2], 3, 4],
+			"output": [1, 2, 3, 4]
+		},
+		{
+			"input": [[1, 2], [3, 4]],
+			"output": [1, 2, 3, 4]
+		},
+		{
+			"input": [[1, [2, 3]], 4],
+			"output": [1, [2, 3], 4]
+		},
+	];
+
+	tests.forEach((test) => {
+		it(`Should return ${test.output} for ${test.input}`, () => {
+			expect(utils.array_flatten(test.input)).to.eql(test.output);
+		});
+	});
+});
diff --git a/test/utils/dynamoose/saveunknown_attribute_allowed_check.js b/test/utils/dynamoose/saveunknown_attribute_allowed_check.js
index 0471b9e6..f04db16e 100644
--- a/test/utils/dynamoose/saveunknown_attribute_allowed_check.js
+++ b/test/utils/dynamoose/saveunknown_attribute_allowed_check.js
@@ -27,6 +27,14 @@ describe("utils.dynamoose.saveunknown_attribute_allowed_check", () => {
 			"input": [["random"], "random.test"],
 			"output": false
 		},
+		{
+			"input": [["random.1"], "random.1"],
+			"output": true
+		},
+		{
+			"input": [["random.1"], "random.2"],
+			"output": false
+		},
 		{
 			"input": [["random.test"], "random"],
 			"output": true
@@ -45,11 +53,11 @@ describe("utils.dynamoose.saveunknown_attribute_allowed_check", () => {
 		},
 		{
 			"input": [["random.*"], "random.0.random"],
-			"output": true
+			"output": false
 		},
 		{
 			"input": [["random.*"], "random.1.random"],
-			"output": true
+			"output": false
 		},
 		{
 			"input": [["random.*.hello"], "random.test.hello"],
diff --git a/test/utils/object/delete.js b/test/utils/object/delete.js
new file mode 100644
index 00000000..9ee5e927
--- /dev/null
+++ b/test/utils/object/delete.js
@@ -0,0 +1,53 @@
+const {expect} = require("chai");
+const utils = require("../../../lib/utils");
+
+describe("utils.object.delete", () => {
+	it("Should be a function", () => {
+		expect(utils.object.delete).to.be.a("function");
+	});
+
+	const tests = [
+		{
+			"input": [{}, "hello"],
+			"output": {}
+		},
+		{
+			"input": [{}, "hello.test"],
+			"output": {}
+		},
+		{
+			"input": [{"hello": "world"}, "hello"],
+			"output": {}
+		},
+		{
+			"input": [{"hello": "world", "test": "data"}, "test"],
+			"output": {"hello": "world"}
+		},
+		{
+			"input": [["world", "universe"], "0"],
+			"output": ["universe"]
+		},
+		{
+			"input": [{"hello": ["world", "universe"]}, "hello.0"],
+			"output": {"hello": ["universe"]}
+		},
+		{
+			"input": [{"name": "Bob", "address": {"country": "world", "zip": 12345}}, "address.country"],
+			"output": {"name": "Bob", "address": {"zip": 12345}}
+		},
+		{
+			"input": [{"name": "Bob", "friends": ["Bob", "Tim"]}, "friends.0"],
+			"output": {"name": "Bob", "friends": ["Tim"]}
+		},
+		{
+			"input": [{"id": 1, "friends": [{"name": "Bob", "id": 1}, {"name": "Tim"}]}, "friends.1.name"],
+			"output": {"id": 1, "friends": [{"name": "Bob", "id": 1}, {}]}
+		}
+	];
+
+	tests.forEach((test) => {
+		it(`Should return ${test.output} for ${test.input}`, () => {
+			expect(utils.object.delete(...test.input)).to.eql(test.output);
+		});
+	});
+});
diff --git a/test/utils/object/entries.js b/test/utils/object/entries.js
new file mode 100644
index 00000000..aa875ffb
--- /dev/null
+++ b/test/utils/object/entries.js
@@ -0,0 +1,37 @@
+const {expect} = require("chai");
+const utils = require("../../../lib/utils");
+
+describe("utils.object.entries", () => {
+	it("Should be a function", () => {
+		expect(utils.object.entries).to.be.a("function");
+	});
+
+	const tests = [
+		{
+			"input": {"hello": "world"},
+			"output": [["hello", "world"]]
+		},
+		{
+			"input": {"hello": "world", "test": "data"},
+			"output": [["hello", "world"], ["test", "data"]]
+		},
+		{
+			"input": {"name": "Bob", "address": {"country": "world", "zip": 12345}},
+			"output": [["name", "Bob"], ["address", {"country": "world", "zip": 12345}], ["address.country", "world"], ["address.zip", 12345]]
+		},
+		{
+			"input": {"name": "Bob", "friends": ["Bob", "Tim"]},
+			"output": [["name", "Bob"], ["friends", ["Bob", "Tim"]], ["friends.0", "Bob"], ["friends.1", "Tim"]]
+		},
+		{
+			"input": {"id": 1, "friends": [{"name": "Bob", "id": 1}, {"name": "Tim"}]},
+			"output": [["id", 1], ["friends", [{"name": "Bob", "id": 1}, {"name": "Tim"}]], ["friends.0", {"name": "Bob", "id": 1}], ["friends.0.name", "Bob"], ["friends.0.id", 1], ["friends.1", {"name": "Tim"}], ["friends.1.name", "Tim"]]
+		}
+	];
+
+	tests.forEach((test) => {
+		it(`Should return ${test.output} for ${test.input}`, () => {
+			expect(utils.object.entries(test.input)).to.eql(test.output);
+		});
+	});
+});