From 80e8bf31d7d5a597b0d12661c0fe204f85a6ab91 Mon Sep 17 00:00:00 2001 From: sbertal Date: Thu, 3 Dec 2015 15:16:15 -0800 Subject: [PATCH] add responseFormatter support --- README.md | 31 +++++++++-- libs/fetcher.js | 24 +++++--- tests/unit/libs/fetcher.js | 110 +++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index a100fede..64b3a19e 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# Fetchr +# Fetchr [![npm version](https://badge.fury.io/js/fetchr.svg)](http://badge.fury.io/js/fetchr) [![Build Status](https://travis-ci.org/yahoo/fetchr.svg?branch=master)](https://travis-ci.org/yahoo/fetchr) [![Dependency Status](https://david-dm.org/yahoo/fetchr.svg)](https://david-dm.org/yahoo/fetchr) [![devDependency Status](https://david-dm.org/yahoo/fetchr/dev-status.svg)](https://david-dm.org/yahoo/fetchr#info=devDependencies) -[![Coverage Status](https://coveralls.io/repos/yahoo/fetchr/badge.png?branch=master)](https://coveralls.io/r/yahoo/fetchr?branch=master) +[![Coverage Status](https://coveralls.io/repos/yahoo/fetchr/badge.png?branch=master)](https://coveralls.io/r/yahoo/fetchr?branch=master) Universal data access layer for web applications. Typically on the server, you call your API or database directly to fetch some data. However, on the client, you cannot always call your services in the same way (i.e, cross domain policies). Instead, XHR requests need to be made to the server which get forwarded to your service. -Having to write code differently for both environments is duplicative and error prone. Fetchr provides an abstraction layer over your data service calls so that you can fetch data using the same API on the server and client side. +Having to write code differently for both environments is duplicative and error prone. Fetchr provides an abstraction layer over your data service calls so that you can fetch data using the same API on the server and client side. ## Install @@ -133,7 +133,7 @@ fetcher .end(function (err, data, meta) { // handle err and/or data returned from data fetcher in this callback }); - + // for create you can use the body() method to pass data fetcher .create('data_api_create') @@ -258,6 +258,29 @@ fetcher }); ``` +## XHR Response Formatting + +For some applications, there may be a situation where you need to modify an XHR response before it is passed to the client. Typically, you would apply your modifications in the service itself. However, if you want to modify the XHR responses across many services (i.e. add debug information), then you can use the `responseFormatter` option. + +`responseFormatter` is a function that is passed into the `Fetcher.middleware` method. It is passed three arguments, the request object, response object and the service response object (i.e. the data returned from your service). The `responseFormatter` function can then modify the service response to add additional information. + +Take a look at the example below: + +```js +/** + Using the app.js from above, you can modify the Fetcher.middleware + method to pass in the responseFormatter function. + */ +app.use('/myCustomAPIEndpoint', Fetcher.middleware({ + responseFormatter: function (req, res, data) { + data.debug = 'some debug information'; + return data; + } +})); +``` + +Now when an XHR request is performed, your response will contain the `debug` property added above. + ## CORS Support Fetchr provides [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) support by allowing you to pass the full origin host into `corsPath` option. diff --git a/libs/fetcher.js b/libs/fetcher.js index adbc8861..3c3b5670 100644 --- a/libs/fetcher.js +++ b/libs/fetcher.js @@ -279,12 +279,20 @@ Fetcher.isRegistered = function (name) { * Returns express/connect middleware for Fetcher * @method middleware * @memberof Fetcher + * @param {Object} [options] Optional configurations + * @param {Function} [options.responseFormatter=no op function] Function to modify the response + before sending to client. First argument is the HTTP request object, + second argument is the HTTP response object and the third argument is the service data object. * @returns {Function} middleware * @param {Object} req * @param {Object} res * @param {Object} next */ -Fetcher.middleware = function () { +Fetcher.middleware = function (options) { + options = options || {}; + var responseFormatter = options.responseFormatter || function noOp(req, res, data) { + return data; + }; return function (req, res, next) { var request; var error; @@ -314,14 +322,14 @@ Fetcher.middleware = function () { } if (err) { var errResponse = getErrorResponse(err); - res.status(errResponse.statusCode).json(errResponse.output); + res.status(errResponse.statusCode).json(responseFormatter(req, res, errResponse.output)); return; } if (req.query.returnMeta) { - res.status(meta.statusCode || 200).json({ + res.status(meta.statusCode || 200).json(responseFormatter(req, res, { data: data, meta: meta - }); + })); } else { // TODO: Remove `returnMeta` feature flag after next release res.status(meta.statusCode || 200).json(data); @@ -361,16 +369,16 @@ Fetcher.middleware = function () { if (meta.headers) { res.set(meta.headers); } - if(err) { + if (err) { var errResponse = getErrorResponse(err); - res.status(errResponse.statusCode).json(errResponse.output); + res.status(errResponse.statusCode).json(responseFormatter(req, res, errResponse.output)); return; } var responseObj = {}; - responseObj[DEFAULT_GUID] = { + responseObj[DEFAULT_GUID] = responseFormatter(req, res, { data: data, meta: meta - }; + }); res.status(meta.statusCode || 200).json(responseObj); }); } diff --git a/tests/unit/libs/fetcher.js b/tests/unit/libs/fetcher.js index 0ec8a2de..eaddd5ab 100644 --- a/tests/unit/libs/fetcher.js +++ b/tests/unit/libs/fetcher.js @@ -569,6 +569,116 @@ describe('Server Fetcher', function () { }); }); + + describe('Response Formatter', function () { + describe('GET', function () { + it('should modify the response object', function (done) { + var operation = 'read'; + var statusCodeSet = false; + var params = { + uuids: ['cd7240d6-aeed-3fed-b63c-d7e99e21ca17', 'cd7240d6-aeed-3fed-b63c-d7e99e21ca17'], + id: 'asdf' + }; + var req = { + method: 'GET', + path: '/' + mockService.name + ';' + qs.stringify(params, ';'), + query: { + returnMeta: true + } + }; + var res = { + json: function(response) { + expect(response).to.exist; + expect(response).to.not.be.empty; + expect(response).to.contain.keys('data', 'meta', 'modified'); + expect(response.data).to.contain.keys('operation', 'args'); + expect(response.data.operation.name).to.equal(operation); + expect(response.data.operation.success).to.be.true; + expect(response.data.args).to.contain.keys('params'); + expect(response.data.args.params).to.deep.equal(params); + expect(response.meta).to.be.empty; + expect(statusCodeSet).to.be.true; + done(); + }, + status: function(code) { + expect(code).to.equal(200); + statusCodeSet = true; + return this; + }, + send: function (code) { + console.log('Not Expected: middleware responded with', code); + } + }; + var next = function () { + console.log('Not Expected: middleware skipped request'); + }; + var middleware = Fetcher.middleware({responseFormatter: function (req, res, data) { + data.modified = true; + return data; + }}); + + middleware(req, res, next); + }); + }); + describe('POST', function () { + it('should modify the response object', function (done) { + var operation = 'create', + statusCodeSet = false, + req = { + method: 'POST', + path: '/' + mockService.name, + body: { + requests: { + g0: { + resource: mockService.name, + operation: operation, + params: { + uuids: ['cd7240d6-aeed-3fed-b63c-d7e99e21ca17', 'cd7240d6-aeed-3fed-b63c-d7e99e21ca17'], + id: 'asdf' + } + } + }, + context: { + site: '', + device: '' + } + } + }, + res = { + json: function(response) { + expect(response).to.exist; + expect(response).to.not.be.empty; + expect(response.g0).to.contain.keys('data', 'meta', 'modified'); + var data = response.g0.data; + expect(data).to.contain.keys('operation', 'args'); + expect(data.operation.name).to.equal(operation); + expect(data.operation.success).to.be.true; + expect(data.args).to.contain.keys('params'); + expect(data.args.params).to.equal(req.body.requests.g0.params); + expect(statusCodeSet).to.be.true; + done(); + }, + status: function(code) { + expect(code).to.equal(200); + statusCodeSet = true; + return this; + }, + send: function (code) { + console.log('Not Expected: middleware responded with', code); + } + }, + next = function () { + console.log('Not Expected: middleware skipped request'); + }, + middleware = Fetcher.middleware({responseFormatter: function (req, res, data) { + data.modified = true; + return data; + }}); + + middleware(req, res, next); + }); + }); + }); }); describe('CRUD Interface', function () {