diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index d765e58..a7a6826 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,30 +1,30 @@ -name-template: 'v$RESOLVED_VERSION' -tag-template: 'v$RESOLVED_VERSION' +name-template: "v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" template: | $CHANGES -category-template: '#### $TITLE' -change-template: '* #$NUMBER - $TITLE (@$AUTHOR)' +category-template: "#### $TITLE" +change-template: "* #$NUMBER - $TITLE (@$AUTHOR)" categories: - - title: 'Breaking changes' - label: 'breaking' - - title: 'Enhancements' - label: 'enhancement' - - title: 'Bug fixes' - label: 'bug' - - title: 'Maintenance' - label: 'chore' + - title: "Breaking changes" + label: "breaking" + - title: "Enhancements" + label: "enhancement" + - title: "Bug fixes" + label: "bug" + - title: "Maintenance" + label: "chore" version-resolver: major: labels: - - 'breaking' + - "breaking" minor: labels: - - 'enhancement' + - "enhancement" patch: labels: - - 'bug' - - 'chore' + - "bug" + - "chore" exclude-labels: - - 'skip-changelog' + - "skip-changelog" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 694610b..37ee610 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node: ['12', '14', '16'] + node: ["12", "14", "16"] name: Node v${{ matrix.node }} steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7eb5bb7..4b66744 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,16 +2,16 @@ name: stale-issues-prs on: workflow_dispatch: schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v3 with: - stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' - stale-pr-message: 'This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' - close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days." + close-issue-message: "This issue was closed because it has been stalled for 5 days with no activity." days-before-stale: 30 days-before-close: 5 stale-issue-label: stale diff --git a/.prettierrc b/.prettierrc index f8dec5b..e052394 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,4 @@ { "tabWidth": 2, - "semi": false, - "singleQuote": true + "semi": false } diff --git a/index.js b/index.js index 7d2d1dd..fc8176e 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./lib/connect-redis') +module.exports = require("./lib/connect-redis") diff --git a/lib/connect-redis.js b/lib/connect-redis.js index a104755..8427215 100644 --- a/lib/connect-redis.js +++ b/lib/connect-redis.js @@ -10,16 +10,16 @@ module.exports = function (session) { // All callbacks should have a noop if none provided for compatibility // with the most Redis clients. const noop = () => {} - const TOMBSTONE = 'TOMBSTONE' + const TOMBSTONE = "TOMBSTONE" class RedisStore extends Store { constructor(options = {}) { super(options) if (!options.client) { - throw new Error('A client must be directly provided to the RedisStore') + throw new Error("A client must be directly provided to the RedisStore") } - this.prefix = options.prefix == null ? 'sess:' : options.prefix + this.prefix = options.prefix == null ? "sess:" : options.prefix this.scanCount = Number(options.scanCount) || 100 this.serializer = options.serializer || JSON this.client = options.client @@ -64,12 +64,12 @@ module.exports = function (session) { return cb(er) } args.push(value) - args.push('EX', this._getTTL(sess)) + args.push("EX", this._getTTL(sess)) let ttl = 1 if (!this.disableTTL) { ttl = this._getTTL(sess) - args.push('EX', ttl) + args.push("EX", ttl) } if (ttl > 0) { @@ -88,14 +88,14 @@ module.exports = function (session) { let key = this.prefix + sid this.client.expire(key, this._getTTL(sess), (err, ret) => { if (err) return cb(err) - if (ret !== 1) return cb(null, 'EXPIRED') - cb(null, 'OK') + if (ret !== 1) return cb(null, "EXPIRED") + cb(null, "OK") }) } destroy(sid, cb = noop) { let key = this.prefix + sid - this.client.set([key, TOMBSTONE, 'EX', 300], (err) => { + this.client.set([key, TOMBSTONE, "EX", 300], (err) => { cb(err, 1) }) } @@ -163,12 +163,12 @@ module.exports = function (session) { } _getAllKeys(cb = noop) { - let pattern = this.prefix + '*' + let pattern = this.prefix + "*" this._scanKeys({}, 0, pattern, this.scanCount, cb) } _scanKeys(keys = {}, cursor, pattern, count, cb = noop) { - let args = [cursor, 'match', pattern, 'count', count] + let args = [cursor, "match", pattern, "count", count] this.client.scan(args, (err, data) => { if (err) return cb(err) @@ -196,7 +196,7 @@ module.exports = function (session) { * @returns {boolean} */ function isObject(item) { - return item && typeof item === 'object' && !Array.isArray(item) + return item && typeof item === "object" && !Array.isArray(item) } /** diff --git a/license b/license index 6e21bd0..752b33b 100644 --- a/license +++ b/license @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2010-2020 TJ Holowaychuk +Copyright (c) 2010-2022 TJ Holowaychuk Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/migration-to-v4.md b/migration-to-v4.md deleted file mode 100644 index 8f02b57..0000000 --- a/migration-to-v4.md +++ /dev/null @@ -1,78 +0,0 @@ -## Migrating to version 4. - -Version 4 has some breaking changes from the previous versions. This documents those changes and how to migrate existing code. - -### No more bundled Redis client. - -The `redis` package is no longer bundled with `connect-redis`. This means you have to bring your own configured client. If you didn't depend on the built-in client, this requires no changes on your part and you can continue passing in the `client` as you did before. - -If you did use the bundled `redis` client, you now have to pass it in instead of having `connect-redis` create it for you. - -Older versions: - -```js -const session = require('express-session') -let RedisStore = require('connect-redis')(session) - -let store = new RedisStore({ - host: 'localhost', - port: 6123, - pass: 'my secret', - db: 1, - unref: true, - logErrors: true, -}) -``` - -Version 4 (make sure you also install the `redis` package): - -```js -const redis = require('redis') -const session = require('express-session') -let RedisStore = require('connect-redis')(session) - -let redisClient = redis.createClient({ - host: 'localhost', - port: 6123, - password: 'my secret', - db: 1, -}) -redisClient.unref() -redisClient.on('error', console.log) - -let store = new RedisStore({ client: redisClient }) -``` - -Given the bundled client does not exist, version 4 no longer has the following options: - -``` -host -port -pass -socket -unref -logErrors -``` - -### Changes to TTL management. - -If you didn't use the `ttl` and `disableTTL` options in the past, this section will not apply to you. - -There was a lot of confusion over how TTL behaves in Redis for sessions and how they expire. For this reason, we have greatly simplified the behavior and have opted to match the behavior found in other stores like `connect-mongo`. - -Now, Redis keys will always expire (have their TTL set) based on the `cookie.expires` date set by and managed by `express-session`. In the case where the cookie has _no expiration_, we will fall back to a custom `ttl`, which keeps the same default of one day. - -We added a `disableTouch` option which causes `touch` calls from `express-session` to do nothing. You may want to enable this under certain circumstances, see the [readme][1] for more details. - -Other notable changes: - -- Version 4 removed the ability to pass in a function as the `ttl`. -- Version 4 removed the `disableTTL` option. - -### Conformity to the `express-session` store API. - -We now support the complete `express-session` store API adding one missing method (`clear`) and modifying another method (`destroy`). - -If you used `destroy` to remove session IDs by passing _in an array_, you can no longer do that, instead you must use the Redis client directly or you can use the new `clear` method if you were removing all tracked keys. - -[1]: readme.md diff --git a/package.json b/package.json index eb5147e..66eef02 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "mockdate": "^2.0.5", "nyc": "^15.0.1", "prettier": "^2.0.5", - "redis": "^3.1.2", + "redis-v4": "npm:redis@4", + "redis-v3": "npm:redis@3", "redis-mock": "^0.56.3" }, "engines": { @@ -33,7 +34,7 @@ "scripts": { "test": "nyc tape \"test/*-test.js\"", "lint": "eslint index.js test lib", - "fmt": "prettier --write \"**/*.{js,md,json,*rc}\"", - "fmt-check": "prettier --check \"**/*.{js,md,json,*rc}\"" + "fmt": "prettier --write .", + "fmt-check": "prettier --check ." } } diff --git a/readme.md b/readme.md index 600c1dc..ec36dcb 100644 --- a/readme.md +++ b/readme.md @@ -2,36 +2,44 @@ **connect-redis** provides Redis session storage for Express. Requires Redis >= `2.0.0`. -**Migrating to V4?** See [this guide](migration-to-v4.md) on what's changed. - ## Installation npm: ```sh -npm install redis@v3 connect-redis express-session +npm install redis connect-redis express-session ``` Yarn: ```sh -yarn add redis@v3 connect-redis express-session +yarn add redis connect-redis express-session ``` ## API ```js -const { createClient } = require('redis') -const session = require('express-session') +const session = require("express-session") +let RedisStore = require("connect-redis")(session) + +// redis@v4 +const { createClient } = require("redis") +let redisClient = createClient({ legacyMode: true }) +redisClient.connect().catch(console.error) -let RedisStore = require('connect-redis')(session) +// redis@v3 +const { createClient } = require("redis") let redisClient = createClient() +// ioredis +const Redis = require("ioredis") +let redisClient = new Redis() + app.use( session({ store: new RedisStore({ client: redisClient }), saveUninitialized: false, - secret: 'keyboard cat', + secret: "keyboard cat", resave: false, }) ) @@ -49,13 +57,10 @@ An instance of [`redis`][1] or a `redis` compatible client. Known compatible and tested clients: -- [redis][1] (v3, **v4 currently is not supported**) +- [redis][1] (v3, v4 with `legacyMode: true`) - [ioredis](https://github.com/luin/ioredis) - [redis-mock](https://github.com/yeahoffline/redis-mock) for testing. -> Note: In order to use [redis][1] v4 for other things and connect-redis for sessions, an acceptable solution for the time being would be to add both v3 and v4 as dependencies and pass the v3 client in to connect-redis. -> [How to install multiple versions of same package?](https://stackoverflow.com/questions/26414587/how-to-install-multiple-versions-of-package-using-npm/56495651#56495651) - ##### prefix Key prefix in Redis (default: `sess:`). @@ -108,7 +113,7 @@ Value used for _count_ parameter in [Redis `SCAN` command](https://redis.io/comm #### How to log Redis errors? ```js -client.on('error', console.error) +client.on("error", console.error) ``` #### How do I handle lost connections to Redis? @@ -119,7 +124,7 @@ By default, the [`redis`][1] client will [auto-reconnect](https://github.com/mra app.use(session(/* setup session here */)) app.use(function (req, res, next) { if (!req.session) { - return next(new Error('oh no')) // handle error + return next(new Error("oh no")) // handle error } next() // otherwise continue }) diff --git a/test/connect-redis-test.js b/test/connect-redis-test.js index a21b934..8d60f0c 100644 --- a/test/connect-redis-test.js +++ b/test/connect-redis-test.js @@ -1,13 +1,14 @@ -const test = require('blue-tape') -const redisSrv = require('../test/redis-server') -const session = require('express-session') -const redis = require('redis') -const ioRedis = require('ioredis') -const redisMock = require('redis-mock') -const MockDate = require('mockdate') - -let RedisStore = require('../')(session) -MockDate.set('2000-11-22') +const test = require("blue-tape") +const redisSrv = require("../test/redis-server") +const session = require("express-session") +const redisV3 = require("redis-v3") +const redisV4 = require("redis-v4") +const ioRedis = require("ioredis") +const redisMock = require("redis-mock") +const MockDate = require("mockdate") + +let RedisStore = require("../")(session) +MockDate.set("2000-11-22") let p = (ctx, method) => @@ -19,167 +20,178 @@ let p = }) }) -test('setup', redisSrv.connect) +test("setup", redisSrv.connect) -test('defaults', async (t) => { - t.throws(() => new RedisStore(), 'client is required') +test("defaults", async (t) => { + t.throws(() => new RedisStore(), "client is required") - var client = redis.createClient(redisSrv.port, 'localhost') + var client = redisV3.createClient(redisSrv.port, "localhost") var store = new RedisStore({ client }) - t.equal(store.client, client, 'stores client') - t.equal(store.prefix, 'sess:', 'defaults to sess:') - t.equal(store.ttl, 86400, 'defaults to one day') - t.equal(store.scanCount, 100, 'defaults SCAN count to 100') - t.equal(store.serializer, JSON, 'defaults to JSON serialization') - t.equal(store.disableTouch, false, 'defaults to having `touch` enabled') - t.equal(store.disableTTL, false, 'defaults to having `ttl` enabled') + t.equal(store.client, client, "stores client") + t.equal(store.prefix, "sess:", "defaults to sess:") + t.equal(store.ttl, 86400, "defaults to one day") + t.equal(store.scanCount, 100, "defaults SCAN count to 100") + t.equal(store.serializer, JSON, "defaults to JSON serialization") + t.equal(store.disableTouch, false, "defaults to having `touch` enabled") + t.equal(store.disableTTL, false, "defaults to having `ttl` enabled") client.end(false) }) -test('node_redis', async (t) => { - var client = redis.createClient(redisSrv.port, 'localhost') +test("node_redis v3", async (t) => { + var client = redisV3.createClient(redisSrv.port, "localhost") var store = new RedisStore({ client }) await lifecycleTest(store, t) client.end(false) }) -test('ioredis', async (t) => { - var client = ioRedis.createClient(redisSrv.port, 'localhost') +test("node_redis v4", async (t) => { + var client = redisV4.createClient({ + url: `redis://localhost:${redisSrv.port}`, + legacyMode: true, + }) + await client.connect() + var store = new RedisStore({ client }) + await lifecycleTest(store, t) + await client.disconnect() +}) + +test("ioredis", async (t) => { + var client = ioRedis.createClient(redisSrv.port, "localhost") var store = new RedisStore({ client }) await lifecycleTest(store, t) client.disconnect() }) -test('redis-mock client', async (t) => { +test("redis-mock client", async (t) => { var client = redisMock.createClient() var store = new RedisStore({ client }) await lifecycleTest(store, t) }) -test('teardown', redisSrv.disconnect) +test("teardown", redisSrv.disconnect) async function lifecycleTest(store, t) { - await p(store, 'set')('123', { foo: 'bar3' }) - let res = await p(store, 'get')('123') - t.same(res, { foo: 'bar3', lastModified: 974851200000 }, 'get value 1') - await p(store, 'set')('123', { - foo: 'bar3', - luke: 'skywalker', - obi: 'wan', + await p(store, "set")("123", { foo: "bar3" }) + let res = await p(store, "get")("123") + t.same(res, { foo: "bar3", lastModified: 974851200000 }, "get value 1") + await p(store, "set")("123", { + foo: "bar3", + luke: "skywalker", + obi: "wan", lastModified: 974851000000, }) - await p(store, 'set')('123', { - luke: 'skywalker', + await p(store, "set")("123", { + luke: "skywalker", lastModified: 974851000000, }) - res = await p(store, 'get')('123') + res = await p(store, "get")("123") t.same( res, - { foo: 'bar3', luke: 'skywalker', obi: 'wan', lastModified: 974851200000 }, - 'get merged value' + { foo: "bar3", luke: "skywalker", obi: "wan", lastModified: 974851200000 }, + "get merged value" ) - res = await p(store, 'clear')() - t.ok(res >= 1, 'cleared key') + res = await p(store, "clear")() + t.ok(res >= 1, "cleared key") - res = await p(store, 'set')('123', { foo: 'bar' }) - t.equal(res, 'OK', 'set value') + res = await p(store, "set")("123", { foo: "bar" }) + t.equal(res, "OK", "set value") - res = await p(store, 'get')('123') - t.same(res, { foo: 'bar', lastModified: 974851200000 }, 'get value') + res = await p(store, "get")("123") + t.same(res, { foo: "bar", lastModified: 974851200000 }, "get value") - res = await p(store.client, 'ttl')('sess:123') - t.ok(res >= 86399, 'check one day ttl') + res = await p(store.client, "ttl")("sess:123") + t.ok(res >= 86399, "check one day ttl") let ttl = 60 let expires = new Date(Date.now() + ttl * 1000).toISOString() - res = await p(store, 'set')('456', { cookie: { expires } }) - t.equal(res, 'OK', 'set cookie expires') + res = await p(store, "set")("456", { cookie: { expires } }) + t.equal(res, "OK", "set cookie expires") - res = await p(store.client, 'ttl')('sess:456') - t.ok(res <= 60, 'check expires ttl') + res = await p(store.client, "ttl")("sess:456") + t.ok(res <= 60, "check expires ttl") ttl = 90 let newExpires = new Date(Date.now() + ttl * 1000).toISOString() // note: cookie.expires will not be updated on redis (see https://github.com/tj/connect-redis/pull/285) - res = await p(store, 'touch')('456', { cookie: { expires: newExpires } }) - t.equal(res, 'OK', 'set cookie expires touch') + res = await p(store, "touch")("456", { cookie: { expires: newExpires } }) + t.equal(res, "OK", "set cookie expires touch") - res = await p(store.client, 'ttl')('sess:456') - t.ok(res > 60, 'check expires ttl touch') + res = await p(store.client, "ttl")("sess:456") + t.ok(res > 60, "check expires ttl touch") - res = await p(store, 'length')() - t.equal(res, 2, 'stored two keys length') + res = await p(store, "length")() + t.equal(res, 2, "stored two keys length") - res = await p(store, 'ids')() + res = await p(store, "ids")() res.sort() - t.same(res, ['123', '456'], 'stored two keys ids') + t.same(res, ["123", "456"], "stored two keys ids") - res = await p(store, 'all')() + res = await p(store, "all")() res.sort((a, b) => (a.id > b.id ? 1 : -1)) t.same( res, [ - { id: '123', foo: 'bar', lastModified: 974851200000 }, - { id: '456', cookie: { expires }, lastModified: 974851200000 }, + { id: "123", foo: "bar", lastModified: 974851200000 }, + { id: "456", cookie: { expires }, lastModified: 974851200000 }, ], - 'stored two keys data' + "stored two keys data" ) - res = await p(store, 'destroy')('456') - t.equal(res, 1, 'destroyed one') + res = await p(store, "destroy")("456") + t.equal(res, 1, "destroyed one") - res = await p(store, 'get')('456') - t.equal(res, undefined, 'tombstoned one') + res = await p(store, "get")("456") + t.equal(res, undefined, "tombstoned one") - res = await p(store, 'set')('456', { a: 'new hope' }) - t.equal(res, undefined, 'tombstoned set') + res = await p(store, "set")("456", { a: "new hope" }) + t.equal(res, undefined, "tombstoned set") - res = await p(store, 'get')('456') - t.equal(res, undefined, 'tombstoned two') + res = await p(store, "get")("456") + t.equal(res, undefined, "tombstoned two") - res = await p(store, 'length')() - t.equal(res, 1, 'one key remains') + res = await p(store, "length")() + t.equal(res, 1, "one key remains") - res = await p(store, 'clear')() - t.equal(res, 2, 'cleared remaining key') + res = await p(store, "clear")() + t.equal(res, 2, "cleared remaining key") - res = await p(store, 'length')() - t.equal(res, 0, 'no key remains') + res = await p(store, "length")() + t.equal(res, 0, "no key remains") let count = 1000 await load(store, count) - res = await p(store, 'length')() - t.equal(res, count, 'bulk count') + res = await p(store, "length")() + t.equal(res, count, "bulk count") - res = await p(store, 'clear')() - t.equal(res, count, 'bulk clear') + res = await p(store, "clear")() + t.equal(res, count, "bulk clear") expires = new Date(Date.now() + ttl * 1000).toISOString() // expires in the future - res = await p(store, 'set')('789', { cookie: { expires } }) - t.equal(res, 'OK', 'set value') + res = await p(store, "set")("789", { cookie: { expires } }) + t.equal(res, "OK", "set value") - res = await p(store, 'length')() - t.equal(res, 1, 'one key exists (session 789)') + res = await p(store, "length")() + t.equal(res, 1, "one key exists (session 789)") expires = new Date(Date.now() - ttl * 1000).toISOString() // expires in the past - res = await p(store, 'set')('789', { cookie: { expires } }) - t.equal(res, 1, 'returns 1 because destroy was invoked') + res = await p(store, "set")("789", { cookie: { expires } }) + t.equal(res, 1, "returns 1 because destroy was invoked") - res = await p(store, 'length')() - t.equal(res, 0, 'no key remains and that includes session 789') + res = await p(store, "length")() + t.equal(res, 0, "no key remains and that includes session 789") } function load(store, count) { return new Promise((resolve, reject) => { let set = (sid) => { store.set( - 's' + sid, + "s" + sid, { cookie: { expires: new Date(Date.now() + 1000) }, - data: 'some data', + data: "some data", }, (err) => { if (err) { diff --git a/test/redis-server.js b/test/redis-server.js index 3b49cd6..6187309 100644 --- a/test/redis-server.js +++ b/test/redis-server.js @@ -1,21 +1,21 @@ -const spawn = require('child_process').spawn +const spawn = require("child_process").spawn const port = (exports.port = 18543) let redisSrv exports.connect = () => new Promise((resolve, reject) => { - redisSrv = spawn('redis-server', ['--port', port, '--loglevel', 'notice'], { - stdio: 'inherit', + redisSrv = spawn("redis-server", ["--port", port, "--loglevel", "notice"], { + stdio: "inherit", }) - redisSrv.on('error', function (err) { - reject(new Error('Error caught spawning the server:' + err.message)) + redisSrv.on("error", function (err) { + reject(new Error("Error caught spawning the server:" + err.message)) }) setTimeout(resolve, 1500) }) exports.disconnect = function () { - redisSrv.kill('SIGKILL') + redisSrv.kill("SIGKILL") return Promise.resolve() }