Repository: dynamoosejs/dynamoose
Pull request #752 (2020-03-21 21:49:39)

Model.update Improvements
- Allow for `saveUnknown`
- Allowing tests for `undefined`


Patch:
diff --git a/lib/Document.js b/lib/Document.js
index 29b44534..29fe1cf4 100644
--- a/lib/Document.js
+++ b/lib/Document.js
@@ -41,7 +41,7 @@ function DocumentCarrier(model) {
 			return recurrsive ? isValid(object) : values.every((value) => isValid(value));
 		}
 	};
-	Document.toDynamo = (object) => aws.converter().marshall(object);
+	Document.toDynamo = (object, settings = {}) => (settings.type === "item" ? aws.converter().input : aws.converter().marshall)(object);
 	Document.fromDynamo = (object) => aws.converter().unmarshall(object);
 	// This function will mutate the object passed in to run any actions to conform to the schema that cannot be achieved through non mutating methods in Document.objectFromSchema (setting timestamps, etc.)
 	Document.prepareForObjectFromSchema = function(object, settings) {
diff --git a/lib/Model.js b/lib/Model.js
index e3dd34d0..a34ee83a 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -316,11 +316,19 @@ Model.prototype.update = function(keyObj, updateObj, settings = {}, callback) {
 				const expressionKey = `#a${index}`;
 				subKey = Array.isArray(value) ? subValue : subKey;
 
-				const dynamoType = this.schema.getAttributeTypeDetails(subKey).dynamodbType;
-
+				const dynamoType = this.schema.getAttributeType(subKey, subValue, {"unknownAttributeAllowed": true});
+				const attributeExists = this.schema.attributes().includes(subKey);
 				const dynamooseUndefined = require("./index").undefined;
 				if (!updateType.attributeOnly && subValue !== dynamooseUndefined) {
-					subValue = (await this.Document.objectFromSchema({[subKey]: dynamoType === "L" && !Array.isArray(subValue) ? [subValue] : subValue}, {"type": "toDynamo", "customTypesDynamo": true, ...updateType.objectFromSchemaSettings}))[subKey];
+					subValue = (await this.Document.objectFromSchema({[subKey]: dynamoType === "L" && !Array.isArray(subValue) ? [subValue] : subValue}, {"type": "toDynamo", "customTypesDynamo": true, "saveUnknown": true, ...updateType.objectFromSchemaSettings}))[subKey];
+				}
+
+				if (subValue === dynamooseUndefined || subValue === undefined) {
+					if (attributeExists) {
+						updateType = updateTypes.find((a) => a.name === "$REMOVE");
+					} else {
+						continue;
+					}
 				}
 
 				if (subValue !== dynamooseUndefined) {
@@ -329,8 +337,6 @@ Model.prototype.update = function(keyObj, updateObj, settings = {}, callback) {
 						subValue = defaultValue;
 						updateType = updateTypes.find((a) => a.name === "$SET");
 					}
-				} else {
-					updateType = updateTypes.find((a) => a.name === "$REMOVE");
 				}
 
 				if (updateType.objectFromSchemaSettings.required) {
@@ -396,9 +402,9 @@ Model.prototype.update = function(keyObj, updateObj, settings = {}, callback) {
 		}));
 
 		Object.values(returnObject.ExpressionAttributeNames).map((attribute, index) => {
-			const dynamoType = this.schema.getAttributeTypeDetails(attribute).dynamodbType;
 			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);
 
 			if (attributeType.toDynamo && !attributeType.isOfType(value, "fromDynamo")) {
diff --git a/lib/Schema.js b/lib/Schema.js
index 5694e061..2c4d307c 100644
--- a/lib/Schema.js
+++ b/lib/Schema.js
@@ -259,8 +259,16 @@ Schema.prototype.getAttributeTypeDetails = function(key) {
 		"dynamodbType": `${returnValue}${isSet ? "S" : ""}`
 	};
 };
-Schema.prototype.getAttributeType = function(key) {
-	return this.getAttributeTypeDetails(key).dynamodbType;
+Schema.prototype.getAttributeType = function(key, value, settings = {}) {
+	try {
+		return this.getAttributeTypeDetails(key).dynamodbType;
+	} catch (e) {
+		if (e.message === `Invalid Attribute: ${key}` && value && settings.unknownAttributeAllowed) {
+			return Object.keys(require("./Document")({}).toDynamo(value, {"type": "item"}))[0];
+		} else {
+			throw e;
+		}
+	}
 };
 
 Schema.prototype.getCreateTableAttributeParams = async function(model) {


Test patch:
diff --git a/test/Model.js b/test/Model.js
index 491e173b..6cf2acc1 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -1208,6 +1208,141 @@ describe("Model", () => {
 					});
 				});
 
+				it("Should send correct params to updateItem for trying to update unknown properties with saveUnknown", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", new dynamoose.Schema({"id": Number, "name": String, "age": Number}, {"saveUnknown": true}));
+					await callType.func(User).bind(User)({"id": 1, "name": "Charlie", "random": "hello world"});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name",
+							"#a1": "random"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							},
+							":v1": {
+								"S": "hello world"
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0, #a1 = :v1",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
+				it("Should send correct params to updateItem for trying to update unknown properties", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					await callType.func(User).bind(User)({"id": 1, "name": "Charlie", "random": "hello world"});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
+				it("Should send correct params to updateItem for trying to update unknown list properties with saveUnknown", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", new dynamoose.Schema({"id": Number, "name": String, "age": Number}, {"saveUnknown": true}));
+					await callType.func(User).bind(User)({"id": 1, "name": "Charlie", "random": ["hello world"]});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name",
+							"#a1": "random"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							},
+							":v1": {
+								"L": [{"S": "hello world"}]
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0, #a1 = :v1",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
+				it("Should send correct params to updateItem for trying to update unknown list properties with saveUnknown as $ADD", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", new dynamoose.Schema({"id": Number, "name": String, "age": Number}, {"saveUnknown": true}));
+					await callType.func(User).bind(User)({"id": 1}, {"$SET": {"name": "Charlie"}, "$ADD": {"random": ["hello world"]}});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name",
+							"#a1": "random"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							},
+							":v1": {
+								"L": [{"S": "hello world"}]
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0, #a1 = list_append(#a1, :v1)",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
+				it("Should send correct params to updateItem for trying to update unknown list properties", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					await callType.func(User).bind(User)({"id": 1, "name": "Charlie", "random": ["hello world"]});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
 				it("Should send correct params to updateItem for single object update", async () => {
 					updateItemFunction = () => Promise.resolve({});
 					await callType.func(User).bind(User)({"id": 1, "name": "Charlie"});
@@ -1312,6 +1447,52 @@ describe("Model", () => {
 					});
 				});
 
+				it("Should send correct params to updateItem when using undefined to restore to default property", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "default": () => "Charlie"}, "age": Number});
+					await callType.func(User).bind(User)({"id": 1, "name": undefined});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name"
+						},
+						"ExpressionAttributeValues": {
+							":v0": {
+								"S": "Charlie"
+							}
+						},
+						"UpdateExpression": "SET #a0 = :v0",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
+				it("Should send correct params to updateItem when using undefined to delete default property", async () => {
+					updateItemFunction = () => Promise.resolve({});
+					User = new dynamoose.Model("User", {"id": Number, "name": String, "age": Number});
+					await callType.func(User).bind(User)({"id": 1, "name": undefined});
+					expect(updateItemParams).to.be.an("object");
+					expect(updateItemParams).to.eql({
+						"ExpressionAttributeNames": {
+							"#a0": "name"
+						},
+						"ExpressionAttributeValues": {},
+						"UpdateExpression": "REMOVE #a0",
+						"Key": {
+							"id": {
+								"N": "1"
+							}
+						},
+						"TableName": "User",
+						"ReturnValues": "ALL_NEW"
+					});
+				});
+
 				it("Should send correct params to updateItem when using dynamoose.undefined to delete default property", async () => {
 					updateItemFunction = () => Promise.resolve({});
 					User = new dynamoose.Model("User", {"id": Number, "name": {"type": String, "default": () => "Charlie"}, "age": Number});