Repository: dynamoosejs/dynamoose
Pull request #829 (2020-04-19 21:19:09)

Changing set type
DynamoDB set types are now defined as `{"type": Set, "schema": [String]}` as opposed to the former `[String]` or `{"type": [String]}`. This is more explict and makes it more clear that the type is a set.


Patch:
diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
index 22a3ade4..139124f6 100644
--- a/BREAKING_CHANGES.md
+++ b/BREAKING_CHANGES.md
@@ -34,6 +34,7 @@
 - The attribute types `Array` & `Object` in Dynamoose v1 don't work without a `schema` option in v2
 - `Scan.null` & `Query.null` have been removed. In most cases this can be replaced with `.not().exists()`.
 - DynamoDB set types are now returned as JavaScript Set's instead of Array's
+- DynamoDB set types are now defined as `{"type": Set, "schema": [String]}` as opposed to the former `[String]` or `{"type": [String]}`. This is more explict and makes it more clear that the type is a set.
 - Trying to save a Document with a property set to `null` will now throw an error. If you would like to remove the property set it to `dynamoose.undefined` to set it to undefined without taking into account the `default` setting, or `undefined` to set it to undefined while taking into account the `default` setting.
 - Expires TTL value is set to be a default value. In Dynamoose v1 there were cases where expires TTL wouldn't be set, in Dynamoose v2, the behavior of if the Expires TTL is set behaves the same as any default value
 - Custom methods have changed behavior:
