diff --git a/lib/application.js b/lib/application.js index ddcdfdbd..c7a19365 100644 --- a/lib/application.js +++ b/lib/application.js @@ -439,6 +439,131 @@ function tryReadConfig(cwd, fileName) { } } +/** + * Install all express middleware required by LoopBack. + * + * It is possible to inject your own middleware by listening on one of the + * following events: + * + * - `middleware:preprocessors` is emitted after all other + * request-preprocessing middleware was installed, but before any + * request-handling middleware is configured. + * + * Usage: + * ```js + * app.once('middleware:preprocessors', function() { + * app.use(loopback.limit('5.5mb')) + * }); + * ``` + * - `middleware:handlers` is emitted when it's time to add your custom + * request-handling middleware. Note that you should not install any + * express routes at this point (express routes are discussed later). + * + * Usage: + * ```js + * app.once('middleware:handlers', function() { + * app.use('/admin', adminExpressApp); + * app.use('/custom', function(req, res, next) { + * res.send(200, { url: req.url }); + * }); + * }); + * ``` + * - `middleware:error-loggers` is emitted at the end, before the loopback + * error handling middleware is installed. This is the point where you + * can install your own middleware to log errors. + * + * Notes: + * - The middleware function must take four parameters, otherwise it won't + * be called by express. + * + * - It should also call `next(err)` to let the loopback error handler convert + * the error to an HTTP error response. + * + * Usage: + * ```js + * var bunyan = require('bunyan'); + * var log = bunyan.createLogger({name: "myapp"}); + * app.once('middleware:error-loggers', function() { + * app.use(function(err, req, res, next) { + * log.error(err); + * next(err); + * }); + * }); + * ``` + * + * Express routes should be added after `installMiddleware` was called. + * This way the express router middleware is injected at the right place in the + * middleware chain. If you add an express route before calling this function, + * bad things will happen: Express will automatically add the router + * middleware and since we haven't added request-preprocessing middleware like + * cookie & body parser yet, your route handlers will receive raw unprocessed + * requests. + * + * This is the correct order in which to call `app` methods: + * ```js + * app.boot(__dirname); // optional + * + * app.installMiddleware(); + * + * // [register your express routes here] + * + * app.listen(); + * ``` + */ +app.installMiddleware = function() { + var loopback = require('../'); + + /* + * Request pre-processing + */ + this.use(loopback.favicon()); + // TODO(bajtos) refactor to app.get('loggerFormat') + var loggerFormat = this.get('env') === 'development' ? 'dev' : 'default'; + this.use(loopback.logger(loggerFormat)); + this.use(loopback.cookieParser(this.get('cookieSecret'))); + this.use(loopback.token({ model: this.models.accessToken })); + this.use(loopback.bodyParser()); + this.use(loopback.methodOverride()); + + // Allow the app to install custom preprocessing middleware + this.emit('middleware:preprocessors'); + + /* + * Request handling + */ + + // LoopBack REST transport + this.use(this.get('restApiRoot') || '/api', loopback.rest()); + + // Allow the app to install custom request handling middleware + this.emit('middleware:handlers'); + + // Let express routes handle requests that were not handled + // by any of the middleware registered above. + // This way LoopBack REST and API Explorer take precedence over + // express routes. + this.use(this.router); + + // The static file server should come after all other routes + // Every request that goes through the static middleware hits + // the file system to check if a file exists. + this.use(loopback.static(path.join(__dirname, 'public'))); + + // Requests that get this far won't be handled + // by any middleware. Convert them into a 404 error + // that will be handled later down the chain. + this.use(loopback.urlNotFound()); + + /* + * Error handling + */ + + // Allow the app to install custom error logging middleware + this.emit('middleware:error-handlers'); + + // The ultimate error handler. + this.use(loopback.errorHandler()); +}; /** * Listen for connections and update the configured port. diff --git a/package.json b/package.json index 7fcdb1a4..2db043ea 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "Platform", "mBaaS" ], - "version": "1.4.3", + "version": "1.5.0", "scripts": { "test": "mocha -R spec" }, diff --git a/test/app.test.js b/test/app.test.js index be1d4013..4087d33a 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -197,6 +197,102 @@ describe('app', function() { }); }); + describe('installMiddleware()', function() { + var app; + beforeEach(function() { app = loopback(); }); + + it('installs loopback.token', function(done) { + app.models.accessToken = loopback.AccessToken; + + app.installMiddleware(); + + app.get('/', function(req, res) { + res.send({ accessTokenId: req.accessToken && req.accessToken.id }); + }); + + app.models.accessToken.create({}, function(err, token) { + if (err) done(err); + request(app).get('/') + .set('Authorization', token.id) + .expect(200, { accessTokenId: token.id }) + .end(done); + }); + }); + + it('emits "middleware:preprocessors" before handlers are installed', + function(done) { + app.on('middleware:preprocessors', function() { + this.use(function(req, res, next) { + req.preprocessed = true; + next(); + }); + }); + + app.installMiddleware(); + + app.get('/', function(req, res) { + res.send({ preprocessed: req.preprocessed }); + }); + + request(app).get('/') + .expect(200, { preprocessed: true}) + .end(done); + } + ); + + it('emits "middleware:handlers before installing express router', + function(done) { + app.on('middleware:handlers', function() { + this.use(function(req, res, next) { + res.send({ handler: 'middleware' }); + }); + }); + + app.installMiddleware(); + + app.get('/', function(req, res) { + res.send({ handler: 'router' }); + }); + + request(app).get('/') + .expect(200, { handler: 'middleware' }) + .end(done); + } + ); + + it('emits "middleware:error-handlers" after all request handlers', + function(done) { + var logs = []; + app.on('middleware:error-handlers', function() { + app.use(function(err, req, res, next) { + logs.push(req.url); + next(err); + }); + }); + + app.installMiddleware(); + + request(app).get('/not-found') + .expect(404) + .end(function(err, res) { + if (err) done(err); + expect(logs).to.eql(['/not-found']); + done(); + }); + } + ); + + it('installs REST transport', function(done) { + app.model(loopback.Application); + app.set('restApiRoot', '/api'); + app.installMiddleware(); + + request(app).get('/api/applications') + .expect(200, []) + .end(done); + }); + }); + describe('listen()', function() { it('starts http server', function(done) { var app = loopback(); diff --git a/test/support.js b/test/support.js index 2262e294..394bc6d4 100644 --- a/test/support.js +++ b/test/support.js @@ -11,6 +11,11 @@ app = null; TaskEmitter = require('strong-task-emitter'); request = require('supertest'); + +// Speed up the password hashing algorithm +// for tests using the built-in User model +loopback.User.settings.saltWorkFactor = 4; + beforeEach(function () { app = loopback();