/** * Sequelize based session store. * * Author: Michael Weibel * 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 }