connect-mongo
Version:
MongoDB session store for Express and Connect
377 lines (328 loc) • 15.6 kB
JavaScript
'use strict';
/* eslint indent: [error, 4] */
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
var Promise = require('bluebird');
var MongoClient = require('mongodb');
function defaultSerializeFunction(session) {
// Copy each property of the session to a new object
var obj = {};
var prop = void 0;
for (prop in session) {
if (prop === 'cookie') {
// Convert the cookie instance to an object, if possible
// This gets rid of the duplicate object under session.cookie.data property
obj.cookie = session.cookie.toJSON ? session.cookie.toJSON() : session.cookie;
} else {
obj[prop] = session[prop];
}
}
return obj;
}
function computeTransformFunctions(options, defaultStringify) {
if (options.serialize || options.unserialize) {
return {
serialize: options.serialize || defaultSerializeFunction,
unserialize: options.unserialize || function (x) {
return x;
}
};
}
if (options.stringify === false || defaultStringify === false) {
return {
serialize: defaultSerializeFunction,
unserialize: function (x) {
return x;
}
};
}
if (options.stringify === true || defaultStringify === true) {
return {
serialize: JSON.stringify,
unserialize: JSON.parse
};
}
}
module.exports = function connectMongo(connect) {
var Store = connect.Store || connect.session.Store;
var MemoryStore = connect.MemoryStore || connect.session.MemoryStore;
var MongoStore = function (_Store) {
_inherits(MongoStore, _Store);
function MongoStore(options) {
_classCallCheck(this, MongoStore);
options = options || {};
/* Fallback */
if (options.fallbackMemory && MemoryStore) {
var _ret;
return _ret = new MemoryStore(), _possibleConstructorReturn(_this, _ret);
}
/* Options */
var _this = _possibleConstructorReturn(this, Object.getPrototypeOf(MongoStore).call(this, options));
_this.ttl = options.ttl || 1209600; // 14 days
_this.collectionName = options.collection || 'sessions';
_this.autoRemove = options.autoRemove || 'native';
_this.autoRemoveInterval = options.autoRemoveInterval || 10;
_this.transformFunctions = computeTransformFunctions(options, true);
_this.options = options;
_this.changeState('init');
var newConnectionCallback = function (err, db) {
if (err) {
_this.connectionFailed(err);
} else {
_this.handleNewConnectionAsync(db);
}
};
if (options.url) {
// New native connection using url + mongoOptions
MongoClient.connect(options.url, options.mongoOptions || {}, newConnectionCallback);
} else if (options.mongooseConnection) {
// Re-use existing or upcoming mongoose connection
if (options.mongooseConnection.readyState === 1) {
_this.handleNewConnectionAsync(options.mongooseConnection.db);
} else {
options.mongooseConnection.once('open', function () {
return _this.handleNewConnectionAsync(options.mongooseConnection.db);
});
}
} else if (options.db && options.db.listCollections) {
// Re-use existing or upcoming native connection
if (options.db.openCalled || options.db.openCalled === undefined) {
// openCalled is undefined in mongodb@2.x
_this.handleNewConnectionAsync(options.db);
} else {
options.db.open(newConnectionCallback);
}
} else if (options.dbPromise) {
options.dbPromise.then(function (db) {
return _this.handleNewConnectionAsync(db);
}).catch(function (err) {
return _this.connectionFailed(err);
});
} else {
throw new Error('Connection strategy not found');
}
_this.changeState('connecting');
return _this;
}
_createClass(MongoStore, [{
key: 'connectionFailed',
value: function connectionFailed(err) {
this.changeState('disconnected');
throw err;
}
}, {
key: 'handleNewConnectionAsync',
value: function handleNewConnectionAsync(db) {
var _this2 = this;
this.db = db;
return this.setCollection(db.collection(this.collectionName)).setAutoRemoveAsync().then(function () {
return _this2.changeState('connected');
});
}
}, {
key: 'setAutoRemoveAsync',
value: function setAutoRemoveAsync() {
var _this3 = this;
var removeQuery = { expires: { $lt: new Date() } };
switch (this.autoRemove) {
case 'native':
return this.collection.ensureIndexAsync({ expires: 1 }, { expireAfterSeconds: 0 });
case 'interval':
this.timer = setInterval(function () {
return _this3.collection.remove(removeQuery, { w: 0 });
}, this.autoRemoveInterval * 1000 * 60);
this.timer.unref();
return Promise.resolve();
default:
return Promise.resolve();
}
}
}, {
key: 'changeState',
value: function changeState(newState) {
if (newState !== this.state) {
this.state = newState;
this.emit(newState);
}
}
}, {
key: 'setCollection',
value: function setCollection(collection) {
if (this.timer) {
clearInterval(this.timer);
}
this.collectionReadyPromise = undefined;
this.collection = collection;
// Promisify used collection methods
['count', 'findOne', 'remove', 'drop', 'update', 'ensureIndex'].forEach(function (method) {
collection[method + 'Async'] = Promise.promisify(collection[method], collection);
});
return this;
}
}, {
key: 'collectionReady',
value: function collectionReady() {
var _this4 = this;
var promise = this.collectionReadyPromise;
if (!promise) {
promise = new Promise(function (resolve, reject) {
switch (_this4.state) {
case 'connected':
resolve(_this4.collection);
break;
case 'connecting':
_this4.once('connected', function () {
return resolve(_this4.collection);
});
break;
case 'disconnected':
reject(new Error('Not connected'));
break;
}
});
this.collectionReadyPromise = promise;
}
return promise;
}
}, {
key: 'computeStorageId',
value: function computeStorageId(sessionId) {
if (this.options.transformId && typeof this.options.transformId === 'function') {
return this.options.transformId(sessionId);
} else {
return sessionId;
}
}
/* Public API */
}, {
key: 'get',
value: function get(sid, callback) {
var _this5 = this;
return this.collectionReady().then(function (collection) {
return collection.findOneAsync({
_id: _this5.computeStorageId(sid),
$or: [{ expires: { $exists: false } }, { expires: { $gt: new Date() } }]
});
}).then(function (session) {
if (session) {
var s = _this5.transformFunctions.unserialize(session.session);
if (_this5.options.touchAfter > 0 && session.lastModified) {
s.lastModified = session.lastModified;
}
_this5.emit('touch', sid);
return s;
}
}).nodeify(callback);
}
}, {
key: 'set',
value: function set(sid, session, callback) {
var _this6 = this;
// removing the lastModified prop from the session object before update
if (this.options.touchAfter > 0 && session && session.lastModified) {
delete session.lastModified;
}
var s = void 0;
try {
s = { _id: this.computeStorageId(sid), session: this.transformFunctions.serialize(session) };
} catch (err) {
return callback(err);
}
if (session && session.cookie && session.cookie.expires) {
s.expires = new Date(session.cookie.expires);
} else {
// If there's no expiration date specified, it is
// browser-session cookie or there is no cookie at all,
// as per the connect docs.
//
// So we set the expiration to two-weeks from now
// - as is common practice in the industry (e.g Django) -
// or the default specified in the options.
s.expires = new Date(Date.now() + this.ttl * 1000);
}
if (this.options.touchAfter > 0) {
s.lastModified = new Date();
}
return this.collectionReady().then(function (collection) {
return collection.updateAsync({ _id: _this6.computeStorageId(sid) }, s, { upsert: true });
}).then(function (rawResponse) {
if (rawResponse.result.upserted) {
_this6.emit('create', sid);
} else {
_this6.emit('update', sid);
}
_this6.emit('set', sid);
}).nodeify(callback);
}
}, {
key: 'touch',
value: function touch(sid, session, callback) {
var _this7 = this;
var updateFields = {},
touchAfter = this.options.touchAfter * 1000,
lastModified = session.lastModified ? session.lastModified.getTime() : 0,
currentDate = new Date();
// if the given options has a touchAfter property, check if the
// current timestamp - lastModified timestamp is bigger than
// the specified, if it's not, don't touch the session
if (touchAfter > 0 && lastModified > 0) {
var timeElapsed = currentDate.getTime() - session.lastModified;
if (timeElapsed < touchAfter) {
return callback();
} else {
updateFields.lastModified = currentDate;
}
}
if (session && session.cookie && session.cookie.expires) {
updateFields.expires = new Date(session.cookie.expires);
} else {
updateFields.expires = new Date(Date.now() + this.ttl * 1000);
}
return this.collectionReady().then(function (collection) {
return collection.updateAsync({ _id: _this7.computeStorageId(sid) }, { $set: updateFields });
}).then(function (result) {
if (result.nModified === 0) {
throw new Error('Unable to find the session to touch');
} else {
_this7.emit('touch', sid);
}
}).nodeify(callback);
}
}, {
key: 'destroy',
value: function destroy(sid, callback) {
var _this8 = this;
return this.collectionReady().then(function (collection) {
return collection.removeAsync({ _id: _this8.computeStorageId(sid) });
}).then(function () {
return _this8.emit('destroy', sid);
}).nodeify(callback);
}
}, {
key: 'length',
value: function length(callback) {
return this.collectionReady().then(function (collection) {
return collection.countAsync({});
}).nodeify(callback);
}
}, {
key: 'clear',
value: function clear(callback) {
return this.collectionReady().then(function (collection) {
return collection.dropAsync();
}).nodeify(callback);
}
}, {
key: 'close',
value: function close() {
if (this.db) {
this.db.close();
}
}
}]);
return MongoStore;
}(Store);
return MongoStore;
};