diff --git a/README.md b/README.md index 4b6e9b20..80fac5e6 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ npm install fetchr --save Follow the steps below to setup Fetchr properly. This assumes you are using the [Express](https://www.npmjs.com/package/express) framework. -### 1. Middleware +### 1. Configure Server On the server side, add the Fetchr middleware into your express app at a custom API endpoint. @@ -40,41 +40,38 @@ app.use(bodyParser.json()); app.use('/myCustomAPIEndpoint', Fetcher.middleware()); ``` -### 2. API xhrPath and xhrTimeout +### 2. Configure Client -`xhrPath` is an optional config property that allows you to customize the endpoint to your services, defaults to `/api`. - -`xhrTimeout` is an optional config property that allows you to set timeout (in ms) for clientside requests, defaults to `3000`. +On the client side, it is necessary for the `xhrPath` option to match the path where the middleware was mounted in the previous step -On the clientside, xhrPath and xhrTimeout will be used for XHR requests. On the serverside, xhrPath and xhrTimeout are not needed and are ignored. - -Note: Even though xhrPath is optional, it is necessary for xhrPath on the clientside fetcher to match the path where the middleware was mounted on in the previous step. +`xhrPath` is an optional config property that allows you to customize the endpoint to your services, defaults to `/api`. ```js var Fetcher = require('fetchr'); var fetcher = new Fetcher({ - xhrPath: '/myCustomAPIEndpoint', - xhrTimeout: 4000 + xhrPath: '/myCustomAPIEndpoint' }); ``` -### 3. Register data fetchers +### 3. Register data services -You will need to register any data fetchers that you wish to use in your application. The interface for your fetcher will be an object that must define a `name` property and at least one [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operation. The `name` propety will be used when you call one of the CRUD operations. +You will need to register any data services that you wish to use in your application. +The interface for your service will be an object that must define a `name` property and at least one [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operation. +The `name` propety will be used when you call one of the CRUD operations. ```js // app.js var Fetcher = require('fetchr'); -var myDataFetcher = require('./dataFetcher'); -Fetcher.registerFetcher(myDataFetcher); +var myDataService = require('./dataService'); +Fetcher.registerService(myDataFetcher); ``` ```js -// dataFetcher.js +// dataService.js module.exports = { // name is required - name: 'data_api_fetcher', - // at least one of the CRUD methods is Required + name: 'data_service', + // at least one of the CRUD methods is required read: function(req, resource, params, config, callback) { //... }, @@ -87,9 +84,11 @@ module.exports = { ### 4. Instantiating the Fetchr Class -Data fetchers might need access to each individual request, for example, to get the current logged in user's session. For this reason, Fetcher will have to be instantiated once per request. +Data services might need access to each individual request, for example, to get the current logged in user's session. +For this reason, Fetcher will have to be instantiated once per request. -On the serverside, this requires fetcher to be instantiated per request, in express middleware. On the clientside, this only needs to happen on page load. +On the serverside, this requires fetcher to be instantiated per request, in express middleware. +On the clientside, this only needs to happen on page load. ```js @@ -97,10 +96,10 @@ On the serverside, this requires fetcher to be instantiated per request, in expr var express = require('express'); var Fetcher = require('fetchr'); var app = express(); -var myDataFetcher = require('./dataFetcher'); +var myDataService = require('./dataService'); -// register the fetcher -Fetcher.registerFetcher(myDataFetcher); +// register the service +Fetcher.registerService(myDataService); // register the middleware app.use('/myCustomAPIEndpoint', Fetcher.middleware()); @@ -113,9 +112,12 @@ app.use(function(req, res, next) { }); // perform read call to get data - fetcher.read('data_api_fetcher', {id: ###}, {}, function (err, data, meta) { + fetcher + .read('data_service') + .params({id: ###}) + .end(function (err, data, meta) { // handle err and/or data returned from data fetcher in this callback - }) + }); }); ``` @@ -126,18 +128,47 @@ var Fetcher = require('fetchr'); var fetcher = new Fetcher({ xhrPath: '/myCustomAPIEndpoint' // xhrPath is REQUIRED on the clientside fetcher instantiation }); -fetcher.read('data_api_fetcher', {id: ###}, {}, function (err, data, meta) { +fetcher + .read('data_api_fetcher') + .params({id: ###}) + .end(function (err, data, meta) { // handle err and/or data returned from data fetcher in this callback -}) + }); ``` ## Usage Examples See the [simple example](https://github.com/yahoo/fetchr/tree/master/examples/simple). +## XHR Timeouts + +`xhrTimeout` is an optional config property that allows you to set timeout (in ms) for all clientside requests, defaults to `3000`. +On the clientside, xhrPath and xhrTimeout will be used for XHR requests. +On the serverside, xhrPath and xhrTimeout are not needed and are ignored. + +```js +var Fetcher = require('fetchr'); +var fetcher = new Fetcher({ + xhrPath: '/myCustomAPIEndpoint', + xhrTimeout: 4000 +}); +``` + +If you have an individual request that you need to ensure has a specific timeout you can do that via the `timeout` option in `clientConfig`: + +```js +fetcher + .read('someData') + .params({id: ###}) + .clientConfig({timeout: 5000}) // wait 5 seconds for this request before timing it out + .end(function (err, data, meta) { + // handle err and/or data returned from data fetcher in this callback + }); +``` + ## 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`. +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. For example: @@ -147,9 +178,11 @@ var fetcher = new Fetcher({ corsPath: 'http://www.foo.com', xhrPath: '/fooProxy' }); -fetcher.read('service', { foo: 1 }, { - cors: true -}, callbackFn); +fetcher + .read('service') + .params({ foo: 1 }) + .clientConfig({ cors: true }) + .end(callbackFn); ``` Additionally, you can also customize how the GET URL is constructed by passing in the `constructGetUri` property when you execute your `read` call: @@ -169,10 +202,14 @@ var fetcher = new Fetcher({ corsPath: 'http://www.foo.com', xhrPath: '/fooProxy' }); -fetcher.read('service', { foo: 1 }, { - cors: true, - constructGetUri: customConstructGetUri -}, callbackFn); +fetcher + .read('service') + .params({ foo: 1 }) + .clientConfig({ + cors: true, + constructGetUri: customConstructGetUri + }) + .end(callbackFn); ``` @@ -201,22 +238,20 @@ This `_csrf` will be sent in all XHR requests as a query parameter so that it ca When calling a Fetcher service you can pass an optional config object. -When this call is made from the client the config object is used to define XHR request options and can be used to override default options: +When this call is made from the client, the config object is used to define XHR request options and can be used to override default options: ```js //app.js - client var config = { timeout: 6000, // Timeout (in ms) for each request - retry: { - interval: 100, // The start interval unit (in ms) - max_retries: 2 // Number of max retries - }, unsafeAllowRetry: false // for POST requests, whether to allow retrying this post }; -fetcher.read('data_api_fetcher', {id: ###}, config, function (err, data, meta) { - //handle err and/or data returned from data fetcher in this callback -}); +fetcher + .read('service') + .params({ id: 1 }) + .clientConfig(config) + .end(callbackFn); ``` For requests from the server, the config object is simply passed into the service being called. diff --git a/examples/simple/server/fetchers/flickr.js b/examples/simple/server/fetchers/flickr.js index 05f87935..dc788341 100644 --- a/examples/simple/server/fetchers/flickr.js +++ b/examples/simple/server/fetchers/flickr.js @@ -16,7 +16,7 @@ FlickrFetcher = { api_key: flickr_api_key, method: params.method || 'flickr.photos.getRecent', per_page: parseInt(params.per_page, 10) || 10, - format: config.format || 'json', + format: 'json', nojsoncallback: config.nojsoncallback || 1 }, url = flickr_api_root + '?' + querystring.stringify(paramsObj); diff --git a/examples/simple/shared/getFlickrPhotos.js b/examples/simple/shared/getFlickrPhotos.js index 0ea14f74..5218d132 100644 --- a/examples/simple/shared/getFlickrPhotos.js +++ b/examples/simple/shared/getFlickrPhotos.js @@ -3,18 +3,17 @@ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ module.exports = function flickrRead (fetcher, callback) { - fetcher.read('flickr', { - method: 'flickr.photos.getRecent', - per_page: 5 - }, - { - format: 'json' - }, - function(err, data) { - if (err) { - callback && callback(new Error('failed to fetch data ' + err.message)); - } - callback && callback(null, data); - }); + fetcher + .read('flickr') + .params({ + method: 'flickr.photos.getRecent', + per_page: 5 + }) + .end(function(err, data) { + if (err) { + callback && callback(new Error('failed to fetch data ' + err.message)); + } + callback && callback(null, data); + }); }; diff --git a/libs/fetcher.client.js b/libs/fetcher.client.js index 9ac89c45..710ce362 100644 --- a/libs/fetcher.client.js +++ b/libs/fetcher.client.js @@ -6,34 +6,22 @@ /*jslint plusplus:true,nomen:true */ /** - * Fetcher is a RESTful data store, that implements the CRUD interface. - * - * In addition, it allows request consolidation. - * If /api accepts multi-request in one HTTP request, remote store - * batches requests into one request. + * Fetcher is a CRUD interface for your data. * @module Fetcher */ var REST = require('./util/http.client'); var debug = require('debug')('FetchrClient'); var lodash = { - isArray: require('lodash/lang/isArray'), isFunction: require('lodash/lang/isFunction'), forEach: require('lodash/collection/forEach'), merge: require('lodash/object/merge'), - noop: require('lodash/utility/noop'), - pick: require('lodash/object/pick'), - some: require('lodash/collection/some'), - values: require('lodash/object/values') + noop: require('lodash/utility/noop') }; -var CORE_REQUEST_FIELDS = ['resource', 'operation', 'params', 'body']; var DEFAULT_GUID = 'g0'; var DEFAULT_XHR_PATH = '/api'; var DEFAULT_XHR_TIMEOUT = 3000; -// By default, wait for 20ms to trigger sweep of the queue, after an item is added to the queue. -var DEFAULT_BATCH_WINDOW = 20; var MAX_URI_LEN = 2048; var OP_READ = 'read'; -var NAME = 'FetcherClient'; var defaultConstructGetUri = require('./util/defaultConstructGetUri'); function parseResponse(response) { @@ -41,7 +29,7 @@ function parseResponse(response) { try { return JSON.parse(response.responseText); } catch (e) { - debug('json parse failed:' + e, 'error', NAME); + debug('json parse failed:' + e, 'error'); return null; } } @@ -49,456 +37,295 @@ function parseResponse(response) { } /** - * The queue sweeps and processes items in the queue when there are items in the queue. - * When a item is pushed into the queue, a timeout is set to guarantee the item will be processed soon. - * If there are any item in the queue before a item, this item can be processed sooner than its timeout. - * - * @class Queue + * A RequestClient instance represents a single fetcher request. + * The constructor requires `operation` (CRUD) and `resource`. + * @class RequestClient + * @param {String} operation The CRUD operation name: 'create|read|update|delete'. + * @param {String} resource name of fetcher/service + * @param {Object} options configuration options for Request * @constructor - * @param {String} id ID for the queue. - * @param {Object} config The configuration object. - * @param {Function} sweepFn The function to be called when queue is swept. - * @param {Array} sweepFn.items The current items in the queue. - * @param {Function} callback The function to be used to process a given item in the queue. - * @param {Object} callback.item The obj that was popped from the queue. */ -/* istanbul ignore next */ -function Queue(id, config, sweepFn, callback) { - this.id = id; - this.config = config || {}; - this._sweep = sweepFn; - this._cb = callback; - this._items = []; - this._timer = null; +function Request (operation, resource, options) { + if (!resource) { + throw new Error('Resource is required for a fetcher request'); + } + + this.operation = operation || OP_READ; + this.resource = resource; + this.options = { + xhrPath: options.xhrPath || DEFAULT_XHR_PATH, + xhrTimeout: options.xhrTimeout || DEFAULT_XHR_TIMEOUT, + corsPath: options.corsPath, + context: options.context || {} + }; + this._params = {}; + this._body = null; + this._clientConfig = {}; } /** - * Global unique id of the queue object. - * @property id - * @type String + * Add params to this fetcher request + * @method params + * @memberof Request + * @param {Object} params Information carried in query and matrix parameters in typical REST API + * @chainable */ +Request.prototype.params = function (params) { + this._params = params || {}; + return this; +}; + /** - * The configuration object for this queue. - * @property config - * @type Object + * Add body to this fetcher request + * @method body + * @memberof Request + * @param {Object} body The JSON object that contains the resource data being updated for this request. + * Not used for read and delete operations. + * @chainable */ -/* istanbul ignore next */ -Queue.prototype = { - /** - * Once an item is pushed to the queue, - * a timer will be set up immediate to sweep and process the items. The time of the - * timeout depends on queue's config (20ms by default). If it is set to a number <= 0, - * the queue will be swept and processed right away. - * @method push - * @param {Object} item The item object to be pushed to the queue - * @chainable - */ - push : function (item) { - if (!item) { - return this; - } - - if (this.config.wait <= 0) { - // process immediately - this._cb(item); - this._items = []; - return this; - } - - var self = this; - this._items.push(item); - - // setup timer - if (!this._timer) { - this._timer = setTimeout(function sweepInterval() { - var items = self._items; - self._items = []; - clearTimeout(self._timer); - self._timer = null; - items = self._sweep(items); - lodash.forEach(items, function eachItem(item) { - self._cb(item); - }); - }, this.config.wait); - } - return this; - } +Request.prototype.body = function (body) { + this._body = body || null; + return this; }; - /** - * Requests that are initiated within a time window are batched and sent to xhr endpoint. - * The received responses are split and routed back to the callback function assigned by initiator - * of each request. - * - * All requests go out from this store is via HTTP POST. Typical structure of the context param is: - *
-     * {
-     *   config: {
-     *     uri : '/api'
-     *   },
-     *   context: {
-     *     _csrf : '5YFuDK6R',
-     *     lang : 'en-US',
-     *     ...
-     *   }
-     * }
-     * 
- * - * @class FetcherClient - * @param {object} options configuration options for Fetcher - * @param {string} [options.xhrPath="/api"] The path for XHR requests - * @param {number} [options.xhrTimout=3000] Timeout in milliseconds for all XHR requests - * @param {number} [options.batchWindow=20] Number of milliseconds to wait to batch requests - * @param {Boolean} [options.corsPath] Base CORS path in case CORS is enabled - * @param {Object} [options.context] The context object that is propagated to all outgoing - * requests as query params. It can contain current-session/context data that should - * persist to all requests. - */ +/** + * Add clientConfig to this fetcher request + * @method clientConfig + * @memberof Request + * @param {Object} config config for this fetcher request + * @chainable + */ +Request.prototype.clientConfig = function (config) { + this._clientConfig = config || {}; + return this; +}; - function Fetcher (options) { - this.xhrPath = options.xhrPath || DEFAULT_XHR_PATH; - this.xhrTimeout = options.xhrTimeout || DEFAULT_XHR_TIMEOUT; - this.corsPath = options.corsPath; - this.batchWindow = options.batchWindow || DEFAULT_BATCH_WINDOW; - this.context = options.context || {}; +/** + * Execute this fetcher request and call callback. + * @method end + * @memberof Request + * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher/service is complete. + * @async + */ +Request.prototype.end = function (callback) { + var clientConfig = this._clientConfig; + var callback = callback || lodash.noop; + var use_post; + var allow_retry_post; + var uri = clientConfig.uri; + var requests; + var params; + var data; + + if (!uri) { + uri = clientConfig.cors ? this.options.corsPath : this.options.xhrPath; } - Fetcher.prototype = { - // ------------------------------------------------------------------ - // Data Access Wrapper Methods - // ------------------------------------------------------------------ - - /** - * create operation (create as in CRUD). - * @method create - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} body The JSON object that contains the resource data that is being created - * @param {Object} clientConfig The "config" object for per-request config data. - * @param {Function} callback callback convention is the same as Node.js - * @static - */ - create: function (resource, params, body, clientConfig, callback) { - this._sync(resource, 'create', params, body, clientConfig, callback); - }, - - /** - * read operation (read as in CRUD). - * @method read - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} clientConfig The "config" object for per-request config data. - * @param {Function} callback callback convention is the same as Node.js - * @static - */ - read: function (resource, params, clientConfig, callback) { - this._sync(resource, 'read', params, undefined, clientConfig, callback); - }, - - /** - * update operation (update as in CRUD). - * @method update - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} body The JSON object that contains the resource data that is being updated - * @param {Object} clientConfig The "config" object for per-request config data. - * @param {Function} callback callback convention is the same as Node.js - * @static - */ - update: function (resource, params, body, clientConfig, callback) { - this._sync(resource, 'update', params, body, clientConfig, callback); - }, - - /** - * delete operation (delete as in CRUD). - * @method delete - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} clientConfig The "config" object for per-request config data. - * @param {Function} callback callback convention is the same as Node.js - * @static - */ - 'delete': function (resource, params, clientConfig, callback) { - this._sync(resource, 'delete', params, undefined, clientConfig, callback); - }, - /** - * Sync data with remote API. - * @method _sync - * @param {String} resource The resource name - * @param {String} operation The CRUD operation name: 'create|read|update|delete'. - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} body The JSON object that contains the resource data that is being updated. Not used - * for read and delete operations. - * @param {Object} clientConfig The "config" object for per-request config data. - * @param {Function} callback callback convention is the same as Node.js - * @static - * @private - */ - _sync: function (resource, operation, params, body, clientConfig, callback) { - if (typeof clientConfig === 'function') { - callback = clientConfig; - clientConfig = {}; - } - - clientConfig = clientConfig || {}; - - var self = this, - request = { - resource: resource, - operation: operation, - params: params, - body: body, - clientConfig: clientConfig, - callback: callback - }; - - if (!lodash.isFunction(this.batch) || !clientConfig.consolidate) { - this.single(request); - return; - } - - // push request to queue so that it can be batched - /* istanbul ignore next */ - if (!this._q) { - this._q = new Queue(this.name, { - wait: Fetcher.batchWindow - }, function afterWait(requests) { - return self.batch(requests); - }, function afterBatch(batched) { - if (!batched) { - return; - } - if (!lodash.isArray(batched)) { - self.single(batched); - } else { - self.multi(batched); - } - }); - } - /* istanbul ignore next */ - this._q.push(request); - }, - // ------------------------------------------------------------------ - // Helper Methods - // ------------------------------------------------------------------ - /** - * Construct GET URI. You can override this for custom GET URI construction - * @method _defaultConstructGetUri - * @private - * @param {String} uri base URI - * @param {String} resource Resource name - * @param {Object} params Parameters to be serialized - * @param {Object} Configuration object - */ - _defaultConstructGetUri: defaultConstructGetUri, - /** - * @method _constructGroupUri - * @private - */ - _constructGroupUri: function (uri) { - var query = [], final_uri = uri; - lodash.forEach(this.context, function eachContext(v, k) { - query.push(k + '=' + encodeURIComponent(v)); - }); - if (query.length > 0) { - final_uri += '?' + query.sort().join('&'); - } - return final_uri; - }, - - // ------------------------------------------------------------------ - // Actual Data Access Methods - // ------------------------------------------------------------------ - - /** - * Execute a single request. - * @method single - * @param {Object} request - * @param {String} request.resource The resource name - * @param {String} request.operation The CRUD operation name: 'create|read|update|delete'. - * @param {Object} request.params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} request.body The JSON object that contains the resource data that is being updated. Not used - * for read and delete operations. - * @param {Object} request.clientConfig The "config" object for per-request config data. - * @param {Function} request.callback callback convention is the same as Node.js - * @protected - * @static - */ - single : function (request) { - if (!request) { - return; - } - - var clientConfig = request.clientConfig; - var callback = request.callback || lodash.noop; - var use_post; - var allow_retry_post; - var uri = clientConfig.uri; - var requests; - var params; - var data; - - if (!uri) { - uri = clientConfig.cors ? this.corsPath : this.xhrPath; - } - - use_post = request.operation !== OP_READ || clientConfig.post_for_read; - - if (!use_post) { - var getUriFn = lodash.isFunction(clientConfig.constructGetUri) ? clientConfig.constructGetUri : defaultConstructGetUri; - var get_uri = getUriFn.call(this, uri, request.resource, request.params, clientConfig, this.context); - if (get_uri.length <= MAX_URI_LEN) { - uri = get_uri; - } else { - use_post = true; - } - } - - if (!use_post) { - return REST.get(uri, {}, lodash.merge({xhrTimeout: this.xhrTimeout}, clientConfig), function getDone(err, response) { - if (err) { - debug('Syncing ' + request.resource + ' failed: statusCode=' + err.statusCode, 'info', NAME); - return callback(err); - } - callback(null, parseResponse(response)); - }); - } - - // individual request is also normalized into a request hash to pass to api - requests = {}; - requests[DEFAULT_GUID] = lodash.pick(request, CORE_REQUEST_FIELDS); - if (!request.body) { - delete requests[DEFAULT_GUID].body; - } - data = { - requests: requests, - context: this.context - }; // TODO: remove. leave here for now for backward compatibility - uri = this._constructGroupUri(uri); - allow_retry_post = (request.operation === OP_READ); - REST.post(uri, {}, data, lodash.merge({unsafeAllowRetry: allow_retry_post, xhrTimeout: this.xhrTimeout}, clientConfig), function postDone(err, response) { - if (err) { - debug('Syncing ' + request.resource + ' failed: statusCode=' + err.statusCode, 'info', NAME); - return callback(err); - } - var result = parseResponse(response); - if (result) { - result = result[DEFAULT_GUID] || {}; - } else { - result = {}; - } - callback(null, result.data); - }); - }, + use_post = this.operation !== OP_READ || clientConfig.post_for_read; + // We use GET request by default for READ operation, but you can override that behavior + // by specifying {post_for_read: true} in your request's clientConfig + if (!use_post) { + var getUriFn = lodash.isFunction(clientConfig.constructGetUri) ? clientConfig.constructGetUri : defaultConstructGetUri; + var get_uri = getUriFn.call(this, uri, this.resource, this._params, clientConfig, this.options.context); + if (!get_uri) { + // If a custom getUriFn returns falsy value, we should run defaultConstructGetUri + // TODO: Add test for this fallback + get_uri = defaultConstructGetUri.call(this, uri, this.resource, this._params, clientConfig, this.options.context); + } + if (get_uri.length <= MAX_URI_LEN) { + uri = get_uri; + } else { + use_post = true; + } + } - /** - * batch the requests. - * @method batch - * @param {Array} requests Array of requests objects to be batched. Each request is an object with properties: - * `resource`, `operation, `params`, `body`, `clientConfig`, `callback`. - * @return {Array} the request batches. - * @protected - * @static - */ - batch : /* istanbul ignore next */ function (requests) { - if (!lodash.isArray(requests) || requests.length <= 1) { - return requests; + if (!use_post) { + return REST.get(uri, {}, lodash.merge({xhrTimeout: this.options.xhrTimeout}, clientConfig), function getDone(err, response) { + if (err) { + debug('Syncing ' + this.resource + ' failed: statusCode=' + err.statusCode, 'info'); + return callback(err); } + callback(null, parseResponse(response)); + }); + } - var batched, - groups = {}; - - lodash.forEach(requests, function eachRequest(request) { - var uri, batch, group_id; - if (request.clientConfig) { - uri = request.clientConfig.uri; - - if (!uri) { - uri = clientConfig.cors ? this.corsPath : this.xhrPath; - } + // individual request is also normalized into a request hash to pass to api + requests = {}; + requests[DEFAULT_GUID] = { + resource: this.resource, + operation: this.operation, + params: this._params + }; + if (this._body) { + requests[DEFAULT_GUID].body = this._body; + } + data = { + requests: requests, + context: this.options.context + }; // TODO: remove. leave here for now for backward compatibility + uri = this._constructGroupUri(uri); + allow_retry_post = (this.operation === OP_READ); + REST.post(uri, {}, data, lodash.merge({unsafeAllowRetry: allow_retry_post, xhrTimeout: this.options.xhrTimeout}, clientConfig), function postDone(err, response) { + if (err) { + debug('Syncing ' + this.resource + ' failed: statusCode=' + err.statusCode, 'info'); + return callback(err); + } + var result = parseResponse(response); + if (result) { + result = result[DEFAULT_GUID] || {}; + } else { + result = {}; + } + callback(null, result.data); + }); +}; - batch = request.clientConfig.batch; - } - group_id = 'uri:' + uri; - if (batch) { - group_id += ';batch:' + batch; - } - if (!groups[group_id]) { - groups[group_id] = []; - } - groups[group_id].push(request); - }); - batched = lodash.values(groups); +/** + * Build a final uri by adding query params to base uri from this.context + * @method _constructGroupUri + * @param {String} uri the base uri + * @private + */ +Request.prototype._constructGroupUri = function (uri) { + var query = []; + var final_uri = uri; + lodash.forEach(this.options.context, function eachContext(v, k) { + query.push(k + '=' + encodeURIComponent(v)); + }); + if (query.length > 0) { + final_uri += '?' + query.sort().join('&'); + } + return final_uri; +}; - if (batched.length < requests.length) { - debug(requests.length + ' requests batched into ' + batched.length, 'info', NAME); - } - return batched; - }, +/** + * Fetcher class for the client. Provides CRUD methods. + * @class FetcherClient + * @param {object} options configuration options for Fetcher + * @param {string} [options.xhrPath="/api"] The path for XHR requests + * @param {number} [options.xhrTimout=3000] Timeout in milliseconds for all XHR requests + * @param {Boolean} [options.corsPath] Base CORS path in case CORS is enabled + * @param {Object} [options.context] The context object that is propagated to all outgoing + * requests as query params. It can contain current-session/context data that should + * persist to all requests. + */ - /** - * Execute multiple requests that have been batched together. - * @method single - * @param {Array} requests The request batch. Each item in this array is a request object with properties: - * `resource`, `operation, `params`, `body`, `clientConfig`, `callback`. - * @protected - * @static - */ - multi : /* istanbul ignore next */ function (requests) { - var uri, - data, - clientConfig, - allow_retry_post = true, - request_map = {}; +function Fetcher (options) { + this.options = options || {}; +} - lodash.some(requests, function findConfig(request) { - if (request.clientConfig) { - clientConfig = request.clientConfig; - return true; - } - return false; - }, this); +Fetcher.prototype = { + // ------------------------------------------------------------------ + // Data Access Wrapper Methods + // ------------------------------------------------------------------ - uri = clientConfig.uri || this.xhrPath; + /** + * create operation (create as in CRUD). + * @method create + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} body The JSON object that contains the resource data that is being created + * @param {Object} clientConfig The "config" object for per-request config data. + * @param {Function} callback callback convention is the same as Node.js + * @static + */ + create: function (resource, params, body, clientConfig, callback) { + var request = new Request('create', resource, this.options); + if (1 === arguments.length) { + return request; + } + // TODO: Remove below this line in release after next + if (typeof clientConfig === 'function') { + callback = clientConfig; + clientConfig = {}; + } + request + .params(params) + .body(body) + .clientConfig(clientConfig) + .end(callback) + }, - data = { - requests: {}, - context: this.context - }; // TODO: remove. leave here for now for backward compatibility + /** + * read operation (read as in CRUD). + * @method read + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} clientConfig The "config" object for per-request config data. + * @param {Function} callback callback convention is the same as Node.js + * @static + */ + read: function (resource, params, clientConfig, callback) { + var request = new Request('read', resource, this.options); + if (1 === arguments.length) { + return request; + } + // TODO: Remove below this line in release after next + if (typeof clientConfig === 'function') { + callback = clientConfig; + clientConfig = {}; + } + request + .params(params) + .clientConfig(clientConfig) + .end(callback) + }, - lodash.forEach(requests, function eachRequest(request, i) { - var guid = 'g' + i; - data.requests[guid] = lodash.pick(request, CORE_REQUEST_FIELDS); - request_map[guid] = request; - if (request.operation !== OP_READ) { - allow_retry_post = false; - } - }); + /** + * update operation (update as in CRUD). + * @method update + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} body The JSON object that contains the resource data that is being updated + * @param {Object} clientConfig The "config" object for per-request config data. + * @param {Function} callback callback convention is the same as Node.js + * @static + */ + update: function (resource, params, body, clientConfig, callback) { + var request = new Request('update', resource, this.options); + if (1 === arguments.length) { + return request; + } + // TODO: Remove below this line in release after next + if (typeof clientConfig === 'function') { + callback = clientConfig; + clientConfig = {}; + } + request + .params(params) + .body(body) + .clientConfig(clientConfig) + .end(callback) + }, - uri = this._constructGroupUri(uri); - REST.post(uri, {}, data, lodash.merge({unsafeAllowRetry: allow_retry_post}, clientConfig), function postDone(err, response) { - if (err) { - lodash.forEach(requests, function passErrorToEachRequest(request) { - request.callback(err); - }); - return; - } - var result = parseResponse(response); - // split result for requests, so that each request gets back only the data that was originally requested - lodash.forEach(request_map, function passREsultToEachRequest(request, guid) { - var res = (result && result[guid]) || {}; - if (request.callback) { - request.callback(res.err || null, res.data || null); - } - }); - }); + /** + * delete operation (delete as in CRUD). + * @method delete + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} clientConfig The "config" object for per-request config data. + * @param {Function} callback callback convention is the same as Node.js + * @static + */ + 'delete': function (resource, params, clientConfig, callback) { + var request = new Request('delete', resource, this.options); + if (1 === arguments.length) { + return request; } - }; + // TODO: Remove below this line in release after next + if (typeof clientConfig === 'function') { + callback = clientConfig; + clientConfig = {}; + } + request + .params(params) + .clientConfig(clientConfig) + .end(callback) + } +}; - module.exports = Fetcher; +module.exports = Fetcher; diff --git a/libs/fetcher.js b/libs/fetcher.js index 051b5443..53ba64e7 100644 --- a/libs/fetcher.js +++ b/libs/fetcher.js @@ -3,9 +3,6 @@ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ -/** - * list of registered fetchers - */ var OP_READ = 'read'; var OP_CREATE = 'create'; var OP_UPDATE = 'update'; @@ -30,308 +27,397 @@ function parseParamValues (params) { }, {}); } -/* - * @module createFetcherClass - * @param {object} options + +/** + * A Request instance represents a single fetcher request. + * The constructor requires `operation` (CRUD) and `resource`. + * @class Request + * @param {String} operation The CRUD operation name: 'create|read|update|delete'. + * @param {String} resource name of service + * @param {Object} options configuration options for Request + * @param {Object} [options.req] The request object from express/connect. It can contain per-request/context data. + * @constructor */ +function Request (operation, resource, options) { + if (!resource) { + throw new Error('Resource is required for a fetcher request'); + } + this.operation = operation || OP_READ; + this.resource = resource; + options = options || {}; + this.req = options.req || {}; + this._params = {}; + this._body = null; + this._clientConfig = {}; +} - /** - * @class Fetcher - * @param {Object} options configuration options for Fetcher - * @param {Object} [options.req] The request object. It can contain per-request/context data. - * @param {string} [options.xhrPath="/api"] The path for XHR requests. Will be ignored server side. - * @constructor - */ - function Fetcher(options) { - this.options = options || {}; - this.req = this.options.req || {}; +/** + * Add params to this fetcher request + * @method params + * @memberof Request + * @param {Object} params Information carried in query and matrix parameters in typical REST API + * @chainable + */ +Request.prototype.params = function (params) { + this._params = params; + return this; +}; +/** + * Add body to this fetcher request + * @method body + * @memberof Request + * @param {Object} body The JSON object that contains the resource data being updated for this request. + * Not used for read and delete operations. + * @chainable + */ +Request.prototype.body = function (body) { + this._body = body; + return this; +}; +/** + * Add clientConfig to this fetcher request + * @method config + * @memberof Request + * @param {Object} config config for this fetcher request + * @chainable + */ +Request.prototype.clientConfig = function (config) { + this._clientConfig = config; + return this; +}; +/** + * Execute this fetcher request and call callback. + * @method end + * @memberof Request + * @param {Fetcher~fetcherCallback} callback callback invoked when service is complete. + */ +Request.prototype.end = function (callback) { + var args = [this.req, this.resource, this._params, this._clientConfig, callback]; + var op = this.operation; + if ((op === OP_CREATE) || (op === OP_UPDATE)) { + args.splice(3, 0, this._body); } - Fetcher.fetchers = {}; + var service = Fetcher.getService(this.resource); + service[op].apply(service, args); +}; - /** - * @method registerFetcher - * @memberof Fetcher - * @param {Function} fetcher - */ - Fetcher.registerFetcher = function (fetcher) { - if (!fetcher || !fetcher.name) { - throw new Error('Fetcher is not defined correctly'); - } - Fetcher.fetchers[fetcher.name] = fetcher; - debug('fetcher ' + fetcher.name + ' added'); - return; - }; - - /** - * @method getFetcher - * @memberof Fetcher - * @param {String} name of fetcher/service - * @returns {Function} fetcher - */ - Fetcher.getFetcher = function (name) { - //Access fetcher by name - var fetcher = Fetcher.isRegistered(name); - if (!fetcher) { - throw new Error('Fetcher "' + name + '" could not be found'); - } - return fetcher; - }; +/** + * Fetcher class for the server. + * Provides interface to register data services and + * to later access those services. + * @class Fetcher + * @param {Object} options configuration options for Fetcher + * @param {Object} [options.req] The express request object. It can contain per-request/context data. + * @param {string} [options.xhrPath="/api"] The path for XHR requests. Will be ignored server side. + * @constructor + */ +function Fetcher (options) { + this.options = options || {}; + this.req = this.options.req || {}; +} - /** - * @method isRegistered - * @memberof Fetcher - * @param {String} name of fetcher/service - * @returns {Boolean} true if fetcher with name was registered - */ - Fetcher.isRegistered = function (name) { - return name && Fetcher.fetchers[name.split('.')[0]]; - }; +Fetcher.services = {}; - /** - * @method middleware - * @memberof Fetcher - * @returns {Function} middleware - * @param {Object} req - * @param {Object} res - * @param {Object} next - */ - Fetcher.middleware = function () { - return function (req, res, next) { - var request; - var error; +/** + * DEPRECATED + * Register a data fetcher + * @method registerFetcher + * @memberof Fetcher + * @param {Function} fetcher + */ +Fetcher.registerFetcher = function (fetcher) { + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('Fetcher.registerFetcher is deprecated. ' + + // 'Please use Fetcher.registerService instead.'); + // } + return Fetcher.registerService(fetcher); +}; - if (req.method === GET) { - var path = req.path.substr(1).split(';'); - var resource = path.shift(); +/** + * Register a data service + * @method registerService + * @memberof Fetcher + * @param {Function} service + */ +Fetcher.registerService = function (fetcher) { + if (!fetcher || !fetcher.name) { + throw new Error('Service is not defined correctly'); + } + Fetcher.services[fetcher.name] = fetcher; + debug('fetcher ' + fetcher.name + ' added'); + return; +}; - if (!Fetcher.isRegistered(resource)) { - error = fumble.http.badRequest('Invalid Fetchr Access', { - debug: 'Bad resource ' + resource - }); - error.source = 'fetchr'; - return next(error); - } +/** + * DEPRECATED + * Retrieve a data fetcher by name + * @method getFetcher + * @memberof Fetcher + * @param {String} name of fetcher + * @returns {Function} fetcher + */ +Fetcher.getFetcher = function (name) { + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('Fetcher.getFetcher is deprecated. ' + + // 'Please use Fetcher.getService instead.'); + // } + return Fetcher.getService(name); +}; - request = { - req: req, - resource: resource, - operation: OP_READ, - params: parseParamValues(qs.parse(path.join('&'))), - config: {}, - callback: function (err, data, meta) { - meta = meta || {}; - if (meta.headers) { - res.set(meta.headers); - } - if (err) { - res.status(err.statusCode || 400).json({ - message: err.message || 'request failed' - }); - return; - } - res.status(meta.statusCode || 200).json(data); - } - }; - } else { - var requests = req.body && req.body.requests; +/** + * Retrieve a data service by name + * @method getService + * @memberof Fetcher + * @param {String} name of service + * @returns {Function} service + */ +Fetcher.getService = function (name) { + //Access service by name + var service = Fetcher.isRegistered(name); + if (!service) { + throw new Error('Service "' + name + '" could not be found'); + } + return service; +}; - if (!requests || Object.keys(requests).length === 0) { - error = fumble.http.badRequest('Invalid Fetchr Access', { - debug: 'No resources' - }); - error.source = 'fetchr'; - return next(error); - } +/** + * Returns true if service with name has been registered + * @method isRegistered + * @memberof Fetcher + * @param {String} name of service + * @returns {Boolean} true if service with name was registered + */ +Fetcher.isRegistered = function (name) { + return name && Fetcher.services[name.split('.')[0]]; +}; - var DEFAULT_GUID = 'g0'; - var singleRequest = requests[DEFAULT_GUID]; +/** + * Returns express/connect middleware for Fetcher + * @method middleware + * @memberof Fetcher + * @returns {Function} middleware + * @param {Object} req + * @param {Object} res + * @param {Object} next + */ +Fetcher.middleware = function () { + return function (req, res, next) { + var request; + var error; - if (!Fetcher.isRegistered(singleRequest.resource)) { - error = fumble.http.badRequest('Invalid Fetchr Access', { - debug: 'Bad resource ' + singleRequest.resource - }); - error.source = 'fetchr'; - return next(error); - } + if (req.method === GET) { + var path = req.path.substr(1).split(';'); + var resource = path.shift(); - request = { - req: req, - resource: singleRequest.resource, - operation: singleRequest.operation, - params: singleRequest.params, - body: singleRequest.body || {}, - config: {}, - callback: function(err, data, meta) { - meta = meta || {}; - if (meta.headers) { - res.set(meta.headers); - } - if(err) { - res.status(err.statusCode || 400).json({ - message: err.message || 'request failed' - }); - return; - } - var responseObj = {}; - responseObj[DEFAULT_GUID] = {data: data}; - res.status(meta.statusCode || 200).json(responseObj); - } - }; + if (!Fetcher.isRegistered(resource)) { + error = fumble.http.badRequest('Invalid Fetchr Access', { + debug: 'Bad resource ' + resource + }); + error.source = 'fetchr'; + return next(error); } + request = new Request(OP_READ, resource, {req: req}); + request + .params(parseParamValues(qs.parse(path.join('&')))) + .end(function (err, data, meta) { + meta = meta || {}; + if (meta.headers) { + res.set(meta.headers); + } + if (err) { + res.status(err.statusCode || 400).json({ + message: err.message || 'request failed' + }); + return; + } + res.status(meta.statusCode || 200).json(data); + }); + } else { + var requests = req.body && req.body.requests; - Fetcher.single(request); - // TODO: Batching and multi requests - }; - }; - - - // ------------------------------------------------------------------ - // Data Access Wrapper Methods - // ------------------------------------------------------------------ - - /** - * Execute a single request. - * @method single - * @memberof Fetcher - * @param {Object} request - * @param {String} request.req The req object from express/connect - * @param {String} request.resource The resource name - * @param {String} request.operation The CRUD operation name: 'create|read|update|delete'. - * @param {Object} request.params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} request.body The JSON object that contains the resource data that is being updated. Not used - * for read and delete operations. - * @param {Object} request.config The config object. It can contain "config" for per-request config data. - * @param {Fetcher~fetcherCallback} request.callback callback invoked when fetcher is complete. - * @protected - * @static - */ - Fetcher.single = function (request) { - var fetcher = Fetcher.getFetcher(request.resource), - op = request.operation, - req = request.req, - resource = request.resource, - params = request.params, - body = request.body, - config = request.config, - callback = request.callback, - args; + if (!requests || Object.keys(requests).length === 0) { + error = fumble.http.badRequest('Invalid Fetchr Access', { + debug: 'No resources' + }); + error.source = 'fetchr'; + return next(error); + } - if (typeof config === 'function') { - callback = config; - config = {}; - } + var DEFAULT_GUID = 'g0'; + var singleRequest = requests[DEFAULT_GUID]; - args = [req, resource, params, config, callback]; + if (!Fetcher.isRegistered(singleRequest.resource)) { + error = fumble.http.badRequest('Invalid Fetchr Access', { + debug: 'Bad resource ' + singleRequest.resource + }); + error.source = 'fetchr'; + return next(error); + } - if ((op === OP_CREATE) || (op === OP_UPDATE)) { - args.splice(3, 0, body); + request = new Request(singleRequest.operation, singleRequest.resource, {req: req}); + request + .params(singleRequest.params) + .body(singleRequest.body || {}) + .end(function(err, data, meta) { + meta = meta || {}; + if (meta.headers) { + res.set(meta.headers); + } + if(err) { + res.status(err.statusCode || 400).json({ + message: err.message || 'request failed' + }); + return; + } + var responseObj = {}; + responseObj[DEFAULT_GUID] = {data: data}; + res.status(meta.statusCode || 200).json(responseObj); + }); } - - fetcher[op].apply(fetcher, args); + // TODO: Batching and multi requests }; +}; - // ------------------------------------------------------------------ - // CRUD Methods - // ------------------------------------------------------------------ +// ------------------------------------------------------------------ +// CRUD Data Access Wrapper Methods +// ------------------------------------------------------------------ - /** - * read operation (read as in CRUD). - * @method read - * @memberof Fetcher.prototype - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. - * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. - * @static - */ - Fetcher.prototype.read = function (resource, params, config, callback) { - var request = { - req: this.req, - resource: resource, - operation: 'read', - params: params, - config: config, - callback: callback - }; - Fetcher.single(request); - }; - /** - * create operation (create as in CRUD). - * @method create - * @memberof Fetcher.prototype - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} body The JSON object that contains the resource data that is being created - * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. - * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. - * @static - */ - Fetcher.prototype.create = function (resource, params, body, config, callback) { - var request = { - req: this.req, - resource: resource, - operation: 'create', - params: params, - body: body, - config: config, - callback: callback - }; - Fetcher.single(request); - }; - /** - * update operation (update as in CRUD). - * @method update - * @memberof Fetcher.prototype - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} body The JSON object that contains the resource data that is being updated - * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. - * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. - * @static - */ - Fetcher.prototype.update = function (resource, params, body, config, callback) { - var request = { - req: this.req, - resource: resource, - operation: 'update', - params: params, - body: body, - config: config, - callback: callback - }; - Fetcher.single(request); - }; - /** - * delete operation (delete as in CRUD). - * @method delete - * @memberof Fetcher.prototype - * @param {String} resource The resource name - * @param {Object} params The parameters identify the resource, and along with information - * carried in query and matrix parameters in typical REST API - * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. - * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. - * @static - */ - Fetcher.prototype['delete'] = function (resource, params, config, callback) { - var request = { - req: this.req, - resource: resource, - operation: 'delete', - params: params, - config: config, - callback: callback - }; - Fetcher.single(request); - }; +/** + * read operation (read as in CRUD). + * @method read + * @memberof Fetcher.prototype + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. + * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. + * @static + */ +Fetcher.prototype.read = function (resource, params, config, callback) { + var request = new Request('read', resource, {req: this.req}); + if (1 === arguments.length) { + return request; + } + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('The recommended way to use fetcher\'s .read method is \n' + + // '.read(\'' + resource + '\').params({foo:bar}).end(callback);'); + // } + // TODO: Remove below this line in release after next + if (typeof config === 'function') { + callback = config; + config = {}; + } + request + .params(params) + .clientConfig(config) + .end(callback) +}; +/** + * create operation (create as in CRUD). + * @method create + * @memberof Fetcher.prototype + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} body The JSON object that contains the resource data that is being created + * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. + * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. + * @static + */ +Fetcher.prototype.create = function (resource, params, body, config, callback) { + var request = new Request('create', resource, {req: this.req}); + if (1 === arguments.length) { + return request; + } + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('The recommended way to use fetcher\'s .create method is \n' + + // '.create(\'' + resource + '\').params({foo:bar}).body({}).end(callback);'); + // } + // TODO: Remove below this line in release after next + if (typeof config === 'function') { + callback = config; + config = {}; + } + request + .params(params) + .body(body) + .clientConfig(config) + .end(callback) +}; +/** + * update operation (update as in CRUD). + * @method update + * @memberof Fetcher.prototype + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} body The JSON object that contains the resource data that is being updated + * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. + * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. + * @static + */ +Fetcher.prototype.update = function (resource, params, body, config, callback) { + var request = new Request('update', resource, {req: this.req}); + if (1 === arguments.length) { + return request; + } + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('The recommended way to use fetcher\'s .update method is \n' + + // '.update(\'' + resource + '\').params({foo:bar}).body({}).end(callback);'); + // } + // TODO: Remove below this line in release after next + if (typeof config === 'function') { + callback = config; + config = {}; + } + request + .params(params) + .body(body) + .clientConfig(config) + .end(callback) +}; +/** + * delete operation (delete as in CRUD). + * @method delete + * @memberof Fetcher.prototype + * @param {String} resource The resource name + * @param {Object} params The parameters identify the resource, and along with information + * carried in query and matrix parameters in typical REST API + * @param {Object} [config={}] The config object. It can contain "config" for per-request config data. + * @param {Fetcher~fetcherCallback} callback callback invoked when fetcher is complete. + * @static + */ +Fetcher.prototype['delete'] = function (resource, params, config, callback) { + var request = new Request('delete', resource, {req: this.req}); + if (1 === arguments.length) { + return request; + } + + // TODO: Uncomment warnings in next minor release + // if ('production' !== process.env.NODE_ENV) { + // console.warn('The recommended way to use fetcher\'s .read method is \n' + + // '.read(\'' + resource + '\').params({foo:bar}).end(callback);'); + // } + // TODO: Remove below this line in release after next + if (typeof config === 'function') { + callback = config; + config = {}; + } + request + .params(params) + .clientConfig(config) + .end(callback) +}; - module.exports = Fetcher; +module.exports = Fetcher; /** * @callback Fetcher~fetcherCallback diff --git a/tests/unit/libs/fetcher.client.js b/tests/unit/libs/fetcher.client.js index 726f73bb..a5f0be33 100644 --- a/tests/unit/libs/fetcher.client.js +++ b/tests/unit/libs/fetcher.client.js @@ -3,202 +3,167 @@ * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. */ /*jshint expr:true*/ -/*globals before,after,describe,it */ +/*globals before,beforeEach,after,afterEach,describe,it */ "use strict"; var libUrl = require('url'); -var expect = require('chai').expect, - mockery = require('mockery'), - Fetcher, - fetcher; - +var expect = require('chai').expect; +var mockery = require('mockery'); +var Fetcher; +var fetcher; var app = require('../../mock/app'); -var corsApp = require('../../mock/corsApp'); var supertest = require('supertest'); var request = require('request'); var qs = require('qs'); var resource = 'mock_service'; - +var DEFAULT_XHR_PATH = '/api'; + +var validateGET; +var validatePOST; +var validateHTTP = function (options) { + options = options || {}; + validateGET = options.validateGET; + validatePOST = options.validatePOST; +}; describe('Client Fetcher', function () { - - before(function () { + beforeEach(function () { + mockery.registerMock('./util/http.client', { + get: function (url, headers, config, callback) { + validateGET && validateGET(url, headers, config); + supertest(app) + .get(url) + .expect(200) + .end(function (err, res) { + callback(err, { + responseText: res.text + }); + }); + }, + post : function (url, headers, body, config, callback) { + expect(url).to.not.be.empty; + expect(callback).to.exist; + expect(body).to.exist; + validatePOST && validatePOST(url, headers, body, config); + supertest(app) + .post(url) + .send(body) + .expect(200) + .end(function (err, res) { + callback(err, { + responseText: res.text + }); + }); + } + }); mockery.enable({ useCleanCache: true, warnOnUnregistered: false }); + Fetcher = require('../../../libs/fetcher.client'); + validateHTTP(); // Important, reset validate functions }); - - describe('#CRUD', function () { - - function testCrud (resource, params, body, config, callback) { - it('should handle CREATE', function (done) { + afterEach(function () { + mockery.deregisterAll(); + mockery.disable(); + }); + var testCrud = function (it, resource, params, body, config, callback) { + it('should handle CREATE', function (done) { var operation = 'create'; - fetcher[operation](resource, params, body, config, callback(operation, done)); + fetcher + [operation](resource) + .params(params) + .body(body) + .clientConfig(config) + .end(callback(operation, done)); }); it('should handle READ', function (done) { var operation = 'read'; - fetcher[operation](resource, params, config, callback(operation, done)); + fetcher + [operation](resource) + .params(params) + .clientConfig(config) + .end(callback(operation, done)); }); it('should handle UPDATE', function (done) { var operation = 'update'; - fetcher[operation](resource, params, body, config, callback(operation, done)); + fetcher + [operation](resource) + .params(params) + .body(body) + .clientConfig(config) + .end(callback(operation, done)); }); it('should handle DELETE', function (done) { var operation = 'delete'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); - } - - describe('with CORS', function () { - var corsPath = 'http://localhost:3001'; - var params = { - uuids: ['1','2','3','4','5'], - corsDomain: 'test1' - }, - body = { stuff: 'is'}, - context = { - _csrf: 'stuff' - }, - callback = function(operation, done) { - return function(err, data) { - if (err){ - done(err); - } - if (data) { - expect(data).to.deep.equal(params); - } - done(); - }; - }; - - before(function(){ - mockery.resetCache(); - mockery.registerMock('./util/http.client', { - get: function (url, headers, config, done) { - expect(url).to.contain(corsPath); - request(url, function (err, res, body) { - if (err) throw err; - expect(res.statusCode).to.equal(200); - done(null, { - responseText: body - }); - }); - }, - post : function (url, headers, body, config, done) { - expect(url).to.not.be.empty; - expect(url).to.equal(corsPath + '?_csrf=' + context._csrf); - expect(callback).to.exist; - expect(body).to.exist; - request.post({ - url: url, - body: JSON.stringify(body) - }, function (err, res, respBody) { - if (err) throw err; - expect(res.statusCode).to.equal(200); - done(null, { - responseText: respBody - }); - }); - } - }); - - Fetcher = require('../../../libs/fetcher.client'); - fetcher = new Fetcher({ - context: context, - corsPath: corsPath - }); + fetcher + [operation](resource) + .params(params) + .clientConfig(config) + .end(callback(operation, done)); }); + }; - after(function() { - mockery.deregisterAll(); + describe('CRUD Interface', function () { + beforeEach(function () { + var context = {_csrf: 'stuff'}; + fetcher = new Fetcher({ + context: context }); - - function constructGetUri (uri, resource, params, config) { - if (config.cors) { - return uri + '/' + resource + '?' + qs.stringify(params, { arrayFormat: 'repeat' }); + validateHTTP({ + validateGET: function (url, headers, config) { + expect(url).to.contain(DEFAULT_XHR_PATH + '/' + resource); + expect(url).to.contain('?_csrf=' + context._csrf); + }, + validatePOST: function (url, headers, body, config) { + expect(url).to.equal(DEFAULT_XHR_PATH + '?_csrf=' + context._csrf); } - } - - testCrud(resource, params, body, { - cors: true, - constructGetUri: constructGetUri - }, callback); + }); }); - - describe('without CORS', function () { - var params = { - uuids: [1,2,3,4,5], - object: { - nested: { - object: true - } - }, - category: '', - selected_filter: 'YPROP:TOPSTORIES' - }, - body = { stuff: 'is'}, - context = { - _csrf: 'stuff' - }, - callback = function(operation, done) { - return function(err, data) { - if (err){ - done(err); - } - expect(data.operation).to.exist; - expect(data.operation.name).to.equal(operation); - expect(data.operation.success).to.equal(true); - expect(data.args).to.exist; - expect(data.args.resource).to.equal(resource); - expect(data.args.params).to.eql(params); - done(); - }; - }; - - before(function(){ - mockery.resetCache(); - mockery.registerMock('./util/http.client', { - get: function (url, headers, config, done) { - supertest(app) - .get(url) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - }, - post : function (url, headers, body, config, done) { - expect(url).to.not.be.empty; - expect(url).to.equal('/api?_csrf='+context._csrf); - expect(callback).to.exist; - expect(body).to.exist; - supertest(app) - .post(url) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); + var params = { + uuids: ['1','2','3','4','5'] + }; + var body = { stuff: 'is'}; + var config = {}; + var callback = function(operation, done) { + return function(err, data) { + if (err){ + done(err); } - }); - - Fetcher = require('../../../libs/fetcher.client'); - fetcher = new Fetcher({ - context: context - }); + expect(data.operation).to.exist; + expect(data.operation.name).to.equal(operation); + expect(data.operation.success).to.equal(true); + expect(data.args).to.exist; + expect(data.args.resource).to.equal(resource); + expect(data.args.params).to.eql(params); + done(); + }; + }; + describe('should work superagent style', function (done) { + testCrud(it, resource, params, body, config, callback); + it('should throw if no resource is given', function () { + expect(fetcher.read).to.throw('Resource is required for a fetcher request'); }); - - after(function() { - mockery.deregisterAll(); + }); + describe('should be backwards compatible', function (done) { + // with config + it('should handle CREATE', function (done) { + var operation = 'create'; + fetcher[operation](resource, params, body, config, callback(operation, done)); + }); + it('should handle READ', function (done) { + var operation = 'read'; + fetcher[operation](resource, params, config, callback(operation, done)); + }); + it('should handle UPDATE', function (done) { + var operation = 'update'; + fetcher[operation](resource, params, body, config, callback(operation, done)); + }); + it('should handle DELETE', function (done) { + var operation = 'delete'; + fetcher[operation](resource, params, config, callback(operation, done)); }); - testCrud(resource, params, body, {}, callback); - + // without config it('should handle CREATE w/ no config', function (done) { var operation = 'create'; fetcher[operation](resource, params, body, callback(operation, done)); @@ -217,227 +182,166 @@ describe('Client Fetcher', function () { }); }); - describe('xhrTimeout', function () { - var DEFAULT_XHR_TIMEOUT = 3000; - var params = { - uuids: [1,2,3,4,5], - category: '' - }, - body = { stuff: 'is'}, - context = { - _csrf: 'stuff' - }, - config = {}, - callback = function(operation, done) { - return function(err, data) { - if (err){ - done(err); - } - done(); - }; + }); + describe('CORS', function () { + // start CORS app at localhost:3001 + var corsApp = require('../../mock/corsApp'); + var corsPath = 'http://localhost:3001'; + var params = { + uuids: ['1','2','3','4','5'], + corsDomain: 'test1' + }, + body = { stuff: 'is'}, + context = { + _csrf: 'stuff' + }, + callback = function(operation, done) { + return function(err, data) { + if (err){ + return done(err); + } + if (data) { + expect(data).to.deep.equal(params); + } + done(); }; - - describe('set xhrTimeout', function () { - before(function(){ - mockery.resetCache(); - mockery.registerMock('./util/http.client', { - get: function (url, headers, config, done) { - expect(config.xhrTimeout).to.equal(4000); - supertest(app) - .get(url) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - }, - post : function (url, headers, body, config, done) { - expect(config.xhrTimeout).to.equal(4000); - supertest(app) - .post(url) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - } - }); - - Fetcher = require('../../../libs/fetcher.client'); - - fetcher = new Fetcher({ - context: context, - xhrTimeout: 4000 - }); - }); - - after(function() { - mockery.deregisterAll(); - }); - - it('should handle CREATE w/ global xhrTimeout', function (done) { - var operation = 'create'; - fetcher[operation](resource, params, body, config, callback(operation, done)); - }); - - it('should handle READ w/ global xhrTimeout', function (done) { - var operation = 'read'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); - - it('should handle UPDATE w/ global xhrTimeout', function (done) { - var operation = 'update'; - fetcher[operation](resource, params, body, config, callback(operation, done)); - }); - - it('should handle DELETE w/ global xhrTimeout', function (done) { - var operation = 'delete'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); + }; + beforeEach(function() { + mockery.deregisterAll(); // deregister default http.client mock + mockery.registerMock('./util/http.client', { // register CORS http.client mock + get: function (url, headers, config, callback) { + expect(url).to.contain(corsPath); + var path = url.substr(corsPath.length); + // constructGetUri above doesn't implement csrf so we don't check csrf here + supertest(corsPath) + .get(path) + .expect(200) + .end(function (err, res) { + callback(err, { + responseText: res.text + }); + }); + }, + post : function (url, headers, body, config, callback) { + expect(url).to.not.be.empty; + expect(callback).to.exist; + expect(body).to.exist; + expect(url).to.equal(corsPath + '?_csrf=' + context._csrf); + var path = url.substring(corsPath.length); + supertest(corsPath) + .post(path) + .send(body) + .expect(200) + .end(function (err, res) { + callback(err, { + responseText: res.text + }); + }); + } }); + mockery.resetCache(); + Fetcher = require('../../../libs/fetcher.client'); + fetcher = new Fetcher({ + context: context, + corsPath: corsPath + }); + }); + afterEach(function () { + mockery.deregisterAll(); // deregister CORS http.client mock + }); - describe('set single call timeout', function () { - before(function(){ - config = {timeout: 5000}; - - mockery.resetCache(); - mockery.registerMock('./util/http.client', { - get: function (url, headers, config, done) { - expect(config.xhrTimeout).to.equal(4000); - expect(config.timeout).to.equal(5000); - supertest(app) - .get(url) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - }, - post : function (url, headers, body, config, done) { - expect(config.xhrTimeout).to.equal(4000); - expect(config.timeout).to.equal(5000); - supertest(app) - .post(url) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - } - }); - - Fetcher = require('../../../libs/fetcher.client'); - - fetcher = new Fetcher({ - context: context, - xhrTimeout: 4000 - }); - }); - after(function() { - mockery.deregisterAll(); - }); + function constructGetUri (uri, resource, params, config) { + if (config.cors) { + return uri + '/' + resource + '?' + qs.stringify(params, { arrayFormat: 'repeat' }); + } + } - it('should handle CREATE w/ config timeout', function (done) { - var operation = 'create'; - fetcher[operation](resource, params, body, config, callback(operation, done)); - }); + testCrud(it, resource, params, body, { + cors: true, + constructGetUri: constructGetUri + }, callback); + }); - it('should handle READ w/ config timeout', function (done) { - var operation = 'read'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); + describe('xhrTimeout', function () { + var DEFAULT_XHR_TIMEOUT = 3000; + var params = { + uuids: [1,2,3,4,5], + category: '' + }, + body = { stuff: 'is'}, + context = { + _csrf: 'stuff' + }, + config = {}, + callback = function(operation, done) { + return function(err, data) { + if (err){ + done(err); + } + done(); + }; + }; - it('should handle UPDATE w/ config timeout', function (done) { - var operation = 'update'; - fetcher[operation](resource, params, body, config, callback(operation, done)); + describe('should be configurable globally', function () { + beforeEach(function(){ + validateHTTP({ + validateGET: function (url, headers, config) { + expect(config.xhrTimeout).to.equal(4000); + }, + validatePOST: function (url, headers, body, config) { + expect(config.xhrTimeout).to.equal(4000); + } }); - it('should handle DELETE w/ config timeout', function (done) { - var operation = 'delete'; - fetcher[operation](resource, params, config, callback(operation, done)); + fetcher = new Fetcher({ + context: context, + xhrTimeout: 4000 }); }); - describe('should handle default', function () { - before(function(){ - config = {}; - mockery.resetCache(); - mockery.registerMock('./util/http.client', { - get: function (url, headers, config, done) { - expect(config.xhrTimeout).to.equal(DEFAULT_XHR_TIMEOUT); - supertest(app) - .get(url) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - }, - post : function (url, headers, body, config, done) { - expect(config.xhrTimeout).to.equal(DEFAULT_XHR_TIMEOUT); - supertest(app) - .post(url) - .send(body) - .expect(200) - .end(function (err, res) { - if (err) throw err; - done(null, { - responseText: res.text - }); - }); - } - }); - - Fetcher = require('../../../libs/fetcher.client'); - - fetcher = new Fetcher({ - context: context - }); - }); + testCrud(it, resource, params, body, config, callback); + }); - after(function() { - mockery.deregisterAll(); - app.cleanup(); + describe('should be configurable per each fetchr call', function () { + config = {timeout: 5000}; + beforeEach(function(){ + validateHTTP({ + validateGET: function (url, headers, config) { + expect(config.xhrTimeout).to.equal(4000); + expect(config.timeout).to.equal(5000); + }, + validatePOST: function (url, headers, body, config) { + expect(config.xhrTimeout).to.equal(4000); + expect(config.timeout).to.equal(5000); + } }); - - it('should handle CREATE w/ default timeout', function (done) { - var operation = 'create'; - fetcher[operation](resource, params, body, config, callback(operation, done)); + fetcher = new Fetcher({ + context: context, + xhrTimeout: 4000 }); + }); - it('should handle READ w/ default timeout', function (done) { - var operation = 'read'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); + testCrud(it, resource, params, body, config, callback); + }); - it('should handle UPDATE w/ default timeout', function (done) { - var operation = 'update'; - fetcher[operation](resource, params, body, config, callback(operation, done)); + describe('should default to DEFAULT_XHR_TIMEOUT of 3000', function () { + beforeEach(function(){ + validateHTTP({ + validateGET: function (url, headers, config) { + expect(config.xhrTimeout).to.equal(DEFAULT_XHR_TIMEOUT); + }, + validatePOST: function (url, headers, body, config) { + expect(config.xhrTimeout).to.equal(DEFAULT_XHR_TIMEOUT); + } }); - it('should handle DELETE w/ default timeout', function (done) { - var operation = 'delete'; - fetcher[operation](resource, params, config, callback(operation, done)); + fetcher = new Fetcher({ + context: context }); }); - }); - }); - after(function () { - mockery.disable(); + testCrud(it, resource, params, body, config, callback); + }); }); - }); diff --git a/tests/unit/libs/fetcher.js b/tests/unit/libs/fetcher.js index 5f6e8aaf..b5a13a33 100644 --- a/tests/unit/libs/fetcher.js +++ b/tests/unit/libs/fetcher.js @@ -18,40 +18,61 @@ var expect = chai.expect, qs = require('querystring'); describe('Server Fetcher', function () { - + beforeEach(function () { + Fetcher.registerService(mockService); + Fetcher.registerService(mockErrorService); + }); + afterEach(function () { + Fetcher.services = {}; // reset services + }); it('should register valid fetchers', function () { - var getFetcher = Fetcher.getFetcher.bind(fetcher); - expect(getFetcher).to.throw(Error, 'Fetcher "undefined" could not be found'); - getFetcher = Fetcher.getFetcher.bind(fetcher, mockService.name); - expect(Object.keys(Fetcher.fetchers)).to.have.length(0); - expect(getFetcher).to.throw(Error, 'Fetcher "' + mockService.name + '" could not be found'); - Fetcher.registerFetcher(mockService); - expect(Object.keys(Fetcher.fetchers)).to.have.length(1); - expect(getFetcher()).to.deep.equal(mockService); - Fetcher.registerFetcher(mockErrorService); - expect(Object.keys(Fetcher.fetchers)).to.have.length(2); + Fetcher.services = {}; // reset services so we can test getService and registerService methods + var getService = Fetcher.getService.bind(fetcher); + expect(getService).to.throw(Error, 'Service "undefined" could not be found'); + getService = Fetcher.getService.bind(fetcher, mockService.name); + expect(getService).to.throw(Error, 'Service "' + mockService.name + '" could not be found'); + expect(Object.keys(Fetcher.services)).to.have.length(0); + Fetcher.registerService(mockService); + expect(Object.keys(Fetcher.services)).to.have.length(1); + expect(getService()).to.deep.equal(mockService); + Fetcher.registerService(mockErrorService); + expect(Object.keys(Fetcher.services)).to.have.length(2); // valid vs invalid - var invalidFetcher = {not_name: 'test_name'}; - var validFetcher = {name: 'test_name'}; - var registerInvalidFetcher = Fetcher.registerFetcher.bind(fetcher, undefined); - expect(registerInvalidFetcher).to.throw(Error, 'Fetcher is not defined correctly'); - registerInvalidFetcher = Fetcher.registerFetcher.bind(fetcher, invalidFetcher); - expect(registerInvalidFetcher).to.throw(Error, 'Fetcher is not defined correctly'); - var registerValidFetcher = Fetcher.registerFetcher.bind(fetcher, validFetcher); - expect(registerValidFetcher).to.not.throw; - delete Fetcher.fetchers[validFetcher.name]; + var invalidService = {not_name: 'test_name'}; + var validService = {name: 'test_name'}; + var registerInvalidService = Fetcher.registerService.bind(fetcher, undefined); + expect(registerInvalidService).to.throw(Error, 'Service is not defined correctly'); + registerInvalidService = Fetcher.registerService.bind(fetcher, invalidService); + expect(registerInvalidService).to.throw(Error, 'Service is not defined correctly'); + var registerValidService = Fetcher.registerService.bind(fetcher, validService); + expect(registerValidService).to.not.throw; }); it('should get fetchers by resource and sub resource', function () { - var getFetcher = Fetcher.getFetcher.bind(fetcher, mockService.name); - expect(getFetcher).to.not.throw; - expect(getFetcher()).to.deep.equal(mockService); - getFetcher = Fetcher.getFetcher.bind(fetcher, mockService.name + '.subResource'); - expect(getFetcher).to.not.throw; - expect(getFetcher()).to.deep.equal(mockService); + var getService = Fetcher.getService.bind(fetcher, mockService.name); + expect(getService).to.not.throw; + expect(getService()).to.deep.equal(mockService); + getService = Fetcher.getService.bind(fetcher, mockService.name + '.subResource'); + expect(getService).to.not.throw; + expect(getService()).to.deep.equal(mockService); }); + describe('should be backwards compatible', function () { + it('#registerFetcher & #getFetcher', function () { + Fetcher.services = {}; // reset services so we can test getFetcher and registerFetcher methods + var getFetcher = Fetcher.getFetcher.bind(fetcher, mockService.name); + expect(getFetcher).to.throw; + Fetcher.registerFetcher(mockService); + expect(getFetcher).to.not.throw; + expect(getFetcher()).to.deep.equal(mockService); + getFetcher = Fetcher.getFetcher.bind(fetcher, mockService.name + '.subResource'); + expect(getFetcher).to.not.throw; + expect(getFetcher()).to.deep.equal(mockService); + }); + }); + + describe('#middleware', function () { describe('#POST', function() { it('should respond to POST api request', function (done) { @@ -401,7 +422,7 @@ describe('Server Fetcher', function () { }); }); - describe('#CRUD', function () { + describe('CRUD Interface', function () { var resource = mockService.name, params = {}, body = {}, @@ -417,39 +438,79 @@ describe('Server Fetcher', function () { done(); }; }; - - it('should handle CREATE', function (done) { - var operation = 'create'; - fetcher[operation](resource, params, body, config, callback(operation, done)); - }); - it('should handle CREATE w/ no config', function (done) { - var operation = 'create'; - fetcher[operation](resource, params, body, callback(operation, done)); - }); - it('should handle READ', function (done) { - var operation = 'read'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); - it('should handle READ w/ no config', function (done) { - var operation = 'read'; - fetcher[operation](resource, params, callback(operation, done)); - }); - it('should handle UPDATE', function (done) { - var operation = 'update'; - fetcher[operation](resource, params, body, config, callback(operation, done)); - }); - it('should handle UPDATE w/ no config', function (done) { - var operation = 'update'; - fetcher[operation](resource, params, body, callback(operation, done)); - }); - it('should handle DELETE', function (done) { - var operation = 'delete'; - fetcher[operation](resource, params, config, callback(operation, done)); - }); - it('should handle DELETE w/ no config', function (done) { - var operation = 'delete'; - fetcher[operation](resource, params, callback(operation, done)); + describe('should work superagent style', function () { + it('should throw if no resource is given', function () { + expect(fetcher.read).to.throw('Resource is required for a fetcher request'); + }); + it('should handle CREATE', function (done) { + var operation = 'create'; + fetcher + [operation](resource) + .params(params) + .body(body) + .clientConfig(config) + .end(callback(operation, done)); + }); + it('should handle READ', function (done) { + var operation = 'read'; + fetcher + [operation](resource) + .params(params) + .clientConfig(config) + .end(callback(operation, done)); + }); + it('should handle UPDATE', function (done) { + var operation = 'update'; + fetcher + [operation](resource) + .params(params) + .body(body) + .clientConfig(config) + .end(callback(operation, done)); + }); + it('should handle DELETE', function (done) { + var operation = 'delete'; + fetcher + [operation](resource) + .params(params) + .clientConfig(config) + .end(callback(operation, done)); + }); }); + describe('should be backwards compatible', function () { + it('should handle CREATE', function (done) { + var operation = 'create'; + fetcher[operation](resource, params, body, config, callback(operation, done)); + }); + it('should handle CREATE w/ no config', function (done) { + var operation = 'create'; + fetcher[operation](resource, params, body, callback(operation, done)); + }); + it('should handle READ', function (done) { + var operation = 'read'; + fetcher[operation](resource, params, config, callback(operation, done)); + }); + it('should handle READ w/ no config', function (done) { + var operation = 'read'; + fetcher[operation](resource, params, callback(operation, done)); + }); + it('should handle UPDATE', function (done) { + var operation = 'update'; + fetcher[operation](resource, params, body, config, callback(operation, done)); + }); + it('should handle UPDATE w/ no config', function (done) { + var operation = 'update'; + fetcher[operation](resource, params, body, callback(operation, done)); + }); + it('should handle DELETE', function (done) { + var operation = 'delete'; + fetcher[operation](resource, params, config, callback(operation, done)); + }); + it('should handle DELETE w/ no config', function (done) { + var operation = 'delete'; + fetcher[operation](resource, params, callback(operation, done)); + }); + }) }); });