217 lines
6.1 KiB
JavaScript
217 lines
6.1 KiB
JavaScript
/**
|
|
* Sequelize based session store.
|
|
*
|
|
* Author: Michael Weibel <michael.weibel@gmail.com>
|
|
* License: MIT
|
|
*/
|
|
|
|
const Op = require('sequelize').Op || {}
|
|
const defaultModel = require('./model')
|
|
const debug = require('debug')('connect:session-sequelize')
|
|
const defaultOptions = {
|
|
checkExpirationInterval: 15 * 60 * 1000, // The interval at which to cleanup expired sessions.
|
|
expiration: 24 * 60 * 60 * 1000, // The maximum age (in milliseconds) of a valid session. Used when cookie.expires is not set.
|
|
disableTouch: false, // When true, we will not update the db in the touch function call. Useful when you want more control over db writes.
|
|
modelKey: 'Session',
|
|
tableName: 'Sessions'
|
|
}
|
|
|
|
function promisify (promise, fn) {
|
|
if (typeof fn === 'function') {
|
|
promise = promise.then(obj => {
|
|
fn(null, obj)
|
|
}).catch(err => {
|
|
if (!err) {
|
|
const error = new Error(err + '')
|
|
error.cause = err
|
|
err = error
|
|
}
|
|
|
|
fn(err)
|
|
})
|
|
}
|
|
return promise
|
|
}
|
|
|
|
class SequelizeStoreException extends Error {
|
|
constructor (message) {
|
|
super(message)
|
|
this.name = 'SequelizeStoreException'
|
|
}
|
|
}
|
|
|
|
module.exports = function SequelizeSessionInit (Store) {
|
|
class SequelizeStore extends Store {
|
|
constructor (options) {
|
|
super(options)
|
|
this.options = { ...defaultOptions, ...(options || {}) }
|
|
|
|
if (!this.options.db) {
|
|
throw new SequelizeStoreException('Database connection is required')
|
|
}
|
|
|
|
this.startExpiringSessions()
|
|
|
|
// Check if specific table should be used for DB connection
|
|
if (this.options.table) {
|
|
debug('Using table: %s for sessions', this.options.table)
|
|
// Get Specifed Table from Sequelize Object
|
|
this.sessionModel =
|
|
this.options.db[this.options.table] || this.options.db.models[this.options.table]
|
|
} else {
|
|
// No Table specified, default to ./model
|
|
debug('No table specified, using default table.')
|
|
this.sessionModel = this.options.db.define(this.options.modelKey, defaultModel, {
|
|
tableName: this.options.tableName || this.options.modelKey
|
|
})
|
|
}
|
|
}
|
|
|
|
sync (options) {
|
|
return this.sessionModel.sync(options)
|
|
}
|
|
|
|
get (sid, fn) {
|
|
debug('SELECT "%s"', sid)
|
|
return promisify(
|
|
this.sessionModel
|
|
.findOne({ where: { sid: sid } })
|
|
.then(function (session) {
|
|
if (!session) {
|
|
debug('Did not find session %s', sid)
|
|
return null
|
|
}
|
|
debug('FOUND %s with data %s', session.sid, session.data)
|
|
|
|
return JSON.parse(session.data)
|
|
}),
|
|
fn
|
|
)
|
|
}
|
|
|
|
set (sid, data, fn) {
|
|
debug('INSERT "%s"', sid)
|
|
const stringData = JSON.stringify(data)
|
|
const expires = this.expiration(data)
|
|
|
|
let defaults = { data: stringData, expires: expires }
|
|
if (this.options.extendDefaultFields) {
|
|
defaults = this.options.extendDefaultFields(defaults, data)
|
|
}
|
|
|
|
return promisify(
|
|
this.sessionModel
|
|
.findCreateFind({
|
|
where: { sid: sid },
|
|
defaults: defaults,
|
|
raw: false,
|
|
useMaster: true
|
|
})
|
|
.then(function sessionCreated ([session]) {
|
|
let changed = false
|
|
Object.keys(defaults).forEach(function (key) {
|
|
if (key === 'data') {
|
|
return
|
|
}
|
|
|
|
if (session.dataValues[key] !== defaults[key]) {
|
|
session[key] = defaults[key]
|
|
changed = true
|
|
}
|
|
})
|
|
if (session.data !== stringData) {
|
|
session.data = JSON.stringify(data)
|
|
changed = true
|
|
}
|
|
if (changed) {
|
|
session.expires = expires
|
|
return session.save().then(() => { return session })
|
|
}
|
|
return session
|
|
}),
|
|
fn
|
|
)
|
|
}
|
|
|
|
touch (sid, data, fn) {
|
|
debug('TOUCH "%s"', sid)
|
|
|
|
if (this.options.disableTouch) {
|
|
debug('TOUCH skipped due to disableTouch "%s"', sid)
|
|
return fn()
|
|
}
|
|
|
|
const expires = this.expiration(data)
|
|
|
|
return promisify(
|
|
this.sessionModel
|
|
.update({ expires: expires }, { where: { sid: sid } })
|
|
.then(function (rows) {
|
|
return rows
|
|
}),
|
|
fn
|
|
)
|
|
}
|
|
|
|
destroy (sid, fn) {
|
|
debug('DESTROYING %s', sid)
|
|
return promisify(
|
|
this.sessionModel
|
|
.findOne({ where: { sid: sid }, raw: false })
|
|
.then(function foundSession (session) {
|
|
// If the session wasn't found, then consider it destroyed already.
|
|
if (session === null) {
|
|
debug('Session not found, assuming destroyed %s', sid)
|
|
return null
|
|
}
|
|
return session.destroy()
|
|
}),
|
|
fn
|
|
)
|
|
}
|
|
|
|
length (fn) {
|
|
return promisify(this.sessionModel.count(), fn)
|
|
}
|
|
|
|
clearExpiredSessions (fn) {
|
|
debug('CLEARING EXPIRED SESSIONS')
|
|
return promisify(
|
|
this.sessionModel
|
|
.destroy({ where: { expires: { [Op.lt || 'lt']: new Date() } } }).catch((error) => debug(`Ignoring error at clearExpiredSessions: ${error}`)),
|
|
fn
|
|
)
|
|
}
|
|
|
|
startExpiringSessions () {
|
|
// Don't allow multiple intervals to run at once.
|
|
this.stopExpiringSessions()
|
|
if (this.options.checkExpirationInterval > 0) {
|
|
this._expirationInterval = setInterval(
|
|
this.clearExpiredSessions.bind(this),
|
|
this.options.checkExpirationInterval
|
|
)
|
|
// allow to terminate the node process even if this interval is still running
|
|
this._expirationInterval.unref()
|
|
}
|
|
}
|
|
|
|
stopExpiringSessions () {
|
|
if (this._expirationInterval) {
|
|
clearInterval(this._expirationInterval)
|
|
// added as a sanity check for testing
|
|
this._expirationInterval = null
|
|
}
|
|
}
|
|
|
|
expiration (data) {
|
|
if (data.cookie && data.cookie.expires && !isNaN(data.cookie.expires)) {
|
|
return data.cookie.expires
|
|
}
|
|
return new Date(Date.now() + this.options.expiration)
|
|
}
|
|
}
|
|
|
|
return SequelizeStore
|
|
}
|