Comparing version 0.5.44 to 0.5.45
@@ -1,7 +0,6 @@ | ||
Contributing Code to `fetchr` | ||
------------------------------- | ||
# Contributing Code to `fetchr` | ||
Please be sure to sign our [CLA][] before you submit pull requests or otherwise contribute to `fetchr`. This protects developers, who rely on [BSD license][]. | ||
[BSD license]: https://github.com/yahoo/fetchr/blob/master/LICENSE.md | ||
[CLA]: https://yahoocla.herokuapp.com/ | ||
[bsd license]: https://github.com/yahoo/fetchr/blob/master/LICENSE.md | ||
[cla]: https://yahoocla.herokuapp.com/ |
@@ -12,3 +12,3 @@ /** | ||
*/ | ||
require("setimmediate"); | ||
require('setimmediate'); | ||
var REST = require('./util/http.client'); | ||
@@ -48,3 +48,3 @@ var deepmerge = require('deepmerge'); | ||
*/ | ||
function pickContext (context, picker, method) { | ||
function pickContext(context, picker, method) { | ||
if (!picker || !picker[method]) { | ||
@@ -60,13 +60,15 @@ return context; | ||
} else if (Array.isArray(p)) { | ||
p.forEach(function(key) { | ||
p.forEach(function (key) { | ||
result[key] = context[key]; | ||
}); | ||
} else if (typeof p === 'function') { | ||
forEach(context, function(value, key) { | ||
forEach(context, function (value, key) { | ||
if (p(value, key, context)) { | ||
result[key] = context[key]; | ||
} | ||
}) | ||
}); | ||
} else { | ||
throw new TypeError('picker must be an string, an array, or a function.'); | ||
throw new TypeError( | ||
'picker must be an string, an array, or a function.' | ||
); | ||
} | ||
@@ -89,3 +91,3 @@ | ||
*/ | ||
function Request (operation, resource, options) { | ||
function Request(operation, resource, options) { | ||
if (!resource) { | ||
@@ -105,3 +107,3 @@ throw new Error('Resource is required for a fetcher request'); | ||
statsCollector: options.statsCollector, | ||
_serviceMeta: options._serviceMeta || [] | ||
_serviceMeta: options._serviceMeta || [], | ||
}; | ||
@@ -172,3 +174,3 @@ this._params = {}; | ||
err: err, | ||
time: Date.now() - self._startTime | ||
time: Date.now() - self._startTime, | ||
}; | ||
@@ -191,9 +193,18 @@ statsCollector(stats); | ||
if (callback) { | ||
return executeRequest(self, function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
setImmediate(callback, null, result && result.data, result && result.meta); | ||
}, function requestFailed(err) { | ||
self._captureMetaAndStats(err); | ||
setImmediate(callback, err); | ||
}); | ||
return executeRequest( | ||
self, | ||
function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
setImmediate( | ||
callback, | ||
null, | ||
result && result.data, | ||
result && result.meta | ||
); | ||
}, | ||
function requestFailed(err) { | ||
self._captureMetaAndStats(err); | ||
setImmediate(callback, err); | ||
} | ||
); | ||
} else { | ||
@@ -203,9 +214,12 @@ var promise = new Promise(function requestExecutor(resolve, reject) { | ||
}); | ||
promise = promise.then(function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
return result; | ||
}, function requestFailed(err) { | ||
self._captureMetaAndStats(err); | ||
throw err; | ||
}); | ||
promise = promise.then( | ||
function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
return result; | ||
}, | ||
function requestFailed(err) { | ||
self._captureMetaAndStats(err); | ||
throw err; | ||
} | ||
); | ||
return promise; | ||
@@ -222,3 +236,3 @@ } | ||
*/ | ||
function executeRequest (request, resolve, reject) { | ||
function executeRequest(request, resolve, reject) { | ||
var clientConfig = request._clientConfig; | ||
@@ -232,3 +246,5 @@ var use_post; | ||
if (!uri) { | ||
uri = clientConfig.cors ? request.options.corsPath : request.options.xhrPath; | ||
uri = clientConfig.cors | ||
? request.options.corsPath | ||
: request.options.xhrPath; | ||
} | ||
@@ -240,4 +256,17 @@ | ||
if (!use_post) { | ||
var getUriFn = isFunction(clientConfig.constructGetUri) ? clientConfig.constructGetUri : defaultConstructGetUri; | ||
var get_uri = getUriFn.call(request, uri, request.resource, request._params, clientConfig, pickContext(request.options.context, request.options.contextPicker, 'GET')); | ||
var getUriFn = isFunction(clientConfig.constructGetUri) | ||
? clientConfig.constructGetUri | ||
: defaultConstructGetUri; | ||
var get_uri = getUriFn.call( | ||
request, | ||
uri, | ||
request.resource, | ||
request._params, | ||
clientConfig, | ||
pickContext( | ||
request.options.context, | ||
request.options.contextPicker, | ||
'GET' | ||
) | ||
); | ||
/* istanbul ignore next */ | ||
@@ -247,3 +276,10 @@ if (!get_uri) { | ||
// TODO: Add test for this fallback | ||
get_uri = defaultConstructGetUri.call(request, uri, request.resource, request._params, clientConfig, request.options.context); | ||
get_uri = defaultConstructGetUri.call( | ||
request, | ||
uri, | ||
request.resource, | ||
request._params, | ||
clientConfig, | ||
request.options.context | ||
); | ||
} | ||
@@ -261,3 +297,3 @@ // TODO: Remove `returnMeta` feature flag after next release | ||
// receive the new format | ||
get_uri += (get_uri.indexOf('?') !== -1) ? '&' : '?'; | ||
get_uri += get_uri.indexOf('?') !== -1 ? '&' : '?'; | ||
get_uri += 'returnMeta=true'; | ||
@@ -273,8 +309,13 @@ if (get_uri.length <= MAX_URI_LEN) { | ||
if (!use_post) { | ||
return REST.get(uri, customHeaders, deepmerge({xhrTimeout: request.options.xhrTimeout}, clientConfig), function getDone(err, response) { | ||
if (err) { | ||
return reject(err); | ||
return REST.get( | ||
uri, | ||
customHeaders, | ||
deepmerge({ xhrTimeout: request.options.xhrTimeout }, clientConfig), | ||
function getDone(err, response) { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve(parseResponse(response)); | ||
} | ||
resolve(parseResponse(response)); | ||
}); | ||
); | ||
} | ||
@@ -287,3 +328,3 @@ | ||
operation: request.operation, | ||
params: request._params | ||
params: request._params, | ||
}; | ||
@@ -295,18 +336,30 @@ if (request._body) { | ||
requests: requests, | ||
context: request.options.context | ||
context: request.options.context, | ||
}; // TODO: remove. leave here for now for backward compatibility | ||
uri = request._constructGroupUri(uri); | ||
allow_retry_post = (request.operation === OP_READ); | ||
return REST.post(uri, customHeaders, data, deepmerge({unsafeAllowRetry: allow_retry_post, xhrTimeout: request.options.xhrTimeout}, clientConfig), function postDone(err, response) { | ||
if (err) { | ||
return reject(err); | ||
allow_retry_post = request.operation === OP_READ; | ||
return REST.post( | ||
uri, | ||
customHeaders, | ||
data, | ||
deepmerge( | ||
{ | ||
unsafeAllowRetry: allow_retry_post, | ||
xhrTimeout: request.options.xhrTimeout, | ||
}, | ||
clientConfig | ||
), | ||
function postDone(err, response) { | ||
if (err) { | ||
return reject(err); | ||
} | ||
var result = parseResponse(response); | ||
if (result) { | ||
result = result[DEFAULT_GUID] || {}; | ||
} else { | ||
result = {}; | ||
} | ||
resolve(result); | ||
} | ||
var result = parseResponse(response); | ||
if (result) { | ||
result = result[DEFAULT_GUID] || {}; | ||
} else { | ||
result = {}; | ||
} | ||
resolve(result); | ||
}); | ||
); | ||
} | ||
@@ -323,5 +376,8 @@ | ||
var final_uri = uri; | ||
forEach(pickContext(this.options.context, this.options.contextPicker, 'POST'), function eachContext(v, k) { | ||
query.push(k + '=' + encodeURIComponent(v)); | ||
}); | ||
forEach( | ||
pickContext(this.options.context, this.options.contextPicker, 'POST'), | ||
function eachContext(v, k) { | ||
query.push(k + '=' + encodeURIComponent(v)); | ||
} | ||
); | ||
if (query.length > 0) { | ||
@@ -354,3 +410,3 @@ final_uri += '?' + query.sort().join('&'); | ||
function Fetcher (options) { | ||
function Fetcher(options) { | ||
this._serviceMeta = []; | ||
@@ -365,3 +421,3 @@ this.options = { | ||
statsCollector: options.statsCollector, | ||
_serviceMeta: this._serviceMeta | ||
_serviceMeta: this._serviceMeta, | ||
}; | ||
@@ -423,6 +479,3 @@ } | ||
} | ||
return request | ||
.params(params) | ||
.clientConfig(clientConfig) | ||
.end(callback); | ||
return request.params(params).clientConfig(clientConfig).end(callback); | ||
}, | ||
@@ -468,3 +521,3 @@ | ||
*/ | ||
'delete': function (resource, params, clientConfig, callback) { | ||
delete: function (resource, params, clientConfig, callback) { | ||
var request = new Request('delete', resource, this.options); | ||
@@ -479,6 +532,3 @@ if (1 === arguments.length) { | ||
} | ||
return request | ||
.params(params) | ||
.clientConfig(clientConfig) | ||
.end(callback); | ||
return request.params(params).clientConfig(clientConfig).end(callback); | ||
}, | ||
@@ -503,5 +553,5 @@ | ||
return this._serviceMeta; | ||
} | ||
}, | ||
}; | ||
module.exports = Fetcher; |
@@ -5,3 +5,3 @@ /** | ||
*/ | ||
require("setimmediate"); | ||
require('setimmediate'); | ||
var OP_READ = 'read'; | ||
@@ -16,3 +16,3 @@ var OP_CREATE = 'create'; | ||
var Promise = global.Promise || require('es6-promise').Promise; | ||
var RESOURCE_SANTIZER_REGEXP = /[^\w\.]+/g; | ||
var RESOURCE_SANTIZER_REGEXP = /[^\w.]+/g; | ||
@@ -36,3 +36,3 @@ function parseValue(value) { | ||
function parseParamValues (params) { | ||
function parseParamValues(params) { | ||
return Object.keys(params).reduce(function (parsed, curr) { | ||
@@ -45,3 +45,5 @@ parsed[curr] = parseValue(params[curr]); | ||
function sanitizeResourceName(resource) { | ||
return resource ? resource.replace(RESOURCE_SANTIZER_REGEXP, '*') : resource; | ||
return resource | ||
? resource.replace(RESOURCE_SANTIZER_REGEXP, '*') | ||
: resource; | ||
} | ||
@@ -58,3 +60,3 @@ | ||
var output = { | ||
message: 'request failed' | ||
message: 'request failed', | ||
}; | ||
@@ -70,7 +72,6 @@ | ||
statusCode: statusCode, | ||
output: output | ||
output: output, | ||
}; | ||
} | ||
/** | ||
@@ -92,3 +93,3 @@ * A Request instance represents a single fetcher request. | ||
*/ | ||
function Request (operation, resource, options) { | ||
function Request(operation, resource, options) { | ||
if (!resource) { | ||
@@ -119,5 +120,10 @@ throw new Error('Resource is required for a fetcher request'); | ||
Request.prototype.params = function (params) { | ||
this._params = (typeof this._paramsProcessor === 'function') | ||
? this._paramsProcessor(this.req, {operation: this.operation, resource: this.resource}, params) | ||
: params; | ||
this._params = | ||
typeof this._paramsProcessor === 'function' | ||
? this._paramsProcessor( | ||
this.req, | ||
{ operation: this.operation, resource: this.resource }, | ||
params | ||
) | ||
: params; | ||
return this; | ||
@@ -169,5 +175,7 @@ }; | ||
params: self._params, | ||
statusCode: err ? err.statusCode : (result && result.meta && result.meta.statusCode || 200), | ||
statusCode: err | ||
? err.statusCode | ||
: (result && result.meta && result.meta.statusCode) || 200, | ||
err: err, | ||
time: Date.now() - self._startTime | ||
time: Date.now() - self._startTime, | ||
}; | ||
@@ -192,16 +200,22 @@ statsCollector(stats); | ||
promise = promise.then(function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
return result; | ||
}, function requestFailed(errData) { | ||
self._captureMetaAndStats(errData); | ||
throw errData.err; | ||
}); | ||
promise = promise.then( | ||
function requestSucceeded(result) { | ||
self._captureMetaAndStats(null, result); | ||
return result; | ||
}, | ||
function requestFailed(errData) { | ||
self._captureMetaAndStats(errData); | ||
throw errData.err; | ||
} | ||
); | ||
if (callback) { | ||
promise.then(function requestSucceeded(result) { | ||
setImmediate(callback, null, result.data, result.meta); | ||
}, function requestFailed(err) { | ||
setImmediate(callback, err); | ||
}); | ||
promise.then( | ||
function requestSucceeded(result) { | ||
setImmediate(callback, null, result.data, result.meta); | ||
}, | ||
function requestFailed(err) { | ||
setImmediate(callback, err); | ||
} | ||
); | ||
} else { | ||
@@ -219,18 +233,24 @@ return promise; | ||
*/ | ||
function executeRequest (request, resolve, reject) { | ||
var args = [request.req, request.resource, request._params, request._clientConfig, function executeRequestCallback(err, data, meta) { | ||
if (err) { | ||
reject({ | ||
err: err, | ||
meta: meta | ||
}); | ||
} else { | ||
resolve({ | ||
data: data, | ||
meta: meta | ||
}); | ||
} | ||
}]; | ||
function executeRequest(request, resolve, reject) { | ||
var args = [ | ||
request.req, | ||
request.resource, | ||
request._params, | ||
request._clientConfig, | ||
function executeRequestCallback(err, data, meta) { | ||
if (err) { | ||
reject({ | ||
err: err, | ||
meta: meta, | ||
}); | ||
} else { | ||
resolve({ | ||
data: data, | ||
meta: meta, | ||
}); | ||
} | ||
}, | ||
]; | ||
var op = request.operation; | ||
if ((op === OP_CREATE) || (op === OP_UPDATE)) { | ||
if (op === OP_CREATE || op === OP_UPDATE) { | ||
args.splice(3, 0, request._body); | ||
@@ -242,7 +262,12 @@ } | ||
if (!service[op]) { | ||
throw new Error('operation: ' + op + ' is undefined on service: ' + request.resource); | ||
throw new Error( | ||
'operation: ' + | ||
op + | ||
' is undefined on service: ' + | ||
request.resource | ||
); | ||
} | ||
service[op].apply(service, args); | ||
} catch (err) { | ||
reject({err: err}); | ||
reject({ err: err }); | ||
} | ||
@@ -266,3 +291,3 @@ } | ||
*/ | ||
function Fetcher (options) { | ||
function Fetcher(options) { | ||
this.options = options || {}; | ||
@@ -313,3 +338,5 @@ this.req = this.options.req || {}; | ||
} else { | ||
throw new Error('"resource" property is missing in service definition.'); | ||
throw new Error( | ||
'"resource" property is missing in service definition.' | ||
); | ||
} | ||
@@ -347,3 +374,5 @@ | ||
if (!service) { | ||
throw new Error('Service "' + sanitizeResourceName(name) + '" could not be found'); | ||
throw new Error( | ||
'Service "' + sanitizeResourceName(name) + '" could not be found' | ||
); | ||
} | ||
@@ -384,8 +413,15 @@ return service; | ||
options = options || {}; | ||
var responseFormatter = options.responseFormatter || function noOp(req, res, data) { | ||
return data; | ||
}; | ||
var responseFormatter = | ||
options.responseFormatter || | ||
function noOp(req, res, data) { | ||
return data; | ||
}; | ||
if (Fetcher._deprecatedServicesDefinitions.length && 'production' !== process.env.NODE_ENV) { | ||
var deprecatedServices = Fetcher._deprecatedServicesDefinitions.sort().join(', '); | ||
if ( | ||
Fetcher._deprecatedServicesDefinitions.length && | ||
'production' !== process.env.NODE_ENV | ||
) { | ||
var deprecatedServices = Fetcher._deprecatedServicesDefinitions | ||
.sort() | ||
.join(', '); | ||
@@ -396,3 +432,4 @@ console.warn( | ||
'following services definitions:\n' + | ||
deprecatedServices + '.' | ||
deprecatedServices + | ||
'.' | ||
); | ||
@@ -413,3 +450,5 @@ } | ||
if (!resource) { | ||
error = fumble.http.badRequest('No resource specified', { debug: 'Bad resource' }); | ||
error = fumble.http.badRequest('No resource specified', { | ||
debug: 'Bad resource', | ||
}); | ||
error.source = 'fetchr'; | ||
@@ -423,3 +462,3 @@ return next(error); | ||
error = fumble.http.badRequest(errorMsg, { | ||
debug: 'Bad resource ' + resourceName | ||
debug: 'Bad resource ' + resourceName, | ||
}); | ||
@@ -434,3 +473,3 @@ error.source = 'fetchr'; | ||
statsCollector: options.statsCollector, | ||
paramsProcessor: options.paramsProcessor | ||
paramsProcessor: options.paramsProcessor, | ||
}); | ||
@@ -447,8 +486,12 @@ request | ||
if (req.query && req.query.returnMeta) { | ||
res.status(errResponse.statusCode).json(responseFormatter(req, res, { | ||
output: errResponse.output, | ||
meta: meta | ||
})); | ||
res.status(errResponse.statusCode).json( | ||
responseFormatter(req, res, { | ||
output: errResponse.output, | ||
meta: meta, | ||
}) | ||
); | ||
} else { | ||
res.status(errResponse.statusCode).json(responseFormatter(req, res, errResponse.output)); | ||
res.status(errResponse.statusCode).json( | ||
responseFormatter(req, res, errResponse.output) | ||
); | ||
} | ||
@@ -458,6 +501,8 @@ return; | ||
if (req.query.returnMeta) { | ||
res.status(meta.statusCode || 200).json(responseFormatter(req, res, { | ||
data: data, | ||
meta: meta | ||
})); | ||
res.status(meta.statusCode || 200).json( | ||
responseFormatter(req, res, { | ||
data: data, | ||
meta: meta, | ||
}) | ||
); | ||
} else { | ||
@@ -473,3 +518,3 @@ // TODO: Remove `returnMeta` feature flag after next release | ||
error = fumble.http.badRequest('No resource specified', { | ||
debug: 'No resources' | ||
debug: 'No resources', | ||
}); | ||
@@ -487,3 +532,3 @@ error.source = 'fetchr'; | ||
error = fumble.http.badRequest(errorMsg, { | ||
debug: 'Bad resource ' + resourceName | ||
debug: 'Bad resource ' + resourceName, | ||
}); | ||
@@ -494,6 +539,17 @@ error.source = 'fetchr'; | ||
var operation = singleRequest.operation; | ||
if(operation !== OP_CREATE && operation !== OP_UPDATE && operation !== OP_DELETE && operation !== OP_READ) { | ||
if ( | ||
operation !== OP_CREATE && | ||
operation !== OP_UPDATE && | ||
operation !== OP_DELETE && | ||
operation !== OP_READ | ||
) { | ||
error = fumble.http.badRequest( | ||
'Unsupported "' + resourceName + '.' + operation + '" operation', | ||
{ debug: 'Only "create", "read", "update" or "delete" operations are allowed' } | ||
'Unsupported "' + | ||
resourceName + | ||
'.' + | ||
operation + | ||
'" operation', | ||
{ | ||
debug: 'Only "create", "read", "update" or "delete" operations are allowed', | ||
} | ||
); | ||
@@ -508,3 +564,3 @@ error.source = 'fetchr'; | ||
statsCollector: options.statsCollector, | ||
paramsProcessor: options.paramsProcessor | ||
paramsProcessor: options.paramsProcessor, | ||
}); | ||
@@ -514,3 +570,3 @@ request | ||
.body(singleRequest.body || {}) | ||
.end(function(err, data) { | ||
.end(function (err, data) { | ||
var meta = serviceMeta[0] || {}; | ||
@@ -522,3 +578,5 @@ if (meta.headers) { | ||
var errResponse = getErrorResponse(err); | ||
res.status(errResponse.statusCode).json(responseFormatter(req, res, errResponse.output)); | ||
res.status(errResponse.statusCode).json( | ||
responseFormatter(req, res, errResponse.output) | ||
); | ||
return; | ||
@@ -529,3 +587,3 @@ } | ||
data: data, | ||
meta: meta | ||
meta: meta, | ||
}); | ||
@@ -539,3 +597,2 @@ res.status(meta.statusCode || 200).json(responseObj); | ||
// ------------------------------------------------------------------ | ||
@@ -560,3 +617,3 @@ // CRUD Data Access Wrapper Methods | ||
serviceMeta: this._serviceMeta, | ||
statsCollector: this.options.statsCollector | ||
statsCollector: this.options.statsCollector, | ||
}); | ||
@@ -576,6 +633,3 @@ if (1 === arguments.length) { | ||
} | ||
return request | ||
.params(params) | ||
.clientConfig(config) | ||
.end(callback); | ||
return request.params(params).clientConfig(config).end(callback); | ||
}; | ||
@@ -598,3 +652,3 @@ /** | ||
serviceMeta: this._serviceMeta, | ||
statsCollector: this.options.statsCollector | ||
statsCollector: this.options.statsCollector, | ||
}); | ||
@@ -614,7 +668,3 @@ if (1 === arguments.length) { | ||
} | ||
return request | ||
.params(params) | ||
.body(body) | ||
.clientConfig(config) | ||
.end(callback); | ||
return request.params(params).body(body).clientConfig(config).end(callback); | ||
}; | ||
@@ -637,3 +687,3 @@ /** | ||
serviceMeta: this._serviceMeta, | ||
statsCollector: this.options.statsCollector | ||
statsCollector: this.options.statsCollector, | ||
}); | ||
@@ -653,7 +703,3 @@ if (1 === arguments.length) { | ||
} | ||
return request | ||
.params(params) | ||
.body(body) | ||
.clientConfig(config) | ||
.end(callback); | ||
return request.params(params).body(body).clientConfig(config).end(callback); | ||
}; | ||
@@ -675,3 +721,3 @@ /** | ||
serviceMeta: this._serviceMeta, | ||
statsCollector: this.options.statsCollector | ||
statsCollector: this.options.statsCollector, | ||
}); | ||
@@ -692,6 +738,3 @@ if (1 === arguments.length) { | ||
} | ||
return request | ||
.params(params) | ||
.clientConfig(config) | ||
.end(callback); | ||
return request.params(params).clientConfig(config).end(callback); | ||
}; | ||
@@ -698,0 +741,0 @@ |
@@ -31,3 +31,9 @@ /** | ||
*/ | ||
module.exports = function defaultConstructGetUri(baseUri, resource, params, config, context) { | ||
module.exports = function defaultConstructGetUri( | ||
baseUri, | ||
resource, | ||
params, | ||
config, | ||
context | ||
) { | ||
var query = []; | ||
@@ -45,5 +51,7 @@ var matrix = []; | ||
try { | ||
matrix.push(k + '=' + encodeURIComponent(jsonifyComplexType(v))); | ||
matrix.push( | ||
k + '=' + encodeURIComponent(jsonifyComplexType(v)) | ||
); | ||
} catch (err) { | ||
console.debug('jsonifyComplexType failed: ' + err) | ||
console.debug('jsonifyComplexType failed: ' + err); | ||
} | ||
@@ -50,0 +58,0 @@ } |
@@ -5,3 +5,3 @@ /** | ||
*/ | ||
/*jslint nomen:true,plusplus:true*/ | ||
/** | ||
@@ -11,2 +11,5 @@ * @module rest-http | ||
var xhr = require('xhr'); | ||
var forEach = require('./forEach'); | ||
/* | ||
@@ -21,4 +24,6 @@ * Default configurations: | ||
interval: 200, | ||
max_retries: 0 | ||
} | ||
maxRetries: 0, | ||
statusCodes: [0, 408, 999], | ||
}, | ||
unsafeAllowRetry: false, | ||
}, | ||
@@ -31,12 +36,11 @@ CONTENT_TYPE = 'Content-Type', | ||
METHOD_DELETE = 'DELETE', | ||
NULL = null, | ||
xhr = require('xhr'); | ||
NULL = null; | ||
var forEach = require('./forEach'); | ||
var INITIAL_ATTEMPT = 0; | ||
//trim polyfill, maybe pull from npm later | ||
if (!String.prototype.trim) { | ||
String.prototype.trim = function () { | ||
return this.replace(/^\s+|\s+$/g, ''); | ||
}; | ||
String.prototype.trim = function () { | ||
return this.replace(/^\s+|\s+$/g, ''); | ||
}; | ||
} | ||
@@ -49,3 +53,3 @@ | ||
} | ||
var needContentType = (method === METHOD_PUT || method === METHOD_POST); | ||
var needContentType = method === METHOD_PUT || method === METHOD_POST; | ||
forEach(headers, function (v, field) { | ||
@@ -78,13 +82,16 @@ if (field.toLowerCase() === 'content-type') { | ||
function shouldRetry(method, config, statusCode) { | ||
var isIdempotent = (method === METHOD_GET || method === METHOD_PUT || method === METHOD_DELETE); | ||
if (!isIdempotent && !config.unsafeAllowRetry) { | ||
function shouldRetry(method, config, statusCode, attempt) { | ||
if (attempt >= config.retry.maxRetries) { | ||
return false; | ||
} | ||
if ((statusCode !== 0 && statusCode !== 408 && statusCode !== 999) || config.tmp.retry_counter >= config.retry.max_retries) { | ||
var isIdempotent = | ||
method === METHOD_GET || | ||
method === METHOD_PUT || | ||
method === METHOD_DELETE; | ||
if (!isIdempotent && !config.unsafeAllowRetry) { | ||
return false; | ||
} | ||
config.tmp.retry_counter++; | ||
config.retry.interval = config.retry.interval * 2; | ||
return true; | ||
return config.retry.statusCodes.indexOf(statusCode) !== -1; | ||
} | ||
@@ -94,14 +101,12 @@ | ||
var cfg = { | ||
unsafeAllowRetry: config.unsafeAllowRetry || false, | ||
retry: { | ||
interval: DEFAULT_CONFIG.retry.interval, | ||
max_retries: DEFAULT_CONFIG.retry.max_retries | ||
} | ||
}, // Performant-but-verbose way of cloning the default config as base | ||
timeout, | ||
interval, | ||
maxRetries; | ||
unsafeAllowRetry: config.unsafeAllowRetry || false, | ||
retry: { | ||
interval: DEFAULT_CONFIG.retry.interval, | ||
maxRetries: DEFAULT_CONFIG.retry.maxRetries, | ||
statusCodes: DEFAULT_CONFIG.retry.statusCodes, | ||
}, | ||
}; | ||
if (config) { | ||
timeout = config.timeout || config.xhrTimeout; | ||
var timeout = config.timeout || config.xhrTimeout; | ||
timeout = parseInt(timeout, 10); | ||
@@ -113,10 +118,24 @@ if (!isNaN(timeout) && timeout > 0) { | ||
if (config.retry) { | ||
interval = parseInt(config.retry && config.retry.interval, 10); | ||
var interval = parseInt(config.retry.interval, 10); | ||
if (!isNaN(interval) && interval > 0) { | ||
cfg.retry.interval = interval; | ||
} | ||
maxRetries = parseInt(config.retry && config.retry.max_retries, 10); | ||
if (config.retry.max_retries !== undefined) { | ||
console.warn( | ||
'"max_retries" is deprecated and will be removed in a future release, use "maxRetries" instead.' | ||
); | ||
} | ||
var maxRetries = parseInt( | ||
config.retry.max_retries || config.retry.maxRetries, | ||
10 | ||
); | ||
if (!isNaN(maxRetries) && maxRetries >= 0) { | ||
cfg.retry.max_retries = maxRetries; | ||
cfg.retry.maxRetries = maxRetries; | ||
} | ||
if (config.retry.statusCodes) { | ||
cfg.retry.statusCodes = config.retry.statusCodes; | ||
} | ||
} | ||
@@ -127,7 +146,2 @@ | ||
} | ||
// tmp stores transient state data, such as retry count | ||
if (config.tmp) { | ||
cfg.tmp = config.tmp; | ||
} | ||
} | ||
@@ -138,32 +152,42 @@ | ||
function doXhr(method, url, headers, data, config, callback) { | ||
var options, timeout; | ||
function doXhr(method, url, headers, data, config, attempt, callback) { | ||
headers = normalizeHeaders(headers, method, config.cors); | ||
config = mergeConfig(config); | ||
// use config.tmp to store temporary values | ||
config.tmp = config.tmp || {retry_counter: 0}; | ||
timeout = config.timeout; | ||
options = { | ||
method : method, | ||
timeout : timeout, | ||
var options = { | ||
method: method, | ||
timeout: config.timeout, | ||
headers: headers, | ||
useXDR: config.useXDR, | ||
withCredentials: config.withCredentials, | ||
on : { | ||
success : function (err, response) { | ||
on: { | ||
success: function (err, response) { | ||
callback(NULL, response); | ||
}, | ||
failure : function (err, response) { | ||
if (!shouldRetry(method, config, response.statusCode)) { | ||
failure: function (err, response) { | ||
if ( | ||
!shouldRetry(method, config, response.statusCode, attempt) | ||
) { | ||
callback(err); | ||
} else { | ||
setTimeout( | ||
function retryXHR() { doXhr(method, url, headers, data, config, callback); }, | ||
config.retry.interval | ||
); | ||
// Use exponential backoff and full jitter strategy published in https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ | ||
var delay = | ||
Math.random() * | ||
config.retry.interval * | ||
Math.pow(2, attempt); | ||
setTimeout(function retryXHR() { | ||
doXhr( | ||
method, | ||
url, | ||
headers, | ||
data, | ||
config, | ||
attempt + 1, | ||
callback | ||
); | ||
}, delay); | ||
} | ||
} | ||
} | ||
}, | ||
}, | ||
}; | ||
@@ -177,52 +201,57 @@ if (data !== undefined && data !== NULL) { | ||
function io(url, options) { | ||
return xhr({ | ||
url: url, | ||
method: options.method || METHOD_GET, | ||
timeout: options.timeout, | ||
headers: options.headers, | ||
body: options.data, | ||
useXDR: options.cors, | ||
withCredentials: options.withCredentials | ||
}, function (err, resp, body) { | ||
var status = resp.statusCode; | ||
var errMessage, errBody; | ||
return xhr( | ||
{ | ||
url: url, | ||
method: options.method || METHOD_GET, | ||
timeout: options.timeout, | ||
headers: options.headers, | ||
body: options.data, | ||
useXDR: options.cors, | ||
withCredentials: options.withCredentials, | ||
}, | ||
function (err, resp, body) { | ||
var status = resp.statusCode; | ||
var errMessage, errBody; | ||
if (!err && (status === 0 || (status >= 400 && status < 600))) { | ||
if (typeof body === 'string') { | ||
try { | ||
errBody = JSON.parse(body); | ||
if (errBody.message) { | ||
errMessage = errBody.message; | ||
} else { | ||
if (!err && (status === 0 || (status >= 400 && status < 600))) { | ||
if (typeof body === 'string') { | ||
try { | ||
errBody = JSON.parse(body); | ||
if (errBody.message) { | ||
errMessage = errBody.message; | ||
} else { | ||
errMessage = body; | ||
} | ||
} catch (e) { | ||
errMessage = body; | ||
} | ||
} catch(e) { | ||
errMessage = body; | ||
} else { | ||
errMessage = status | ||
? 'Error ' + status | ||
: 'Internal Fetchr XMLHttpRequest Error'; | ||
} | ||
} else { | ||
errMessage = status ? 'Error ' + status : 'Internal Fetchr XMLHttpRequest Error'; | ||
} | ||
err = new Error(errMessage); | ||
err.statusCode = status; | ||
err.body = errBody || body; | ||
if (err.body) { | ||
err.output = err.body.output; | ||
err.meta = err.body.meta; | ||
err = new Error(errMessage); | ||
err.statusCode = status; | ||
err.body = errBody || body; | ||
if (err.body) { | ||
err.output = err.body.output; | ||
err.meta = err.body.meta; | ||
} | ||
} | ||
} | ||
resp.responseText = body; | ||
resp.responseText = body; | ||
if (err) { | ||
// getting detail info from xhr module | ||
err.rawRequest = resp.rawRequest; | ||
err.url = resp.url; | ||
err.timeout = options.timeout; | ||
if (err) { | ||
// getting detail info from xhr module | ||
err.rawRequest = resp.rawRequest; | ||
err.url = resp.url; | ||
err.timeout = options.timeout; | ||
options.on.failure.call(null, err, resp); | ||
} else { | ||
options.on.success.call(null, null, resp); | ||
options.on.failure.call(null, err, resp); | ||
} else { | ||
options.on.success.call(null, null, resp); | ||
} | ||
} | ||
}); | ||
); | ||
} | ||
@@ -241,10 +270,19 @@ | ||
* @param {Number} [config.timeout=3000] Timeout (in ms) for each request | ||
* @param {Object} config.retry Retry config object. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.max_retries=2] Number of max retries. | ||
* @param {Object} config.retry Retry config object. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.maxRetries=0] Number of max retries. | ||
* @param {Number} [config.retry.statusCodes=[0, 408, 999]] Response status codes to be retried. | ||
* @param {Boolean} [config.cors] Whether to enable CORS & use XDR on IE8/9. | ||
* @param {Function} callback The callback function, with two params (error, response) | ||
*/ | ||
get : function (url, headers, config, callback) { | ||
return doXhr(METHOD_GET, url, headers, NULL, config, callback); | ||
get: function (url, headers, config, callback) { | ||
return doXhr( | ||
METHOD_GET, | ||
url, | ||
headers, | ||
NULL, | ||
config, | ||
INITIAL_ATTEMPT, | ||
callback | ||
); | ||
}, | ||
@@ -259,9 +297,18 @@ | ||
* @param {Number} [config.timeout=3000] Timeout (in ms) for each request | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.max_retries=2] Number of max retries. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.maxRetries=0] Number of max retries. | ||
* @param {Number} [config.retry.statusCodes=[0, 408, 999]] Response status codes to be retried. | ||
* @param {Boolean} [config.cors] Whether to enable CORS & use XDR on IE8/9. | ||
* @param {Function} callback The callback function, with two params (error, response) | ||
*/ | ||
put : function (url, headers, data, config, callback) { | ||
return doXhr(METHOD_PUT, url, headers, data, config, callback); | ||
put: function (url, headers, data, config, callback) { | ||
return doXhr( | ||
METHOD_PUT, | ||
url, | ||
headers, | ||
data, | ||
config, | ||
INITIAL_ATTEMPT, | ||
callback | ||
); | ||
}, | ||
@@ -277,9 +324,18 @@ | ||
* @param {Boolean} [config.unsafeAllowRetry=false] Whether to allow retrying this post. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.max_retries=2] Number of max retries. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.maxRetries=0] Number of max retries. | ||
* @param {Number} [config.retry.statusCodes=[0, 408, 999]] Response status codes to be retried. | ||
* @param {Boolean} [config.cors] Whether to enable CORS & use XDR on IE8/9. | ||
* @param {Function} callback The callback function, with two params (error, response) | ||
*/ | ||
post : function (url, headers, data, config, callback) { | ||
return doXhr(METHOD_POST, url, headers, data, config, callback); | ||
post: function (url, headers, data, config, callback) { | ||
return doXhr( | ||
METHOD_POST, | ||
url, | ||
headers, | ||
data, | ||
config, | ||
INITIAL_ATTEMPT, | ||
callback | ||
); | ||
}, | ||
@@ -293,10 +349,19 @@ | ||
* @param {Number} [config.timeout=3000] Timeout (in ms) for each request | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.max_retries=2] Number of max retries. | ||
* @param {Number} [config.retry.interval=200] The start interval unit (in ms). | ||
* @param {Number} [config.retry.maxRetries=0] Number of max retries. | ||
* @param {Number} [config.retry.statusCodes=[0, 408, 999]] Response status codes to be retried. | ||
* @param {Boolean} [config.cors] Whether to enable CORS & use XDR on IE8/9. | ||
* @param {Function} callback The callback function, with two params (error, response) | ||
*/ | ||
'delete' : function (url, headers, config, callback) { | ||
return doXhr(METHOD_DELETE, url, headers, NULL, config, callback); | ||
} | ||
delete: function (url, headers, config, callback) { | ||
return doXhr( | ||
METHOD_DELETE, | ||
url, | ||
headers, | ||
NULL, | ||
config, | ||
INITIAL_ATTEMPT, | ||
callback | ||
); | ||
}, | ||
}; |
@@ -1,6 +0,4 @@ | ||
Software License Agreement (BSD License) | ||
======================================== | ||
# Software License Agreement (BSD License) | ||
Copyright (c) 2014, Yahoo! Inc. All rights reserved. | ||
---------------------------------------------------- | ||
## Copyright (c) 2014, Yahoo! Inc. All rights reserved. | ||
@@ -11,8 +9,8 @@ Redistribution and use of this software in source and binary forms, with or | ||
* Redistributions of source code must retain the above copyright notice, this | ||
- Redistributions of source code must retain the above copyright notice, this | ||
list of conditions and the following disclaimer. | ||
* Redistributions in binary form must reproduce the above copyright notice, | ||
- Redistributions in binary form must reproduce the above copyright notice, | ||
this list of conditions and the following disclaimer in the documentation | ||
and/or other materials provided with the distribution. | ||
* Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be | ||
- Neither the name of Yahoo! Inc. nor the names of YUI's contributors may be | ||
used to endorse or promote products derived from this software without | ||
@@ -19,0 +17,0 @@ specific prior written permission of Yahoo! Inc. |
{ | ||
"name": "fetchr", | ||
"version": "0.5.44", | ||
"version": "0.5.45", | ||
"description": "Fetchr augments Flux applications by allowing Flux stores to be used on server and client to fetch data", | ||
@@ -9,3 +9,5 @@ "main": "index.js", | ||
"cover": "istanbul cover --dir artifacts -- ./node_modules/mocha/bin/_mocha tests/unit/ --recursive --reporter spec --timeout 20000 --exit", | ||
"lint": "eslint .", | ||
"format": "prettier --write .", | ||
"format:check": "prettier --check .", | ||
"lint": "eslint . && npm run format:check", | ||
"test": "NODE_ENV=test mocha tests/unit/ --recursive --reporter spec --timeout 20000 --exit" | ||
@@ -43,2 +45,3 @@ }, | ||
"pre-commit": "^1.0.0", | ||
"prettier": "^2.3.2", | ||
"qs": "^6.7.0", | ||
@@ -55,3 +58,7 @@ "request": "^2.81.0", | ||
"dispatchr" | ||
] | ||
], | ||
"prettier": { | ||
"singleQuote": true, | ||
"tabWidth": 4 | ||
} | ||
} |
327
README.md
# 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) | ||
![Build Status](https://github.com/yahoo/fetchr/actions/workflows/node.js.yml/badge.svg) | ||
[![Coverage Status](https://coveralls.io/repos/yahoo/fetchr/badge.png?branch=master)](https://coveralls.io/r/yahoo/fetchr?branch=master) | ||
@@ -15,3 +15,3 @@ | ||
``` | ||
```bash | ||
npm install fetchr --save | ||
@@ -31,6 +31,6 @@ ``` | ||
```js | ||
var express = require('express'); | ||
var Fetcher = require('fetchr'); | ||
var bodyParser = require('body-parser'); | ||
var app = express(); | ||
import express from 'express'; | ||
import Fetcher from 'fetchr'; | ||
import bodyParser from 'body-parser'; | ||
const app = express(); | ||
@@ -50,5 +50,5 @@ // you need to use body-parser middleware before fetcher middleware | ||
```js | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint' | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', | ||
}); | ||
@@ -60,6 +60,6 @@ ``` | ||
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 | ||
your application. The interface for your service will be an object | ||
that must define a `resource` property and at least one | ||
[CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) | ||
operation. The `resource` property will be used when you call one of the | ||
operation. The `resource` property will be used when you call one of the | ||
CRUD operations. | ||
@@ -69,4 +69,4 @@ | ||
// app.js | ||
var Fetcher = require('fetchr'); | ||
var myDataService = require('./dataService'); | ||
import Fetcher from 'fetchr'; | ||
import myDataService from './dataService'; | ||
Fetcher.registerService(myDataService); | ||
@@ -77,8 +77,8 @@ ``` | ||
// dataService.js | ||
module.exports = { | ||
export default { | ||
// resource is required | ||
resource: 'data_service', | ||
// at least one of the CRUD methods is required | ||
read: function(req, resource, params, config, callback) { | ||
//... | ||
read: function (req, resource, params, config, callback) { | ||
//... | ||
}, | ||
@@ -89,3 +89,3 @@ // other methods | ||
// delete: function(req, resource, params, config, callback) {} | ||
} | ||
}; | ||
``` | ||
@@ -101,9 +101,8 @@ | ||
```js | ||
// app.js - server | ||
var express = require('express'); | ||
var Fetcher = require('fetchr'); | ||
var app = express(); | ||
var myDataService = require('./dataService'); | ||
import express from 'express'; | ||
import Fetcher from 'fetchr'; | ||
import myDataService from './dataService'; | ||
const app = express(); | ||
@@ -118,3 +117,3 @@ // register the service | ||
// instantiated fetcher with access to req object | ||
var fetcher = new Fetcher({ | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', // xhrPath will be ignored on the serverside fetcher instantiation | ||
@@ -136,4 +135,4 @@ req: req | ||
// app.js - client | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint' // xhrPath is REQUIRED on the clientside fetcher instantiation | ||
@@ -157,4 +156,2 @@ }); | ||
## Usage Examples | ||
@@ -177,16 +174,16 @@ | ||
// dataService.js | ||
module.exports = { | ||
export default { | ||
resource: 'data_service', | ||
read: function(req, resource, params, config, callback) { | ||
read: function (req, resource, params, config, callback) { | ||
// business logic | ||
var data = 'response'; | ||
var meta = { | ||
const data = 'response'; | ||
const meta = { | ||
headers: { | ||
'cache-control': 'public, max-age=3600' | ||
'cache-control': 'public, max-age=3600', | ||
}, | ||
statusCode: 200 // You can even provide a custom statusCode for the xhr response | ||
statusCode: 200, // You can even provide a custom statusCode for the xhr response | ||
}; | ||
callback(null, data, meta); | ||
} | ||
} | ||
}, | ||
}; | ||
``` | ||
@@ -210,3 +207,2 @@ | ||
## Updating Configuration | ||
@@ -221,5 +217,5 @@ | ||
// Start | ||
var fetcher = new Fetcher({ | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', | ||
xhrTimeout: 2000 | ||
xhrTimeout: 2000, | ||
}); | ||
@@ -229,7 +225,6 @@ | ||
fetcher.updateOptions({ | ||
xhrTimeout: 4000 | ||
xhrTimeout: 4000, | ||
}); | ||
``` | ||
## Error Handling | ||
@@ -240,10 +235,10 @@ | ||
```js | ||
module.exports = { | ||
export default { | ||
resource: 'FooService', | ||
read: function create(req, resource, params, configs, callback) { | ||
var err = new Error('it failed'); | ||
const err = new Error('it failed'); | ||
err.statusCode = 404; | ||
err.output = { message: "Not found", more: "meta data" }; | ||
err.output = { message: 'Not found', more: 'meta data' }; | ||
return callback(err); | ||
} | ||
}, | ||
}; | ||
@@ -267,7 +262,7 @@ ``` | ||
The xhr object is returned by the `.end()` method as long as you're *not* chaining promises. | ||
The xhr object is returned by the `.end()` method as long as you're _not_ chaining promises. | ||
This is useful if you want to abort a request before it is completed. | ||
```js | ||
var req = fetcher | ||
const req = fetcher | ||
.read('someData') | ||
@@ -283,4 +278,5 @@ .params({id: ###}) | ||
However, you can't acces the xhr object if using promise chaining like so: | ||
```js | ||
var req = fetcher | ||
const req = fetcher | ||
.read('someData') | ||
@@ -300,6 +296,6 @@ .params({id: ###}) | ||
```js | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', | ||
xhrTimeout: 4000 | ||
xhrTimeout: 4000, | ||
}); | ||
@@ -333,8 +329,11 @@ ``` | ||
*/ | ||
app.use('/myCustomAPIEndpoint', Fetcher.middleware({ | ||
paramsProcessor: function (req, serviceInfo, params) { | ||
console.log(serviceInfo.resource, serviceInfo.operation); | ||
return Object.assign({foo: 'fillDefaultValueForFoo'}, params); | ||
} | ||
})); | ||
app.use( | ||
'/myCustomAPIEndpoint', | ||
Fetcher.middleware({ | ||
paramsProcessor: function (req, serviceInfo, params) { | ||
console.log(serviceInfo.resource, serviceInfo.operation); | ||
return Object.assign({ foo: 'fillDefaultValueForFoo' }, params); | ||
}, | ||
}) | ||
); | ||
``` | ||
@@ -355,8 +354,11 @@ | ||
*/ | ||
app.use('/myCustomAPIEndpoint', Fetcher.middleware({ | ||
responseFormatter: function (req, res, data) { | ||
data.debug = 'some debug information'; | ||
return data; | ||
} | ||
})); | ||
app.use( | ||
'/myCustomAPIEndpoint', | ||
Fetcher.middleware({ | ||
responseFormatter: function (req, res, data) { | ||
data.debug = 'some debug information'; | ||
return data; | ||
}, | ||
}) | ||
); | ||
``` | ||
@@ -373,6 +375,6 @@ | ||
```js | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
corsPath: 'http://www.foo.com', | ||
xhrPath: '/fooProxy' | ||
xhrPath: '/fooProxy', | ||
}); | ||
@@ -389,3 +391,3 @@ fetcher | ||
```js | ||
var qs = require('qs'); | ||
import qs from 'qs'; | ||
function customConstructGetUri(uri, resource, params, config) { | ||
@@ -399,6 +401,6 @@ // this refers to the Fetcher object itself that this function is invoked with. | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
corsPath: 'http://www.foo.com', | ||
xhrPath: '/fooProxy' | ||
xhrPath: '/fooProxy', | ||
}); | ||
@@ -410,3 +412,3 @@ fetcher | ||
cors: true, | ||
constructGetUri: customConstructGetUri | ||
constructGetUri: customConstructGetUri, | ||
}) | ||
@@ -416,3 +418,2 @@ .end(callbackFn); | ||
## CSRF Protection | ||
@@ -429,7 +430,8 @@ | ||
```js | ||
var fetcher = new Fetcher({ | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', //xhrPath is REQUIRED on the clientside fetcher instantiation | ||
context: { // These context values are persisted with XHR calls as query params | ||
_csrf: 'Ax89D94j' | ||
} | ||
context: { | ||
// These context values are persisted with XHR calls as query params | ||
_csrf: 'Ax89D94j', | ||
}, | ||
}); | ||
@@ -448,16 +450,67 @@ ``` | ||
//app.js - client | ||
var config = { | ||
const config = { | ||
timeout: 6000, // Timeout (in ms) for each request | ||
unsafeAllowRetry: false // for POST requests, whether to allow retrying this post | ||
unsafeAllowRetry: false, // for POST requests, whether to allow retrying this post | ||
}; | ||
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. | ||
## Retry | ||
You can set Fetchr to retry failed requests automatically by setting a `retry` settings in the client configuration: | ||
```js | ||
fetcher | ||
.read('service') | ||
.params({ id: 1 }) | ||
.clientConfig(config) | ||
.end(callbackFn); | ||
.clientConfig({ | ||
retry: { | ||
maxRetries: 2, | ||
}, | ||
}) | ||
.end(); | ||
``` | ||
For requests from the server, the config object is simply passed into the service being called. | ||
With this configuration, Fetchr will retry all requests that fail with 408 status code or with an XHR 0 status code two more times before returning an error. The interval between each request respects | ||
the following formula, based on the exponential backoff and full jitter strategy published in [this AWS architecture blog post](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/): | ||
```js | ||
Math.random() * Math.pow(2, attempt) * interval; | ||
``` | ||
`attempt` is the number of the current retry attempt starting | ||
from 0. By default `interval` corresponds to 200ms. | ||
You can customize the retry behavior by adding more properties in the | ||
`retry` object: | ||
```js | ||
fetcher | ||
.read('resource') | ||
.clientConfig({ | ||
retry: { | ||
maxRetries: 5, | ||
interval: 1000, | ||
statusCodes: [408, 502], | ||
}, | ||
}) | ||
.end(); | ||
``` | ||
With the above configuration, Fetchr will retry all failed (408 or 502 status code) requests for a maximum of 5 times. The interval between each request will still use the formula from above, but the interval of 1000ms will be used instead. | ||
**Note:** Fetchr doesn't retry POST requests for safety reasons. You can enable retries for POST requests by setting the `unsafeAllowRetry` property to `true`: | ||
```js | ||
fetcher | ||
.create('resource') | ||
.clientConfig({ | ||
retry: { maxRetries: 2 }, | ||
unsafeAllowRetry: true, | ||
}) | ||
.end(); | ||
``` | ||
## Context Variables | ||
@@ -470,6 +523,7 @@ | ||
```js | ||
var fetcher = new Fetcher({ | ||
context: { // These context values are persisted with XHR calls as query params | ||
const fetcher = new Fetcher({ | ||
context: { | ||
// These context values are persisted with XHR calls as query params | ||
_csrf: 'Ax89D94j', | ||
device: 'desktop' | ||
device: 'desktop', | ||
}, | ||
@@ -483,15 +537,16 @@ contextPicker: { | ||
return true; | ||
} | ||
}, | ||
// for other method e.g., POST, if you don't define the picker, it will pick the entire context object | ||
} | ||
}, | ||
}); | ||
var fetcher = new Fetcher({ | ||
context: { // These context values are persisted with XHR calls as query params | ||
const fetcher = new Fetcher({ | ||
context: { | ||
// These context values are persisted with XHR calls as query params | ||
_csrf: 'Ax89D94j', | ||
device: 'desktop' | ||
device: 'desktop', | ||
}, | ||
contextPicker: { | ||
GET: ['device'] // predicate can be an array of strings | ||
} | ||
GET: ['device'], // predicate can be an array of strings | ||
}, | ||
}); | ||
@@ -507,13 +562,9 @@ ``` | ||
```js | ||
var config = { | ||
const config = { | ||
headers: { | ||
'X-VERSION': '1.0.0' | ||
} | ||
'X-VERSION': '1.0.0', | ||
}, | ||
}; | ||
fetcher | ||
.read('service') | ||
.params({ id: 1 }) | ||
.clientConfig(config) | ||
.end(callbackFn); | ||
fetcher.read('service').params({ id: 1 }).clientConfig(config).end(callbackFn); | ||
``` | ||
@@ -524,21 +575,20 @@ | ||
```js | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
headers: { | ||
'X-VERSION': '1.0.0' | ||
} | ||
'X-VERSION': '1.0.0', | ||
}, | ||
}); | ||
``` | ||
## Stats Monitoring & Analysis | ||
To collect fetcher service's success/failure/latency stats, you can configure `statsCollector` for `Fetchr`. The `statsCollector` function will be invoked with one argumment: `stats`. The `stats` object will contain the following fields: | ||
To collect fetcher service's success/failure/latency stats, you can configure `statsCollector` for `Fetchr`. The `statsCollector` function will be invoked with one argumment: `stats`. The `stats` object will contain the following fields: | ||
* **resource:** The name of the resource for the request | ||
* **operation:** The name of the operation, `create|read|update|delete` | ||
* **params:** The params object for the resource | ||
* **statusCode:** The status code of the response | ||
* **err:** The error object of failed request; null if request was successful | ||
* **time:** The time spent for this request, in milliseconds | ||
- **resource:** The name of the resource for the request | ||
- **operation:** The name of the operation, `create|read|update|delete` | ||
- **params:** The params object for the resource | ||
- **statusCode:** The status code of the response | ||
- **err:** The error object of failed request; null if request was successful | ||
- **time:** The time spent for this request, in milliseconds | ||
@@ -548,4 +598,4 @@ ### Fetcher Instance | ||
```js | ||
var Fetcher = require('fetchr'); | ||
var fetcher = new Fetcher({ | ||
import Fetcher from 'fetchr'; | ||
const fetcher = new Fetcher({ | ||
xhrPath: '/myCustomAPIEndpoint', | ||
@@ -555,7 +605,14 @@ statsCollector: function (stats) { | ||
// like aggregating stats or filtering out stats you don't want to monitor | ||
console.log('Request for resource', stats.resource, | ||
'with', stats.operation, | ||
'returned statusCode:', stats.statusCode, | ||
' within', stats.time, 'ms'); | ||
} | ||
console.log( | ||
'Request for resource', | ||
stats.resource, | ||
'with', | ||
stats.operation, | ||
'returned statusCode:', | ||
stats.statusCode, | ||
' within', | ||
stats.time, | ||
'ms' | ||
); | ||
}, | ||
}); | ||
@@ -567,12 +624,22 @@ ``` | ||
```js | ||
app.use('/myCustomAPIEndpoint', Fetcher.middleware({ | ||
statsCollector: function (stats) { | ||
// just console logging as a naive example. there is a lot more you can do here, | ||
// like aggregating stats or filtering out stats you don't want to monitor | ||
console.log('Request for resource', stats.resource, | ||
'with', stats.operation, | ||
'returned statusCode:', stats.statusCode, | ||
' within', stats.time, 'ms'); | ||
} | ||
})); | ||
app.use( | ||
'/myCustomAPIEndpoint', | ||
Fetcher.middleware({ | ||
statsCollector: function (stats) { | ||
// just console logging as a naive example. there is a lot more you can do here, | ||
// like aggregating stats or filtering out stats you don't want to monitor | ||
console.log( | ||
'Request for resource', | ||
stats.resource, | ||
'with', | ||
stats.operation, | ||
'returned statusCode:', | ||
stats.statusCode, | ||
' within', | ||
stats.time, | ||
'ms' | ||
); | ||
}, | ||
}) | ||
); | ||
``` | ||
@@ -582,3 +649,3 @@ | ||
- [Fetchr](https://github.com/yahoo/fetchr/blob/master/docs/fetchr.md) | ||
- [Fetchr](https://github.com/yahoo/fetchr/blob/master/docs/fetchr.md) | ||
@@ -590,2 +657,2 @@ ## License | ||
[LICENSE file]: https://github.com/yahoo/fetchr/blob/master/LICENSE.md | ||
[license file]: https://github.com/yahoo/fetchr/blob/master/LICENSE.md |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
81990
1594
622
15
10