Repository: dynamoosejs/dynamoose
Pull request #742 (2020-03-21 01:03:51)

Undefined enum
There was a bug where enum behaved just like required if `undefined` wasn't passed into the enum array. This PR fixes that so a property can be undefined or null and still conform to the enum.


Patch:
diff --git a/docs/api/Schema.md b/docs/api/Schema.md
index 1030884c..1a289600 100644
--- a/docs/api/Schema.md
+++ b/docs/api/Schema.md
@@ -257,6 +257,8 @@ You can set an attribute to be required when saving documents to DynamoDB. By de
 
 You can set an attribute to have an enum array, which means it must match one of the values specified in the enum array. By default this setting is undefined and not set to anything.
 
+This property is not a replacement for `required`. If the value is undefined or null, the enum will not be checked. If you want to require the property and also have an `enum` you must use both `enum` & `required`.
+
 ```js
 {
 	"name": {
diff --git a/lib/Document.js b/lib/Document.js
index a6e8576a..d80a5712 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -196,9 +196,12 @@ function DocumentCarrier(model) {
 		if (settings.enum) {
 			await Promise.all(Document.attributesWithSchema(returnObject).map(async (key) => {
 				const value = utils.object.get(returnObject, key);
-				const enumArray = await model.schema.getAttributeSettingValue("enum", key);
-				if (enumArray && !enumArray.includes(value)) {
-					throw new Error.ValidationError(`${key} must equal ${JSON.stringify(enumArray)}, but is set to ${value}`);
+				const isValueUndefined = typeof value === "undefined" || value === null;
+				if (!isValueUndefined) {
+					const enumArray = await model.schema.getAttributeSettingValue("enum", key);
+					if (enumArray && !enumArray.includes(value)) {
+						throw new Error.ValidationError(`${key} must equal ${JSON.stringify(enumArray)}, but is set to ${value}`);
+					}
 				}
 			}));
 		}


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 7161748e..e50af594 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -1420,6 +1420,16 @@ describe("Document", () => {
 				"output": {"id": "ID_test"},
 				"schema": {"id": {"type": String, "validate": "ID_test"}}
 			},
+			{
+				"input": [{"id": "test"}, {"enum": true}],
+				"output": {"id": "test"},
+				"schema": {"id": {"type": String}, "age": {"type": Number, "enum": [10, 20]}}
+			},
+			{
+				"input": [{"id": "test"}, {"enum": true, "required": true}],
+				"error": new Error.ValidationError("age is a required property but has no value when trying to save document"),
+				"schema": {"id": {"type": String}, "age": {"type": Number, "enum": [10, 20], "required": true}}
+			},
 			{
 				"input": [{"id": 1, "ttl": 1}, {"type": "fromDynamo", "checkExpiredItem": true}],
 				"model": ["User", {"id": Number}, {"create": false, "waitForActive": false, "expires": 1000}],
diff --git a/test/Model.js b/test/Model.js
index 34eb6eaa..d6dd754b 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -1910,6 +1910,31 @@ describe("Model", () => {
 					});
 				});
 
+				it("Shouldn't conform to enum if property isn't being updated", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "enum": ["Bob", "Tim"]}, "data": String});
+					await callType.func(User).bind(User)({"id": 1}, {"data": "test"});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "data"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "test"
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
 				it("Should throw error if AWS throws error", () => {
 					updateItemFunction = () => Promise.reject({"error": "ERROR"});