/**
* @file Volt Core Library - api method
*
* @license
* (c) 2017 NS BASIC Corporation. All rights reserved.
*/
// TODO: consider if it's worthwhile to somehow document all the
// possible data and error objects that this callback could spawn
// TODO: make some custom error objects - the errors below would be
// more easily handled if we returned different types of errors for
// each error state
/**
* Volt general callback - used by all asynchronous core methods
*
* In cases where the callback is omitted, a promise is returned from
* the method instead. The promise is resolved with `data` on success.
* On error, it's rejected with `error`. In cases where the error has
* data available, it can be found in `error.data`.
*
* @callback voltCallback
* @param {Error|null} error - an Error object, or null if no error occurred
* @param [data] - the data from an asynchronous request, or undefined if there is no data
*/
$volt.register('api', function (core, state) {
'use strict';
var timeout = 30 * 1000; // the request timeout, in ms
var base = '/api';
if (state.domain) {
base = 'https://' + state.domain + base;
}
function interpolate(str, params) {
var key, match;
for (key in params) {
if (params.hasOwnProperty(key)) {
str = str.replace(new RegExp('\\{\\{' + key + '\\}\\}', 'g'),
params[key]);
}
}
// check for uninterpolated params in str
match = /\{\{(.+?)\}\}/.exec(str);
if (match) {
throw new Error('Key error: ' + match[1]);
}
return str;
}
/**
* Make an API request
*
* The `url` parameter (see below) can contain a simplified template
* that is interpolated from the `params` parameter. For example:
*
* url = 'https://example.com/user/{{userId}}';
* params = { userId: 'Uyh67d' };
*
* @function api
* @private
* @memberOf $volt
*
* @param {string} method - the method of the API call (e.g. 'GET', 'POST', etc...)
* @param {string} url - the URL of the API endpoint (can contain references to params to be interpolated)
* @param {object} params - the params to be interpolated into the URL (or an empty object if there are none)
* @param {object} body - the object to be JSON encoded and passed as the body of the request, a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) object, or undefined if no body is required
* @param {voltCallback} [callback] - called once the API request either fails or completes - if not passed, a promise is returned
*/
return function (method, url, params, body, callback) {
/* jshint maxstatements: 18 */
var xhr = new XMLHttpRequest();
callback = core.methodAsPromised(callback);
url = base + url;
url = interpolate(url, params);
// cheap hack to disable GET caching
url += url.indexOf('?') > -1 ? '&' : '?';
url += '_=' + (new Date()).getTime().toString();
// IE11 wants the XHR open before configuring it
xhr.open(method, url);
xhr.timeout = timeout; // should the user be able to override?
xhr.addEventListener('timeout', function () {
callback(new Error('A network timeout occurred.'));
});
xhr.addEventListener('error', function () {
callback(new Error('A network error occurred.'));
});
xhr.addEventListener('load', function () {
var contentType = xhr.getResponseHeader('Content-Type') || '';
var expected = 'application/json';
var error = null, data;
if (contentType.substr(0, expected.length) === expected) {
// this could fail - but if it does, it means something has
// gone terribly wrong - allow the exception to be thrown
data = JSON.parse(xhr.responseText);
} else {
data = xhr.responseText;
}
if (xhr.status >= 400) {
error = new Error('An API error occurred.');
error.status = xhr.status;
if (xhr.status === 401) {
// bad or no authentication, clear the state
state.clear();
}
}
callback(error, data);
});
// add the bearer token if needed
if (state.accessToken) {
xhr.setRequestHeader('Authorization', 'Bearer ' + state.accessToken);
}
if (typeof body !== 'undefined' && !(body instanceof FormData)) {
// there's probably a cleaner way to do this...
xhr.setRequestHeader('Content-Type', 'application/json');
body = JSON.stringify(body);
}
// send the request (an undefined body is ignored)
xhr.send(body);
return callback.promise;
};
});