diff --git a/README.md b/README.md index d16d0c1..07d29e8 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ unless the `client` options is provided to override them. ☣️ Legacy reap behaviors use DynamoDB [`scan`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-dynamodb/classes/scancommand.html) functionality that can incur significant costs. Should instead enable [DynamoDB TTL](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/TTL.html) and select the `expires` field. TODO should we just remove it since we're already making a breaking change? +- `expiresIn` Optional set the number of seconds for DynamoDB TTL. Defaults to the cookie's maxAge. Must be a positive integer. ## Usage @@ -42,6 +43,8 @@ var options = { ], // Optional skip throw missing special keys in session, if set true skipThrowMissingSpecialKeys: true, + // Optional set the number of seconds for DynamoDB TTL + expiresIn: 3600, }; ``` diff --git a/index.d.ts b/index.d.ts index ef98734..34e644f 100644 --- a/index.d.ts +++ b/index.d.ts @@ -37,6 +37,13 @@ declare namespace ConnectDynamoDB { * Useful if the table already exists or if you want to skip existence checks in a serverless environment such as AWS Lambda. */ initialized?: boolean; + /** + * Set the number of seconds added to the item expiry time. + * + * Setting this to 0 will use the `maxAge` value from the session cookie. + * @default 0 + */ + expiresIn?: number; } interface DynamoDBStoreOptionsSpecialKey { diff --git a/lib/connect-dynamodb.js b/lib/connect-dynamodb.js index fce270c..2bc6982 100644 --- a/lib/connect-dynamodb.js +++ b/lib/connect-dynamodb.js @@ -68,6 +68,20 @@ module.exports = function (connect) { if (this.reapInterval > 0) { this._reap = setInterval(this.reap.bind(this), this.reapInterval); } + if (options.expiresIn) { + if (!Number.isInteger(options.expiresIn)) { + console.warn("`expiresIn` must be an integer. Reverting to default behaviour"); + this.expiresIn = 0; + } + else if (options.expiresIn < 0) { + console.warn("Negative `expiresIn` values are not supported. Reverting to default behaviour"); + this.expiresIn = 0; + } else { + this.expiresIn = options.expiresIn; + } + } else { + this.expiresIn = 0; + } } /* @@ -344,11 +358,17 @@ module.exports = function (connect) { */ DynamoDBStore.prototype.getExpiresValue = function (sess) { const now = Math.floor(Date.now() / 1000); - const expires = + + if (this.expiresIn != 0) { + return now + this.expiresIn; + } + else { + const expires = typeof sess.cookie.maxAge === "number" ? now + (sess.cookie.maxAge / 1000) : now + oneDayInSeconds; return expires; + } }; /** diff --git a/test/test.js b/test/test.js index 8f8c6fc..539ac01 100644 --- a/test/test.js +++ b/test/test.js @@ -5,6 +5,7 @@ const { DynamoDBClient, CreateTableCommand, DeleteTableCommand, + GetItemCommand, ScalarAttributeType, } = require("@aws-sdk/client-dynamodb"); const ConnectDynamoDB = require(__dirname + "/../lib/connect-dynamodb.js"); @@ -45,6 +46,16 @@ describe("DynamoDBStore", () => { const sessionId = Math.random().toString(); describe("Instantiation", () => { + let consoleWarnStub; + + beforeEach(() => { + consoleWarnStub = sinon.stub(console, 'warn'); + }) + + afterEach(() => { + consoleWarnStub.restore(); + }) + it("should be able to be created", () => { store.should.be.an.instanceOf(DynamoDBStore); }); @@ -68,6 +79,30 @@ describe("DynamoDBStore", () => { }) .finally(done); }); + + it("should store a valid expiresIn", () => { + const store = new DynamoDBStore({ + table: "sessions-test", + expiresIn: 3600 + }); + store.expiresIn.should.equal(3600); + }); + + it("should revert expiresIn to 0 when set to a non-integer", () => { + const store = new DynamoDBStore({ + table: "sessions-test", + expiresIn: 1.5 + }); + store.expiresIn.should.equal(0); + }); + + it("should revert expiresIn to 0 when set to a negative integer", () => { + const store = new DynamoDBStore({ + table: "sessions-test", + expiresIn: -10 + }); + store.expiresIn.should.equal(0); + }); }); describe("Initializing", () => { @@ -173,6 +208,16 @@ describe("DynamoDBStore", () => { }); describe("Setting", () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(1000000000); + }) + + afterEach(() => { + clock.restore(); + }) + it("should store data correctly", async () => { return new Promise((resolve, reject) => { const name = Math.random().toString(); @@ -231,6 +276,38 @@ describe("DynamoDBStore", () => { }); }); }); + + it("should set correct expiry when expiresIn option is set", async () => { + const storeWithExpiry = new DynamoDBStore({ + client: client, + table: tableName, + expiresIn: 1000 + }); + + await new Promise((resolve, reject) => { + storeWithExpiry.set( + sessionId, + { + cookie: { maxAge: 2000 }, + name: "test" + }, + (err) => { + if (err) return reject(err); + resolve(); + } + ); + }); + + const result = await client.send( + new GetItemCommand({ + TableName: tableName, + Key: { id: { S: "sess:" + sessionId } } + }) + ); + + const expiryValue = parseInt(result.Item.expires.N); + expiryValue.should.equal(1000000 + 1000); + }); }); describe("Getting", () => {