Skip to content

Commit

Permalink
Save all enumerable properties on req.session
Browse files Browse the repository at this point in the history
closes #17
closes #18
  • Loading branch information
dougwilson committed Aug 9, 2015
1 parent 29da418 commit 824e823
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 67 deletions.
3 changes: 3 additions & 0 deletions HISTORY.md
Expand Up @@ -2,7 +2,10 @@
===

* Change default cookie name to `express.sess`
* Change `.populated` to `.isPopulated`
* Remove the `key` option; use `name` instead
* Save all enumerable properties on `req.session`
- Including `_`-prefixed properties

1.2.0 / 2015-07-01
==================
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -74,7 +74,7 @@ Represents the session for the given request.

Is `true` if the session is new.

#### .populated
#### .isPopulated

Determine if the session has been populated with data or is empty.

Expand Down
169 changes: 108 additions & 61 deletions index.js
Expand Up @@ -58,47 +58,57 @@ function cookieSession(options) {

return function _cookieSession(req, res, next) {
var cookies = req.sessionCookies = new Cookies(req, res, keys);
var sess, json;
var sess

// to pass to Session()
req.sessionOptions = Object.create(opts)
req.sessionKey = name

req.__defineGetter__('session', function(){
req.__defineGetter__('session', function getSession() {
// already retrieved
if (sess) return sess;

// unset
if (false === sess) return null;

json = cookies.get(name, req.sessionOptions)
var str = cookies.get(name, req.sessionOptions)

if (json) {
debug('parse %s', json);
if (str) {
debug('parse %s', str)
try {
sess = new Session(req, decode(json));
sess = Session.deserialize(req, str)
} catch (err) {
// backwards compatibility:
// create a new session if parsing fails.
// new Buffer(string, 'base64') does not seem to crash
// when `string` is not base64-encoded.
// but `JSON.parse(string)` will crash.
if (!(err instanceof SyntaxError)) throw err;
sess = new Session(req);
sess = Session.create(req)
}
} else {
debug('new session');
sess = new Session(req);
sess = Session.create(req)
}

return sess;
});

req.__defineSetter__('session', function(val){
if (null == val) return sess = false;
if ('object' == typeof val) return sess = new Session(req, val);
throw new Error('req.session can only be set as null or an object.');
});
req.__defineSetter__('session', function setSession(val) {
if (val == null) {
// unset session
sess = false
return val
}

if (typeof val === 'object') {
// create a new session
sess = Session.create(this, val)
return sess
}

throw new Error('req.session can only be set as null or an object.')
})

onHeaders(res, function setHeaders() {
if (sess === undefined) {
Expand All @@ -110,14 +120,12 @@ function cookieSession(options) {
if (sess === false) {
// remove
cookies.set(name, '', req.sessionOptions)
} else if (!json && !sess.length) {
// do nothing if new and not populated
} else if (sess.changed(json)) {
// save
sess.save();
} else if ((!sess.isNew || sess.isPopulated) && sess.isChanged) {
// save populated or non-new changed session
sess.save()
}
} catch (e) {
debug('error saving session %s', e.message);
debug('error saving session %s', e.message)
}
});

Expand All @@ -134,10 +142,8 @@ function cookieSession(options) {
*/

function Session(ctx, obj) {
this._ctx = ctx

Object.defineProperty(this, 'isNew', {
value: !obj
Object.defineProperty(this, '_ctx', {
value: ctx
})

if (obj) {
Expand All @@ -148,42 +154,64 @@ function Session(ctx, obj) {
}

/**
* JSON representation of the session.
*
* @return {Object}
* @public
* Create new session.
* @private
*/

Session.create = function create(req, obj) {
var ctx = new SessionContext(req)
return new Session(ctx, obj)
}

/**
* Create session from serialized form.
* @private
*/

Session.prototype.inspect =
Session.prototype.toJSON = function toJSON() {
var keys = Object.keys(this)
var obj = {}
Session.deserialize = function deserialize(req, str) {
var ctx = new SessionContext(req)
var obj = decode(str)

for (var i = 0; i < keys.length; i++) {
var key = keys[i]
ctx._new = false
ctx._val = str

if (key[0] !== '_') {
obj[key] = this[key]
}
}
return new Session(ctx, obj)
}

/**
* Serialize a session to a string.
* @private
*/

return obj
Session.serialize = function serialize(sess) {
return encode(sess)
}

/**
* Check if the session has changed relative to the `prev`
* JSON value from the request.
* Return if the session is changed for this request.
*
* @param {String} [prev]
* @return {Boolean}
* @private
* @public
*/

Session.prototype.changed = function(prev){
if (!prev) return true;
this._json = encode(this);
return this._json != prev;
};
Object.defineProperty(Session.prototype, 'isChanged', {
get: function getIsChanged() {
return this._ctx._new || this._ctx._val !== Session.serialize(this)
}
})

/**
* Return if the session is new for this request.
*
* @return {Boolean}
* @public
*/

Object.defineProperty(Session.prototype, 'isNew', {
get: function getIsNew() {
return this._ctx._new
}
})

/**
* Return how many values there are in the session object.
Expand All @@ -193,9 +221,11 @@ Session.prototype.changed = function(prev){
* @public
*/

Session.prototype.__defineGetter__('length', function(){
return Object.keys(this.toJSON()).length;
});
Object.defineProperty(Session.prototype, 'length', {
get: function getLength() {
return Object.keys(this).length
}
})

/**
* populated flag, which is just a boolean alias of .length.
Expand All @@ -204,25 +234,42 @@ Session.prototype.__defineGetter__('length', function(){
* @public
*/

Session.prototype.__defineGetter__('populated', function(){
return !!this.length;
});
Object.defineProperty(Session.prototype, 'isPopulated', {
get: function getIsPopulated() {
return Boolean(this.length)
}
})

/**
* Save session changes by performing a Set-Cookie.
* @private
*/

Session.prototype.save = function save() {
var ctx = this._ctx
var val = Session.serialize(this)

var cookies = ctx.req.sessionCookies
var name = ctx.req.sessionKey
var opts = ctx.req.sessionOptions

debug('save %s', val)
cookies.set(name, val, opts)
}

/**
* Session context to tie session to req.
*
* @param {Request} req
* @private
*/

Session.prototype.save = function(){
var ctx = this._ctx;
var json = this._json || encode(this);
var opts = ctx.sessionOptions;
var name = ctx.sessionKey;
function SessionContext(req) {
this.req = req

debug('save %s', json);
ctx.sessionCookies.set(name, json, opts);
};
this._new = true
this._val = undefined
}

/**
* Decode the base64 cookie value to an object.
Expand Down
11 changes: 6 additions & 5 deletions test/test.js
Expand Up @@ -93,6 +93,7 @@ describe('Cookie Session', function(){
var server = app.listen();
request(server)
.post('/')
.expect('Set-Cookie', /express\.sess/)
.expect(204, function(err, res){
if (err) return done(err);
var cookie = res.headers['set-cookie'];
Expand Down Expand Up @@ -278,12 +279,12 @@ describe('Cookie Session', function(){
var app = App();
app.use(function (req, res, next) {
req.session = {};
res.end('lkajsdlkfjasdf');
res.end('hello, world');
})

request(app.listen())
.get('/')
.expect(200, function(err, res){
.expect(200, 'hello, world', function (err, res) {
if (err) return done(err);
assert.strictEqual(res.header['set-cookie'], undefined);
done();
Expand Down Expand Up @@ -321,11 +322,11 @@ describe('Cookie Session', function(){
})

describe('req.session', function () {
describe('.populated', function () {
describe('.isPopulated', function () {
it('should be false on new session', function (done) {
var app = App();
app.use(function (req, res, next) {
res.end(String(req.session.populated))
res.end(String(req.session.isPopulated))
})

request(app.listen())
Expand All @@ -337,7 +338,7 @@ describe('Cookie Session', function(){
var app = App();
app.use(function (req, res, next) {
req.session.message = 'hello!'
res.end(String(req.session.populated))
res.end(String(req.session.isPopulated))
})

request(app.listen())
Expand Down

0 comments on commit 824e823

Please sign in to comment.