Repository: dynamoosejs/dynamoose
Pull request #726 (2020-03-09 04:08:08)

dynamoose.undefined


Patch:
diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
index 5a726947..8eaeb339 100644
--- a/BREAKING_CHANGES.md
+++ b/BREAKING_CHANGES.md
@@ -16,7 +16,7 @@
 - `Document.original` formerly (`model.originalItem`) no longer returns the last item saved, but the item first retrieved from DynamoDB
 - `expires` has been moved from the Schema settings to the Model settings
 - `expires.ttl` is now milliseconds as opposed to seconds
-- `expires.defaultExpires` is no longer an option
+- `expires.defaultExpires` is no longer an option (most behavior from this option can be replicated by using the new `dynamoose.undefined` feature)
 - `expires.returnExpiredItems` has been renamed to `expires.items.returnExpired`
 - `Model.transaction.conditionCheck` has been renamed to `Model.transaction.condition`
 - `Model.transaction.condition` options parameter now gets appended to the object returned. This means you can no longer use the helpers that Dynamoose provided to make conditions. Instead, pass in the DynamoDB API level conditions you wish to use
diff --git a/docs/api/General.md b/docs/api/General.md
index 14285342..c779e918 100644
--- a/docs/api/General.md
+++ b/docs/api/General.md
@@ -101,3 +101,17 @@ This function is used to revert the `AWS.DynamoDB.Converter` instance that Dynam
 ```js
 dynamoose.aws.converter.revert();
 ```
+
+## dynamoose.undefined
+
+Setting an attribute value to this will cause it to bypass the `default` value, and set it to `undefined` in the database.
+
+```js
+const dynamoose = require("dynamoose");
+
+const User = new dynamoose.Model("User", {"id": String, "name": {"type": String, "default": "Bob"}});
+const user = new User({"id": 1, "name": dynamoose.undefined});
+await user.save();
+// {"id": 1}
+// will be saved to the database (notice the `name` property is undefined and did not use the `default` property)
+```
diff --git a/lib/Document.js b/lib/Document.js
index 496aad15..421fdb3a 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -108,6 +108,7 @@ function DocumentCarrier(model) {
 			return null;
 		}
 
+		const dynamooseUndefined = require("./index").undefined;
 		function mainCheck(finalObjectPre, key, value) {
 			const finalObject = {...finalObjectPre};
 			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
@@ -127,7 +128,7 @@ function DocumentCarrier(model) {
 			}
 			const saveUnknownSetting = model.schema.getSettingValue("saveUnknown");
 			const saveUnknownAllowed = Array.isArray(saveUnknownSetting) ? saveUnknownSetting.includes(key) : saveUnknownSetting;
-			if ((existsInSchema && (typeMatches || (settings.customTypesDynamo && customValueType === expectedType))) || (settings.saveUnknown && saveUnknownAllowed)) {
+			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) {
@@ -156,10 +157,14 @@ function DocumentCarrier(model) {
 		if (settings.defaults || settings.forceDefault) {
 			await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
 				const value = utils.object.get(returnObject, key);
-				const defaultValue = await model.schema.defaultCheck(key, value, settings);
-				if (defaultValue) {
-					const newObject = mainCheck({}, key, defaultValue);
-					utils.object.set(returnObject, key, utils.object.get(newObject, key));
+				if (value === dynamooseUndefined) {
+					utils.object.set(returnObject, key, undefined);
+				} else {
+					const defaultValue = await model.schema.defaultCheck(key, value, settings);
+					if (defaultValue) {
+						const newObject = mainCheck({}, key, defaultValue);
+						utils.object.set(returnObject, key, utils.object.get(newObject, key));
+					}
 				}
 			}));
 		}
@@ -199,15 +204,6 @@ function DocumentCarrier(model) {
 			}));
 		}
 
-		// Object.keys(attributeTypeMap).forEach((key) => {
-		// 	const attributeType = attributeTypeMap[key];
-		// 	const existingValue = utils.object.get(returnObject, key);
-		//
-		// 	if (attributeType && attributeType[settings.type]) {
-		// 		utils.object.set(returnObject, key, attributeType[settings.type](existingValue));
-		// 	}
-		// });
-
 		return returnObject;
 	};
 	Document.prototype.toDynamo = async function(settings = {}) {
diff --git a/lib/index.js b/lib/index.js
index 8e4cb481..75106428 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -2,5 +2,6 @@ module.exports = {
 	"Model": require("./Model"),
 	"Schema": require("./Schema"),
 	"transaction": require("./Transaction"),
-	"aws": require("./aws")
+	"aws": require("./aws"),
+	"undefined": Symbol("dynamoose.undefined")
 };


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 55ba1095..7a3bcf40 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -2,10 +2,9 @@ const chaiAsPromised = require("chai-as-promised");
 const chai = require("chai");
 chai.use(chaiAsPromised);
 const {expect} = chai;
+const dynamoose = require("../lib");
+const {Model, Schema, aws} = dynamoose;
 const Document = require("../lib/Document");
-const Model = require("../lib/Model");
-const Schema = require("../lib/Schema");
-const aws = require("../lib/aws");
 const util = require("util");
 const Error = require("../lib/Error");
 const utils = require("../lib/utils");
@@ -145,6 +144,18 @@ describe("Document", () => {
 					}]);
 				});
 
+				it("Should not use default if dynamoose.undefined used as value for that property", async () => {
+					const Robot = new Model("Robot", {"id": Number, "age": {"type": Number, "default": 1}});
+					const robot = new Robot({"id": 2, "age": dynamoose.undefined});
+
+					putItemFunction = () => Promise.resolve();
+					await callType.func(robot).bind(robot)();
+					expect(putParams).to.eql([{
+						"Item": {"id": {"N": "2"}},
+						"TableName": "Robot"
+					}]);
+				});
+
 				it("Should return correct result after saving", async () => {
 					putItemFunction = () => Promise.resolve();
 					const result = await callType.func(user).bind(user)();
@@ -941,7 +952,7 @@ describe("Document", () => {
 		});
 	});
 
-	describe("document.save", () => {
+	describe("document.delete", () => {
 		let User, user, deleteParams, deleteItemFunction;
 		beforeEach(() => {
 			Model.defaults = {