Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,40 @@ Function used to generate new session IDs. Defaults to [`uid(24)`](https://githu

Allows to access or modify the session data.

#### request.sessionStore

Access the store instance from `options.store`. It proxies the `get`, `set`, `destroy` method on the store instance, such that it returns a promise when the callback argument is not passed. Other methods and properties on the store instance can be accessed transparently.

```ts
// Using callbacks
request.sessionStore.get(sessionId, (err, session) => {})
request.sessionStore.set(sessionId, session, (err) => {})
request.sessionStore.destroy(sessionId, (err) => {})
// Using promises
request.sessionStore.get(sessionId).then((session) => {}).catch((err) => {})
request.sessionStore.set(sessionId, session).catch((err) => {})
request.sessionStore.destroy(sessionId).catch((err) => {})
// Using async/await
const session = await request.sessionStore.get(sessionId)
await request.sessionStore.set(sessionId, session)
await request.sessionStore.destroy(sessionId)
// Some store implementations has extra methods, they can be used normally
request.sessionStore.touch(sessionId, session, (err) => {})
```

#### request.destroySession(callback)

Allows to destroy the session in the store
Allows to destroy the session in the store.
If callback is not passed, request.destroySession returns a promise that will reject if an error has occured.

```ts
// Using callbacks
request.destroySession((err) => {})
// Using promises
request.destroySession.catch((err) => {})
// Using async/await
await request.destroySession()
```

#### Session#touch()

Expand Down
18 changes: 18 additions & 0 deletions lib/destroy-session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use strict'

module.exports = function destroySession (done) {
Comment thread
climba03003 marked this conversation as resolved.
const request = this
if (!done) {
return new Promise((resolve, reject) => {
request.sessionStore.destroy(request.session.sessionId, (err) => {
request.session = null
if (err) reject(err)
else resolve()
})
})
}
request.sessionStore.destroy(request.session.sessionId, (err) => {
request.session = null
done(err)
})
}
13 changes: 4 additions & 9 deletions lib/fastifySession.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

const uid = require('uid-safe').sync
const fastifyPlugin = require('fastify-plugin')
const destroySession = require('./destroy-session')
const createProxyStore = require('./proxy-store')
const Store = require('./store')
const Session = require('./session')
const metadata = require('./metadata')
Expand All @@ -14,11 +16,12 @@ function session (fastify, options, next) {
}

options = ensureDefaults(options)
const proxyStore = createProxyStore(options.store)

fastify.decorate('decryptSession', (sessionId, request, callback) => {
decryptSession(sessionId, options, request, callback)
})
fastify.decorateRequest('sessionStore', { getter: () => options.store })
fastify.decorateRequest('sessionStore', { getter: () => proxyStore })
fastify.decorateRequest('session', null)
fastify.decorateRequest('destroySession', destroySession)
fastify.addHook('onRequest', onRequest(options))
Expand Down Expand Up @@ -138,14 +141,6 @@ function newSession (secret, request, cookieOpts, idGenerator, done) {
done()
}

function destroySession (done) {
const request = this
request.sessionStore.destroy(request.session.sessionId, (err) => {
request.session = null
done(err)
})
}

function checkOptions (options) {
if (!options.secret) {
return new Error('the secret option is required!')
Expand Down
50 changes: 50 additions & 0 deletions lib/proxy-store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict'

module.exports = function createProxyStore (store) {
const promisifyStore = {
get (sessionId, callback) {
if (callback) {
return store.get(sessionId, callback)
}
return new Promise((resolve, reject) => {
store.get(sessionId, (err, session) => {
if (err) reject(err)
else resolve(session)
})
})
},

set (sessionId, session, callback) {
if (callback) {
return store.set(sessionId, session, callback)
}
return new Promise((resolve, reject) => {
store.set(sessionId, session, (err) => {
if (err) reject(err)
else resolve()
})
})
},

destroy (sessionId, callback) {
if (callback) {
return store.destroy(sessionId, callback)
}
return new Promise((resolve, reject) => {
store.destroy(sessionId, (err) => {
if (err) reject(err)
else resolve()
})
})
}
}

return new Proxy(store, {
get (target, property) {
if (property in promisifyStore) {
return promisifyStore[property]
}
return target[property]
}
})
}
88 changes: 88 additions & 0 deletions test/destroy-session.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
'use strict'

const test = require('ava')
const { testServer, request, DEFAULT_OPTIONS } = require('./util')

class ErrorStore {
get (sessionId, callback) {
throw new Error('Not implemented')
}

set (sessionId, session, callback) {
throw new Error('Not implemented')
}

destroy (sessionId, callback) {
callback(new Error('ErrorStore#destroy'))
}
}

test('should successfully destroy the session with callback api', async (t) => {
t.plan(3)
const port = await testServer((request, reply) => {
request.destroySession((err) => {
t.falsy(err)
t.is(request.session, null)
reply.send(200)
})
}, DEFAULT_OPTIONS)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})

test('should fail to destroy the session with callback api', async (t) => {
t.plan(3)
const options = {
secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk',
store: new ErrorStore()
}
const port = await testServer((request, reply) => {
request.destroySession((err) => {
t.true(err instanceof Error)
t.is(err.message, 'ErrorStore#destroy')
reply.send(200)
})
}, options)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})

test('should successfully destroy the session with promise api', async (t) => {
t.plan(2)
const port = await testServer(async (request, reply) => {
try {
await request.destroySession()
t.is(request.session, null)
reply.send(200)
} catch (err) {
t.fail(err)
}
}, DEFAULT_OPTIONS)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})

test('should fail to destroy the session with promise api', async (t) => {
t.plan(2)
const options = {
secret: 'cNaoPYAwF60HZJzkcNaoPYAwF60HZJzk',
store: new ErrorStore()
}
const port = await testServer(async (request, reply) => {
await t.throwsAsync(async () => await request.destroySession(), {
instanceOf: Error,
message: 'ErrorStore#destroy'
})
reply.send(200)
}, options)

const { response } = await request(`http://localhost:${port}`)

t.is(response.statusCode, 200)
})
159 changes: 159 additions & 0 deletions test/proxy-store.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
'use strict'

const test = require('ava')
const MemoryStore = require('../lib/store')
const createProxyStore = require('../lib/proxy-store')

test('should work with callbacks for `get`, `set`, `destroy', async (t) => {
t.plan(6)
const store = new MemoryStore()
const proxyStore = createProxyStore(store)
await new Promise(resolve => {
proxyStore.set('one', { foo: 1 }, (err) => {
t.falsy(err)
proxyStore.get('one', (err, data) => {
t.falsy(err)
t.deepEqual(data, { foo: 1 })
proxyStore.destroy('one', (err) => {
t.falsy(err)
proxyStore.get('one', (err, data) => {
t.falsy(err)
t.falsy(data)
resolve()
})
})
})
})
})
})

test('should work with promise api for `get`, `set`, `destroy`', async (t) => {
t.plan(2)
const store = new MemoryStore()
const proxyStore = createProxyStore(store)
await proxyStore.set('one', { foo: 1 })
const data = await proxyStore.get('one')
t.deepEqual(data, { foo: 1 })
await proxyStore.destroy('one')
const empty = await proxyStore.get('one')
t.falsy(empty)
})

test('should reject promise if callback argument is not used', async (t) => {
t.plan(3)
class ErrorStore {
get (sessionId, callback) {
callback(new Error('ErrorStore#get'), null)
}

set (sessionId, session, callback) {
callback(new Error('ErrorStore#set'))
}

destroy (sessionId, callback) {
callback(new Error('ErrorStore#destroy'))
}
}
const store = new ErrorStore()
const proxyStore = createProxyStore(store)
try {
await proxyStore.get('id')
t.fail('expect promise rejection with no callback argument')
} catch (err) {
t.true(err instanceof Error)
}
try {
await proxyStore.set('id', { foo: 1 })
t.fail('expect promise rejection with no callback argument')
} catch (err) {
t.true(err instanceof Error)
}
try {
await proxyStore.destroy('id')
t.fail('expect promise rejection with no callback argument')
} catch (err) {
t.true(err instanceof Error)
}
})

test('should not reject promise if callback is passed', async (t) => {
t.plan(4)
class ErrorStore {
get (sessionId, callback) {
callback(new Error('ErrorStore#get'), null)
}

set (sessionId, session, callback) {
callback(new Error('ErrorStore#set'))
}

destroy (sessionId, callback) {
callback(new Error('ErrorStore#destroy'))
}
}
const store = new ErrorStore()
const proxyStore = createProxyStore(store)
try {
await proxyStore.get('id', (err, data) => {
t.true(err instanceof Error)
t.falsy(data)
})
} catch (err) {
t.fail('unexpected promise rejection with callback argument passed')
}
try {
await proxyStore.set('id', { foo: 1 }, (err) => {
t.true(err instanceof Error)
})
} catch (err) {
t.fail('unexpected promise rejection with callback argument passed')
}
try {
await proxyStore.destroy('id', (err) => {
t.true(err instanceof Error)
})
} catch (err) {
t.fail('unexpected promise rejection with callback argument passed')
}
})

test('should not mutate store instance by monkey-patch methods', (t) => {
t.plan(3)
class Store {
constructor () {
this.originalMethods = { get: this.get, set: this.set, destroy: this.destroy }
}

get () {}
set () {}
destroy () {}
}
const store = new Store()
const originalMethods = store.originalMethods
createProxyStore(store)
t.is(store.get, originalMethods.get)
t.is(store.set, originalMethods.set)
t.is(store.destroy, originalMethods.destroy)
})

test('should allow referencing store methods other than `get`, `set`, `destroy`', (t) => {
t.plan(4)
class Store {
get () {}
set () {}
destroy () {}
foo () {
return 1
}

bar () {
return this.foo() + 1
}
}
const store = new Store()
const proxyStore = createProxyStore(store)
t.is(proxyStore.foo(), 1)
t.is(proxyStore.bar(), 2)
t.is(proxyStore.foo, store.foo)
t.is(proxyStore.bar, store.bar)
})
Loading