diff --git a/docs/docs/api/Schema.md b/docs/docs/api/Schema.md
index 09a68fdd..0bcfe399 100644
--- a/docs/docs/api/Schema.md
+++ b/docs/docs/api/Schema.md
@@ -59,7 +59,16 @@ const schema = new dynamoose.Schema({
 | Object | False | M | False | True |   |   |
 | Array | False | L | False | True |   |   |
 
-If you use a set you will define the type surrounded by brackets. For example a String Set would be defined as a type of `[String]`. Set's are different from Array's since they require each item in the Set be unique. If you use a Set, it will use the underlying JavaScript Set instance as opposed to an Array.
+Set's are different from Array's since they require each item in the Set be unique. If you use a Set, it will use the underlying JavaScript Set instance as opposed to an Array. If you use a set you will define the type surrounded by brackets in the [`schema`](#schema-object--array) setting. For example to define a string set you would do something like:
+
+```js
+{
+	"friends": {
+		"type": Set,
+		"schema": [String]
+	}
+}
+```
 
 When using `saveUnknown` with a set, the type recognized by Dynamoose will be the underlying JavaScript Set constructor. If you have a set type defined in your schema the underlying type will be an Array.
 
diff --git a/lib/Schema.js b/lib/Schema.js
index 14dabc60..7cbb83a4 100644
--- a/lib/Schema.js
+++ b/lib/Schema.js
@@ -291,16 +291,20 @@ Schema.prototype.getAttributeTypeDetails = function(key, settings = {}) {
 		typeVal = typeVal.value;
 	}
 
-	const isSet = Array.isArray(typeVal);
+	const getType = (typeVal) => {
+		let type;
+		if (typeof typeVal === "function") {
+			const regexFuncName = /^Function ([^(]+)\(/iu;
+			[, type] = typeVal.toString().match(regexFuncName);
+		} else {
+			type = typeVal;
+		}
+		return type;
+	};
+	let type = getType(typeVal);
+	const isSet = type.toLowerCase() === "set";
 	if (isSet) {
-		typeVal = typeVal[0];
-	}
-	let type;
-	if (typeof typeVal === "function") {
-		const regexFuncName = /^Function ([^(]+)\(/iu;
-		[, type] = typeVal.toString().match(regexFuncName);
-	} else {
-		type = typeVal;
+		type = getType(this.getAttributeSettingValue("schema", key)[0]);
 	}
 
 	const returnObject = retrieveTypeInfo(type, isSet, key, typeSettings);


Test patch:
diff --git a/test/Document.js b/test/Document.js
index 735bc062..6599b8f0 100644
--- a/test/Document.js
+++ b/test/Document.js
@@ -206,7 +206,7 @@ describe("Document", () => {
 
 				it("Should save with correct object with string set", async () => {
 					putItemFunction = () => Promise.resolve();
-					User = dynamoose.model("User", {"id": Number, "friends": [String]});
+					User = dynamoose.model("User", {"id": Number, "friends": {"type": Set, "schema": [String]}});
 					user = new User({"id": 1, "friends": ["Charlie", "Tim", "Bob"]});
 					await callType.func(user).bind(user)();
 					expect(putParams).to.eql([{
@@ -240,7 +240,7 @@ describe("Document", () => {
 
 				it("Should save with correct object with number set", async () => {
 					putItemFunction = () => Promise.resolve();
-					User = dynamoose.model("User", {"id": Number, "numbers": [Number]});
+					User = dynamoose.model("User", {"id": Number, "numbers": {"type": Set, "schema": [Number]}});
 					user = new User({"id": 1, "numbers": [5, 7]});
 					await callType.func(user).bind(user)();
 					expect(putParams).to.eql([{
@@ -262,7 +262,7 @@ describe("Document", () => {
 
 				it("Should save with correct object with date set", async () => {
 					putItemFunction = () => Promise.resolve();
-					User = dynamoose.model("User", {"id": Number, "times": [Date]});
+					User = dynamoose.model("User", {"id": Number, "times": {"type": Set, "schema": [Date]}});
 					const time = new Date();
 					user = new User({"id": 1, "times": [time, new Date(0)]});
 					await callType.func(user).bind(user)();
@@ -285,7 +285,7 @@ describe("Document", () => {
 
 				it("Should save with correct object with buffer set", async () => {
 					putItemFunction = () => Promise.resolve();
-					User = dynamoose.model("User", {"id": Number, "data": [Buffer]});
+					User = dynamoose.model("User", {"id": Number, "data": {"type": Set, "schema": [Buffer]}});
 					user = new User({"id": 1, "data": [Buffer.from("testdata"), Buffer.from("testdata2")]});
 					await callType.func(user).bind(user)();
 					expect(putParams).to.eql([{
@@ -1634,7 +1634,7 @@ describe("Document", () => {
 			},
 			{
 				"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]}}}),
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": {"type": Set, "schema": [String]}}}}),
 				"output": {"id": 1, "items": {"data": new Set(["hello", "world"])}}
 			},
 			{
@@ -1644,12 +1644,12 @@ describe("Document", () => {
 			},
 			{
 				"input": [{"id": 1, "items": {"data": ["hello", "world"]}}, {"type": "toDynamo"}],
-				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": [String]}}}),
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": {"type": Set, "schema": [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]}}}),
+				"schema": new Schema({"id": Number, "items": {"type": Object, "schema": {"data": {"type": Set, "schema": [String]}}}}),
 				"output": {"id": 1, "items": {"data": {"wrapperName": "Set", "type": "String", "values": ["hello", "world"]}}}
 			},
 			{
diff --git a/test/Model.js b/test/Model.js
index 5ce09554..b2340ff6 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -801,7 +801,7 @@ describe("Model", () => {
 				});
 
 				it("Should return object with correct values for string set", async () => {
-					User = dynamoose.model("User", {"id": Number, "friends": [String]});
+					User = dynamoose.model("User", {"id": Number, "friends": {"type": Set, "schema": [String]}});
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "friends": {"SS": ["Charlie", "Bob"]}}});
 					const user = await callType.func(User).bind(User)(1);
 					expect(user).to.be.an("object");
@@ -821,7 +821,7 @@ describe("Model", () => {
 				});
 
 				it("Should return object with correct values for number set", async () => {
-					User = dynamoose.model("User", {"id": Number, "numbers": [Number]});
+					User = dynamoose.model("User", {"id": Number, "numbers": {"type": Set, "schema": [Number]}});
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "numbers": {"NS": ["5", "7"]}}});
 					const user = await callType.func(User).bind(User)(1);
 					expect(user).to.be.an("object");
@@ -841,7 +841,7 @@ describe("Model", () => {
 				});
 
 				it("Should return object with correct values for date set", async () => {
-					User = dynamoose.model("User", {"id": Number, "times": [Date]});
+					User = dynamoose.model("User", {"id": Number, "times": {"type": Set, "schema": [Date]}});
 					const time = new Date();
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "times": {"NS": [time.getTime(), 0]}}});
 					const user = await callType.func(User).bind(User)(1);
@@ -862,7 +862,7 @@ describe("Model", () => {
 				});
 
 				it("Should return object with correct values for buffer set", async () => {
-					User = dynamoose.model("User", {"id": Number, "data": [Buffer]});
+					User = dynamoose.model("User", {"id": Number, "data": {"type": Set, "schema": [Buffer]}});
 					getItemFunction = () => Promise.resolve({"Item": {"id": {"N": "1"}, "data": {"BS": [Buffer.from("testdata"), Buffer.from("testdata2")]}}});
 					const user = await callType.func(User).bind(User)(1);
 					expect(user).to.be.an("object");
@@ -2431,7 +2431,7 @@ describe("Model", () => {
 
 				it("Should not throw error if not modifying required property", () => {
 					updateItemFunction = () => Promise.resolve({});
-					User = dynamoose.model("User", {"id": Number, "name": {"type": String, "required": true}, "friends": [String]});
+					User = dynamoose.model("User", {"id": Number, "name": {"type": String, "required": true}, "friends": {"type": Set, "schema": [String]}});
 
 					return expect(callType.func(User).bind(User)({"id": 1}, {"friends": ["Bob"]})).to.not.be.rejected;
 				});
@@ -2577,7 +2577,7 @@ describe("Model", () => {
 
 				it("Should use forceDefault value if adding to property that is a string set", async () => {
 					updateItemFunction = () => Promise.resolve({});
-					User = dynamoose.model("User", {"id": Number, "friends": {"type": [String], "default": ["Bob"], "forceDefault": true}});
+					User = dynamoose.model("User", {"id": Number, "friends": {"type": Set, "schema": [String], "default": ["Bob"], "forceDefault": true}});
 					await callType.func(User).bind(User)({"id": 1}, {"$ADD": {"friends": ["Tim"]}});
 					expect(updateItemParams).to.be.an("object");
 					expect(updateItemParams).to.eql({
diff --git a/test/Schema.js b/test/Schema.js
index 5bd24cdc..024dfe08 100644
--- a/test/Schema.js
+++ b/test/Schema.js
@@ -54,17 +54,17 @@ describe("Schema", () => {
 	it.skip("Should throw error if attribute names only contains number", () => {
 		expect(() => new dynamoose.Schema({"1": String})).to.throw("Attributes names must not be numbers.");
 		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"1": String, "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not be numbers.");
-		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"1": [String], "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not be numbers.");
+		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"1": {"type": Set, "schema": [String]}, "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not be numbers.");
 	});
 
 	it.skip("Should throw error if attribute names contains star", () => {
 		expect(() => new dynamoose.Schema({"*": String})).to.throw("Attributes names must not include stars.");
 		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"*": String, "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not include stars.");
-		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"*": [String], "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not include stars.");
+		expect(() => new dynamoose.Schema({"id": String, "friends": {"type": Array, "schema": [{"type": Object, "schema": {"*": {"type": Set, "schema": [String]}, "data": {"type": Array, "schema": [Buffer, String]}}}]}})).to.throw("Attributes names must not include stars.");
 	});
 
 	it("Should not throw error if valid schema passed in", () => {
-		expect(() => new dynamoose.Schema({"id": Number, "friends": [String]})).to.not.throw();
+		expect(() => new dynamoose.Schema({"id": Number, "friends": {"type": Set, "schema": [String]}})).to.not.throw();
 	});
 
 	describe("getAttributeType", () => {
@@ -81,15 +81,15 @@ describe("Schema", () => {
 					"metadata": Object,
 					"friends": Array,
 					"picture": Buffer,
-					"favoriteFoods": [String],
-					"favoriteNumbers": [Number],
-					"favoriteDates": [Date],
-					"favoritePictures": [Buffer],
-					"favoriteTypes": [Boolean],
-					"favoriteObjects": [Object],
-					"favoriteFriends": [Array],
+					"favoriteFoods": {"type": Set, "schema": [String]},
+					"favoriteNumbers": {"type": Set, "schema": [Number]},
+					"favoriteDates": {"type": Set, "schema": [Date]},
+					"favoritePictures": {"type": Set, "schema": [Buffer]},
+					"favoriteTypes": {"type": Set, "schema": [Boolean]},
+					"favoriteObjects": {"type": Set, "schema": [Object]},
+					"favoriteFriends": {"type": Set, "schema": [Array]},
 					"emptyItem": Symbol,
-					"emptyItems": [Symbol]
+					"emptyItems": {"type": Set, "schema": [Symbol]}
 				}
 			},
 			{
@@ -139,7 +139,9 @@ describe("Schema", () => {
 						} else {
 							const schemaObject = {...schemaObj.schema};
 							Object.keys(schemaObject).forEach((key) => {
-								schemaObject[key] = {"type": schemaObject[key]};
+								if (!schemaObject[key].schema) {
+									schemaObject[key] = {"type": schemaObject[key]};
+								}
 							});
 							schema = new dynamoose.Schema(schemaObject);
 						}