connect-oriento
Version:
Orient-DB session store for Express and Connect using the Oriento Node driver
310 lines (281 loc) • 11 kB
JavaScript
;
var async = require("async"),
crypto = require("crypto"),
util = require("util"),
oriento = require("oriento");
var Session = require("./session"),
defaults = require("./defaults");
var INIT = -2,
DISCONNECTED = -1,
CONNECTING = 0,
CONNECTED = 1;
var initForSession = function(connect, callback) {
var Store = connect.Store || connect.session.Store;
function OrientoStore(options, callback) {
try {
Store.call(this, defaults.ensureGenericStoreOptions(options));
this._options = defaults.ensureOrientoStoreOptions(options);
this._setState(INIT);
this._ensureConnection();
} catch(err) {
if (callback) {
callback(err);
return;
} else {
throw err;
}
}
/* ensure DB has the class and the index on id */
this._ensureDBClass(callback);
/* setup purge scheduler */
setInterval(function(store) { store._purgeExpired(); }, this._options.purgeInterval, this);
/* ugly Oriento issue of connection timeout */
var interval = this._options.pingInterval;
/* init DB ping to now */
this._db.ping = new Date();
setInterval(pingDb, interval, this._db, interval);
}
util.inherits(OrientoStore, Store);
OrientoStore.prototype.get = function(sid, callback) {
var self = this, db = self._db;
sid = self._getHashedSid(sid);
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
Session.findById(db, sid, function(err, dbInstance) {
var session = null;
if (!err && dbInstance) {
session = JSON.parse(dbInstance.session || "{}");
if (!session.cookie) {
session = null;
err = new Error("persisted session is expected to have a cookie");
}
}
done(err, session);
});
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype.set = function(sid, session, callback) {
var self = this, db = self._db;
sid = self._getHashedSid(sid);
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
done(!(session && session.cookie) ? new Error("session is expected to have a cookie") : null);
},
function(done) {
Session.findById(db, sid, done);
},
function(dbInstance, done) {
if (!dbInstance) {
// new session, otherwise update
dbInstance = new Session({ id: sid });
}
dbInstance.session = JSON.stringify(session);
dbInstance.touched = new Date();
dbInstance.expiry = self._computeExpiry(session);
dbInstance.save(db, function(err, dbInstance) {
var session = {};
if (!err && dbInstance) {
session = JSON.parse(dbInstance.session || "{}");
if (!session.cookie) {
session = null;
err = new Error("persisted session is expected to have a cookie");
}
}
done(err, session);
});
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype.touch = function(sid, session, callback) {
var self = this, db = self._db;
sid = self._getHashedSid(sid);
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
Session.findById(db, sid, done);
},
function(dbInstance, done) {
done(!(dbInstance && dbInstance.session) ? new Error("can only touch already persisted session") : null, dbInstance);
},
function(dbInstance, done) {
var now = new Date();
if (now - dbInstance.touched < self._options.minTouchInterval) {
// do not touch if interval too small
return done(null, dbInstance.touched);
}
dbInstance.touched = now;
dbInstance.expiry = self._computeExpiry(session);
dbInstance.save(db, function(err) {
done(err, now);
});
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype.destroy = function(sid, callback) {
var self = this, db = self._db;
sid = self._getHashedSid(sid);
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
Session.delete(db, { id: sid }, done);
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype.length = function(callback) {
var self = this, db = self._db;
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
db.select("count(*)").from(Session.name).scalar().then(function(total) {
done(null, total);
}).catch(done);
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype.clear = function(callback) {
var self = this, db = self._db;
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
Session.delete(db, {}, done);
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype._ensureConnection = function(callback) {
var self = this;
if (!callback) {
callback = defaults.returnOneCallback;
}
if (self._state == CONNECTED && self._db) {
return callback(null, self._db);
}
/* ignore that we may be already connecting */
self._setState(CONNECTING);
var err;
try {
self._db = oriento(self._options.server).use(self._options.server.db);
self._setState(CONNECTED);
} catch(ex) {
err = ex;
self._setState(DISCONNECTED);
}
return callback(err, self._db);
};
OrientoStore.prototype._ensureDBClass = function(callback) {
var self = this, db = self._db,
idIndexName = Session.name + ".id";
return async.waterfall([
function(done) {
done(self._state < CONNECTED ? new Error("disconnected from the DB") : null);
},
function(done) {
db.class.get(Session.name).then(function(DBSessionClass) {
done(null, DBSessionClass);
}).catch(function() {
db.class.create(Session.name, "V").then(function(DBSessionClass) {
console.log("created " + Session.name + " cluster");
done(null, DBSessionClass);
}).catch(done);
});
},
function(DBSessionClass, done) {
DBSessionClass.property.get("id").then(function(prop) {
if (prop == null) {
DBSessionClass.property.create({ name: 'id', type: 'String' }).then(function() {
console.log("created " + idIndexName + " property");
done(null, DBSessionClass);
}).catch(done);
} else {
done(null, DBSessionClass);
}
});
},
function(DBSessionClass, done) {
db.index.get(idIndexName).then(function(){
done(null, DBSessionClass);
}).catch(function() {
db.index.create({ name: idIndexName, type: 'unique'}).then(function() {
console.log("created " + idIndexName + " index");
done(null);
}).catch(done);
});
}
], callback || defaults.returnOneCallback);
};
OrientoStore.prototype._getHashedSid = function(sid) {
var hash = this._options.hash;
if (hash) {
return crypto.createHash(hash.algorithm).update(hash.salt + sid).digest('hex');
} else {
return sid;
}
};
OrientoStore.prototype._setState = function(state) {
var event;
switch (state) {
case DISCONNECTED:
event = "disconnected";
break;
case CONNECTING:
event = "connecting";
break;
case CONNECTED:
event = "connected";
break;
default:
event = "init";
}
this.emit(event);
this._state = state;
};
OrientoStore.prototype._computeExpiry = function(session) {
if (session && session.cookie && session.cookie.expires) {
return new Date(session.cookie.expires);
}
return new Date(Date.now() + this._options.maxAge);
};
OrientoStore.prototype._purgeExpired = function(callback) {
var self = this, db = self._db;
callback = callback || defaults.returnOneCallback;
db.query("DELETE VERTEX WHERE expiry < :date AND @class=:clazz", {
params: {
date: new Date(),
clazz: Session.name
}
}).then(function(total) {
callback(null, total);
}).catch(callback);
};
return OrientoStore;
};
module.exports = initForSession;
/**
* @deprecated Oriento drops the binary connection,
* so keep this one until the issue is resolved
*/
var pingDb = function(db, interval) {
var now = new Date();
// do not ping the same DB multiple times
if (now - db.ping >= interval) {
db.ping = now;
db.query("SELECT FROM OUser").then(function() {
}).catch(function(err) {
console.error(err);
});
}
};