@mendeley/api
Version:
Mendeley API JavaScript SDK
325 lines (280 loc) • 9.28 kB
JavaScript
var Request = require('./request');
var assign = require('object-assign');
var pagination = require('./pagination');
/**
* Utilities
*
* @namespace
* @name utilities
*/
module.exports = {
passFilter: passFilter,
requestFun: requestFun,
requestWithDataFun: requestWithDataFun,
requestWithFileFun: requestWithFileFun,
paginationFilter: pagination.filter
};
/**
* Pass the second argument as it is. This is to allow receiving non-standard
* API responses (no 'data' property in response body).
*
* @param {object} options Additional options, ignored in this case
* @param {object} response Response body
* @returns {object} Untouched response body
*/
function passFilter(options, response) {
return response;
}
function dataFilter(options, response) {
return response.data;
}
function normaliseOptions(options) {
options.requestFilter = options.requestFilter || passFilter;
options.responseFilter = options.responseFilter || dataFilter;
options.args = options.args || [];
options.headers = options.headers || {};
return options;
}
/**
* Determines of the HTTP request was successful or not based on the response
* HTTP result code. This is to allow treating HTTP redirects as successful as
* axios handles them as failed by default.
*
* @private
* @param {number} status HTTP status code number
* @returns {bool} Result says if the request was successful or not
*/
function allowRedirectHttpCodes(status) {
return status >= 200 && status < 400;
}
/**
* A general purpose request functions
*
* @private
* @param {function} [responseFilter] - Optional filter to control which part of the response the promise resolves with
* @param {string} method
* @param {string} uriTemplate
* @param {array} uriVars
* @param {array} headers
* @returns {function}
*/
function requestFun(options) {
options = normaliseOptions(options);
return function() {
var args = Array.prototype.slice.call(arguments, 0);
var url = getUrl(options, args);
var params = args[options.args.length];
var request = {
method: options.method,
responseType: 'json',
url: url,
headers: getRequestHeaders(options.headers),
params: params
};
if (options.noFollow) {
request.maxRedirects = 0;
request.validateStatus = allowRedirectHttpCodes;
}
var settings = {
authFlow: options.authFlow()
};
if (options.timeout) {
settings.timeout = options.timeout;
}
if (options.method === 'GET') {
settings.maxRetries = 1;
}
// pass-through axios property for how querystring params are serialized.
if(options.paramsSerializer) {
request.paramsSerializer = options.paramsSerializer;
}
request = options.requestFilter(options, request);
return Request.create(request, settings)
.send()
.then(options.responseFilter.bind(null, options));
};
}
/**
* Get a request function that sends data i.e. for POST, PUT, PATCH
* The data will be taken from the calling argument after any uriVar arguments.
*
* @private
* @param {function} [responseFilter] - Optional filter to control which part of the response the promise resolves with
* @param {string} method - The HTTP method
* @param {string} uriTemplate - A URI template e.g. /documents/{id}
* @param {array} uriVars - The variables for the URI template in the order
* they will be passed to the function e.g. ['id']
* @param {object} headers - Any additional headers to send
* e.g. { 'Content-Type': 'application/vnd.mendeley-documents+1.json'}
* @param {bool} followLocation - follow the returned location header? Default is false
* @returns {function}
*/
function requestWithDataFun(options) {
options = normaliseOptions(options);
return function() {
var args = Array.prototype.slice.call(arguments, 0);
var url = getUrl(options, args);
var data = args[options.args.length];
var request = {
method: options.method,
url: url,
headers: getRequestHeaders(options.headers, data),
data: data
};
var settings = {
authFlow: options.authFlow(),
followLocation: options.followLocation
};
if (options.timeout) {
settings.timeout = options.timeout;
}
request = options.requestFilter(options, request);
return Request.create(request, settings)
.send()
.then(options.responseFilter.bind(null, options));
};
}
/**
* Get a request function that sends a file
*
* @private
* @param {function} [responseFilter] - Optional filter to control which part of the response the promise resolves with
* @param {string} method
* @param {string} uriTemplate
* @param {string} linkType - Type of the element to link this file to
* @param {object} headers - Any additional headers to send
* @returns {function}
*/
function requestWithFileFun(options) {
options = normaliseOptions(options);
return function() {
var args = Array.prototype.slice.call(arguments, 0);
var url = getUrl(options, args);
var file = args[0];
var linkId = args[1];
var requestHeaders = assign({}, getRequestHeaders(uploadHeaders(options, file, linkId), options.method), options.headers);
var progressHandler;
if (typeof args[args.length - 1] === 'function') {
progressHandler = args[args.length - 1];
}
var request = {
method: options.method,
url: url,
headers: requestHeaders,
data: file,
onUploadProgress: progressHandler,
onDownloadProgress: progressHandler
};
var settings = {
authFlow: options.authFlow()
};
if (options.timeout) {
settings.timeout = options.timeout;
}
request = options.requestFilter(options, request);
return Request.create(request, settings)
.send()
.then(options.responseFilter.bind(null, options));
};
}
/**
* Provide the correct encoding for UTF-8 Content-Disposition header value.
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
*
* @private
* @param {string} str
* @returns {string}
*/
function encodeRFC5987ValueChars(str) {
return encodeURIComponent(str).
replace(/'/g, '%27').
replace(/\(/g, '%28').
replace(/\)/g, '%29').
replace(/\*/g, '%2A');
}
/**
* Get headers for an upload
*
* @private
* @param {object} file
* @param {string} [file.type='application/octet-stream'] Value for the Content-Type header
* @param {string} file.name File name e.g. 'foo.pdf'
* @param {string} linkId
* @param {string} linkType either 'group' or 'document'
* @returns {object}
*/
function uploadHeaders(options, file, linkId) {
var headers = {
'Content-Type': !!file.type ? file.type : 'application/octet-stream',
'Content-Disposition': 'attachment; filename*=UTF-8\'\'' + encodeRFC5987ValueChars(file.name)
};
if (options.linkType && linkId) {
var baseUrl = options.baseUrl(options.method, options.resource, options.headers);
switch(options.linkType) {
case 'group':
headers.Link = '<' + baseUrl + '/groups/' + linkId +'>; rel="group"';
break;
case 'document':
headers.Link = '<' + baseUrl + '/documents/' + linkId +'>; rel="document"';
break;
}
}
return headers;
}
/**
* Generate a URL from a template with properties and values
*
* @private
* @param {string} uriTemplate
* @param {array} uriProps
* @param {array} uriValues
* @returns {string}
*/
function getUrl(options, args) {
var baseUrl = options.baseUrl(options.method, options.resource, options.headers);
if (!options.args.length) {
return baseUrl + options.resource;
}
var uriParams = {};
options.args.forEach(function(prop, index) {
uriParams[prop] = args[index];
});
return baseUrl + expandUriTemplate(options.resource, uriParams);
}
/**
* Get the headers for a request
*
* @private
* @param {array} headers
* @param {array} data
* @returns {array}
*/
function getRequestHeaders(headers, data) {
for (var headerName in headers) {
var val = headers[headerName];
if (typeof val === 'function') {
headers[headerName] = val(data);
}
}
return headers;
}
/**
* Populate a URI template with data
*
* @private
* @param {string} template
* @param {object} data
* @returns {string}
*/
function expandUriTemplate(template, data) {
var matches = template.match(/\{[a-z]+\}/gi);
matches.forEach(function(match) {
var prop = match.replace(/[\{\}]/g, '');
if (!data.hasOwnProperty(prop)) {
throw new Error('Endpoint requires ' + prop);
}
template = template.replace(match, data[prop]);
});
return template;
}
;