Repository: dynamoosejs/dynamoose
Pull request #754 (2020-03-22 23:02:56)

TTL Update Fixes
There was a bug where if we tried to enable TTL when it was already enabled, it would throw an error. This PR fixes that by checking for existing TTL details before attempting to enable it.


Patch:
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 2418f4c6..9c03c5a7 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -6,7 +6,7 @@ The following is a chart of IAM permissions you need in order to run Dynamoose f
 
 | Dynamoose Action | IAM Permission | Notes |
 |------------------|----------------|-------|
-| new Model() | `createTable`, `describeTable`, `updateTable`, `updateTimeToLive` | `createTable` is only used if `create` is set to true. `describeTable` is only used if `waitForActive` OR `create` is set to true. `updateTable` is only used if `update` is set to true. `updateTimeToLive` is only used if `create` or `update` is set to true and there is an `expires` setting on the model. |
+| new Model() | `createTable`, `describeTable`, `updateTable`, `updateTimeToLive`, `describeTimeToLive` | `createTable` is only used if `create` is set to true. `describeTable` is only used if `waitForActive` OR `create` is set to true. `updateTable` is only used if `update` is set to true. `updateTimeToLive` & `describeTimeToLive` is only used if `create` or `update` is set to true and there is an `expires` setting on the model. |
 | Model.get | `getItem` |  |
 | Model.scan | `scan` | This permission is only required on `scan.exec` |
 | Model.query | `query` | This permission is only required on `query.exec` |
@@ -15,6 +15,7 @@ The following is a chart of IAM permissions you need in order to run Dynamoose f
 | Model.delete | `deleteItem` |  |
 | document.save | `putItem` |  |
 | document.delete | `deleteItem` |  |
+| dynamoose.transaction | `transactGetItems`, `transactWriteItems` |  |
 
 ## Why is it recommended to set `create` & `waitForActive` model options to false for production environments?
 
diff --git a/lib/Model.js b/lib/Model.js
index a34ee83a..61beb4c5 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -144,13 +144,42 @@ async function createTableRequest(model) {
 	};
 }
 function updateTimeToLive(model) {
-	return aws.ddb().updateTimeToLive({
-		"TableName": model.name,
-		"TimeToLiveSpecification": {
-			"AttributeName": model.options.expires.attribute,
-			"Enabled": true
+	return {
+		"promise": async () => {
+			let ttlDetails;
+
+			async function updateDetails() {
+				ttlDetails = await aws.ddb().describeTimeToLive({
+					"TableName": model.name
+				}).promise();
+			}
+			await updateDetails();
+
+			function updateTTL() {
+				return aws.ddb().updateTimeToLive({
+					"TableName": model.name,
+					"TimeToLiveSpecification": {
+						"AttributeName": model.options.expires.attribute,
+						"Enabled": true
+					}
+				});
+			}
+
+			switch (ttlDetails.TimeToLiveDescription.TimeToLiveStatus) {
+			case "DISABLING":
+				while (ttlDetails.TimeToLiveDescription.TimeToLiveStatus === "DISABLING") {
+					await utils.timeout(1000);
+					await updateDetails();
+				}
+				// fallthrough
+			case "DISABLED":
+				await updateTTL();
+				break;
+			default:
+				break;
+			}
 		}
-	});
+	};
 }
 function waitForActive(model) {
 	return () => new Promise((resolve, reject) => {


Test patch:
diff --git a/test/Model.js b/test/Model.js
index 6cf2acc1..2c281e58 100644
--- a/test/Model.js
+++ b/test/Model.js
@@ -543,7 +543,7 @@ describe("Model", () => {
 		});
 
 		describe("Time To Live", () => {
-			let updateTTLParams = [];
+			let updateTTLParams = [], describeTTL, describeTTLFunction;
 			beforeEach(() => {
 				dynamoose.Model.defaults = {
 					"create": false,
@@ -571,15 +571,23 @@ describe("Model", () => {
 						return {
 							"promise": Promise.resolve()
 						};
+					},
+					"describeTimeToLive": () => {
+						return describeTTLFunction ? describeTTLFunction() : {
+							"promise": () => Promise.resolve(describeTTL)
+						};
 					}
 				});
 			});
 			afterEach(() => {
 				updateTTLParams = [];
+				describeTTL = null;
+				describeTTLFunction = null;
 				dynamoose.aws.ddb.revert();
 			});
 
-			it("Should call updateTimeToLive with correct parameters", async () => {
+			it("Should call updateTimeToLive with correct parameters if TTL is disabled", async () => {
+				describeTTL = {"TimeToLiveDescription": {"TimeToLiveStatus": "DISABLED"}};
 				const tableName = "Cat";
 				new dynamoose.Model(tableName, {"id": String}, {"expires": 1000});
 				await utils.set_immediate_promise();
@@ -592,7 +600,49 @@ describe("Model", () => {
 				}]);
 			});
 
-			it("Should call updateTimeToLive with correct parameters for custom attribute", async () => {
+			it("Should not call updateTimeToLive with correct parameters if TTL is enabled", async () => {
+				describeTTL = {"TimeToLiveDescription": {"TimeToLiveStatus": "ENABLED"}};
+				const tableName = "Cat";
+				new dynamoose.Model(tableName, {"id": String}, {"expires": 1000});
+				await utils.set_immediate_promise();
+				expect(updateTTLParams).to.eql([]);
+			});
+
+			it("Should not call updateTimeToLive with correct parameters if TTL is enabling", async () => {
+				describeTTL = {"TimeToLiveDescription": {"TimeToLiveStatus": "ENABLING"}};
+				const tableName = "Cat";
+				new dynamoose.Model(tableName, {"id": String}, {"expires": 1000});
+				await utils.set_immediate_promise();
+				expect(updateTTLParams).to.eql([]);
+			});
+
+			it("Should call updateTimeToLive with correct parameters for custom attribute if TTL is disabling", async () => {
+				const startTime = Date.now();
+				let timesCalledDescribeTTL = 0;
+				describeTTLFunction = () => {
+					return {
+						"promise": async () => {
+							timesCalledDescribeTTL++;
+							return Promise.resolve(timesCalledDescribeTTL < 2 ? {"TimeToLiveDescription": {"TimeToLiveStatus": "DISABLING"}} : {"TimeToLiveDescription": {"TimeToLiveStatus": "DISABLED"}});
+						}
+					};
+				};
+				const tableName = "Cat";
+				const model = new dynamoose.Model(tableName, {"id": String}, {"expires": {"ttl": 1000, "attribute": "expires"}});
+				await model.Model.pendingTaskPromise();
+				expect(updateTTLParams).to.eql([{
+					"TableName": tableName,
+					"TimeToLiveSpecification": {
+						"Enabled": true,
+						"AttributeName": "expires"
+					}
+				}]);
+				expect(timesCalledDescribeTTL).to.eql(2);
+				expect(Date.now() - startTime).to.be.at.least(1000);
+			});
+
+			it("Should call updateTimeToLive with correct parameters for custom attribute if TTL is disabled", async () => {
+				describeTTL = {"TimeToLiveDescription": {"TimeToLiveStatus": "DISABLED"}};
 				const tableName = "Cat";
 				new dynamoose.Model(tableName, {"id": String}, {"expires": {"ttl": 1000, "attribute": "expires"}});
 				await utils.set_immediate_promise();