seneca-context
Advanced tools
Comparing version 0.3.0 to 0.4.0
'use strict'; | ||
var seneca = require('seneca')(); | ||
var senecaContext = require('../index'); | ||
var app = require('express')(); | ||
@@ -12,21 +11,21 @@ var bodyParser = require('body-parser'); | ||
seneca.add('role:api,path:work', function (message, done) { | ||
debug('role:api,path:work', message.tx$, message.context$); | ||
this.act('role:worker,cmd:work', done); | ||
debug('role:api,path:work', message.tx$, message.context$); | ||
this.act('role:worker,cmd:work', done); | ||
}); | ||
seneca.add('role:worker2,cmd:wait', function (message, done) { | ||
debug('role:worker2,cmd:wait', message.tx$, message.context$); | ||
done(null, message.context$); | ||
debug('role:worker2,cmd:wait', message.tx$, message.context$); | ||
done(null, message.context$); | ||
}); | ||
seneca.use(senecaContext.setContextPlugin); | ||
seneca.use(senecaContext.getContextPlugin, {pin: 'role:worker2'}); | ||
seneca.use('../plugins/setContext'); | ||
seneca.use('../plugins/getContext', {pin: 'role:worker2'}); | ||
seneca.act('role:web', { | ||
use: { | ||
prefix: '/api', | ||
pin: 'role:api,path:*', | ||
map: { | ||
work: {GET: true} | ||
} | ||
use: { | ||
prefix: '/api', | ||
pin: 'role:api,path:*', | ||
map: { | ||
work: {GET: true} | ||
} | ||
} | ||
}); | ||
@@ -33,0 +32,0 @@ |
@@ -9,4 +9,4 @@ 'use strict'; | ||
seneca.add('role:worker,cmd:work', function (message, done) { | ||
debug('role:worker,cmd:work', message.tx$, message.context$); | ||
this.act('role:worker2,cmd:wait', done); | ||
debug('role:worker,cmd:work', message.tx$, message.context$); | ||
this.act('role:worker2,cmd:wait', done); | ||
}); | ||
@@ -13,0 +13,0 @@ |
124
index.js
@@ -7,61 +7,7 @@ 'use strict'; | ||
module.exports = { | ||
setContextPlugin: setContextPlugin, | ||
getContextPlugin: getContextPlugin, | ||
getContext: getContext | ||
getContext: getContext, | ||
setContext: setContext | ||
}; | ||
/** | ||
* A seneca plugin, which automatically saves the context for all HTTP requests. | ||
* | ||
* @param {{ | ||
* // A function which creates a context based on the HTTP request and response. | ||
* // It is used by the `setContextPlugin`. | ||
* // The `defaultContext` is `{requestId: req.headers[options.contextHeader]}`. | ||
* // Default is noop. | ||
* createContext: function (request, response, defaultContext, function(error, context)) | ||
* | ||
* // The name of the HTTP request header containing the request context. | ||
* // Default is 'x-request-id'. | ||
* contextHeader: string | ||
* }} options | ||
*/ | ||
function setContextPlugin(options) { | ||
var seneca = this; | ||
var plugin = 'save-context'; | ||
options = seneca.util.deepextend({ | ||
createContext: createContext, | ||
contextHeader: 'x-request-id' | ||
}, options); | ||
seneca.act({ | ||
role: 'web', | ||
plugin: plugin, | ||
use: processRequest.bind(null, options) | ||
}); | ||
return {name: plugin}; | ||
} | ||
/** | ||
* A seneca plugin, which automatically exposes the context as a property of the incoming message. | ||
* | ||
* @param {{ | ||
* pin: string|Object // a seneca pattern to which this plugin should be applied | ||
* }} options | ||
*/ | ||
function getContextPlugin(options) { | ||
var seneca = this; | ||
var plugin = 'load-context'; | ||
seneca.wrap(options.pin, function (message, done) { | ||
var seneca = this; | ||
message.context$ = getContext(seneca); | ||
seneca.prior(message, done); | ||
}); | ||
return {name: plugin}; | ||
} | ||
/** | ||
* Loads the context from the seneca transaction ID. | ||
@@ -73,13 +19,18 @@ * | ||
function getContext(seneca) { | ||
var transactionId = seneca.fixedargs.tx$; | ||
var context = seneca.fixedargs.context$; | ||
var transactionId = seneca.fixedargs.tx$; | ||
var context = seneca.fixedargs.context$; | ||
if (!context) { | ||
context = seneca.fixedargs.context$ = JSON.parse(URLSafeBase64.decode(transactionId).toString('utf8')); | ||
debug('context loaded from tx$ and cached in context$', transactionId, context); | ||
} else { | ||
debug('context loaded from context$', transactionId, context); | ||
if (typeof context === 'undefined') { | ||
try { | ||
context = seneca.fixedargs.context$ = JSON.parse(URLSafeBase64.decode(transactionId).toString('utf8')); | ||
debug('context loaded from tx$ and cached in context$', transactionId, context); | ||
} catch (error) { | ||
context = null; | ||
debug('context cannot be loaded from tx$', transactionId, error); | ||
} | ||
} else { | ||
debug('context loaded from context$', transactionId, context); | ||
} | ||
return context; | ||
return context; | ||
} | ||
@@ -94,44 +45,5 @@ | ||
function setContext(seneca, context) { | ||
seneca.fixedargs.tx$ = URLSafeBase64.encode(new Buffer(JSON.stringify(context))); | ||
seneca.fixedargs.context$ = context; | ||
debug('context saved', seneca.fixedargs.tx$, context); | ||
seneca.fixedargs.tx$ = URLSafeBase64.encode(new Buffer(JSON.stringify(context))); | ||
seneca.fixedargs.context$ = context; | ||
debug('context saved', seneca.fixedargs.tx$, context); | ||
} | ||
/** | ||
* Derives a context from an HTTP request and | ||
* ensures that it is available to all seneca actions in this transaction. | ||
*/ | ||
function processRequest(options, req, res, next) { | ||
debug('processing HTTP request'); | ||
var seneca = req.seneca; | ||
options.createContext(req, res, createDefaultContext(options, req), function (error, context) { | ||
if (error) { | ||
next(error); | ||
} else { | ||
setContext(seneca, context); | ||
next(); | ||
} | ||
}); | ||
} | ||
/** | ||
* Creates a context based on the value of the `options.contextHeader` header, or the original value of seneca tx$. | ||
*/ | ||
function createDefaultContext(options, req) { | ||
var context = { | ||
requestId: req.headers[options.contextHeader] || req.seneca.fixedargs.tx$ | ||
}; | ||
debug('created default context', context); | ||
return context; | ||
} | ||
/** | ||
* Default implementation of createContext, which responds with the default context. | ||
*/ | ||
function createContext(req, res, context, done) { | ||
debug('default createContext - does nothing', context); | ||
process.nextTick(done.bind(null, null, context)); | ||
} |
{ | ||
"name": "seneca-context", | ||
"version": "0.3.0", | ||
"version": "0.4.0", | ||
"description": "Generate a context object based on an HTTP request and easily access it any seneca services involved in processing that request.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -17,27 +17,26 @@ # seneca-context | ||
var seneca = require('seneca')(); | ||
var senecaContext = require('seneca-context'); | ||
seneca.add('role:api,path:work', function (message, done) { | ||
// context is implicitly propagated to the next seneca action | ||
this.act('role:worker,cmd:work', done); | ||
// context is implicitly propagated to the next seneca action | ||
this.act('role:worker,cmd:work', done); | ||
}); | ||
seneca.add('role:worker,cmd:work', function (message, done) { | ||
// context is accessible at the `context$` property of the message | ||
done(null, message.context$); | ||
// context is accessible at the `context$` property of the message | ||
done(null, message.context$); | ||
}); | ||
// Creates the context from HTTP requests and propagates it to all actions within the transaction. | ||
seneca.use(senecaContext.setContextPlugin); | ||
seneca.use('seneca-context/plugins/setContext'); | ||
// Adds the `context$` property to the incoming messages matching the `pin`. | ||
seneca.use(senecaContext.getContextPlugin, {pin: 'role:worker'}); | ||
seneca.use('seneca-context/plugins/getContext', {pin: 'role:worker'}); | ||
seneca.act('role:web', { | ||
use: { | ||
prefix: '/api', | ||
pin: 'role:api,path:*', | ||
map: { | ||
work: {GET: true} | ||
} | ||
use: { | ||
prefix: '/api', | ||
pin: 'role:api,path:*', | ||
map: { | ||
work: {GET: true} | ||
} | ||
} | ||
}); | ||
@@ -44,0 +43,0 @@ |
@@ -9,2 +9,31 @@ var createSeneca = require('seneca'); | ||
describe('seneca-context', function () { | ||
describe('basic getContext and setContext', function () { | ||
it('should get and set context', function (testDone) { | ||
var seneca = createSeneca(); | ||
var context = {a: {b: {c: 1}}, d: [2, 3, 4]}; | ||
var context2 = {test: 'data'}; | ||
seneca.add('role:test,cmd:run', function (msg, done) { | ||
expect(senecaContext.getContext(this)).to.be.null; | ||
senecaContext.setContext(this, context); | ||
expect(senecaContext.getContext(this)).to.deep.equal(context); | ||
this.act({role: 'test', cmd: 'run2'}, done); | ||
}); | ||
seneca.add('role:test,cmd:run2', function (msg, done) { | ||
expect(senecaContext.getContext(this)).to.deep.equal(context); | ||
senecaContext.setContext(this, context2); | ||
expect(senecaContext.getContext(this)).to.deep.equal(context2); | ||
// If possible, a cached context from context$ property is returned. | ||
// Check, if it can be recreated from tx$. | ||
expect(this.fixedargs.context$).to.deep.equal(context2); | ||
delete this.fixedargs.context$; | ||
expect(senecaContext.getContext(this)).to.deep.equal(context2); | ||
expect(this.fixedargs.context$).to.deep.equal(context2); | ||
done(); | ||
}); | ||
seneca.act({role: 'test', cmd: 'run'}, testDone); | ||
}); | ||
}); | ||
describe('plugins and propagating context through a socket', function () { | ||
var app, server, seneca1, seneca2; | ||
@@ -15,118 +44,119 @@ var requestId = 'test-request-id-1'; | ||
afterEach(function (done) { | ||
vasync.parallel({ | ||
funcs: [ | ||
server.close.bind(server), | ||
seneca1.close.bind(seneca1), | ||
seneca2.close.bind(seneca2) | ||
] | ||
}, done); | ||
vasync.parallel({ | ||
funcs: [ | ||
server.close.bind(server), | ||
seneca1.close.bind(seneca1), | ||
seneca2.close.bind(seneca2) | ||
] | ||
}, done); | ||
}); | ||
it('should propagate and eventually return the request context', function (done) { | ||
// set up seneca1 | ||
// -------------- | ||
seneca1 = createSeneca(); | ||
seneca1.add('role:seneca1,cmd:task1', function task1(message, done) { | ||
// message.context$ is automatically populated on the first action called by seneca-web | ||
message.context$.should.deep.equal(expectedContext); | ||
// set up seneca1 | ||
// -------------- | ||
seneca1 = createSeneca(); | ||
seneca1.add('role:seneca1,cmd:task1', function task1(message, done) { | ||
// message.context$ is automatically populated on the first action called by seneca-web | ||
message.context$.should.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(message.context$).and.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(message.context$).and.deep.equal(expectedContext); | ||
this.act('role:seneca2,cmd:task2', {trace: message.trace + '1'}, done); | ||
}); | ||
seneca1.add('role:seneca1,cmd:task3', function task3(message, done) { | ||
// message.context$ is populated here by `senecaContext.getContextPlugin` | ||
message.context$.should.deep.equal(expectedContext); | ||
this.act('role:seneca2,cmd:task2', {trace: message.trace + '1'}, done); | ||
}); | ||
seneca1.add('role:seneca1,cmd:task3', function task3(message, done) { | ||
// message.context$ is populated here by `getContext` plugin | ||
message.context$.should.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(message.context$).and.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(message.context$).and.deep.equal(expectedContext); | ||
done(null, {trace: message.trace + '3', context: message.context$}); | ||
}); | ||
seneca1.use(senecaContext.setContextPlugin, { | ||
createContext: function (req, res, context, done) { | ||
process.nextTick(done.bind(null, null, seneca1.util.deepextend({test: 'abc'}, context))); | ||
}, | ||
contextHeader: 'x-context' | ||
}); | ||
seneca1.use(senecaContext.getContextPlugin, {pin: 'role:seneca1,cmd:task3'}); | ||
seneca1.act('role:web', { | ||
use: { | ||
prefix: '/', | ||
pin: 'role:seneca1,cmd:*', | ||
map: { | ||
task1: {GET: true} | ||
} | ||
} | ||
}); | ||
seneca1.client({port: 9011}); | ||
seneca1.listen({port: 9010}); | ||
done(null, {trace: message.trace + '3', context: message.context$}); | ||
}); | ||
seneca1.use('../plugins/setContext', { | ||
createContext: function (req, res, context, done) { | ||
process.nextTick(done.bind(null, null, seneca1.util.deepextend({test: 'abc'}, context))); | ||
}, | ||
contextHeader: 'x-context' | ||
}); | ||
seneca1.use('../plugins/getContext', {pin: 'role:seneca1,cmd:task3'}); | ||
seneca1.act('role:web', { | ||
use: { | ||
prefix: '/', | ||
pin: 'role:seneca1,cmd:*', | ||
map: { | ||
task1: {GET: true} | ||
} | ||
} | ||
}); | ||
seneca1.client({port: 9011}); | ||
seneca1.listen({port: 9010}); | ||
// set up seneca2 | ||
// -------------- | ||
seneca2 = createSeneca(); | ||
seneca2.add('role:seneca2,cmd:task2', function task2(message, done) { | ||
expect(message.context$).to.be.undefined; | ||
// set up seneca2 | ||
// -------------- | ||
seneca2 = createSeneca(); | ||
seneca2.add('role:seneca2,cmd:task2', function task2(message, done) { | ||
expect(message.context$).to.be.undefined; | ||
// load context from tx$ | ||
var context = senecaContext.getContext(this); | ||
context.should.deep.equal(expectedContext); | ||
// load context from tx$ | ||
var context = senecaContext.getContext(this); | ||
context.should.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(context).and.deep.equal(expectedContext); | ||
// load cached context | ||
senecaContext.getContext(this).should.equal(context).and.deep.equal(expectedContext); | ||
// message should not be modified | ||
expect(message.context$).to.be.undefined; | ||
// message should not be modified | ||
expect(message.context$).to.be.undefined; | ||
this.act('role:seneca1,cmd:task3', {trace: message.trace + '2'}, done); | ||
}); | ||
seneca2.client({port: 9010}); | ||
seneca2.listen({port: 9011}); | ||
this.act('role:seneca1,cmd:task3', {trace: message.trace + '2'}, done); | ||
}); | ||
seneca2.client({port: 9010}); | ||
seneca2.listen({port: 9011}); | ||
// set up express | ||
// -------------- | ||
app = express(); | ||
app.use(bodyParser.json()); | ||
app.use(seneca1.export('web')); | ||
// set up express | ||
// -------------- | ||
app = express(); | ||
app.use(bodyParser.json()); | ||
app.use(seneca1.export('web')); | ||
// wait for seneca and express to start | ||
vasync.parallel({ | ||
funcs: [ | ||
function (onExpressReady) { | ||
server = app.listen(3010, onExpressReady); | ||
}, | ||
seneca1.ready.bind(seneca1), | ||
seneca2.ready.bind(seneca2) | ||
] | ||
}, function (error) { | ||
if (error) { | ||
return done(error); | ||
// wait for seneca and express to start | ||
vasync.parallel({ | ||
funcs: [ | ||
function (onExpressReady) { | ||
server = app.listen(3010, onExpressReady); | ||
}, | ||
seneca1.ready.bind(seneca1), | ||
seneca2.ready.bind(seneca2) | ||
] | ||
}, function (error) { | ||
if (error) { | ||
return done(error); | ||
} | ||
// send an HTTP request | ||
http.get({ | ||
hostname: '127.0.0.1', | ||
port: 3010, | ||
path: '/task1?trace=request', | ||
headers: { | ||
'X-Context': requestId | ||
} | ||
}, function (res) { | ||
var responseText = ''; | ||
res.on('data', function (data) { | ||
responseText += data; | ||
}); | ||
res.on('end', function () { | ||
try { | ||
var response = JSON.parse(responseText); | ||
response.should.deep.equal({context: expectedContext, trace: 'request123'}); | ||
done(); | ||
} catch (error) { | ||
done(error); | ||
} | ||
// send an HTTP request | ||
http.get({ | ||
hostname: '127.0.0.1', | ||
port: 3010, | ||
path: '/task1?trace=request', | ||
headers: { | ||
'X-Context': requestId | ||
} | ||
}, function (res) { | ||
var responseText = ''; | ||
res.on('data', function (data) { | ||
responseText += data; | ||
}); | ||
res.on('end', function () { | ||
try { | ||
var response = JSON.parse(responseText); | ||
response.should.deep.equal({context: expectedContext, trace: 'request123'}); | ||
done(); | ||
} catch (error) { | ||
done(error); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
15466
13
323
61